@feasibleone/blong-gogo 1.17.0 → 1.18.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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.18.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.17.1...blong-gogo-v1.18.0) (2026-04-26)
4
+
5
+
6
+ ### Features
7
+
8
+ * add support for the browser platform ([f483e14](https://github.com/feasibleone/blong/commit/f483e149711acdeebcf9ee5afa3bf7ac7a6b669c))
9
+ * implement wiring pipeline simplifications ([0a3dc01](https://github.com/feasibleone/blong/commit/0a3dc01e97c158d30491e8b76bef7d6bb4987529))
10
+ * model handling ([168e77e](https://github.com/feasibleone/blong/commit/168e77ec3284bbcebf5d7d6bb07cc624d0d1a56e))
11
+ * model mocks ([118a142](https://github.com/feasibleone/blong/commit/118a14254f87b8d1cf0b4d217939a9d49942f93e))
12
+ * pino cacache log transport with ULID-linked terminal entries and VSCode click-to-inspect ([#126](https://github.com/feasibleone/blong/issues/126)) ([b3a91f5](https://github.com/feasibleone/blong/commit/b3a91f5d8e6fab1eaf02b81bbd20f2136e5ef267))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * dependencies ([a21d8eb](https://github.com/feasibleone/blong/commit/a21d8ebe4ce9b476060dec8eeb9017a5eb7f07b5))
18
+ * dependencies ([705b2f7](https://github.com/feasibleone/blong/commit/705b2f7a72ac2dc93aea0f586394dde71192c1b6))
19
+ * eslint ([a3b9f2b](https://github.com/feasibleone/blong/commit/a3b9f2bed4f958abfb378d97d372b3e7a6cd5a21))
20
+ * generic adapter ([e5ac7e7](https://github.com/feasibleone/blong/commit/e5ac7e7073aaf0e12258830585a38b055e2905ae))
21
+ * optimize browser loading ([0aac985](https://github.com/feasibleone/blong/commit/0aac985a24520810dc05eb34d314ed7dec4f1962))
22
+ * remove heft lint ([c4d0eaa](https://github.com/feasibleone/blong/commit/c4d0eaa81714c04e9f9adec01c6ae7fa068f1948))
23
+
24
+ ## [1.17.1](https://github.com/feasibleone/blong/compare/blong-gogo-v1.17.0...blong-gogo-v1.17.1) (2026-04-10)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * build ([32951e4](https://github.com/feasibleone/blong/commit/32951e46561648651ee4e032545655007c05e274))
30
+
3
31
  ## [1.17.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.16.0...blong-gogo-v1.17.0) (2026-04-10)
4
32
 
5
33
 
package/bin/blong-dev.ts CHANGED
@@ -1,7 +1,7 @@
1
- #!/usr/bin/env -S node --watch --inspect
1
+ #!/usr/bin/env -S node --watch --conditions=development --inspect
2
2
 
3
3
  import minimist from 'minimist';
4
- import {autoRun} from '../src/loadServer.ts';
4
+ import {autoRun} from '../src/runServer.ts';
5
5
 
6
6
  const argv: {_: string[]} = minimist(process.argv.slice(2));
7
7
 
package/bin/blong.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env -S node
2
2
 
3
3
  import minimist from 'minimist';
4
- import {autoRun} from '../src/loadServer.ts';
4
+ import {autoRun} from '../src/runServer.ts';
5
5
 
6
6
  const argv: {_: string[]} = minimist(process.argv.slice(2));
7
7
 
package/package.json CHANGED
@@ -1,16 +1,15 @@
1
1
  {
2
2
  "name": "@feasibleone/blong-gogo",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/feasibleone/blong.git"
6
6
  },
7
7
  "type": "module",
8
8
  "exports": {
9
9
  ".": {
10
- "browser": "./src/browser.ts",
11
- "import": "./src/load.ts"
10
+ "browser": "./src/loadBrowser.ts",
11
+ "import": "./src/loadServer.ts"
12
12
  },
13
- "./browser": "./src/adapter/browser.ts",
14
13
  "./ConfigRuntime.js": "./src/ConfigRuntime.ts"
15
14
  },
16
15
  "bin": {
@@ -20,7 +19,7 @@
20
19
  },
21
20
  "dependencies": {
22
21
  "@apidevtools/swagger-parser": "^12.1.0",
23
- "@aws-sdk/client-s3": "^3.1003.0",
22
+ "@aws-sdk/client-s3": "^3.1037.0",
24
23
  "@fastify/basic-auth": "^6.2.0",
25
24
  "@fastify/bearer-auth": "^10.1.2",
26
25
  "@fastify/cookie": "^11.0.2",
@@ -38,13 +37,12 @@
38
37
  "@slack/webhook": "^7.0.7",
39
38
  "ajv-formats": "^3.0.1",
40
39
  "browser-process-hrtime": "^1.0.0",
40
+ "cacache": "^20.0.4",
41
41
  "chokidar": "^5.0.0",
42
- "fastify": "^5.8.2",
42
+ "fastify": "^5.8.4",
43
43
  "fastify-plugin": "^5.1.0",
44
- "glob": "^13.0.6",
45
44
  "got": "^14.6.6",
46
45
  "jose": "^6.2.1",
47
- "undici": "^8.0.1",
48
46
  "knex": "^3.1.0",
49
47
  "ky": "^1.14.2",
50
48
  "lru-cache": "^11.2.4",
@@ -57,33 +55,33 @@
57
55
  "openapi-types": "^12.1.3",
58
56
  "p-queue": "^9.1.0",
59
57
  "pino": "^10.3.1",
58
+ "pino-abstract-transport": "^3.0.0",
60
59
  "pino-pretty": "^13.1.3",
61
60
  "reconnect-core": "^1.3.0",
61
+ "tinyglobby": "^0.2.16",
62
62
  "typebox": "^1.1.5",
63
63
  "ulidx": "^2.4.1",
64
+ "undici": "^8.0.1",
64
65
  "ut-bitsyntax": "^6.2.7",
65
66
  "ut-dns-discovery": "^6.2.3",
66
67
  "ut-function.interpolate": "1.1.3",
67
68
  "ut-function.merge": "^1.5.6",
68
- "ut-function.timing": "^1.2.0",
69
- "ut-port": "^6.45.2",
70
69
  "uuid": "^13.0.0",
71
- "yaml": "^2.8.2"
70
+ "yaml": "^2.8.3"
72
71
  },
73
72
  "devDependencies": {
74
- "@rushstack/eslint-config": "^4.6.4",
75
73
  "@rushstack/heft": "^1.2.6",
76
74
  "@rushstack/heft-lint-plugin": "^1.2.6",
77
75
  "@rushstack/heft-typescript-plugin": "^1.3.1",
76
+ "@types/cacache": "^20.0.1",
78
77
  "@types/node": "^24",
79
- "eslint": "~9.39.2",
80
78
  "playwright": "^1.58.2",
81
- "tap": "^21.6.2",
79
+ "tap": "^21.6.3",
82
80
  "typescript": "^5.9.3"
83
81
  },
84
82
  "scripts": {
85
- "build": "true",
86
83
  "browser-check": "node scripts/browser-compat-check.mjs",
84
+ "build": "true",
87
85
  "ci-publish": "node ../../common/scripts/install-run-rush-pnpm.js publish --access public --provenance",
88
86
  "ci-unit": "tap src/ConfigRuntime.test.ts src/lib.test.ts --allow-incomplete-coverage"
89
87
  }
@@ -0,0 +1,374 @@
1
+ import type {
2
+ Config,
3
+ Errors,
4
+ IAdapterFactory,
5
+ IApi,
6
+ IErrorMap,
7
+ IMeta,
8
+ ITypedError,
9
+ } from '@feasibleone/blong/types';
10
+ import type net from 'node:net';
11
+ import PQueue from 'p-queue';
12
+ import merge from 'ut-function.merge';
13
+
14
+ import ConfigRuntime from './ConfigRuntime.ts';
15
+ import loop from './loop.ts';
16
+
17
+ const errorMap: IErrorMap = {
18
+ 'adapter.configValidation': 'Adapter config validation:\r\n{message}',
19
+ 'adapter.missingParameters': 'Missing parameters',
20
+ 'adapter.missingMeta': 'Missing metadata',
21
+ 'adapter.notConnected': 'No connection',
22
+ 'adapter.disconnect': 'Adapter disconnected',
23
+ 'adapter.disconnectBeforeResponse': 'Disconnect before response received',
24
+ 'adapter.stream': 'Adapter stream error',
25
+ 'adapter.timeout': 'Timeout',
26
+ 'adapter.echoTimeout': 'Echo retries limit exceeded',
27
+ 'adapter.unhandled': 'Unhandled adapter error',
28
+ 'adapter.bufferOverflow': 'Message size of {size} exceeds the maximum of {max}',
29
+ 'adapter.socketTimeout': 'Socket timeout',
30
+ 'adapter.receiveTimeout': 'Receive timeout',
31
+ 'adapter.dispatchFailure': 'Cannot dispatch message to bus',
32
+ 'adapter.methodNotFound': 'Method {method} not found',
33
+ 'adapter.queueNotFound': 'Queue not found',
34
+ 'adapter.invalidPullStream': 'Invalid pull stream',
35
+ 'adapter.paramsValidation': 'Method {method} parameters failed validation for: {fields}',
36
+ 'adapter.resultValidation': 'Method {method} result failed validation for: {fields}',
37
+ 'adapter.deadlock':
38
+ 'Method {method} was recursively called, which may cause a deadlock!\nx-b3-traceid: {traceId}\nx-ut-stack: {sequence}',
39
+ 'adapter.noMeta': '$meta not passed',
40
+ 'adapter.noMetaForward': '$meta.forward not passed to method {method}',
41
+ 'adapter.noTraceId': "$meta.forward['x-b3-traceid'] not passed to method {method}",
42
+ };
43
+
44
+ let _errors: Errors<typeof errorMap>;
45
+
46
+ const reserved: string[] = [
47
+ 'reducer',
48
+ 'start',
49
+ 'stop',
50
+ 'ready',
51
+ 'init',
52
+ 'namespace',
53
+ 'send',
54
+ 'requestSend',
55
+ 'responseSend',
56
+ 'errorSend',
57
+ 'receive',
58
+ 'requestReceive',
59
+ 'responseReceive',
60
+ 'errorReceive',
61
+ ];
62
+
63
+ /**
64
+ * AdapterBase — the runtime-provided base class for all adapters.
65
+ *
66
+ * This class replaces the plain-object literal that was previously defined
67
+ * inline in `adapter()`. It is exposed through the runtime injection
68
+ * mechanism — realms and solutions never import it directly. Custom
69
+ * adapters extend it via prototype-chain inheritance
70
+ * (`Object.setPrototypeOf(current, baseInstance)`), preserving the existing
71
+ * hot-reload semantics.
72
+ */
73
+ export class AdapterBase<T = Record<string, unknown>, C = Record<string, unknown>> {
74
+ errors = _errors;
75
+ exec: unknown = null;
76
+ imported: object = {};
77
+ config: Config<T, C> = {} as Config<T, C>;
78
+ configBase: string;
79
+ log: unknown = null;
80
+ importedMap?: Map<string, object>;
81
+
82
+ // These are prefixed with _ rather than using # private class fields.
83
+ // The adapter uses Object.setPrototypeOf(current, base) to set the base as
84
+ // the prototype of the plain handler-result object. When prototype methods
85
+ // are then called on `current`, `this` is `current` (not `base`), so
86
+ // JavaScript's brand-checking on # private fields throws. Regular
87
+ // properties are found via normal prototype-chain lookup, which works.
88
+ _register: IApi['register'];
89
+ _subscribe: IApi['subscribe'];
90
+ _dispatch: IApi['dispatch'];
91
+ _methodId: IApi['methodId'];
92
+ _getPath: IApi['getPath'];
93
+ // _api is stored (not _attachHandlers directly) because Registry.ts sets
94
+ // api.attachHandlers = ... only *after* the port factory returns. Storing
95
+ // the api object and reading api.attachHandlers lazily (at call time)
96
+ // ensures we always see the real function rather than the initial undefined.
97
+ _api: Pick<IApi, 'attachHandlers'>;
98
+ _createLog: IApi['createLog'];
99
+ _attachCheckpoint: IApi['attachCheckpoint'];
100
+ _activationNames: string[];
101
+ _queue: PQueue;
102
+ _portLoop: any; // eslint-disable-line @typescript-eslint/no-explicit-any
103
+ _resolveConnected: (value: boolean) => void;
104
+ _connected: Promise<boolean>;
105
+
106
+ constructor(
107
+ api: Pick<
108
+ IApi,
109
+ | 'register'
110
+ | 'subscribe'
111
+ | 'dispatch'
112
+ | 'methodId'
113
+ | 'getPath'
114
+ | 'attachHandlers'
115
+ | 'createLog'
116
+ | 'attachCheckpoint'
117
+ >,
118
+ configBase: string,
119
+ activationNames: string[] = [],
120
+ ) {
121
+ this.configBase = configBase;
122
+ this._register = api.register;
123
+ this._subscribe = api.subscribe;
124
+ this._dispatch = api.dispatch;
125
+ this._methodId = api.methodId;
126
+ this._getPath = api.getPath;
127
+ this._api = api;
128
+ this._createLog = api.createLog;
129
+ this._attachCheckpoint = api.attachCheckpoint;
130
+ this._activationNames = activationNames;
131
+ let resolveConnected: (value: boolean) => void;
132
+ this._connected = new Promise<boolean>(resolve => {
133
+ resolveConnected = resolve;
134
+ });
135
+ this._resolveConnected = resolveConnected;
136
+ }
137
+
138
+ activeConfig(): object {
139
+ return ConfigRuntime.mergeActivationConfig(this, this._activationNames);
140
+ }
141
+
142
+ async init(...configs: object[]): Promise<void> {
143
+ this.config = merge(this.activeConfig(), ...configs) as Config<T, C>;
144
+ this.log = this._createLog?.(this.config.logLevel || 'info', {
145
+ ...this.config.log,
146
+ name: this.config.id,
147
+ context: this.config.type ?? 'dispatch',
148
+ });
149
+ const id = this.config.id.replace(/\./g, '-');
150
+ this._queue = new PQueue({concurrency: this.config.concurrency || 100});
151
+ this._register(
152
+ {
153
+ [`${id}.start`]: this.start.bind(this),
154
+ [`${id}.stop`]: this.stop.bind(this),
155
+ },
156
+ 'ports',
157
+ this.config.id,
158
+ this.config.pkg,
159
+ );
160
+ this._subscribe(
161
+ {
162
+ [`${id}.drain`]: this.drain.bind(this),
163
+ },
164
+ 'ports',
165
+ this.config.id,
166
+ this.config.pkg,
167
+ );
168
+ }
169
+
170
+ error(error: ITypedError, $meta: IMeta): void {
171
+ if ((this.log as {error?: (...args: unknown[]) => void})?.error) {
172
+ if (error.type && $meta?.expect?.includes?.(error.type)) return;
173
+ if ($meta) error.method = $meta.method;
174
+ (this.log as {error: (...args: unknown[]) => void}).error(error);
175
+ }
176
+ }
177
+
178
+ findValidation($meta: IMeta): unknown {
179
+ return null;
180
+ }
181
+
182
+ handles(name: string): boolean {
183
+ if (reserved.includes(name)) return true;
184
+ const id = this.config.id.replace(/\./g, '-');
185
+ return []
186
+ .concat(this.config.namespace || this.config.imports || id)
187
+ .some(namespace => name.startsWith(namespace));
188
+ }
189
+
190
+ methodPath(methodName: string): string {
191
+ const afterSlash = methodName.split('/', 2)[1];
192
+ if (afterSlash !== undefined) return afterSlash;
193
+ const strip = this.config?.stripNamespace;
194
+ if (strip) return methodName.split('.').slice(strip).join('.');
195
+ return methodName;
196
+ }
197
+
198
+ getConversion($meta: IMeta, type: 'send' | 'receive'): {fn: unknown; name: string} {
199
+ let fn;
200
+ let name: string;
201
+ if ($meta) {
202
+ if ($meta.method) {
203
+ const path = this._getPath($meta.method);
204
+ name = [path, $meta.mtid, type].join('.');
205
+ fn = this.findHandler(name);
206
+ if (!fn) {
207
+ name = [this.methodPath(path), $meta.mtid, type].join('.');
208
+ fn = this.findHandler(name);
209
+ }
210
+ }
211
+ if (!fn) {
212
+ name = [$meta.opcode, $meta.mtid, type].join('.');
213
+ fn = this.findHandler(name);
214
+ }
215
+ if (!fn) {
216
+ name = [$meta.mtid, type].join('.');
217
+ fn = this.findHandler(name);
218
+ }
219
+ }
220
+ if (!fn && (!$meta || $meta.mtid !== 'event')) {
221
+ name = type;
222
+ fn = this.findHandler(name);
223
+ }
224
+ return {fn, name};
225
+ }
226
+
227
+ async dispatch(...args: unknown[]): Promise<unknown> {
228
+ const result = this._dispatch(...args);
229
+ if (!result)
230
+ (this.log as {error?: (...args: unknown[]) => void})?.error?.(
231
+ this.errors['adapter.dispatchFailure']({args}),
232
+ );
233
+ return result;
234
+ }
235
+
236
+ async event(event: string, data?: object, mapper?: string): Promise<unknown> {
237
+ (this.log as {info?: (...args: unknown[]) => void})?.info?.({
238
+ $meta: {mtid: 'event', method: `adapter.${event}`},
239
+ ...data,
240
+ });
241
+ const eventHandlers = [];
242
+ this.importedMap?.forEach(
243
+ imp =>
244
+ Object.prototype.hasOwnProperty.call(imp, event) &&
245
+ eventHandlers.push(imp[event]),
246
+ );
247
+ let result: unknown = data;
248
+ switch (mapper) {
249
+ case 'asyncMap':
250
+ result = await Promise.all(
251
+ eventHandlers.map(handler => handler.call(this, data)),
252
+ );
253
+ break;
254
+ case 'reduce':
255
+ default:
256
+ for (const eventHandler of eventHandlers) {
257
+ result = await eventHandler.call(this, result);
258
+ }
259
+ break;
260
+ }
261
+ return result;
262
+ }
263
+
264
+ drain(): void {}
265
+
266
+ findHandler(methodName: string): unknown {
267
+ methodName = this._methodId(methodName);
268
+ return this.imported[methodName];
269
+ }
270
+
271
+ async request(...params: unknown[]): Promise<unknown> {
272
+ return this._queue.add(this._portLoop(params, true));
273
+ }
274
+
275
+ async publish(...params: unknown[]): Promise<unknown> {
276
+ await this._queue.add(this._portLoop(params, false));
277
+ return [true, params[params.length - 1]];
278
+ }
279
+
280
+ async ready(): Promise<unknown> {
281
+ return this.event('ready');
282
+ }
283
+
284
+ forNamespaces<R>(reducer: (prev: R, current: unknown) => R, initial: R): R {
285
+ const id = this.config.id.replace(/\./g, '-');
286
+ return []
287
+ .concat(this.config.namespace || this.config.imports || id)
288
+ .reduce(reducer.bind(this), initial);
289
+ }
290
+
291
+ async start(): Promise<unknown> {
292
+ await this._api.attachHandlers(this, this.config.imports, true);
293
+ const {req, pub} = this.forNamespaces(
294
+ (prev, next) => {
295
+ if (typeof next === 'string') {
296
+ prev.req[`${next}.request`] = this.request.bind(this);
297
+ prev.pub[`${next}.publish`] = this.publish.bind(this);
298
+ }
299
+ return prev;
300
+ },
301
+ {req: {}, pub: {}},
302
+ );
303
+ this._register(req, 'ports', this.config.id, this.config.pkg);
304
+ this._subscribe(pub, 'ports', this.config.id, this.config.pkg);
305
+ const {context, ...config} = this.config; // eslint-disable-line @typescript-eslint/no-unused-vars
306
+ return this.event('start', {configBase: this.configBase, config});
307
+ }
308
+
309
+ async link(patterns: unknown, target: {imported?: object} = {}): Promise<object> {
310
+ await this._api.attachHandlers(target, patterns, false);
311
+ return target.imported;
312
+ }
313
+
314
+ async handle(...params: unknown[]): Promise<unknown> {
315
+ const $meta = params && params.length > 1 && (params[params.length - 1] as IMeta);
316
+ if ($meta && typeof $meta === 'object') this._attachCheckpoint?.($meta);
317
+ const method = ($meta && $meta.method) || 'exec';
318
+ const handler = this.findHandler(method) || this.imported['exec'];
319
+ if (handler instanceof Function) {
320
+ return handler.apply(this, params);
321
+ } else {
322
+ throw this.errors['adapter.methodNotFound']({params: {method}});
323
+ }
324
+ }
325
+
326
+ connect(
327
+ what?: net.Socket | (() => void),
328
+ context?: any, // eslint-disable-line @typescript-eslint/no-explicit-any
329
+ ): void {
330
+ what ??= this.handle.bind(this);
331
+ context ??= this.config.context;
332
+ this._portLoop = loop(what, this as any, context); // eslint-disable-line @typescript-eslint/no-explicit-any
333
+ this._resolveConnected(true);
334
+ }
335
+
336
+ async connected(): Promise<boolean> {
337
+ return this._connected;
338
+ }
339
+
340
+ async stop(): Promise<unknown> {
341
+ return this.event('stop');
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Create an adapter instance from the API and handler definitions.
347
+ *
348
+ * The `AdapterBase` class is instantiated here (not imported by realms) and
349
+ * placed at the end of the handler prototype chain. This preserves the
350
+ * constraint that realms/solutions never depend on blong-gogo.
351
+ */
352
+ export default async function adapter<T, C>(
353
+ api: IApi,
354
+ configBase: string,
355
+ activationNames: string[] = [],
356
+ ): Promise<ReturnType<IAdapterFactory>> {
357
+ const {adapter: adapterFactory, utError, handlers, remote, rpc, local, registry, type} = api;
358
+ _errors ||= utError.register(errorMap);
359
+
360
+ const base = new AdapterBase<T, C>(api, configBase, activationNames);
361
+
362
+ const result = handlers({utError, remote, type});
363
+ let current = result;
364
+ while (current.extends) {
365
+ const parent = await (typeof current.extends === 'string'
366
+ ? adapterFactory(current.extends)({utError, remote, rpc, local, registry})
367
+ : current.extends({utError, remote, rpc, local, registry}));
368
+ Object.setPrototypeOf(current, parent);
369
+ current = parent;
370
+ }
371
+ Object.setPrototypeOf(current, base);
372
+
373
+ return result as ReturnType<IAdapterFactory>;
374
+ }
package/src/ApiSchema.ts CHANGED
@@ -2,17 +2,15 @@ import type {
2
2
  GatewaySchema,
3
3
  IApiSchema,
4
4
  ILog,
5
+ IPlatformApi,
5
6
  PathItemObject,
6
7
  SchemaObject,
7
8
  } from '@feasibleone/blong/types';
8
9
  import {Internal} from '@feasibleone/blong/types';
9
- import {createReadStream, statSync, writeFileSync, type Dirent} from 'node:fs';
10
- import path, {basename, dirname, extname} from 'node:path';
10
+ import {type Dirent} from 'node:fs';
11
11
 
12
- import {join} from 'path';
13
12
  import {identifier} from './lib.ts';
14
13
  import loadApi from './loadApi.ts';
15
- import scan from './scan.ts';
16
14
 
17
15
  interface IConfig {
18
16
  logLevel?: Parameters<ILog['logger']>[0];
@@ -24,6 +22,7 @@ export default class ApiSchema extends Internal implements IApiSchema {
24
22
  logLevel: 'debug',
25
23
  generate: true,
26
24
  };
25
+ #platform: IPlatformApi;
27
26
 
28
27
  #loaded: Record<string, GatewaySchema> = {};
29
28
  #namespace: Record<string, Record<string, GatewaySchema>> = {};
@@ -36,8 +35,9 @@ export default class ApiSchema extends Internal implements IApiSchema {
36
35
  }
37
36
  > = {};
38
37
 
39
- public constructor(config: IConfig, {log}: {log: ILog}) {
38
+ public constructor(config: IConfig, {log, platform}: {log: ILog; platform: IPlatformApi}) {
40
39
  super({log});
40
+ this.#platform = platform;
41
41
  this.merge(this.#config, config);
42
42
  }
43
43
 
@@ -45,6 +45,13 @@ export default class ApiSchema extends Internal implements IApiSchema {
45
45
  return operation?.['x-blong-method'] || operation.operationId;
46
46
  }
47
47
 
48
+ public loadApi(
49
+ locations: string | string[] | object | object[] | {assets: object},
50
+ source: string = process.cwd(),
51
+ ): ReturnType<typeof loadApi> {
52
+ return loadApi(locations, source, this.#platform);
53
+ }
54
+
48
55
  public async schema(
49
56
  {
50
57
  namespace,
@@ -57,8 +64,8 @@ export default class ApiSchema extends Internal implements IApiSchema {
57
64
  ): Promise<Record<string, GatewaySchema>> {
58
65
  const result: Record<string, GatewaySchema> = {};
59
66
  if (url) {
60
- const dir = dirname(url.startsWith('file://') ? url.slice(7) : url);
61
- const files = await scan(dir);
67
+ const dir = this.#platform.dirname(url.startsWith('file://') ? url.slice(7) : url);
68
+ const files = await this.#platform.scan(dir);
62
69
  namespace = namespace || {};
63
70
  for (const file of files) {
64
71
  if (
@@ -67,9 +74,9 @@ export default class ApiSchema extends Internal implements IApiSchema {
67
74
  file.name.endsWith('.yml') ||
68
75
  file.name.endsWith('.json'))
69
76
  ) {
70
- const [name] = basename(file.name).split('.');
77
+ const [name] = this.#platform.basename(file.name).split('.');
71
78
  namespace[name] ||= [];
72
- namespace[name].push(join(dir, file.name));
79
+ namespace[name].push(this.#platform.join(dir, file.name));
73
80
  }
74
81
  }
75
82
  }
@@ -83,7 +90,7 @@ export default class ApiSchema extends Internal implements IApiSchema {
83
90
  );
84
91
 
85
92
  for (const [name, locations] of Object.entries(namespace)) {
86
- const bundle = await loadApi(locations, source);
93
+ const bundle = await loadApi(locations, source, this.#platform);
87
94
  const {namespace = name, destination} = bundle['x-blong'] ?? {};
88
95
  this.#namespace[namespace] ||= {};
89
96
  Object.entries(bundle.paths).forEach(([path, methods]: [string, PathItemObject]) => {
@@ -132,17 +139,17 @@ export default class ApiSchema extends Internal implements IApiSchema {
132
139
  for (const [method, operation] of Object.entries(this.#loaded)) {
133
140
  const filename = operation.subject + this.method(operation.operation) + '.ts';
134
141
  if (method.startsWith(prefix) && !record.existing.has(filename.toLowerCase())) {
135
- generate.push(path.join(record.dir, filename));
142
+ generate.push(this.#platform.join(record.dir, filename));
136
143
  }
137
144
  }
138
145
  }
139
146
  for (const filename of generate.concat(Array.from(this.#generateFile))) {
140
- const method = basename(filename, extname(filename));
147
+ const method = this.#platform.basename(filename, this.#platform.extname(filename));
141
148
  const schema = this.#loaded[method.toLowerCase()];
142
149
  if (schema) {
143
150
  // console.log(schema.operation.responses);
144
151
  this.log?.warn?.(`Writing ${filename}`);
145
- writeFileSync(
152
+ this.#platform.writeFileSync(
146
153
  filename,
147
154
  `import unchanged from '@feasibleone/blong';
148
155
  import {type IMeta, handler} from '@feasibleone/blong/types';
@@ -256,16 +263,8 @@ export default handler(
256
263
 
257
264
  public async generateFile(filename: string): Promise<boolean> {
258
265
  if (this.#config.generate === false) return false;
259
- if (statSync(filename).size !== 0) return false;
260
- let content = '';
261
- await new Promise<void>((resolve, reject) => {
262
- createReadStream(filename, {end: 50, encoding: 'utf-8'})
263
- .on('data', chunk => {
264
- content += chunk;
265
- })
266
- .on('error', reject)
267
- .on('close', resolve);
268
- });
266
+ if (this.#platform.statSync(filename).size !== 0) return false;
267
+ let content = this.#platform.readFileSync(filename, {encoding: 'utf-8'}).toString('utf-8');
269
268
  if (content.includes('import unchanged from')) {
270
269
  this.#generateFile.add(filename);
271
270
  return false;
@@ -288,6 +287,8 @@ export default handler(
288
287
  existing: new Set(),
289
288
  };
290
289
  }
291
- files.forEach(file => record.existing.add(basename(file.name).toLowerCase()));
290
+ files.forEach(file =>
291
+ record.existing.add(this.#platform.basename(file.name).toLowerCase()),
292
+ );
292
293
  }
293
294
  }