@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.
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--m9X09Oo.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-nKG_bmEN.js +0 -2
  45. package/shell/assets/index-Bm6IWtZi.js +0 -184
package/dist/devfile.js CHANGED
@@ -27,22 +27,10 @@ export function assertClaimsInput(value, source) {
27
27
  }
28
28
  return value;
29
29
  }
30
- function normalizeOverrides(overrides) {
31
- if (typeof overrides !== 'object' || overrides === null || Array.isArray(overrides)) {
32
- throw new Error('overrides must be an object of id: origin');
33
- }
34
- const result = {};
35
- for (const [id, origin] of Object.entries(overrides)) {
36
- if (typeof origin !== 'string') {
37
- throw new Error(`override "${id}" must be a string origin`);
38
- }
39
- const [, normalized] = parseOverride(`${id}=${origin}`);
40
- result[id] = normalized;
41
- }
42
- return result;
43
- }
44
- const FILE_KEYS = ['profile', 'overrides', 'domains', 'idleTimeoutMs', 'landing', 'lab', 'claims'];
30
+ const FILE_KEYS = ['domains', 'cell', 'idleTimeoutMs', 'landing', 'lab', 'claims'];
45
31
  const DOMAIN_KEYS = ['id', 'nav', 'devPort'];
32
+ const CELL_KEYS = ['domains'];
33
+ const CELL_DOMAIN_KEYS = ['id', 'nav', 'at'];
46
34
  const NAV_KEYS = ['label', 'icon'];
