@feasibleone/blong-gogo 1.13.8 → 1.14.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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.14.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.13.8...blong-gogo-v1.14.0) (2026-03-20)
4
+
5
+
6
+ ### Features
7
+
8
+ * realm loading and sim improvements ([a2c0ebe](https://github.com/feasibleone/blong/commit/a2c0ebe1fa1608ab4af5ffdc26e7518219710bdb))
9
+
3
10
  ## [1.13.8](https://github.com/feasibleone/blong/compare/blong-gogo-v1.13.7...blong-gogo-v1.13.8) (2026-03-18)
4
11
 
5
12
 
package/bin/blong-dev.ts CHANGED
@@ -1,9 +1,47 @@
1
1
  #!/usr/bin/env -S node --watch --inspect
2
2
 
3
3
  import minimist from 'minimist';
4
- import {resolve} from 'node:path';
4
+ import {existsSync} from 'node:fs';
5
+ import {basename, resolve} from 'node:path';
5
6
  import load from '../src/load.ts';
6
7
 
7
8
  const argv: {_: string[]} = minimist(process.argv.slice(2));
9
+ const cwd = process.cwd();
10
+ const target = argv._[0];
8
11
 
9
- (await import(resolve(argv._[0] ?? 'index.ts'))).default(load);
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
+ }
package/bin/blong.ts CHANGED
@@ -1,9 +1,48 @@
1
1
  #!/usr/bin/env -S node
2
2
 
3
3
  import minimist from 'minimist';
4
- import {resolve} from 'node:path';
4
+ import { existsSync } from 'node:fs';
5
+ import { basename, join, resolve } from 'node:path';
5
6
  import load from '../src/load.ts';
6
7
 
7
8
  const argv: {_: string[]} = minimist(process.argv.slice(2));
9
+ const cwd = process.cwd();
10
+ const target = argv._[0];
8
11
 
9
- (await import(resolve(argv._[0] ?? 'index.ts'))).default(load);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feasibleone/blong-gogo",
3
- "version": "1.13.8",
3
+ "version": "1.14.0",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/feasibleone/blong.git"
6
6
  },
package/src/load.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import load from '@feasibleone/blong-config';
2
2
  import {
3
3
  Internal,
4
+ browser as browserFactory,
4
5
  kind,
6
+ server as serverFactory,
5
7
  type IApiSchema,
6
8
  type IErrorFactory,
7
9
  type ILog,
@@ -12,17 +14,19 @@ import {
12
14
  type Kinds,
13
15
  type SolutionFactory,
14
16
  } from '@feasibleone/blong/types';
15
- import {existsSync} from 'fs';
16
- import {readdir} from 'fs/promises';
17
- import {createRequire} from 'node:module';
18
- import {basename, dirname, join} from 'path';
19
- import {Type, type TSchema} from 'typebox';
17
+ import { existsSync } from 'fs';
18
+ import { readdir } from 'fs/promises';
19
+ import { createRequire } from 'node:module';
20
+ import { basename, dirname, extname, join } from 'path';
21
+
22
+ import { Type, type TSchema } from 'typebox';
20
23
  import merge from 'ut-function.merge';
24
+ import { methodParts } from './lib.ts';
21
25
 
22
- import type {Dirent} from 'fs';
26
+ import type { Dirent } from 'fs';
23
27
  import layerProxy from './layerProxy.ts';
24
- import RealmImpl, {type IRealm} from './Realm.ts';
25
- import type {IWatch} from './Watch.ts';
28
+ import RealmImpl, { type IRealm } from './Realm.ts';
29
+ import type { IWatch } from './Watch.ts';
26
30
 
27
31
  const LAYER_FILE = 'layer' as const;
28
32
 
@@ -72,6 +76,50 @@ async function discoverLayerFolders(
72
76
  return result;
73
77
  }
74
78
 
79
+ /**
80
+ * Discover test method names from a realm's test/test/ folder.
81
+ * Imports each handler file with a null proxy to extract method names.
82
+ */
83
+ async function discoverRealmTestMethods(base: string): Promise<string[]> {
84
+ const testDir = join(base, 'test', 'test');
85
+ if (!existsSync(testDir)) return [];
86
+ const nullFn: unknown = new Proxy(function () {
87
+ return nullFn;
88
+ } as unknown as object, {
89
+ get(_, key) {
90
+ if (typeof key === 'symbol') return undefined;
91
+ return nullFn;
92
+ },
93
+ apply() {
94
+ return nullFn;
95
+ },
96
+ });
97
+ const files = (await readdir(testDir, {withFileTypes: true})).filter(
98
+ f => f.isFile() && /\.(ts|mts|js|mjs)$/i.test(f.name) && !f.name.startsWith('~'),
99
+ );
100
+ const methods: string[] = [];
101
+ for (const file of files) {
102
+ const filePath = join(testDir, file.name);
103
+ try {
104
+ const mod = await import(filePath);
105
+ const fn = mod?.default;
106
+ if (typeof fn === 'function') {
107
+ const result = fn({lib: nullFn, handler: nullFn, errors: nullFn, config: {}, remote: nullFn, local: nullFn});
108
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
109
+ for (const key of Object.keys(result)) methods.push(methodParts(key));
110
+ } else {
111
+ methods.push(methodParts(basename(file.name, extname(file.name))));
112
+ }
113
+ } else {
114
+ methods.push(methodParts(basename(file.name, extname(file.name))));
115
+ }
116
+ } catch {
117
+ methods.push(methodParts(basename(file.name, extname(file.name))));
118
+ }
119
+ }
120
+ return methods;
121
+ }
122
+
75
123
  const scan = async (...path: string[]): Promise<Dirent[]> =>
