@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.
- 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-C9pWqM_D.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-BiiVtiYJ.js +0 -2
- package/shell/assets/index-CqB5Mswt.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
|
-
|
|
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
|
|
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
|
-
|
|
152
|
-
|
|
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 (
|
|
160
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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),
|
package/dist/hosts.d.ts
ADDED
|
@@ -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
|
}
|
package/dist/platform/host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** A platform MFE the package carries, and the port the
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
30
|
-
*
|
|
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
|
/**
|
package/dist/platform/host.js
CHANGED
|
@@ -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
|
|
28
|
-
*
|
|
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
|
|
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
|
|
36
|
+
&& !options.elsewhere.includes(entry.id)
|
|
37
37
|
&& (allowed === undefined || allowed.includes(entry.id)));
|
|
38
38
|
}
|
|
39
39
|
const errorCode = (error) => error?.code;
|
package/dist/platform/list.d.ts
CHANGED
|
@@ -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
|
|
4
|
-
*
|
|
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[];
|
package/dist/platform/list.js
CHANGED
|
@@ -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
|
|
3
|
-
*
|
|
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:
|
|
7
|
-
{ id: 'settings', devPort:
|
|
8
|
-
{ id: 'home', devPort:
|
|
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
|
|
12
|
-
*
|
|
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;
|
package/dist/platform/server.js
CHANGED
|
@@ -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
|
|
13
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
});
|
package/dist/ports.d.ts
ADDED
|
@@ -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;
|
package/dist/preamble.js
ADDED
|
@@ -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
|
+
}
|
package/dist/serve.d.ts
ADDED
|
@@ -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>;
|