@aistrike-dev/ui-shell-dev 2.0.3 → 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.
Files changed (45) hide show
  1. package/README.md +41 -17
  2. package/dist/args.d.ts +9 -1
  3. package/dist/args.js +51 -5
  4. package/dist/cell.d.ts +29 -0
  5. package/dist/cell.js +194 -0
  6. package/dist/cli.js +25 -1
  7. package/dist/devfile.d.ts +25 -9
  8. package/dist/devfile.js +75 -30
  9. package/dist/hosts.d.ts +17 -0
  10. package/dist/hosts.js +51 -0
  11. package/dist/inject.js +0 -3
  12. package/dist/platform/host.d.ts +5 -7
  13. package/dist/platform/host.js +4 -4
  14. package/dist/platform/list.d.ts +2 -2
  15. package/dist/platform/list.js +6 -5
  16. package/dist/platform/server.d.ts +3 -2
  17. package/dist/platform/server.js +21 -10
  18. package/dist/ports.d.ts +12 -0
  19. package/dist/ports.js +14 -0
  20. package/dist/preamble.d.ts +15 -0
  21. package/dist/preamble.js +44 -0
  22. package/dist/serve.d.ts +28 -0
  23. package/dist/serve.js +161 -0
  24. package/dist/token.d.ts +6 -1
  25. package/dist/token.js +2 -2
  26. package/dist/vite/plugin.d.ts +12 -3
  27. package/dist/vite/plugin.js +126 -27
  28. package/dist/vite/targets.d.ts +27 -0
  29. package/dist/vite/targets.js +35 -0
  30. package/package.json +2 -2
  31. package/platform/assistant/mock.js +1809 -222
  32. package/platform/assistant/ui/chunks/ui-CnxV0v5I.js +280 -0
  33. package/platform/assistant/ui/entry.js +2 -2
  34. package/platform/home/ui/chunks/ui-BJZEfOZI.js +174 -0
  35. package/platform/home/ui/entry.js +1 -1
  36. package/platform/settings/ui/chunks/ui-BRCsapmB.js +271 -0
  37. package/platform/settings/ui/entry.js +2 -2
  38. package/shell/assets/LabHost-D3o-OJsx.js +2 -0
  39. package/shell/assets/index-DW0Z5YeB.js +185 -0
  40. package/shell/index.html +1 -1
  41. package/platform/assistant/ui/chunks/ui-C9pWqM_D.js +0 -182
  42. package/platform/home/ui/chunks/ui-SNPmLvIE.js +0 -174
  43. package/platform/settings/ui/chunks/ui-C21hUUVD.js +0 -271
  44. package/shell/assets/LabHost-BiiVtiYJ.js +0 -2
  45. package/shell/assets/index-CqB5Mswt.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 }));
@@ -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 page (with the
28
- * dev config, the Vite client and any plugin's HTML, such as the React preamble), `/assets/*` the shell build, and
29
- * the MFE stays at the Vite base (`/ui/latest/`). One origin, HMR included. Add it after `mfe()`.
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;
@@ -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 page (with the
71
- * dev config, the Vite client and any plugin's HTML, such as the React preamble), `/assets/*` the shell build, and
72
- * the MFE stays at the Vite base (`/ui/latest/`). One origin, HMR included. Add it after `mfe()`.
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, profile, file) => {
141
+ const startPlatform = (server) => {
83
142
  if (options.platform === false || !config) {
84
143
  return null;
85
144
  }
86
- const platformDir = resolve(options.platformDir ?? defaultPlatformDir());
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 scheme = server.config.server.https ? 'https' : 'http';
137
- const file = loadDevFile(options.devFile, config.root);
195
+ const { logger } = config;
138
196
  await mountLabLock(this, server, file, config.root);
139
- let session;
140
- if (options.noToken !== true) {
141
- const claims = { ...(file?.claims ?? {}), ...(options.claims ?? {}) };
142
- session = mintToken({ key: loadOrCreateKey(), claims, ttlMs: options.ttlMs }).session;
143
- }
144
- // The host the browser reached this server by. The shell builds the other MFEs' local origins on it, so a tablet
145
- // on the network, here by this machine's address, loads every MFE from that address and not from its own localhost.
146
- const configFor = (origin) => {
147
- return {
148
- ...buildDevConfig(file, { overrides: { [id]: origin }, landing: file?.landing ?? id }, session),
149
- devHost: new URL(origin).hostname,
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
- validateDevConfig(configFor('http://localhost'));
153
- const platform = startPlatform(server, configFor('http://localhost').profile ?? 'local', file);
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 devConfig = configFor(`${scheme}://${req.headers.host ?? 'localhost'}`);
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
- sendHtml(res, await server.transformIndexHtml(req.url ?? '/', html, req.originalUrl));
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
- if (pathname.startsWith(base) || PASS_THROUGH.some((prefix) => pathname.startsWith(prefix))) {
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}/ (serving ${id} from this 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.0.3",
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.0.0-alpha.13",
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",