@aistrike-dev/ui-shell-dev 1.0.0 → 1.0.2

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 CHANGED
@@ -19,6 +19,20 @@ export default defineConfig({
19
19
  `vite` now serves the shell at `/` with a minted dev session, and your MFE at `/ui/latest/`, on one origin, with HMR.
20
20
  Other domains resolve by the profile's convention unless `shell.dev.json` overrides them.
21
21
 
22
+ ## Built-in MFEs
23
+
24
+ The package carries prebuilt copies of the MFEs that ship with the shell (assistant, settings and home) with their mock
25
+ APIs, and the plugin serves each on the port the shell looks for it on: assistant 5104, settings 5101, home 5102. You
26
+ get the whole frame (Home, Settings, the assistant pane on ⌘I / Ctrl+I) with nothing else running.
27
+
28
+ - A server already listening on one of those ports wins: the plugin leaves that port alone, so a real dev server for
29
+ that MFE (with HMR) is used instead. A port freed later is taken over on the next page load.
30
+ - A real dev server for one of them started *after* this one fails, because the bundled copy holds its port. Start the
31
+ one you are editing first, or restart this server after starting it.
32
+ - `shellDevHost({ platform: false })` serves none; `shellDevHost({ platform: ['assistant'] })` only those listed. A
33
+ domain that `shell.dev.json` overrides, or any profile other than `local`, is never bundled.
34
+ - The copies are as old as the package (in this repository, the last `pnpm nx build dev-host`).
35
+
22
36
  Keep Vite's `server.allowedHosts` at its default: the plugin derives your MFE's origin from the request's `Host`
23
37
  header, which is only safe while Vite rejects unknown hosts. With `server.https` Vite skips host validation, so the
24
38
  self-override trusts the `Host` header as-is.
@@ -49,9 +63,9 @@ aistrike-shell token [--claims <path>] [--ttl 30d|12h|45m|90s] [--out <path>] [-
49
63
 
50
64
  `token` mints a dev token and prints it (or `{ token, session }` with `--json`), optionally writing it to `--out`.
51
65
 
52
- The package's own e2e (`pnpm nx e2e dev-host`) serves the settings MFE on port 5101 and fails loudly if a settings dev server already occupies it.
66
+ The package's own e2e (`pnpm nx e2e dev-host`) serves the settings MFE on port 5101, with the bundled home and assistant on 5102 and 5104; it fails loudly if a dev server already occupies 5101, and stop any on 5102 or 5104 first. It also checks that `npm pack` would publish the bundled copies.
53
67
 
54
- Opening `/` in dev lands on your own MFE (the plugin injects `landing` with your domain id; `shell.dev.json` may set `landing` to another domain id; an id that is not in the shell's domain list falls back to `/home`). The other domains in the profile (for example `home` on port 5102) show as failed in the rail unless their dev servers are running too; that is expected.
68
+ Opening `/` in dev lands on your own MFE (the plugin injects `landing` with your domain id; `shell.dev.json` may set `landing` to another domain id; an id that is not in the shell's domain list falls back to `/home`). A domain in the profile that is neither bundled nor running shows as failed in the rail.
55
69
 
56
70
  ## Checking Fast Refresh (spike 1)
57
71
 
@@ -0,0 +1,19 @@
1
+ import type { PlatformEntry } from './host.js';
2
+ /** A platform MFE as the package is built from it: `mock` says whether its build must carry a mock API. */
3
+ export interface PackagedEntry extends PlatformEntry {
4
+ mock: boolean;
5
+ }
6
+ export interface CopyOptions {
7
+ /** The workspace's `apps/`. */
8
+ appsDir: string;
9
+ /** The package's `platform/`. */
10
+ targetDir: string;
11
+ entries: readonly PackagedEntry[];
12
+ }
13
+ /** Every module a built file imports that is not `node:*`; the dev host loads the mock in plain Node, without them. */
14
+ export declare function foreignImports(code: string): string[];
15
+ /**
16
+ * Platform-MFE spec 3.2: each app's `dist/` to `<id>/ui/` and its `dist-mock/mock.js` to `<id>/mock.js`, then
17
+ * `platform.json`. Throws, naming what to fix, for a missing build, a missing mock or a mock that imports a package.
18
+ */
19
+ export declare function copyPlatform(options: CopyOptions): void;
@@ -0,0 +1,38 @@
1
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const IMPORT = /\b(?:from|import)\s*\(?\s*["']([^"']+)["']/g;
4
+ /** Every module a built file imports that is not `node:*`; the dev host loads the mock in plain Node, without them. */
5
+ export function foreignImports(code) {
6
+ return [...code.matchAll(IMPORT)].map((match) => match[1]).filter((specifier) => !specifier.startsWith('node:'));
7
+ }
8
+ /**
9
+ * Platform-MFE spec 3.2: each app's `dist/` to `<id>/ui/` and its `dist-mock/mock.js` to `<id>/mock.js`, then
10
+ * `platform.json`. Throws, naming what to fix, for a missing build, a missing mock or a mock that imports a package.
11
+ */
12
+ export function copyPlatform(options) {
13
+ for (const { id, mock } of options.entries) {
14
+ const build = join(options.appsDir, id, 'dist');
15
+ if (!existsSync(join(build, 'manifest.json'))) {
16
+ throw new Error(`platform build missing at ${build}; run: pnpm nx build ${id}`);
17
+ }
18
+ const mockFile = join(options.appsDir, id, 'dist-mock', 'mock.js');
19
+ if (mock) {
20
+ if (!existsSync(mockFile)) {
21
+ throw new Error(`${id}: mock build missing at ${mockFile}; run: pnpm nx build ${id}`);
22
+ }
23
+ const foreign = foreignImports(readFileSync(mockFile, 'utf8'));
24
+ if (foreign.length > 0) {
25
+ throw new Error(`${id}: ${mockFile} imports ${foreign.join(', ')}; a mock may import only node:*`);
26
+ }
27
+ }
28
+ cpSync(build, join(options.targetDir, id, 'ui'), { recursive: true });
29
+ if (mock) {
30
+ copyFileSync(mockFile, join(options.targetDir, id, 'mock.js'));
31
+ }
32
+ }
33
+ mkdirSync(options.targetDir, { recursive: true });
34
+ const list = options.entries.map(({ id, devPort }) => {
35
+ return { id, devPort };
36
+ });
37
+ writeFileSync(join(options.targetDir, 'platform.json'), `${JSON.stringify(list, null, 2)}\n`);
38
+ }
@@ -0,0 +1,6 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ /**
3
+ * Mirrors the SDK's `sameHostCors` (an MFE dev server's CORS): a page on the same host, any port, may read anything,
4
+ * and its preflights are answered. Returns true when it answered the request itself (a preflight's 204).
5
+ */
6
+ export declare function sameHostCors(req: IncomingMessage, res: ServerResponse): boolean;
@@ -0,0 +1,36 @@
1
+ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
2
+ const ANY_METHOD = 'GET, HEAD, PUT, PATCH, POST, DELETE';
3
+ /** Mirrors `isSameHost` in the SDK's `vite/plugin.ts` (the SDK is a devDependency here; `cors.test.ts` pins both). */
4
+ function isSameHost(origin, host) {
5
+ if (host === undefined) {
6
+ return false;
7
+ }
8
+ try {
9
+ const a = new URL(origin).hostname;
10
+ const b = new URL(`http://${host}`).hostname;
11
+ return a === b || (LOOPBACK.has(a) && LOOPBACK.has(b));
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ /**
18
+ * Mirrors the SDK's `sameHostCors` (an MFE dev server's CORS): a page on the same host, any port, may read anything,
19
+ * and its preflights are answered. Returns true when it answered the request itself (a preflight's 204).
20
+ */
21
+ export function sameHostCors(req, res) {
22
+ const origin = req.headers.origin;
23
+ res.setHeader('Vary', 'Origin');
24
+ if (origin === undefined || !isSameHost(origin, req.headers.host)) {
25
+ return false;
26
+ }
27
+ res.setHeader('Access-Control-Allow-Origin', origin);
28
+ if (req.method !== 'OPTIONS' || req.headers['access-control-request-method'] === undefined) {
29
+ return false;
30
+ }
31
+ res.setHeader('Access-Control-Allow-Methods', ANY_METHOD);
32
+ res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers'] ?? '');
33
+ res.statusCode = 204;
34
+ res.end();
35
+ return true;
36
+ }
@@ -0,0 +1,37 @@
1
+ /** A platform MFE the package carries, and the port the shell's `local` profile looks for it on. */
2
+ export interface PlatformEntry {
3
+ id: string;
4
+ devPort: number;
5
+ }
6
+ export interface PlatformLogger {
7
+ info: (msg: string) => void;
8
+ warn: (msg: string) => void;
9
+ }
10
+ export interface SelectOptions {
11
+ ownId: string;
12
+ profile: string;
13
+ overrides: {
14
+ [id: string]: string;
15
+ };
16
+ option: boolean | string[] | undefined;
17
+ }
18
+ export interface PlatformHost {
19
+ /** Tries every entry it does not hold; resolves when every attempt has settled. Never rejects. */
20
+ bind: (options?: {
21
+ quiet?: boolean;
22
+ }) => Promise<void>;
23
+ held: () => PlatformEntry[];
24
+ close: () => Promise<void>;
25
+ }
26
+ /** `<platformDir>/platform.json`, keeping the entries whose build is present. A missing or broken list is none. */
27
+ export declare function readPlatform(platformDir: string): PlatformEntry[];
28
+ /**
29
+ * Platform-MFE spec 3.3 step 1: never the MFE being developed (it is this server), never a domain the dev file points
30
+ * elsewhere, nothing outside the `local` profile (the shell would not look at local ports), and what the option allows.
31
+ */
32
+ export declare function selectPlatform(entries: PlatformEntry[], options: SelectOptions): PlatformEntry[];
33
+ /**
34
+ * Serves each platform MFE from `<platformDir>/<id>/` on its own `devPort`. A port something else holds is left to it:
35
+ * a dev server already running for that domain wins.
36
+ */
37
+ export declare function createPlatformHost(platformDir: string, entries: PlatformEntry[], logger: PlatformLogger): PlatformHost;
@@ -0,0 +1,144 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { createPlatformServer } from './server.js';
5
+ function isEntry(value) {
6
+ if (typeof value !== 'object' || value === null) {
7
+ return false;
8
+ }
9
+ const { id, devPort } = value;
10
+ return typeof id === 'string' && typeof devPort === 'number' && Number.isInteger(devPort) && devPort > 0 && devPort <= 65535;
11
+ }
12
+ /** `<platformDir>/platform.json`, keeping the entries whose build is present. A missing or broken list is none. */
13
+ export function readPlatform(platformDir) {
14
+ let parsed;
15
+ try {
16
+ parsed = JSON.parse(readFileSync(join(platformDir, 'platform.json'), 'utf8'));
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ if (!Array.isArray(parsed) || !parsed.every(isEntry)) {
22
+ return [];
23
+ }
24
+ return parsed.filter((entry) => existsSync(join(platformDir, entry.id, 'ui', 'manifest.json')));
25
+ }
26
+ /**
27
+ * Platform-MFE spec 3.3 step 1: never the MFE being developed (it is this server), never a domain the dev file points
28
+ * elsewhere, nothing outside the `local` profile (the shell would not look at local ports), and what the option allows.
29
+ */
30
+ export function selectPlatform(entries, options) {
31
+ if (options.option === false || options.profile !== 'local') {
32
+ return [];
33
+ }
34
+ const allowed = Array.isArray(options.option) ? options.option : undefined;
35
+ return entries.filter((entry) => entry.id !== options.ownId
36
+ && !(entry.id in options.overrides)
37
+ && (allowed === undefined || allowed.includes(entry.id)));
38
+ }
39
+ const errorCode = (error) => error?.code;
40
+ const reason = (error) => (error instanceof Error ? error.message : String(error));
41
+ /** On every interface, as `mfe()`'s dev servers listen (`host: true`); IPv4 alone where the machine has no IPv6. */
42
+ async function listen(server, port) {
43
+ const attempt = async (options) => new Promise((done, fail) => {
44
+ const onError = (error) => {
45
+ fail(error);
46
+ };
47
+ server.once('error', onError);
48
+ server.listen(options, () => {
49
+ server.off('error', onError);
50
+ done();
51
+ });
52
+ });
53
+ try {
54
+ await attempt({ port, host: '::', ipv6Only: false });
55
+ }
56
+ catch (error) {
57
+ if (errorCode(error) !== 'EAFNOSUPPORT') {
58
+ throw error;
59
+ }
60
+ await attempt({ port, host: '0.0.0.0' });
61
+ }
62
+ }
63
+ /**
64
+ * Serves each platform MFE from `<platformDir>/<id>/` on its own `devPort`. A port something else holds is left to it:
65
+ * a dev server already running for that domain wins.
66
+ */
67
+ export function createPlatformHost(platformDir, entries, logger) {
68
+ const servers = new Map();
69
+ const mocks = new Map();
70
+ const binding = new Map();
71
+ /** Once per id for the life of the host, so a broken mock warns once and the mocks' state survives a rebind. */
72
+ const loadMock = async (id) => {
73
+ const file = join(platformDir, id, 'mock.js');
74
+ if (!existsSync(file)) {
75
+ return [];
76
+ }
77
+ try {
78
+ const module = await import(pathToFileURL(file).href);
79
+ if (typeof module.createMockHandlers !== 'function') {
80
+ logger.warn(`${id}: ${file} exports no createMockHandlers; serving it without its mock API`);
81
+ return [];
82
+ }
83
+ return module.createMockHandlers();
84
+ }
85
+ catch (error) {
86
+ logger.warn(`${id}: its mock API failed to load (${reason(error)}); serving it without`);
87
+ return [];
88
+ }
89
+ };
90
+ const bindOne = async (entry, quiet) => {
91
+ let handlers = mocks.get(entry.id);
92
+ if (handlers === undefined) {
93
+ handlers = loadMock(entry.id);
94
+ mocks.set(entry.id, handlers);
95
+ }
96
+ const server = createPlatformServer({ id: entry.id, uiDir: join(platformDir, entry.id, 'ui'), handlers: await handlers });
97
+ try {
98
+ await listen(server, entry.devPort);
99
+ }
100
+ catch (error) {
101
+ if (quiet) {
102
+ return;
103
+ }
104
+ if (errorCode(error) === 'EADDRINUSE') {
105
+ logger.info(`${entry.id}: using the server already on :${String(entry.devPort)}`);
106
+ }
107
+ else {
108
+ logger.warn(`${entry.id}: could not listen on :${String(entry.devPort)} (${reason(error)})`);
109
+ }
110
+ return;
111
+ }
112
+ servers.set(entry.id, server);
113
+ logger.info(`${entry.id}: bundled copy on :${String(entry.devPort)}`);
114
+ };
115
+ return {
116
+ async bind(options = {}) {
117
+ await Promise.all(entries.filter((entry) => !servers.has(entry.id)).map(async (entry) => {
118
+ // A page load during a bind waits for it rather than racing a second server onto the same port.
119
+ let pending = binding.get(entry.id);
120
+ if (pending === undefined) {
121
+ pending = bindOne(entry, options.quiet === true).finally(() => {
122
+ binding.delete(entry.id);
123
+ });
124
+ binding.set(entry.id, pending);
125
+ }
126
+ await pending;
127
+ }));
128
+ },
129
+ held() {
130
+ return entries.filter((entry) => servers.has(entry.id));
131
+ },
132
+ async close() {
133
+ await Promise.all([...binding.values()]);
134
+ const open = [...servers.values()];
135
+ servers.clear();
136
+ await Promise.all(open.map(async (server) => new Promise((done) => {
137
+ server.close(() => {
138
+ done();
139
+ });
140
+ server.closeAllConnections();
141
+ })));
142
+ },
143
+ };
144
+ }
@@ -0,0 +1,6 @@
1
+ import type { PackagedEntry } from './copy.js';
2
+ /**
3
+ * The platform MFEs the package carries (platform-MFE spec 3.1), each on the `devPort` of the shell's domain list
4
+ * (`list.test.ts` pins them). The copy script writes this to `platform/platform.json`. Never the lab: it is dev-only.
5
+ */
6
+ export declare const PLATFORM: readonly PackagedEntry[];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The platform MFEs the package carries (platform-MFE spec 3.1), each on the `devPort` of the shell's domain list
3
+ * (`list.test.ts` pins them). The copy script writes this to `platform/platform.json`. Never the lab: it is dev-only.
4
+ */
5
+ export const PLATFORM = [
6
+ { id: 'assistant', devPort: 5104, mock: true },
7
+ { id: 'settings', devPort: 5101, mock: true },
8
+ { id: 'home', devPort: 5102, mock: false },
9
+ ];
@@ -0,0 +1,14 @@
1
+ import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
2
+ /** A mock API handler as each platform MFE builds it (`createMockHandlers()`): true when it answered the request. */
3
+ export type MockHandler = (req: IncomingMessage, res: ServerResponse) => boolean;
4
+ export interface PlatformServerOptions {
5
+ id: string;
6
+ /** The MFE's production build (`platform/<id>/ui`). */
7
+ uiDir: string;
8
+ handlers: MockHandler[];
9
+ }
10
+ /**
11
+ * One bundled platform MFE, served as its own dev server would be on its own port: same-host CORS, the build at
12
+ * `/ui/latest/` (no redirect; the resolver accepts a manifest at `latest`, as from a dev server) and the mocks at `/api/`.
13
+ */
14
+ export declare function createPlatformServer(options: PlatformServerOptions): Server;
@@ -0,0 +1,44 @@
1
+ import { createServer } from 'node:http';
2
+ import { normalize, resolve, sep } from 'node:path';
3
+ import { sendFile } from '../inject.js';
4
+ import { sameHostCors } from './cors.js';
5
+ const UI = '/ui/latest/';
6
+ function answer(res, status, text) {
7
+ res.statusCode = status;
8
+ res.setHeader('Content-Type', 'text/plain');
9
+ res.end(text);
10
+ }
11
+ /**
12
+ * One bundled platform MFE, served as its own dev server would be on its own port: same-host CORS, the build at
13
+ * `/ui/latest/` (no redirect; the resolver accepts a manifest at `latest`, as from a dev server) and the mocks at `/api/`.
14
+ */
15
+ export function createPlatformServer(options) {
16
+ const uiDir = resolve(options.uiDir);
17
+ return createServer((req, res) => {
18
+ if (sameHostCors(req, res)) {
19
+ return;
20
+ }
21
+ let pathname;
22
+ try {
23
+ pathname = decodeURIComponent(new URL(req.url ?? '/', 'http://localhost').pathname);
24
+ }
25
+ catch {
26
+ answer(res, 400, 'bad request');
27
+ return;
28
+ }
29
+ if (pathname.startsWith('/api/')) {
30
+ if (!options.handlers.some((handle) => handle(req, res))) {
31
+ answer(res, 404, 'not found');
32
+ }
33
+ return;
34
+ }
35
+ if (pathname.startsWith(UI) && (req.method === 'GET' || req.method === 'HEAD')) {
36
+ const target = resolve(uiDir, `.${normalize(`/${pathname.slice(UI.length)}`)}`);
37
+ if (target.startsWith(uiDir + sep)) {
38
+ sendFile(target, res);
39
+ return;
40
+ }
41
+ }
42
+ answer(res, 404, 'not found');
43
+ });
44
+ }
@@ -10,6 +10,13 @@ export interface ShellDevHostOptions {
10
10
  ttlMs?: number;
11
11
  /** Mint no token (shell starts signed out). */
12
12
  noToken?: boolean;
13
+ /**
14
+ * Serve the bundled platform MFEs (assistant, settings, home) on their own ports, each where nothing else holds its
15
+ * port. Default true; false serves none; a list serves only those ids.
16
+ */
17
+ platform?: boolean | string[];
18
+ /** Path to the platform MFEs' builds. Default: the `platform/` directory shipped in this package. */
19
+ platformDir?: string;
13
20
  }
14
21
  /**
15
22
  * Vite's HTML transform prefixes root-relative URLs with its base (`/ui/latest/`); the shell's build lives at the root,
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { extname, join, normalize, resolve, sep } from 'node:path';
3
3
  import { buildDevConfig, loadDevFile } from '../devfile.js';
4
4
  import { CONTENT_TYPES, injectDevConfig, sendFile, validateDevConfig } from '../inject.js';
5
+ import { createPlatformHost, readPlatform, selectPlatform } from '../platform/host.js';
5
6
  import { loadOrCreateKey, mintToken } from '../token.js';
6
7
  const MFE_PLUGIN = 'aistrike:mfe';
7
8
  const MISSING_MFE = 'shellDevHost() needs the mfe() plugin from @aistrike-dev/ui-shell-sdk/vite';
@@ -10,6 +11,9 @@ const PASS_THROUGH = ['/api/', '/@', '/__', '/node_modules/'];
10
11
  function defaultShellDir() {
11
12
  return resolve(import.meta.dirname, '../../shell');
12
13
  }
14
+ function defaultPlatformDir() {
15
+ return resolve(import.meta.dirname, '../../platform');
16
+ }
13
17
  function mfeId(config) {
14
18
  const plugin = config.plugins.find((p) => p.name === MFE_PLUGIN);
15
19
  const api = plugin?.api;
@@ -47,6 +51,52 @@ export function shellDevHost(options = {}) {
47
51
  const shellDir = resolve(options.shellDir ?? defaultShellDir());
48
52
  let config;
49
53
  let id = '';
54
+ /**
55
+ * Platform-MFE spec 3.3: binds once the dev server listens (at once in middleware mode, which has no server of its
56
+ * own), and closes before Vite's own close or restart returns, so a restart's new instance finds the ports free.
57
+ */
58
+ const startPlatform = (server, profile, file) => {
59
+ if (options.platform === false || !config) {
60
+ return null;
61
+ }
62
+ const platformDir = resolve(options.platformDir ?? defaultPlatformDir());
63
+ const overrides = file?.overrides ?? {};
64
+ const entries = selectPlatform(readPlatform(platformDir), { ownId: id, profile, overrides, option: options.platform });
65
+ if (entries.length === 0) {
66
+ return null;
67
+ }
68
+ const host = createPlatformHost(platformDir, entries, config.logger);
69
+ const bind = () => {
70
+ host.bind().catch(() => undefined); // never rejects; each failure is logged where it happens
71
+ };
72
+ if (server.httpServer === null) {
73
+ bind();
74
+ }
75
+ else {
76
+ server.httpServer.once('listening', bind);
77
+ server.httpServer.once('close', () => {
78
+ host.close().catch(() => undefined);
79
+ });
80
+ }
81
+ const close = server.close.bind(server);
82
+ server.close = async () => {
83
+ await host.close();
84
+ await close();
85
+ };
86
+ const restart = server.restart.bind(server);
87
+ server.restart = async (forceOptimize) => {
88
+ await host.close();
89
+ await restart(forceOptimize);
90
+ };
91
+ return {
92
+ host,
93
+ retry: async () => {
94
+ if (host.held().length < entries.length) {
95
+ await host.bind({ quiet: true });
96
+ }
97
+ },
98
+ };
99
+ };
50
100
  return {
51
101
  name: 'aistrike:shell-dev-host',
52
102
  apply: 'serve',
@@ -75,6 +125,7 @@ export function shellDevHost(options = {}) {
75
125
  };
76
126
  };
77
127
  validateDevConfig(configFor('http://localhost'));
128
+ const platform = startPlatform(server, configFor('http://localhost').profile ?? 'local', file);
78
129
  const serveShell = async (req, res, pathname) => {
79
130
  if (pathname.startsWith('/assets/')) {
80
131
  const target = resolve(shellDir, `.${normalize(pathname)}`);
@@ -89,6 +140,8 @@ export function shellDevHost(options = {}) {
89
140
  notFound(res);
90
141
  return;
91
142
  }
143
+ // A server that held a platform MFE's port may have stopped since; a reload is when to take it over.
144
+ await platform?.retry();
92
145
  const devConfig = configFor(`${scheme}://${req.headers.host ?? 'localhost'}`);
93
146
  const source = join(shellDir, 'index.html');
94
147
  const html = markShellAssets(injectDevConfig(readFileSync(source, 'utf8'), devConfig, source));
@@ -117,6 +170,9 @@ export function shellDevHost(options = {}) {
117
170
  if (local !== undefined) {
118
171
  config?.logger.info(` shell ${new URL(local).origin}/ (serving ${id} from this origin)`);
119
172
  }
173
+ for (const entry of platform?.host.held() ?? []) {
174
+ config?.logger.info(` ${entry.id} http://localhost:${String(entry.devPort)}/ (bundled)`);
175
+ }
120
176
  };
121
177
  },
122
178
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aistrike-dev/ui-shell-dev",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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": {
@@ -16,7 +16,7 @@
16
16
  "node": ">=22.12"
17
17
  },
18
18
  "bin": {
19
- "aistrike-shell": "./bin/aistrike-shell.mjs"
19
+ "aistrike-shell": "bin/aistrike-shell.mjs"
20
20
  },
21
21
  "exports": {
22
22
  ".": {
@@ -33,15 +33,17 @@
33
33
  "dist",
34
34
  "bin",
35
35
  "shell",
36
+ "platform",
36
37
  "LICENSE"
37
38
  ],
38
39
  "scripts": {
39
- "build": "rm -rf dist shell && tsc -p tsconfig.build.json && node scripts/copy-shell.mjs",
40
+ "compile": "rm -rf dist && tsc -p tsconfig.build.json",
41
+ "build": "rm -rf shell platform && node scripts/copy-shell.mjs && node scripts/copy-platform.mjs",
40
42
  "prepack": "cp ../../LICENSE ./LICENSE",
41
43
  "typecheck": "tsc -p tsconfig.json --noEmit",
42
44
  "test": "vitest run",
43
45
  "lint": "eslint .",
44
- "e2e": "playwright test"
46
+ "e2e": "node scripts/check-pack.mjs && playwright test"
45
47
  },
46
48
  "peerDependencies": {
47
49
  "vite": "^8.0.0"
@@ -68,14 +70,48 @@
68
70
  "shell"
69
71
  ],
70
72
  "targets": {
73
+ "compile": {
74
+ "dependsOn": [
75
+ {
76
+ "projects": [
77
+ "sdk"
78
+ ],
79
+ "target": "build"
80
+ }
81
+ ],
82
+ "inputs": [
83
+ "production",
84
+ "^production"
85
+ ],
86
+ "outputs": [
87
+ "{projectRoot}/dist"
88
+ ],
89
+ "cache": true
90
+ },
71
91
  "build": {
72
92
  "dependsOn": [
93
+ "compile",
73
94
  "^build",
74
- "shell:build"
95
+ "shell:build",
96
+ "assistant:build",
97
+ "settings:build",
98
+ "home:build"
99
+ ],
100
+ "inputs": [
101
+ "production",
102
+ "^production",
103
+ "{workspaceRoot}/apps/assistant/**/*",
104
+ "{workspaceRoot}/apps/settings/**/*",
105
+ "{workspaceRoot}/apps/home/**/*",
106
+ "!{workspaceRoot}/apps/*/**/*.test.ts",
107
+ "!{workspaceRoot}/apps/*/**/*.test.tsx",
108
+ "!{workspaceRoot}/apps/*/dist/**",
109
+ "!{workspaceRoot}/apps/*/dist-mock/**",
110
+ "!{workspaceRoot}/apps/*/node_modules/**"
75
111
  ],
76
112
  "outputs": [
77
- "{projectRoot}/dist",
78
- "{projectRoot}/shell"
113
+ "{projectRoot}/shell",
114
+ "{projectRoot}/platform"
79
115
  ]
80
116
  },
81
117
  "e2e": {