@feasibleone/blong-gogo 1.13.7 → 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 +15 -0
- package/bin/blong-dev.ts +40 -2
- package/bin/blong.ts +41 -2
- package/package.json +1 -1
- package/src/load.ts +115 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
10
|
+
## [1.13.8](https://github.com/feasibleone/blong/compare/blong-gogo-v1.13.7...blong-gogo-v1.13.8) (2026-03-18)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* update Dockerfile to include missing dependencies for builder and release stages ([bc93b00](https://github.com/feasibleone/blong/commit/bc93b00421745191a1e79e4a4f0bc832ed0fbd3a))
|
|
16
|
+
* update Dockerfile to install additional dependencies for release image ([f4861fe](https://github.com/feasibleone/blong/commit/f4861fe2446198399a43454d3b2be2d4f9115f64))
|
|
17
|
+
|
|
3
18
|
## [1.13.7](https://github.com/feasibleone/blong/compare/blong-gogo-v1.13.6...blong-gogo-v1.13.7) (2026-03-18)
|
|
4
19
|
|
|
5
20
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
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
|
-
|
|
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
|
|
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');
|