47
35
  function isPlainObject(value) {
48
36
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -87,10 +75,10 @@ function validateDomain(value, path) {
87
75
  }
88
76
  rejectUnknownKeys(value, DOMAIN_KEYS, path);
89
77
  const validNav = validateNav(nav, `${path}.nav`);
90
- if (!isPort(devPort)) {
78
+ if (devPort !== undefined && !isPort(devPort)) {
91
79
  throw new Error(`${path}.devPort must be an integer from 1 to 65535`);
92
80
  }
93
- return { id, nav: validNav, devPort };
81
+ return { id, nav: validNav };
94
82
  }
95
83
  function validateDomains(value) {
96
84
  if (!Array.isArray(value)) {
@@ -98,6 +86,59 @@ function validateDomains(value) {
98
86
  }
99
87
  return value.map((entry, index) => validateDomain(entry, `domains[${index}]`));
100
88
  }
89
+ /** `at`: a port on this machine, or a bare `http:` origin (host, optional port): the dev cell forwards over HTTP. */
90
+ function validateAt(value, id, path) {
91
+ if (isPort(value)) {
92
+ return `http://127.0.0.1:${String(value)}`;
93
+ }
94
+ if (typeof value !== 'string') {
95
+ throw new Error(`${path}.at must be a port or an origin`);
96
+ }
97
+ const origin = parseOverride(`${id}=${value}`)[1];
98
+ if (!origin.startsWith('http:')) {
99
+ throw new Error(`${path}.at must be an http: origin: the dev cell forwards over HTTP, not "${origin}"`);
100
+ }
101
+ return origin;
102
+ }
103
+ function validateCellDomain(value, path) {
104
+ if (!isPlainObject(value)) {
105
+ throw new Error(`${path} must be an object`);
106
+ }
107
+ rejectUnknownKeys(value, CELL_DOMAIN_KEYS, path);
108
+ const { id, nav, at } = value;
109
+ if (typeof id !== 'string' || !isValidDomainId(id)) {
110
+ throw new Error(`${path}.id must be a DNS label`);
111
+ }
112
+ const domain = { id, at: validateAt(at, id, path) };
113
+ if (nav !== undefined) {
114
+ domain.nav = validateNav(nav, `${path}.nav`);
115
+ }
116
+ return domain;
117
+ }
118
+ /**
119
+ * The ui-shell dev server's cell. A string would name a deployed cell (KinD, `dev`), which the shell can use only once it
120
+ * signs in through that cell's identity provider: not yet (cell resolution design 4).
121
+ */
122
+ function validateCell(value) {
123
+ if (typeof value === 'string') {
124
+ throw new Error('cell: a deployed cell needs sign-in through its identity provider, which the shell does not have yet; ' +
125
+ 'list each domain\'s dev server in cell.domains instead');
126
+ }
127
+ if (!isPlainObject(value)) {
128
+ throw new Error('cell must be an object');
129
+ }
130
+ rejectUnknownKeys(value, CELL_KEYS, 'cell');
131
+ if (!Array.isArray(value.domains)) {
132
+ throw new Error('cell.domains must be an array');
133
+ }
134
+ const domains = value.domains.map((entry, index) => validateCellDomain(entry, `cell.domains[${index}]`));
135
+ const ids = domains.map((domain) => domain.id);
136
+ const twice = ids.find((id, index) => ids.indexOf(id) !== index);
137
+ if (twice !== undefined) {
138
+ throw new Error(`cell.domains lists "${twice}" twice`);
139
+ }
140
+ return { domains };
141
+ }
101
142
  function validateIdle(value) {
102
143
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
103
144
  throw new Error('idleTimeoutMs must be a finite number of at least 0');
@@ -148,20 +189,21 @@ function validateFile(parsed, name) {
148
189
  if (!isPlainObject(parsed)) {
149
190
  throw new Error('must be a JSON object');
150
191
  }
151
- rejectUnknownKeys(parsed, FILE_KEYS, '');
152
- const result = {};
153
- if (parsed.profile !== undefined) {
154
- if (typeof parsed.profile !== 'string') {
155
- throw new Error('profile must be a string');
156
- }
157
- result.profile = parsed.profile;
192
+ if ('profile' in parsed) {
193
+ throw new Error('profile is no longer read; delete it: every domain is loaded from the dev host, which forwards it');
158
194
  }
159
- if (parsed.overrides !== undefined) {
160
- result.overrides = normalizeOverrides(parsed.overrides);
195
+ if ('overrides' in parsed) {
196
+ throw new Error('overrides is no longer read; delete it: an MFE project runs the built-in MFEs and its own, and the ' +
197
+ 'ui-shell dev server\'s cell.domains says where other domains run');
161
198
  }
199
+ rejectUnknownKeys(parsed, FILE_KEYS, '');
200
+ const result = {};
162
201
  if (parsed.domains !== undefined) {
163
202
  result.domains = validateDomains(parsed.domains);
164
203
  }
204
+ if (parsed.cell !== undefined) {
205
+ result.cell = validateCell(parsed.cell);
206
+ }
165
207
  return readExtras(parsed, result);
166
208
  }
167
209
  catch (error) {
@@ -193,12 +235,15 @@ function landingOf(file, args) {
193
235
  function labOf(file) {
194
236
  return file?.lab === undefined ? {} : { lab: file.lab !== false };
195
237
  }
196
- /** Explicit overrides win over the file; the profile defaults to `local`; the idle timeout defaults to off for local work. */
238
+ /**
239
+ * What the shell is told: the domains it does not compile in (by name; where they run stays here), the landing route,
240
+ * the lab and the session. An explicit landing wins over the file; the idle timeout defaults to off for local work.
241
+ */
197
242
  export function buildDevConfig(file, args, session) {
243
+ const all = [...(file?.domains ?? []), ...(file?.cell?.domains ?? [])];
244
+ const named = all.flatMap(({ id, nav }) => (nav === undefined ? [] : [{ id, nav }]));
198
245
  return {
199
- profile: args.profile ?? file?.profile ?? 'local',
200
- overrides: { ...(file?.overrides ?? {}), ...args.overrides },
201
- domains: file?.domains ?? [],
246
+ domains: named,
202
247
  idleTimeoutMs: file?.idleTimeoutMs ?? 0,
203
248
  landing: landingOf(file, args),
204
249
  ...labOf(file),
@@ -0,0 +1,17 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ import type { Duplex } from 'node:stream';
3
+ export declare const REFUSED_HOST: string;
4
+ /**
5
+ * The `Host`s a dev server answers, as Vite's default `allowedHosts` does: `localhost`, `*.localhost`, and IP literals
6
+ * (IPv4, bracketed IPv6), each with or without a port. Any other name could be a DNS-rebinding page, which would read
7
+ * the dev token the console hands out and everything the dev cell forwards.
8
+ */
9
+ export declare function isAllowedHost(host: string | undefined): host is string;
10
+ /** Answers a request whose `Host` is not allowed. */
11
+ export declare function refuseHost(res: ServerResponse): void;
12
+ /** Answers a WebSocket upgrade whose `Host` is not allowed. */
13
+ export declare function refuseUpgradeHost(socket: Duplex): void;
14
+ /** How many dev tokens a dev server keeps, one per address a browser reached it by. */
15
+ export declare const MAX_SESSIONS = 32;
16
+ /** Keeps `value` for `key`, letting the oldest entry go once `MAX_SESSIONS` are kept. */
17
+ export declare function rememberSession<T>(sessions: Map<string, T>, key: string, value: T): void;
package/dist/hosts.js ADDED
@@ -0,0 +1,51 @@
1
+ import { isIP } from 'node:net';
2
+ const PORT = /^\d{1,5}$/;
3
+ export const REFUSED_HOST = 'This dev server answers only localhost and IP addresses. ' +
4
+ 'Open it at localhost or this machine\'s address.';
5
+ /** `localhost`, a name under it, or an IP literal (IPv6 in brackets). */
6
+ function isLocalName(name) {
7
+ if (name.startsWith('[')) {
8
+ return name.endsWith(']') && isIP(name.slice(1, -1)) === 6;
9
+ }
10
+ return isIP(name) === 4 || name === 'localhost' || (name.endsWith('.localhost') && name.length > '.localhost'.length);
11
+ }
12
+ /**
13
+ * The `Host`s a dev server answers, as Vite's default `allowedHosts` does: `localhost`, `*.localhost`, and IP literals
14
+ * (IPv4, bracketed IPv6), each with or without a port. Any other name could be a DNS-rebinding page, which would read
15
+ * the dev token the console hands out and everything the dev cell forwards.
16
+ */
17
+ export function isAllowedHost(host) {
18
+ if (host === undefined) {
19
+ return false;
20
+ }
21
+ const value = host.trim().toLowerCase();
22
+ const split = value.startsWith('[') ? value.indexOf(']') + 1 : value.indexOf(':');
23
+ if (split === 0) {
24
+ return false;
25
+ }
26
+ const name = split === -1 ? value : value.slice(0, split);
27
+ const port = split === -1 ? '' : value.slice(split);
28
+ return (port === '' || (port.startsWith(':') && PORT.test(port.slice(1)))) && isLocalName(name);
29
+ }
30
+ /** Answers a request whose `Host` is not allowed. */
31
+ export function refuseHost(res) {
32
+ res.statusCode = 403;
33
+ res.setHeader('Content-Type', 'text/plain');
34
+ res.end(REFUSED_HOST);
35
+ }
36
+ /** Answers a WebSocket upgrade whose `Host` is not allowed. */
37
+ export function refuseUpgradeHost(socket) {
38
+ socket.end(`HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n${REFUSED_HOST}`);
39
+ }
40
+ /** How many dev tokens a dev server keeps, one per address a browser reached it by. */
41
+ export const MAX_SESSIONS = 32;
42
+ /** Keeps `value` for `key`, letting the oldest entry go once `MAX_SESSIONS` are kept. */
43
+ export function rememberSession(sessions, key, value) {
44
+ if (sessions.size >= MAX_SESSIONS) {
45
+ const oldest = sessions.keys().next();
46
+ if (oldest.done !== true) {
47
+ sessions.delete(oldest.value);
48
+ }
49
+ }
50
+ sessions.set(key, value);
51
+ }
package/dist/inject.js CHANGED
@@ -17,9 +17,6 @@ export const CONTENT_TYPES = {
17
17
  '.txt': 'text/plain; charset=utf-8',
18
18
  };
19
19
  export function validateDevConfig(devConfig) {
20
- if (devConfig.profile === undefined || devConfig.profile === '') {
21
- throw new Error('dev config must name a profile');
22
- }
23
20
  if (devConfig.session !== undefined && !(devConfig.session.expiresAt > Date.now())) {
24
21
  throw new Error('dev session is already expired; mint a new token');
25
22
  }
@@ -1,4 +1,4 @@
1
- /** A platform MFE the package carries, and the port the shell's `local` profile looks for it on. */
1
+ /** A platform MFE the package carries, and the port the dev host forwards its paths to. */
2
2
  export interface PlatformEntry {
3
3
  id: string;
4
4
  devPort: number;
@@ -9,10 +9,8 @@ export interface PlatformLogger {
9
9
  }
10
10
  export interface SelectOptions {
11
11
  ownId: string;
12
- profile: string;
13
- overrides: {
14
- [id: string]: string;
15
- };
12
+ /** Domains another server serves (the ui-shell dev server's `cell.domains`): never bundled. */
13
+ elsewhere: readonly string[];
16
14
  option: boolean | string[] | undefined;
17
15
  }
18
16
  export interface PlatformHost {
@@ -26,8 +24,8 @@ export interface PlatformHost {
26
24
  /** `<platformDir>/platform.json`, keeping the entries whose build is present. A missing or broken list is none. */
27
25
  export declare function readPlatform(platformDir: string): PlatformEntry[];
28
26
  /**
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.
27
+ * Platform-MFE spec 3.3 step 1: never the MFE being developed (it is this server), never a domain another server serves,
28
+ * and what the option allows.
31
29
  */
32
30
  export declare function selectPlatform(entries: PlatformEntry[], options: SelectOptions): PlatformEntry[];
33
31
  /**
@@ -24,16 +24,16 @@ export function readPlatform(platformDir) {
24
24
  return parsed.filter((entry) => existsSync(join(platformDir, entry.id, 'ui', 'manifest.json')));
25
25
  }
26
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.
27
+ * Platform-MFE spec 3.3 step 1: never the MFE being developed (it is this server), never a domain another server serves,
28
+ * and what the option allows.
29
29
  */
30
30
  export function selectPlatform(entries, options) {
31
- if (options.option === false || options.profile !== 'local') {
31
+ if (options.option === false) {
32
32
  return [];
33
33
  }
34
34
  const allowed = Array.isArray(options.option) ? options.option : undefined;
35
35
  return entries.filter((entry) => entry.id !== options.ownId
36
- && !(entry.id in options.overrides)
36
+ && !options.elsewhere.includes(entry.id)
37
37
  && (allowed === undefined || allowed.includes(entry.id)));
38
38
  }
39
39
  const errorCode = (error) => error?.code;
@@ -1,6 +1,6 @@
1
1
  import type { PackagedEntry } from './copy.js';
2
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.
3
+ * The platform MFEs the package carries (platform-MFE spec 3.1), each on its port in `DEV_PORTS`. The copy script writes
4
+ * this to `platform/platform.json`. Never the lab: it is dev-only.
5
5
  */
6
6
  export declare const PLATFORM: readonly PackagedEntry[];
@@ -1,9 +1,10 @@
1
+ import { DEV_PORTS } from '../ports.js';
1
2
  /**
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.
3
+ * The platform MFEs the package carries (platform-MFE spec 3.1), each on its port in `DEV_PORTS`. The copy script writes
4
+ * this to `platform/platform.json`. Never the lab: it is dev-only.
4
5
  */
5
6
  export const PLATFORM = [
6
- { id: 'assistant', devPort: 5104, mock: true },
7
- { id: 'settings', devPort: 5101, mock: true },
8
- { id: 'home', devPort: 5102, mock: false },
7
+ { id: 'assistant', devPort: DEV_PORTS.assistant, mock: true },
8
+ { id: 'settings', devPort: DEV_PORTS.settings, mock: true },
9
+ { id: 'home', devPort: DEV_PORTS.home, mock: false },
9
10
  ];
@@ -8,7 +8,8 @@ export interface PlatformServerOptions {
8
8
  handlers: MockHandler[];
9
9
  }
10
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/`.
11
+ * One bundled platform MFE, served as its own dev server would be on its own port, in the cell's layout (cell resolution
12
+ * design 3): same-host CORS, the build at `/<id>/ui/latest/` (no redirect; the resolver accepts a manifest at `latest`,
13
+ * as from a dev server) and the mocks at `/<id>/api/`, which reach them as `/api/`.
13
14
  */
14
15
  export declare function createPlatformServer(options: PlatformServerOptions): Server;
@@ -2,18 +2,28 @@ import { createServer } from 'node:http';
2
2
  import { normalize, resolve, sep } from 'node:path';
3
3
  import { sendFile } from '../inject.js';
4
4
  import { sameHostCors } from './cors.js';
5
- const UI = '/ui/latest/';
6
5
  function answer(res, status, text) {
7
6
  res.statusCode = status;
8
7
  res.setHeader('Content-Type', 'text/plain');
9
8
  res.end(text);
10
9
  }
10
+ /** The file in the build a path under `ui` names, or `null` for one outside it (`..` included). */
11
+ function buildFile(uiDir, ui, pathname) {
12
+ if (!pathname.startsWith(ui)) {
13
+ return null;
14
+ }
15
+ const target = resolve(uiDir, `.${normalize(`/${pathname.slice(ui.length)}`)}`);
16
+ return target.startsWith(uiDir + sep) ? target : null;
17
+ }
11
18
  /**
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/`.
19
+ * One bundled platform MFE, served as its own dev server would be on its own port, in the cell's layout (cell resolution
20
+ * design 3): same-host CORS, the build at `/<id>/ui/latest/` (no redirect; the resolver accepts a manifest at `latest`,
21
+ * as from a dev server) and the mocks at `/<id>/api/`, which reach them as `/api/`.
14
22
  */
15
23
  export function createPlatformServer(options) {
16
24
  const uiDir = resolve(options.uiDir);
25
+ const ui = `/${options.id}/ui/latest/`;
26
+ const api = `/${options.id}/api/`;
17
27
  return createServer((req, res) => {
18
28
  if (sameHostCors(req, res)) {
19
29
  return;
@@ -26,18 +36,19 @@ export function createPlatformServer(options) {
26
36
  answer(res, 400, 'bad request');
27
37
  return;
28
38
  }
29
- if (pathname.startsWith('/api/')) {
39
+ const raw = req.url ?? '/';
40
+ if (raw.startsWith(api)) {
41
+ // The mocks see what a service behind a cell's route sees: the path without its domain prefix.
42
+ req.url = `/api/${raw.slice(api.length)}`;
30
43
  if (!options.handlers.some((handle) => handle(req, res))) {
31
44
  answer(res, 404, 'not found');
32
45
  }
33
46
  return;
34
47
  }
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
- }
48
+ const target = req.method === 'GET' || req.method === 'HEAD' ? buildFile(uiDir, ui, pathname) : null;
49
+ if (target !== null) {
50
+ sendFile(target, res);
51
+ return;
41
52
  }
42
53
  answer(res, 404, 'not found');
43
54
  });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Where each compiled domain's dev server listens (cell resolution design 3). The shell knows no ports: it loads every
3
+ * domain from `<the dev host>/<id>`, and the dev host forwards there. `ports.test.ts` keeps these ids equal to the
4
+ * shell's compiled domain list and each port equal to the domain's own `mfe({ devPort })`.
5
+ */
6
+ export declare const DEV_PORTS: {
7
+ readonly home: 5102;
8
+ readonly settings: 5101;
9
+ readonly assistant: 5104;
10
+ };
11
+ /** The port of a compiled domain, or `undefined` for any other id. */
12
+ export declare function devPortOf(id: string): number | undefined;
package/dist/ports.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Where each compiled domain's dev server listens (cell resolution design 3). The shell knows no ports: it loads every
3
+ * domain from `<the dev host>/<id>`, and the dev host forwards there. `ports.test.ts` keeps these ids equal to the
4
+ * shell's compiled domain list and each port equal to the domain's own `mfe({ devPort })`.
5
+ */
6
+ export const DEV_PORTS = {
7
+ home: 5102,
8
+ settings: 5101,
9
+ assistant: 5104,
10
+ };
11
+ /** The port of a compiled domain, or `undefined` for any other id. */
12
+ export function devPortOf(id) {
13
+ return Object.hasOwn(DEV_PORTS, id) ? DEV_PORTS[id] : undefined;
14
+ }
@@ -0,0 +1,15 @@
1
+ /** Where `@vitejs/plugin-react` serves React Refresh's runtime on a dev server whose Vite base is `/<id>/ui/latest/`. */
2
+ export declare function refreshRuntimePath(id: string): string;
3
+ /**
4
+ * Whether a domain's server is a Vite dev server with `@vitejs/plugin-react`: it serves React Refresh's runtime. Its
5
+ * component modules then refuse to run unless the page installed the runtime's preamble.
6
+ */
7
+ export declare function servesRefreshRuntime(origin: string, id: string, timeoutMs?: number): Promise<boolean>;
8
+ /**
9
+ * The preamble `@vitejs/plugin-react` writes into the page its own Vite server serves, for a page another server serves
10
+ * (the ui-shell console): one module script per runtime URL, each installing that runtime into React's global hook.
11
+ * Each MFE bundles its own React, so each dev server's runtime registers its own components.
12
+ */
13
+ export declare function refreshPreamble(runtimeUrls: readonly string[]): string;
14
+ /** Puts `snippet` before the page's first module script, so it runs before the shell's entry. */
15
+ export declare function beforeFirstModule(html: string, snippet: string, source: string): string;
@@ -0,0 +1,44 @@
1
+ import { escapeJsonForScript } from './escape.js';
2
+ /** Where `@vitejs/plugin-react` serves React Refresh's runtime on a dev server whose Vite base is `/<id>/ui/latest/`. */
3
+ export function refreshRuntimePath(id) {
4
+ return `/${id}/ui/latest/@react-refresh`;
5
+ }
6
+ /**
7
+ * Whether a domain's server is a Vite dev server with `@vitejs/plugin-react`: it serves React Refresh's runtime. Its
8
+ * component modules then refuse to run unless the page installed the runtime's preamble.
9
+ */
10
+ export async function servesRefreshRuntime(origin, id, timeoutMs = 1000) {
11
+ try {
12
+ const res = await fetch(`${origin}${refreshRuntimePath(id)}`, { signal: AbortSignal.timeout(timeoutMs) });
13
+ await res.body?.cancel();
14
+ return res.ok && (res.headers.get('content-type') ?? '').includes('javascript');
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ /**
21
+ * The preamble `@vitejs/plugin-react` writes into the page its own Vite server serves, for a page another server serves
22
+ * (the ui-shell console): one module script per runtime URL, each installing that runtime into React's global hook.
23
+ * Each MFE bundles its own React, so each dev server's runtime registers its own components.
24
+ */
25
+ export function refreshPreamble(runtimeUrls) {
26
+ return runtimeUrls.map((url) => [
27
+ '<script type="module">',
28
+ `import RefreshRuntime from ${escapeJsonForScript(JSON.stringify(url))};`,
29
+ 'RefreshRuntime.injectIntoGlobalHook(window);',
30
+ 'window.$RefreshReg$ = () => {};',
31
+ 'window.$RefreshSig$ = () => (type) => type;',
32
+ 'window.__vite_plugin_react_preamble_installed__ = true;',
33
+ '</script>',
34
+ ].join('\n')).join('\n');
35
+ }
36
+ /** Puts `snippet` before the page's first module script, so it runs before the shell's entry. */
37
+ export function beforeFirstModule(html, snippet, source) {
38
+ const marker = '<script type="module"';
39
+ const at = html.indexOf(marker);
40
+ if (at === -1) {
41
+ throw new Error(`${source} has no module script to inject before`);
42
+ }
43
+ return snippet === '' ? html : `${html.slice(0, at)}${snippet}\n ${html.slice(at)}`;
44
+ }
@@ -0,0 +1,28 @@
1
+ import { type PlatformLogger } from './platform/host.js';
2
+ export declare const DEFAULT_CONSOLE_PORT = 3000;
3
+ export declare const DEFAULT_CELL_PORT = 4000;
4
+ export interface ServeOptions {
5
+ /** The shell's devhost build. */
6
+ shellDir: string;
7
+ /** The platform MFEs' builds. */
8
+ platformDir: string;
9
+ /** The dev file, relative to `cwd`. Default `shell.dev.json`. */
10
+ config?: string;
11
+ cwd: string;
12
+ port?: number;
13
+ cellPort?: number;
14
+ /** Where the dev key lives. Default: the user's config directory. */
15
+ keyDir?: string;
16
+ logger: PlatformLogger;
17
+ }
18
+ export interface Serving {
19
+ consolePort: number;
20
+ cellPort: number;
21
+ close: () => Promise<void>;
22
+ }
23
+ /**
24
+ * The ui-shell dev server (cell resolution design 3): the console, which serves the shell's devhost build with a dev
25
+ * token, and the dev cell, an origin of its own that forwards each domain to the server its `cell.domains` names, or
26
+ * to the built-in MFE. The shell loads every domain from the cell, cross-origin, as from a deployed cell.
27
+ */
28
+ export declare function serveDevShell(options: ServeOptions): Promise<Serving>;