76
124
  (await readdir(join(...path), {withFileTypes: true})).sort((a, b) =>
77
125
  a < b ? -1 : a > b ? 1 : 0,
@@ -125,7 +173,65 @@ export default async function loadRealm<T extends TSchema>(
125
173
  const defKind = kind(def);
126
174
  if (!rootKind) {
127
175
  if (defKind === 'server' || defKind === 'browser') rootKind = defKind;
128
- else throw new Error(`Root realm must be of kind "server" or "browser", got "${defKind}"`);
176
+ else if (defKind === 'solution') {
177
+ // Realm passed directly as root — wrap in a minimal suite for isolated testing
178
+ const realmMod = await def({type: Type});
179
+ const realmUrl = realmMod.url as string;
180
+ const realmFilePath = realmUrl.startsWith('file://') ? realmUrl.slice(7) : realmUrl;
181
+ const realmBase = dirname(realmFilePath);
182
+ const realmFileName = basename(realmFilePath);
183
+ const realmName = basename(realmBase);
184
+ let pkg: {name: string; version: string};
185
+ try {
186
+ pkg = createRequire(realmUrl)('./package.json') as {name: string; version: string};
187
+ } catch {
188
+ pkg = {name: realmName, version: '0.0.0'};
189
+ }
190
+ const testMethods = await discoverRealmTestMethods(realmBase);
191
+ const realmChild = Object.defineProperty(
192
+ async function () {
193
+ return {default: def};
194
+ },
195
+ 'name',
196
+ {value: realmName, configurable: true},
197
+ );
198
+ if (realmFileName.startsWith('browser')) {
199
+ // Browser realm — wrap in a minimal browser suite
200
+ const wrapper = browserFactory((() => ({
201
+ url: '',
202
+ pkg,
203
+ children: [realmChild],
204
+ config: {
205
+ default: {remote: {canSkipSocket: true}, [realmName]: {}},
206
+ integration: {watch: {test: testMethods}},
207
+ },
208
+ })) as () => any);
209
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
210
+ return loadRealm(wrapper as any, name, parentConfig, configNames);
211
+ } else {
212
+ // Server realm — single platform if no browser.ts alongside, else two-platform server half
213
+ const hasBrowser = existsSync(join(realmBase, 'browser.ts'));
214
+ const wrapper = serverFactory((() => ({
215
+ url: '',
216
+ pkg,
217
+ children: [realmChild],
218
+ config: {
219
+ default: {
220
+ rpcServer: {port: 0},
221
+ gateway: {port: hasBrowser ? 8080 : 0},
222
+ [realmName]: {},
223
+ },
224
+ integration: hasBrowser
225
+ ? {default: {}} // browser side handles watch.test and canSkipSocket
226
+ : {default: {}, remote: {canSkipSocket: true}, watch: {test: testMethods}},
227
+ },
228
+ })) as () => any);
229
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
230
+ return loadRealm(wrapper as any, name, parentConfig, configNames);
231
+ }
232
+ } else {
233
+ throw new Error(`Root realm must be of kind "server" or "browser", got "${defKind}"`);
234
+ }
129
235
  }
130
236
  const mod = await def({type: Type});
131
237
  if (!('pkg' in mod)) mod.pkg = createRequire(mod.url)('./package.json');