@aistrike-dev/ui-shell-dev 2.1.0 → 2.1.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/README.md +41 -17
- package/dist/args.d.ts +9 -1
- package/dist/args.js +51 -5
- package/dist/cell.d.ts +29 -0
- package/dist/cell.js +194 -0
- package/dist/cli.js +25 -1
- package/dist/devfile.d.ts +25 -9
- package/dist/devfile.js +75 -30
- package/dist/hosts.d.ts +17 -0
- package/dist/hosts.js +51 -0
- package/dist/inject.js +0 -3
- package/dist/platform/host.d.ts +5 -7
- package/dist/platform/host.js +4 -4
- package/dist/platform/list.d.ts +2 -2
- package/dist/platform/list.js +6 -5
- package/dist/platform/server.d.ts +3 -2
- package/dist/platform/server.js +21 -10
- package/dist/ports.d.ts +12 -0
- package/dist/ports.js +14 -0
- package/dist/preamble.d.ts +15 -0
- package/dist/preamble.js +44 -0
- package/dist/serve.d.ts +28 -0
- package/dist/serve.js +161 -0
- package/dist/token.d.ts +6 -1
- package/dist/token.js +2 -2
- package/dist/vite/plugin.d.ts +12 -3
- package/dist/vite/plugin.js +126 -27
- package/dist/vite/targets.d.ts +27 -0
- package/dist/vite/targets.js +35 -0
- package/package.json +2 -2
- package/platform/assistant/mock.js +1809 -222
- package/platform/assistant/ui/chunks/ui-CnxV0v5I.js +280 -0
- package/platform/assistant/ui/entry.js +2 -2
- package/platform/home/ui/chunks/ui-BJZEfOZI.js +174 -0
- package/platform/home/ui/entry.js +1 -1
- package/platform/settings/ui/chunks/ui-BRCsapmB.js +271 -0
- package/platform/settings/ui/entry.js +2 -2
- package/shell/assets/LabHost-D3o-OJsx.js +2 -0
- package/shell/assets/index-DW0Z5YeB.js +185 -0
- package/shell/index.html +1 -1
- package/platform/assistant/ui/chunks/ui--m9X09Oo.js +0 -182
- package/platform/home/ui/chunks/ui-SNPmLvIE.js +0 -174
- package/platform/settings/ui/chunks/ui-C21hUUVD.js +0 -271
- package/shell/assets/LabHost-nKG_bmEN.js +0 -2
- package/shell/assets/index-Bm6IWtZi.js +0 -184
package/dist/serve.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { extname, join, normalize, resolve, sep } from 'node:path';
|
|
4
|
+
import { closeCell, createCellServer } from './cell.js';
|
|
5
|
+
import { buildDevConfig, loadDevFile } from './devfile.js';
|
|
6
|
+
import { CONTENT_TYPES, injectDevConfig, sendFile, validateDevConfig } from './inject.js';
|
|
7
|
+
import { createPlatformHost, readPlatform, selectPlatform } from './platform/host.js';
|
|
8
|
+
import { isAllowedHost, refuseHost, rememberSession } from './hosts.js';
|
|
9
|
+
import { DEV_PORTS } from './ports.js';
|
|
10
|
+
import { beforeFirstModule, refreshPreamble, refreshRuntimePath, servesRefreshRuntime } from './preamble.js';
|
|
11
|
+
import { loadOrCreateKey, mintToken } from './token.js';
|
|
12
|
+
import { cellTargets } from './vite/targets.js';
|
|
13
|
+
export const DEFAULT_CONSOLE_PORT = 3000;
|
|
14
|
+
export const DEFAULT_CELL_PORT = 4000;
|
|
15
|
+
/** The ui-shell dev server's file: where each domain runs, in `cell`, and nothing an MFE project's file says. */
|
|
16
|
+
function assertCellFile(file, name) {
|
|
17
|
+
if (file?.domains !== undefined) {
|
|
18
|
+
throw new Error(`${name}: domains is an MFE project's own domain; list each domain in cell.domains instead`);
|
|
19
|
+
}
|
|
20
|
+
const unnamed = file?.cell?.domains.find((domain) => domain.nav === undefined && !Object.hasOwn(DEV_PORTS, domain.id));
|
|
21
|
+
if (unnamed !== undefined) {
|
|
22
|
+
throw new Error(`${name}: cell.domains: "${unnamed.id}" is not a compiled domain, so it needs nav.label`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function listen(server, port) {
|
|
26
|
+
return await new Promise((done, fail) => {
|
|
27
|
+
server.once('error', fail);
|
|
28
|
+
server.listen(port, () => {
|
|
29
|
+
server.off('error', fail);
|
|
30
|
+
const address = server.address();
|
|
31
|
+
done(typeof address === 'object' && address !== null ? address.port : port);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function closeServer(server) {
|
|
36
|
+
await new Promise((done) => {
|
|
37
|
+
server.close(() => {
|
|
38
|
+
done();
|
|
39
|
+
});
|
|
40
|
+
server.closeAllConnections();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function answer(res, status, text) {
|
|
44
|
+
res.statusCode = status;
|
|
45
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
46
|
+
res.end(text);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The ui-shell dev server (cell resolution design 3): the console, which serves the shell's devhost build with a dev
|
|
50
|
+
* token, and the dev cell, an origin of its own that forwards each domain to the server its `cell.domains` names, or
|
|
51
|
+
* to the built-in MFE. The shell loads every domain from the cell, cross-origin, as from a deployed cell.
|
|
52
|
+
*/
|
|
53
|
+
export async function serveDevShell(options) {
|
|
54
|
+
const name = options.config ?? 'shell.dev.json';
|
|
55
|
+
const file = loadDevFile(name, options.cwd);
|
|
56
|
+
assertCellFile(file, name);
|
|
57
|
+
const shellDir = resolve(options.shellDir);
|
|
58
|
+
const listed = file?.cell?.domains ?? [];
|
|
59
|
+
const packaged = readPlatform(options.platformDir);
|
|
60
|
+
const elsewhere = listed.map((domain) => domain.id);
|
|
61
|
+
const bundled = selectPlatform(packaged, { ownId: '', elsewhere, option: true });
|
|
62
|
+
const platform = createPlatformHost(options.platformDir, bundled, options.logger);
|
|
63
|
+
await platform.bind();
|
|
64
|
+
const cell = createCellServer({ targets: () => cellTargets({ listed, platform: packaged }), logger: options.logger });
|
|
65
|
+
let cellPort;
|
|
66
|
+
try {
|
|
67
|
+
cellPort = await listen(cell, options.cellPort ?? DEFAULT_CELL_PORT);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
await platform.close();
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
const key = loadOrCreateKey(options.keyDir);
|
|
74
|
+
const sessions = new Map();
|
|
75
|
+
/**
|
|
76
|
+
* The token for a browser that reached the console as `host` (an allowed one, `isAllowedHost`): its `cell` is the dev
|
|
77
|
+
* cell on that same address. At most `MAX_SESSIONS` are kept; the oldest gives way.
|
|
78
|
+
*/
|
|
79
|
+
const sessionFor = (host) => {
|
|
80
|
+
const cellHost = `${new URL(`http://${host}`).hostname}:${String(cellPort)}`;
|
|
81
|
+
let session = sessions.get(cellHost);
|
|
82
|
+
if (session === undefined) {
|
|
83
|
+
session = mintToken({ key, claims: file?.claims, cell: cellHost }).session;
|
|
84
|
+
rememberSession(sessions, cellHost, session);
|
|
85
|
+
}
|
|
86
|
+
return session;
|
|
87
|
+
};
|
|
88
|
+
validateDevConfig(buildDevConfig(file, {}, sessionFor('localhost')));
|
|
89
|
+
/** Listed domains whose server is a Vite dev server with React Refresh; a server not up yet is asked again next load. */
|
|
90
|
+
const refreshing = new Set();
|
|
91
|
+
const preambleFor = async (cellHost) => {
|
|
92
|
+
await Promise.all(listed.filter((domain) => !refreshing.has(domain.id)).map(async (domain) => {
|
|
93
|
+
if (await servesRefreshRuntime(domain.at, domain.id)) {
|
|
94
|
+
refreshing.add(domain.id);
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
97
|
+
const ids = listed.map((domain) => domain.id).filter((id) => refreshing.has(id));
|
|
98
|
+
return refreshPreamble(ids.map((id) => `http://${cellHost}${refreshRuntimePath(id)}`));
|
|
99
|
+
};
|
|
100
|
+
const servePage = async (host, res) => {
|
|
101
|
+
const source = join(shellDir, 'index.html');
|
|
102
|
+
// A watch build empties its output before it writes again.
|
|
103
|
+
if (!existsSync(source)) {
|
|
104
|
+
answer(res, 503, 'The shell is building. Reload in a moment.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const session = sessionFor(host);
|
|
108
|
+
const devConfig = buildDevConfig(file, {}, session);
|
|
109
|
+
const page = injectDevConfig(readFileSync(source, 'utf8'), devConfig, source);
|
|
110
|
+
const preamble = await preambleFor(String(session.claims.cell));
|
|
111
|
+
res.setHeader('Content-Type', CONTENT_TYPES['.html'] ?? 'text/html');
|
|
112
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
113
|
+
res.end(beforeFirstModule(page, preamble, source));
|
|
114
|
+
};
|
|
115
|
+
const consoleServer = createServer((req, res) => {
|
|
116
|
+
if (!isAllowedHost(req.headers.host)) {
|
|
117
|
+
refuseHost(res);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let pathname;
|
|
121
|
+
try {
|
|
122
|
+
pathname = decodeURIComponent(new URL(req.url ?? '/', 'http://localhost').pathname);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
answer(res, 400, 'bad request');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (pathname.startsWith('/assets/')) {
|
|
129
|
+
const target = resolve(shellDir, `.${normalize(pathname)}`);
|
|
130
|
+
if (target.startsWith(shellDir + sep)) {
|
|
131
|
+
sendFile(target, res);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
answer(res, 404, 'not found');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (extname(pathname) !== '' && pathname !== '/index.html') {
|
|
138
|
+
answer(res, 404, 'not found');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
servePage(req.headers.host, res).catch((error) => {
|
|
142
|
+
options.logger.warn(`console: ${error instanceof Error ? error.message : String(error)}`);
|
|
143
|
+
answer(res, 500, 'the shell page could not be served');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
let consolePort;
|
|
147
|
+
try {
|
|
148
|
+
consolePort = await listen(consoleServer, options.port ?? DEFAULT_CONSOLE_PORT);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
await Promise.all([closeCell(cell), platform.close()]);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
consolePort,
|
|
156
|
+
cellPort,
|
|
157
|
+
close: async () => {
|
|
158
|
+
await Promise.all([closeServer(consoleServer), closeCell(cell), platform.close()]);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
package/dist/token.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export declare function loadOrCreateKey(dir?: string): DevKey;
|
|
|
31
31
|
export interface MintOptions {
|
|
32
32
|
key: DevKey;
|
|
33
33
|
claims?: DevClaimsInput;
|
|
34
|
+
/**
|
|
35
|
+
* The `cell` claim: the host the browser reached the dev host by (`localhost:5103`), which is the cell in dev (cell
|
|
36
|
+
* resolution design 3). Set by the plugin, never by a claims file.
|
|
37
|
+
*/
|
|
38
|
+
cell?: string;
|
|
34
39
|
ttlMs?: number;
|
|
35
40
|
now?: number;
|
|
36
41
|
}
|
|
@@ -38,7 +43,7 @@ export interface Minted {
|
|
|
38
43
|
token: string;
|
|
39
44
|
session: Session;
|
|
40
45
|
}
|
|
41
|
-
export declare function mintToken({ key, claims, ttlMs, now }: MintOptions): Minted;
|
|
46
|
+
export declare function mintToken({ key, claims, cell, ttlMs, now }: MintOptions): Minted;
|
|
42
47
|
export declare function decodeToken(token: string): {
|
|
43
48
|
header: unknown;
|
|
44
49
|
payload: unknown;
|
package/dist/token.js
CHANGED
|
@@ -67,11 +67,11 @@ export function loadOrCreateKey(dir = configDir()) {
|
|
|
67
67
|
return key;
|
|
68
68
|
}
|
|
69
69
|
const b64url = (input) => Buffer.from(input).toString('base64url');
|
|
70
|
-
export function mintToken({ key, claims = {}, ttlMs = DEFAULT_TTL_MS, now = Date.now() }) {
|
|
70
|
+
export function mintToken({ key, claims = {}, cell, ttlMs = DEFAULT_TTL_MS, now = Date.now() }) {
|
|
71
71
|
if (!(ttlMs > 0)) {
|
|
72
72
|
throw new Error(`ttl must be positive, got ${ttlMs}`);
|
|
73
73
|
}
|
|
74
|
-
const merged = { ...DEFAULT_CLAIMS, ...claims };
|
|
74
|
+
const merged = { ...DEFAULT_CLAIMS, ...claims, ...(cell === undefined ? {} : { cell }) };
|
|
75
75
|
const iat = Math.floor(now / 1000);
|
|
76
76
|
const exp = Math.floor((now + ttlMs) / 1000);
|
|
77
77
|
const header = b64url(JSON.stringify({ alg: 'ES256', typ: 'JWT', kid: key.kid }));
|
package/dist/vite/plugin.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
import { type DevClaimsInput } from '../token.js';
|
|
3
|
+
/** The dev cell's port when none is given: the console's own plus this (5103 → 6103). */
|
|
4
|
+
export declare const CELL_PORT_OFFSET = 1000;
|
|
3
5
|
export interface ShellDevHostOptions {
|
|
4
6
|
/** Path to the shell devhost build. Default: the `shell/` directory shipped in this package. */
|
|
5
7
|
shellDir?: string;
|
|
@@ -17,6 +19,11 @@ export interface ShellDevHostOptions {
|
|
|
17
19
|
platform?: boolean | string[];
|
|
18
20
|
/** Path to the platform MFEs' builds. Default: the `platform/` directory shipped in this package. */
|
|
19
21
|
platformDir?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The dev cell's port. Default: the console's port plus `CELL_PORT_OFFSET`, or any free port when that is past the
|
|
24
|
+
* last port or taken. A port given here is kept: when it is taken, the cell does not start and a warning says so.
|
|
25
|
+
*/
|
|
26
|
+
cellPort?: number;
|
|
20
27
|
}
|
|
21
28
|
/**
|
|
22
29
|
* Vite's HTML transform prefixes root-relative URLs with its base (`/ui/latest/`); the shell's build lives at the root,
|
|
@@ -24,8 +31,10 @@ export interface ShellDevHostOptions {
|
|
|
24
31
|
*/
|
|
25
32
|
export declare function markShellAssets(html: string): string;
|
|
26
33
|
/**
|
|
27
|
-
* Serves the shell from the MFE's own Vite dev server: `/` and every extension-less path get the shell
|
|
28
|
-
* dev config, the Vite client and any plugin's HTML, such as the React preamble), `/assets/*` the shell
|
|
29
|
-
* the MFE stays at the Vite base (
|
|
34
|
+
* Serves the shell from the MFE's own Vite dev server, as a console: `/` and every extension-less path get the shell
|
|
35
|
+
* page (with the dev config, the Vite client and any plugin's HTML, such as the React preamble), `/assets/*` the shell
|
|
36
|
+
* build, and the MFE stays at the Vite base (`/<id>/ui/latest/`). Beside it runs the dev cell, an origin of its own,
|
|
37
|
+
* which forwards the MFE and the built-in MFEs, so the shell loads them cross-origin as from a deployed cell (cell
|
|
38
|
+
* resolution design 3). Add it after `mfe()`.
|
|
30
39
|
*/
|
|
31
40
|
export declare function shellDevHost(options?: ShellDevHostOptions): Plugin;
|
package/dist/vite/plugin.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { extname, join, normalize, resolve, sep } from 'node:path';
|
|
3
|
+
import { closeCell, createCellServer } from '../cell.js';
|
|
3
4
|
import { buildDevConfig, loadDevFile } from '../devfile.js';
|
|
4
5
|
import { CONTENT_TYPES, injectDevConfig, sendFile, validateDevConfig } from '../inject.js';
|
|
6
|
+
import { rememberSession } from '../hosts.js';
|
|
5
7
|
import { createPlatformHost, readPlatform, selectPlatform } from '../platform/host.js';
|
|
6
8
|
import { loadOrCreateKey, mintToken } from '../token.js';
|
|
9
|
+
import { cellTargets, domainPrefix, onTheCell } from './targets.js';
|
|
10
|
+
/** The dev cell's port when none is given: the console's own plus this (5103 → 6103). */
|
|
11
|
+
export const CELL_PORT_OFFSET = 1000;
|
|
7
12
|
const MFE_PLUGIN = 'aistrike:mfe';
|
|
8
13
|
const MISSING_MFE = 'shellDevHost() needs the mfe() plugin from @aistrike-dev/ui-shell-sdk/vite';
|
|
9
14
|
const PASS_THROUGH = ['/api/', '/@', '/__', '/node_modules/'];
|
|
@@ -23,6 +28,56 @@ function mfeId(config) {
|
|
|
23
28
|
}
|
|
24
29
|
return id;
|
|
25
30
|
}
|
|
31
|
+
/** An MFE project runs the built-in MFEs and its own (cell resolution design 3): its file names no other domain. */
|
|
32
|
+
function assertOwnOnly(file, id) {
|
|
33
|
+
const other = file?.domains?.find((domain) => domain.id !== id);
|
|
34
|
+
if (other !== undefined) {
|
|
35
|
+
throw new Error(`shell.dev.json: domains may name only this MFE ("${id}"), not "${other.id}"; ` +
|
|
36
|
+
'run the ui-shell dev server to load other domains from their dev servers');
|
|
37
|
+
}
|
|
38
|
+
if (file?.cell !== undefined) {
|
|
39
|
+
throw new Error('shell.dev.json: cell is read only by the ui-shell dev server; delete it');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** The highest TCP port. */
|
|
43
|
+
const MAX_PORT = 65_535;
|
|
44
|
+
/**
|
|
45
|
+
* Starts the dev cell on `port` (0: any free port) and stops it with the server. A port already taken is logged, not
|
|
46
|
+
* fatal; with `anyIfTaken` (the port was not pinned) the cell takes any free port instead. `listening` receives the port
|
|
47
|
+
* it got.
|
|
48
|
+
*/
|
|
49
|
+
function startCell(server, port, anyIfTaken, targets, logger, listening) {
|
|
50
|
+
const cell = createCellServer({ targets, logger });
|
|
51
|
+
let fellBack = false;
|
|
52
|
+
cell.on('error', (error) => {
|
|
53
|
+
if (anyIfTaken && !fellBack && error.code === 'EADDRINUSE') {
|
|
54
|
+
fellBack = true;
|
|
55
|
+
logger.info(`cell: :${String(port)} is taken; listening on any free port`);
|
|
56
|
+
cell.listen(0);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
logger.warn(`cell: could not listen on :${String(port)} (${error.code ?? error.message}); set shellDevHost({ cellPort })`);
|
|
60
|
+
});
|
|
61
|
+
cell.once('listening', () => {
|
|
62
|
+
listening(cell.address().port);
|
|
63
|
+
});
|
|
64
|
+
cell.listen(port);
|
|
65
|
+
// Upgraded connections (an open page's HMR socket) are destroyed too: otherwise a restart would wait on them forever.
|
|
66
|
+
const stop = async () => closeCell(cell);
|
|
67
|
+
server.httpServer?.once('close', () => {
|
|
68
|
+
stop().catch(() => undefined);
|
|
69
|
+
});
|
|
70
|
+
const close = server.close.bind(server);
|
|
71
|
+
server.close = async () => {
|
|
72
|
+
await stop();
|
|
73
|
+
await close();
|
|
74
|
+
};
|
|
75
|
+
const restart = server.restart.bind(server);
|
|
76
|
+
server.restart = async (forceOptimize) => {
|
|
77
|
+
await stop();
|
|
78
|
+
await restart(forceOptimize);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
26
81
|
const SHELL_ASSET_TAG = /<(script|link)\b([^>]*?\s(?:src|href)=["']\/assets\/)/gi;
|
|
27
82
|
/**
|
|
28
83
|
* Vite's HTML transform prefixes root-relative URLs with its base (`/ui/latest/`); the shell's build lives at the root,
|
|
@@ -67,25 +122,27 @@ function notFound(res) {
|
|
|
67
122
|
res.end('not found');
|
|
68
123
|
}
|
|
69
124
|
/**
|
|
70
|
-
* Serves the shell from the MFE's own Vite dev server: `/` and every extension-less path get the shell
|
|
71
|
-
* dev config, the Vite client and any plugin's HTML, such as the React preamble), `/assets/*` the shell
|
|
72
|
-
* the MFE stays at the Vite base (
|
|
125
|
+
* Serves the shell from the MFE's own Vite dev server, as a console: `/` and every extension-less path get the shell
|
|
126
|
+
* page (with the dev config, the Vite client and any plugin's HTML, such as the React preamble), `/assets/*` the shell
|
|
127
|
+
* build, and the MFE stays at the Vite base (`/<id>/ui/latest/`). Beside it runs the dev cell, an origin of its own,
|
|
128
|
+
* which forwards the MFE and the built-in MFEs, so the shell loads them cross-origin as from a deployed cell (cell
|
|
129
|
+
* resolution design 3). Add it after `mfe()`.
|
|
73
130
|
*/
|
|
74
131
|
export function shellDevHost(options = {}) {
|
|
75
132
|
const shellDir = resolve(options.shellDir ?? defaultShellDir());
|
|
133
|
+
const platformDir = resolve(options.platformDir ?? defaultPlatformDir());
|
|
76
134
|
let config;
|
|
77
135
|
let id = '';
|
|
136
|
+
let file = null;
|
|
78
137
|
/**
|
|
79
138
|
* Platform-MFE spec 3.3: binds once the dev server listens (at once in middleware mode, which has no server of its
|
|
80
139
|
* own), and closes before Vite's own close or restart returns, so a restart's new instance finds the ports free.
|
|
81
140
|
*/
|
|
82
|
-
const startPlatform = (server
|
|
141
|
+
const startPlatform = (server) => {
|
|
83
142
|
if (options.platform === false || !config) {
|
|
84
143
|
return null;
|
|
85
144
|
}
|
|
86
|
-
const
|
|
87
|
-
const overrides = file?.overrides ?? {};
|
|
88
|
-
const entries = selectPlatform(readPlatform(platformDir), { ownId: id, profile, overrides, option: options.platform });
|
|
145
|
+
const entries = selectPlatform(readPlatform(platformDir), { ownId: id, elsewhere: [], option: options.platform });
|
|
89
146
|
if (entries.length === 0) {
|
|
90
147
|
return null;
|
|
91
148
|
}
|
|
@@ -127,30 +184,63 @@ export function shellDevHost(options = {}) {
|
|
|
127
184
|
configResolved(resolved) {
|
|
128
185
|
config = resolved;
|
|
129
186
|
id = mfeId(resolved);
|
|
187
|
+
file = loadDevFile(options.devFile, resolved.root);
|
|
188
|
+
assertOwnOnly(file, id);
|
|
130
189
|
},
|
|
131
190
|
async configureServer(server) {
|
|
132
191
|
if (!config) {
|
|
133
192
|
throw new Error('aistrike:shell-dev-host: configureServer ran before configResolved');
|
|
134
193
|
}
|
|
135
194
|
const base = config.base;
|
|
136
|
-
const
|
|
137
|
-
const file = loadDevFile(options.devFile, config.root);
|
|
195
|
+
const { logger } = config;
|
|
138
196
|
await mountLabLock(this, server, file, config.root);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
197
|
+
const key = options.noToken === true ? null : loadOrCreateKey();
|
|
198
|
+
const claims = { ...(file?.claims ?? {}), ...(options.claims ?? {}) };
|
|
199
|
+
const sessions = new Map();
|
|
200
|
+
let cellPort = options.cellPort ?? null;
|
|
201
|
+
/** The cell's host for a browser that reached the console as `host`: the same address, the cell's port. */
|
|
202
|
+
const cellHostFor = (host) => {
|
|
203
|
+
const hostname = new URL(`http://${host}`).hostname;
|
|
204
|
+
return cellPort === null ? null : `${hostname}:${String(cellPort)}`;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* The token for a browser that reached the console as `host`: its `cell` is the dev cell on that same address, so a
|
|
208
|
+
* tablet on the network loads every domain from this machine. One per host (Vite's `allowedHosts` admits it), at
|
|
209
|
+
* most `MAX_SESSIONS`.
|
|
210
|
+
*/
|
|
211
|
+
const sessionFor = (host) => {
|
|
212
|
+
const cell = cellHostFor(host);
|
|
213
|
+
if (key === null || cell === null) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
let session = sessions.get(cell);
|
|
217
|
+
if (session === undefined) {
|
|
218
|
+
session = mintToken({ key, claims, cell, ttlMs: options.ttlMs }).session;
|
|
219
|
+
rememberSession(sessions, cell, session);
|
|
220
|
+
}
|
|
221
|
+
return session;
|
|
151
222
|
};
|
|
152
|
-
|
|
153
|
-
|
|
223
|
+
const configFor = (host) => buildDevConfig(file, { landing: file?.landing ?? id }, sessionFor(host));
|
|
224
|
+
validateDevConfig(configFor('localhost'));
|
|
225
|
+
const platform = startPlatform(server);
|
|
226
|
+
const ownPaths = domainPrefix(id);
|
|
227
|
+
// The cell starts once the console listens: its port, and where this MFE is, follow from the console's.
|
|
228
|
+
server.httpServer?.once('listening', () => {
|
|
229
|
+
const consolePort = server.httpServer?.address()?.port;
|
|
230
|
+
if (consolePort === undefined) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// The console's port plus the offset, unless that is past the last port (a high ephemeral console port) or
|
|
234
|
+
// taken: then any free port. A pinned `cellPort` is kept. The token names the port the cell got.
|
|
235
|
+
const pinned = cellPort !== null;
|
|
236
|
+
const wanted = cellPort ?? consolePort + CELL_PORT_OFFSET;
|
|
237
|
+
const own = { id, origin: `http://127.0.0.1:${String(consolePort)}` };
|
|
238
|
+
// Every packaged copy's port, served by its bundled copy or by a real dev server that holds it.
|
|
239
|
+
const packaged = readPlatform(platformDir);
|
|
240
|
+
startCell(server, wanted <= MAX_PORT ? wanted : 0, !pinned, () => cellTargets({ own, platform: packaged }), logger, (got) => {
|
|
241
|
+
cellPort = got;
|
|
242
|
+
});
|
|
243
|
+
});
|
|
154
244
|
const serveShell = async (req, res, pathname) => {
|
|
155
245
|
if (pathname.startsWith('/assets/')) {
|
|
156
246
|
const target = resolve(shellDir, `.${normalize(pathname)}`);
|
|
@@ -167,10 +257,13 @@ export function shellDevHost(options = {}) {
|
|
|
167
257
|
}
|
|
168
258
|
// A server that held a platform MFE's port may have stopped since; a reload is when to take it over.
|
|
169
259
|
await platform?.retry();
|
|
170
|
-
const
|
|
260
|
+
const host = req.headers.host ?? 'localhost';
|
|
261
|
+
const devConfig = configFor(host);
|
|
171
262
|
const source = join(shellDir, 'index.html');
|
|
172
263
|
const html = markShellAssets(injectDevConfig(readFileSync(source, 'utf8'), devConfig, source));
|
|
173
|
-
|
|
264
|
+
const page = await server.transformIndexHtml(req.url ?? '/', html, req.originalUrl);
|
|
265
|
+
const cellHost = cellHostFor(host);
|
|
266
|
+
sendHtml(res, cellHost === null ? page : onTheCell(page, base, `http://${cellHost}`));
|
|
174
267
|
};
|
|
175
268
|
server.middlewares.use((req, res, next) => {
|
|
176
269
|
let pathname;
|
|
@@ -182,7 +275,10 @@ export function shellDevHost(options = {}) {
|
|
|
182
275
|
res.end('bad request');
|
|
183
276
|
return;
|
|
184
277
|
}
|
|
185
|
-
|
|
278
|
+
// The MFE (Vite's base, which the cell forwards here), its API (which `mfe()` rewrote to `/api/` already), and
|
|
279
|
+
// Vite's own paths.
|
|
280
|
+
const passes = pathname.startsWith(base) || ownPaths.test(pathname) || PASS_THROUGH.some((prefix) => pathname.startsWith(prefix));
|
|
281
|
+
if (passes) {
|
|
186
282
|
next();
|
|
187
283
|
return;
|
|
188
284
|
}
|
|
@@ -193,7 +289,10 @@ export function shellDevHost(options = {}) {
|
|
|
193
289
|
printUrls();
|
|
194
290
|
const local = server.resolvedUrls?.local[0];
|
|
195
291
|
if (local !== undefined) {
|
|
196
|
-
config?.logger.info(` shell ${new URL(local).origin}/ (
|
|
292
|
+
config?.logger.info(` shell ${new URL(local).origin}/ (the console)`);
|
|
293
|
+
}
|
|
294
|
+
if (cellPort !== null) {
|
|
295
|
+
config?.logger.info(` cell http://localhost:${String(cellPort)}/ (${id} and the built-in MFEs, as the shell loads them)`);
|
|
197
296
|
}
|
|
198
297
|
for (const entry of platform?.host.held() ?? []) {
|
|
199
298
|
config?.logger.info(` ${entry.id} http://localhost:${String(entry.devPort)}/ (bundled)`);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CellDomain } from '../devfile.js';
|
|
2
|
+
import type { PlatformEntry } from '../platform/host.js';
|
|
3
|
+
export interface TargetsInput {
|
|
4
|
+
/** The MFE an MFE project's dev server develops, at that server. */
|
|
5
|
+
own?: {
|
|
6
|
+
id: string;
|
|
7
|
+
origin: string;
|
|
8
|
+
};
|
|
9
|
+
/** The ui-shell dev server's `cell.domains`: each at its own server. */
|
|
10
|
+
listed?: readonly CellDomain[];
|
|
11
|
+
/** The bundled platform MFEs, on the ports their copies listen on. */
|
|
12
|
+
platform: readonly PlatformEntry[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Where the dev cell forwards each domain, which only the dev host knows (cell resolution design 3): a compiled domain
|
|
16
|
+
* on its port in `DEV_PORTS`, a bundled copy on the port it was packaged with, then a listed domain or the MFE being
|
|
17
|
+
* developed at its server, in that order of precedence.
|
|
18
|
+
*/
|
|
19
|
+
export declare function cellTargets(input: TargetsInput): Map<string, string>;
|
|
20
|
+
/** A domain's UI and API paths, which its server answers; every other path under `/<id>/` is a shell route. */
|
|
21
|
+
export declare function domainPrefix(id: string): RegExp;
|
|
22
|
+
/**
|
|
23
|
+
* The console page loads the developed MFE's dev assets (Vite's client, the React refresh runtime) from the cell, as
|
|
24
|
+
* every module of it is: one URL per module, so the refresh runtime the page installs is the one the modules register
|
|
25
|
+
* with, and Vite's client opens its HMR socket on the cell.
|
|
26
|
+
*/
|
|
27
|
+
export declare function onTheCell(html: string, base: string, cellOrigin: string): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DEV_PORTS } from '../ports.js';
|
|
2
|
+
const LOOPBACK = 'http://127.0.0.1';
|
|
3
|
+
/**
|
|
4
|
+
* Where the dev cell forwards each domain, which only the dev host knows (cell resolution design 3): a compiled domain
|
|
5
|
+
* on its port in `DEV_PORTS`, a bundled copy on the port it was packaged with, then a listed domain or the MFE being
|
|
6
|
+
* developed at its server, in that order of precedence.
|
|
7
|
+
*/
|
|
8
|
+
export function cellTargets(input) {
|
|
9
|
+
const targets = new Map();
|
|
10
|
+
for (const [id, port] of Object.entries(DEV_PORTS)) {
|
|
11
|
+
targets.set(id, `${LOOPBACK}:${String(port)}`);
|
|
12
|
+
}
|
|
13
|
+
for (const entry of input.platform) {
|
|
14
|
+
targets.set(entry.id, `${LOOPBACK}:${String(entry.devPort)}`);
|
|
15
|
+
}
|
|
16
|
+
for (const domain of input.listed ?? []) {
|
|
17
|
+
targets.set(domain.id, domain.at);
|
|
18
|
+
}
|
|
19
|
+
if (input.own !== undefined) {
|
|
20
|
+
targets.set(input.own.id, input.own.origin);
|
|
21
|
+
}
|
|
22
|
+
return targets;
|
|
23
|
+
}
|
|
24
|
+
/** A domain's UI and API paths, which its server answers; every other path under `/<id>/` is a shell route. */
|
|
25
|
+
export function domainPrefix(id) {
|
|
26
|
+
return new RegExp(`^/${id}/(?:ui|api)(?:[/?#]|$)`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The console page loads the developed MFE's dev assets (Vite's client, the React refresh runtime) from the cell, as
|
|
30
|
+
* every module of it is: one URL per module, so the refresh runtime the page installs is the one the modules register
|
|
31
|
+
* with, and Vite's client opens its HMR socket on the cell.
|
|
32
|
+
*/
|
|
33
|
+
export function onTheCell(html, base, cellOrigin) {
|
|
34
|
+
return html.replaceAll(`"${base}`, `"${cellOrigin}${base}`).replaceAll(`'${base}`, `'${cellOrigin}${base}`);
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aistrike-dev/ui-shell-dev",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "A Vite plugin that serves the AiStrike UI shell from your micro front-end's dev server, plus a dev-token CLI.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@aistrike-dev/ui": "6.
|
|
61
|
+
"@aistrike-dev/ui": "6.2.0",
|
|
62
62
|
"@aistrike-dev/ui-shell-sdk": "workspace:*",
|
|
63
63
|
"@playwright/test": "^1.63.0",
|
|
64
64
|
"@types/node": "^24.0.0",
|