@entrinsik/vite-plugin-informer 2.10.0 → 2.12.0
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 +7 -3
- package/bin/init.js +13 -2
- package/bin/workspace.js +15 -7
- package/index.d.ts +14 -0
- package/package.json +1 -1
- package/src/agent-dev.js +8 -11
- package/src/dev-bag.js +241 -0
- package/src/dev-channel-handlers.js +335 -0
- package/src/dev-channel-shim.js +276 -39
- package/src/dev-channels.js +138 -28
- package/src/dev-dependencies.js +140 -4
- package/src/dev-platform.js +81 -5
- package/src/dev-streams.js +176 -27
- package/src/env.js +17 -1
- package/src/index.js +87 -9
- package/src/server-routes.js +79 -211
- package/src/streams-client.js +121 -8
package/src/dev-streams.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { extname } from 'node:path';
|
|
2
|
+
import { extname, basename } from 'node:path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Dev-mode app streams (I5-12979) — the local stand-in for the production
|
|
@@ -151,6 +151,47 @@ export function createStreamStore(limits = LIMITS) {
|
|
|
151
151
|
return { create, get, bytes, remove, reserve, seal, sweep, touch, items, limits };
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The staged streams of one type, oldest first (I5-13030). Dev has a single
|
|
156
|
+
* user, so the store IS their ledger — no per-user filter to apply.
|
|
157
|
+
*/
|
|
158
|
+
export function listStreams(store, type) {
|
|
159
|
+
store.sweep();
|
|
160
|
+
return [...store.items.values()].filter(item => item.type === type).sort((a, b) => a.createdAt - b.createdAt);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Where the dev origin serves a download: the same shape the bag's download.url takes. */
|
|
164
|
+
export function downloadUrl(item, base = '') {
|
|
165
|
+
return `${base}/_downloads/${encodeURIComponent(item.id)}${item.filename ? `/${encodeURIComponent(item.filename)}` : ''}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* MIRRORED from modules/app/lib/app-stream-forwarding.js (I5-13030) and pinned
|
|
170
|
+
* by the parity test: how a filled stream is named when the guest did not
|
|
171
|
+
* name it — the upstream's Content-Disposition, else the request path.
|
|
172
|
+
*/
|
|
173
|
+
export function filenameFromDisposition(header) {
|
|
174
|
+
if (typeof header !== 'string') return null;
|
|
175
|
+
const utf8 = /filename\*\s*=\s*(?:UTF-8|utf-8)''([^;]+)/.exec(header);
|
|
176
|
+
if (utf8) {
|
|
177
|
+
try { return decodeURIComponent(utf8[1].trim()); } catch { /* fall through to the plain form */ }
|
|
178
|
+
}
|
|
179
|
+
const plain = /filename\s*=\s*"?([^";]+)"?/.exec(header);
|
|
180
|
+
return plain ? plain[1].trim() : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function filenameFor(headers, url) {
|
|
184
|
+
const named = filenameFromDisposition(headers && headers['content-disposition']);
|
|
185
|
+
if (named) return basename(named);
|
|
186
|
+
if (typeof url === 'string') {
|
|
187
|
+
try {
|
|
188
|
+
const last = basename(new URL(url, 'http://upstream.invalid').pathname);
|
|
189
|
+
if (last && last !== '/') return last;
|
|
190
|
+
} catch { /* not a URL we can name a file after */ }
|
|
191
|
+
}
|
|
192
|
+
return 'download';
|
|
193
|
+
}
|
|
194
|
+
|
|
154
195
|
/** Guest-facing handle: never the store item itself. */
|
|
155
196
|
export function toHandle(item, extra = {}) {
|
|
156
197
|
return {
|
|
@@ -402,35 +443,38 @@ export function createStreamServices({ store, query, base = '' }) {
|
|
|
402
443
|
return { rowCount };
|
|
403
444
|
}
|
|
404
445
|
|
|
446
|
+
/** A live upload handle — what uploads.get() and a filled `into: 'upload'` both hand out. */
|
|
447
|
+
function uploadHandle(item) {
|
|
448
|
+
return {
|
|
449
|
+
...toHandle(item),
|
|
450
|
+
text: async (encoding) => {
|
|
451
|
+
if (encoding !== undefined && encoding !== null && !Buffer.isEncoding(encoding)) throw httpError(422, `Unknown text encoding "${encoding}"`);
|
|
452
|
+
return inline(item).toString(encoding || 'utf8');
|
|
453
|
+
},
|
|
454
|
+
json: async () => {
|
|
455
|
+
const text = inline(item).toString('utf8');
|
|
456
|
+
try {
|
|
457
|
+
return JSON.parse(text);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
throw httpError(422, `Upload is not valid JSON: ${err.message}`);
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
base64: async () => inline(item).toString('base64'),
|
|
463
|
+
extractText: async () => { throw httpError(501, 'upload.extractText() is not available in dev — the platform text extractor runs on the Informer server; test it against a deployed app'); },
|
|
464
|
+
copyInto: async (table, opts) => await copyInto(item, table, opts || {}),
|
|
465
|
+
discard: async () => { store.remove(item.id); return true; }
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
405
469
|
const uploads = {
|
|
406
470
|
async get(id) {
|
|
407
|
-
|
|
408
|
-
return {
|
|
409
|
-
...toHandle(item),
|
|
410
|
-
text: async (encoding) => {
|
|
411
|
-
if (encoding !== undefined && encoding !== null && !Buffer.isEncoding(encoding)) throw httpError(422, `Unknown text encoding "${encoding}"`);
|
|
412
|
-
return inline(item).toString(encoding || 'utf8');
|
|
413
|
-
},
|
|
414
|
-
json: async () => {
|
|
415
|
-
const text = inline(item).toString('utf8');
|
|
416
|
-
try {
|
|
417
|
-
return JSON.parse(text);
|
|
418
|
-
} catch (err) {
|
|
419
|
-
throw httpError(422, `Upload is not valid JSON: ${err.message}`);
|
|
420
|
-
}
|
|
421
|
-
},
|
|
422
|
-
base64: async () => inline(item).toString('base64'),
|
|
423
|
-
extractText: async () => { throw httpError(501, 'upload.extractText() is not available in dev — the platform text extractor runs on the Informer server; test it against a deployed app'); },
|
|
424
|
-
copyInto: async (table, opts) => await copyInto(item, table, opts || {}),
|
|
425
|
-
discard: async () => { store.remove(item.id); return true; }
|
|
426
|
-
};
|
|
471
|
+
return uploadHandle(ownedUpload(id));
|
|
427
472
|
}
|
|
428
473
|
};
|
|
429
474
|
|
|
430
475
|
function downloadHandle(item) {
|
|
431
|
-
const url = `${base}/_downloads/${encodeURIComponent(item.id)}${item.filename ? `/${encodeURIComponent(item.filename)}` : ''}`;
|
|
432
476
|
const handle = {
|
|
433
|
-
...toHandle(item, { url }),
|
|
477
|
+
...toHandle(item, { url: downloadUrl(item, base) }),
|
|
434
478
|
async write(chunk) {
|
|
435
479
|
writableDownload(item.id);
|
|
436
480
|
const buf = toBytes(chunk);
|
|
@@ -504,7 +548,93 @@ export function createStreamServices({ store, query, base = '' }) {
|
|
|
504
548
|
}
|
|
505
549
|
}
|
|
506
550
|
|
|
507
|
-
|
|
551
|
+
/**
|
|
552
|
+
* Forwarding to an integration (I5-13030). Prod streams the bytes between
|
|
553
|
+
* the staging store and the upstream; the dev proxy reaches the real
|
|
554
|
+
* request route over HTTP with a JSON body, so here the bytes ride that
|
|
555
|
+
* route's base64 envelope instead — the same semantics, under the
|
|
556
|
+
* envelope's own cap (dev-dependencies.js names the gap when a file is
|
|
557
|
+
* over it). What the store does is identical: an outbound handle is read,
|
|
558
|
+
* an `into` target is claimed before the call and filled after it.
|
|
559
|
+
*/
|
|
560
|
+
const forwarding = {
|
|
561
|
+
/** The bytes behind an owned, sealed upload reference, with what describes them. */
|
|
562
|
+
body(ref) {
|
|
563
|
+
const item = ownedUpload(ref.id);
|
|
564
|
+
return { bytes: store.bytes(item), contentType: item.contentType || 'application/octet-stream', filename: item.filename, size: item.size };
|
|
565
|
+
},
|
|
566
|
+
/**
|
|
567
|
+
* Claim an `into` target now — a download the guest created (unsealed,
|
|
568
|
+
* unwritten) or 'upload' for a fresh one — and return fill(), which
|
|
569
|
+
* lands the upstream body and seals it. Over the byte cap, fill() fails
|
|
570
|
+
* 413 and leaves a download empty and unsealed, a fresh upload gone.
|
|
571
|
+
*/
|
|
572
|
+
receiver(target, url) {
|
|
573
|
+
if (target === 'upload') {
|
|
574
|
+
store.reserve(limits.maxUploadBytes);
|
|
575
|
+
const item = store.create('upload', { filename: null, contentType: null, size: 0, chunks: 0, reserved: limits.maxUploadBytes });
|
|
576
|
+
let settled = false;
|
|
577
|
+
return {
|
|
578
|
+
// Prod's appStream.release: the upstream delivered no body,
|
|
579
|
+
// so give the claim back rather than leaving an upload
|
|
580
|
+
// nobody holds a handle to until the TTL.
|
|
581
|
+
release() {
|
|
582
|
+
if (settled) return;
|
|
583
|
+
settled = true;
|
|
584
|
+
store.remove(item.id);
|
|
585
|
+
},
|
|
586
|
+
fill(bytes, headers = {}) {
|
|
587
|
+
settled = true;
|
|
588
|
+
if (bytes.length > limits.maxUploadBytes) {
|
|
589
|
+
store.remove(item.id);
|
|
590
|
+
throw httpError(413, `The upstream body exceeds the ${limits.maxUploadBytes} byte limit`, { maxUploadBytes: limits.maxUploadBytes });
|
|
591
|
+
}
|
|
592
|
+
item.contentType = (headers['content-type'] || 'application/octet-stream').split(';')[0].trim();
|
|
593
|
+
item.filename = filenameFor(headers, url);
|
|
594
|
+
// An `into` fill is the append producer, which never sets
|
|
595
|
+
// a chunk size — prod reports null, and so must the
|
|
596
|
+
// listing below, not just the handle returned here.
|
|
597
|
+
item.chunkSize = null;
|
|
598
|
+
item.chunks = 1;
|
|
599
|
+
item.chunkData.set(1, Buffer.from(bytes));
|
|
600
|
+
item.size = bytes.length;
|
|
601
|
+
item.complete = true;
|
|
602
|
+
store.seal(item);
|
|
603
|
+
// The same extra KEYS prod's handle carries for a filled
|
|
604
|
+
// upload. Dev is one in-memory buffer, so chunks is always
|
|
605
|
+
// 1 where prod reports the real append-producer count.
|
|
606
|
+
return { ...uploadHandle(item), complete: true, chunks: 1, chunkSize: null };
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
if (isStreamRef(target, 'download')) {
|
|
611
|
+
const item = writableDownload(target.id);
|
|
612
|
+
if (item.parts.length) throw httpError(409, 'Download already has bytes; `into` fills a download from empty and seals it');
|
|
613
|
+
return {
|
|
614
|
+
// The guest still owns this one, so there is nothing to
|
|
615
|
+
// reclaim — it stays empty and unsealed, as prod leaves it.
|
|
616
|
+
release() {},
|
|
617
|
+
fill(bytes, headers = {}) {
|
|
618
|
+
if (bytes.length > limits.maxUploadBytes) {
|
|
619
|
+
item.parts = [];
|
|
620
|
+
item.size = 0;
|
|
621
|
+
throw httpError(413, `The upstream body exceeds the ${limits.maxUploadBytes} byte limit`, { maxUploadBytes: limits.maxUploadBytes });
|
|
622
|
+
}
|
|
623
|
+
if (!item.contentType) item.contentType = (headers['content-type'] || 'application/octet-stream').split(';')[0].trim();
|
|
624
|
+
if (!item.filename) item.filename = filenameFor(headers, url);
|
|
625
|
+
item.parts = [Buffer.from(bytes)];
|
|
626
|
+
item.size = bytes.length;
|
|
627
|
+
pending.delete(item.id);
|
|
628
|
+
store.seal(item);
|
|
629
|
+
return { ...downloadHandle(item), complete: true };
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
throw httpError(400, `request(): \`into\` must be a download handle or 'upload'`);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
return { uploads, downloads, resolveQueryParams, endDownload, finalize, forwarding };
|
|
508
638
|
}
|
|
509
639
|
|
|
510
640
|
// What a guest hands write(): a string or bytes. Anything else is a 422 in
|
|
@@ -591,7 +721,7 @@ async function readBody(req) {
|
|
|
591
721
|
|
|
592
722
|
function uploadSummary(item) {
|
|
593
723
|
const { received, missing } = missingChunks(item);
|
|
594
|
-
return toHandle(item, { chunkSize: item.chunkSize, chunks: item.chunks, complete: item.complete, received, missing });
|
|
724
|
+
return toHandle(item, { chunkSize: Number(item.chunkSize) || null, chunks: item.chunks, complete: item.complete, received, missing });
|
|
595
725
|
}
|
|
596
726
|
|
|
597
727
|
/**
|
|
@@ -612,6 +742,11 @@ export function createUploadsMiddleware(store) {
|
|
|
612
742
|
res.setHeader('Location', `/_uploads/${item.id}`);
|
|
613
743
|
return sendJson(res, 201, uploadSummary(item));
|
|
614
744
|
}
|
|
745
|
+
// GET /_uploads — what is staged (I5-13030). Dev has one user, so
|
|
746
|
+
// the store's uploads are all theirs, oldest first like prod.
|
|
747
|
+
if (parts.length === 0 && method === 'GET') {
|
|
748
|
+
return sendJson(res, 200, listStreams(store, 'upload').map(item => toHandle(item, { chunkSize: Number(item.chunkSize) || null, chunks: item.chunks, complete: item.complete })));
|
|
749
|
+
}
|
|
615
750
|
if (parts.length === 0) return next();
|
|
616
751
|
|
|
617
752
|
const item = store.get('upload', parts[0]);
|
|
@@ -639,14 +774,28 @@ export function createUploadsMiddleware(store) {
|
|
|
639
774
|
};
|
|
640
775
|
}
|
|
641
776
|
|
|
642
|
-
/**
|
|
777
|
+
/**
|
|
778
|
+
* Connect middleware for GET /_downloads/{id}/{filename?}?keep&inline, plus
|
|
779
|
+
* (I5-13030) GET /_downloads to list what is staged and DELETE /_downloads/{id}
|
|
780
|
+
* to drop one unserved.
|
|
781
|
+
*/
|
|
643
782
|
export function createDownloadsMiddleware(store) {
|
|
644
783
|
return function downloadsMiddleware(req, res, next) {
|
|
645
784
|
try {
|
|
646
|
-
|
|
785
|
+
const method = req.method.toUpperCase();
|
|
647
786
|
const url = new URL(req.url, 'http://dev.local');
|
|
648
787
|
const [id] = url.pathname.split('/').filter(Boolean);
|
|
788
|
+
if (!id && method === 'GET') {
|
|
789
|
+
return sendJson(res, 200, listStreams(store, 'download').map(item => toHandle(item, { complete: item.complete, url: downloadUrl(item) })));
|
|
790
|
+
}
|
|
649
791
|
if (!id) return next();
|
|
792
|
+
if (method === 'DELETE') {
|
|
793
|
+
if (!store.get('download', id)) throw httpError(404, 'Not Found');
|
|
794
|
+
store.remove(id);
|
|
795
|
+
res.statusCode = 204;
|
|
796
|
+
return res.end();
|
|
797
|
+
}
|
|
798
|
+
if (method !== 'GET') return next();
|
|
650
799
|
const item = store.get('download', id);
|
|
651
800
|
if (!item || !item.complete) throw httpError(404, 'Not Found');
|
|
652
801
|
return serveDownload(store, item, res, {
|
package/src/env.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve, dirname, parse as parsePath } from 'node:path';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -71,6 +71,22 @@ export function envWritePath({ mode, cwd } = {}) {
|
|
|
71
71
|
return resolve(dir, useMode ? `.env.${mode}` : '.env');
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* A variable as defined in the app's OWN env file (the one envWritePath
|
|
76
|
+
* names), ignoring the shell and any parent .env loadEnv walked up to. Null
|
|
77
|
+
* when the file is missing or leaves the variable unset or empty.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} name
|
|
80
|
+
* @param {{ mode?: string, cwd?: string }} options
|
|
81
|
+
* @returns {string|null}
|
|
82
|
+
*/
|
|
83
|
+
export function localEnvValue(name, { mode, cwd } = {}) {
|
|
84
|
+
const path = envWritePath({ mode, cwd });
|
|
85
|
+
if (!existsSync(path)) return null;
|
|
86
|
+
const value = dotenv.parse(readFileSync(path, 'utf8'))[name];
|
|
87
|
+
return value ? value : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
/**
|
|
75
91
|
* Parse --mode <name> from a process.argv array.
|
|
76
92
|
*
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,8 @@ import { buildDeclarations } from './openapi-to-dts.js';
|
|
|
8
8
|
import { loadEnv, envWritePath } from './env.js';
|
|
9
9
|
import { createMiddleware as createServerRoutes } from './server-routes.js';
|
|
10
10
|
import { createAgentMiddleware } from './agent-dev.js';
|
|
11
|
-
import { createDevChannels, validateChannels, BROADCAST_EVENT, DEV_CHANNEL_EVENT } from './dev-channels.js';
|
|
11
|
+
import { createDevChannels, validateChannels, BROADCAST_EVENT, DEV_CHANNEL_EVENT, DEV_CHANNEL_API } from './dev-channels.js';
|
|
12
|
+
import { createDevChannelHandlers, validateChannelHandlers } from './dev-channel-handlers.js';
|
|
12
13
|
import { renderDevChannelScript } from './dev-channel-shim.js';
|
|
13
14
|
import { createStreamStore, createUploadsMiddleware, createDownloadsMiddleware } from './dev-streams.js';
|
|
14
15
|
import { streamsClientSource } from './streams-client.js';
|
|
@@ -38,6 +39,36 @@ function buildMock(options) {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The app id dev surfaces address on the server (the opt-in dev `embed()`
|
|
44
|
+
* posts to its `_embed` route). `informer-init` generates it locally into
|
|
45
|
+
* package.json `informer.id` and the first deploy creates the app under it.
|
|
46
|
+
* Absent, the surface says so at call time rather than the dev server
|
|
47
|
+
* refusing to start.
|
|
48
|
+
*/
|
|
49
|
+
function readAppId(projectRoot) {
|
|
50
|
+
const pkgPath = resolve(projectRoot, 'package.json');
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw = readFileSync(pkgPath, 'utf8');
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// Unreadable is not the same as "no id set", and the two want
|
|
56
|
+
// different fixes. Saying which one it is beats telling someone to
|
|
57
|
+
// set a field in a file they are already looking at — or that the
|
|
58
|
+
// dev server never found.
|
|
59
|
+
if (err.code !== 'ENOENT') console.warn(`[informer] Could not read ${pkgPath}: ${err.message}`);
|
|
60
|
+
else console.warn(`[informer] No package.json at ${pkgPath}; dev surfaces that address the deployed app are unavailable.`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const pkg = JSON.parse(raw);
|
|
65
|
+
return (pkg.informer && pkg.informer.id) || null;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.warn(`[informer] Could not parse ${pkgPath}: ${err.message}`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
41
72
|
/**
|
|
42
73
|
* Occupied-spot check for the dev /api mount: does informer.yaml's
|
|
43
74
|
* access.apis declare this method+path? Mirrors the server's matchWhitelist
|
|
@@ -96,13 +127,16 @@ async function writeAppDepTypes (projectRoot, dts) {
|
|
|
96
127
|
*
|
|
97
128
|
* - Proxies /api requests to the Informer server with Basic auth
|
|
98
129
|
* - Runs server/ route handlers locally via ssrLoadModule (if server/ dir exists)
|
|
99
|
-
* -
|
|
100
|
-
* `channel()` mock
|
|
130
|
+
* - Runs channels/ handlers locally (join / joined / leave / send) behind
|
|
131
|
+
* the `channel()` mock injected with the window.__INFORMER__ context in
|
|
132
|
+
* dev mode, whose frames arrive over Vite's dev websocket
|
|
101
133
|
* - Sets base to './' so built assets use relative paths
|
|
102
134
|
*
|
|
103
135
|
* @param {Object} [options]
|
|
104
|
-
* @param {{ report?: object, theme?: 'light'|'dark', roles?: string[] }} [options.mock]
|
|
105
|
-
* window.__INFORMER__ mock injected in dev.
|
|
136
|
+
* @param {{ report?: object, theme?: 'light'|'dark', roles?: string[], user?: object, platform?: object }} [options.mock]
|
|
137
|
+
* window.__INFORMER__ mock injected in dev. `platform` merges into the dev
|
|
138
|
+
* descriptor, so `{ capabilities: { embeddings: true } }` opts one capability
|
|
139
|
+
* in and leaves the rest. See InformerPluginOptions in index.d.ts.
|
|
106
140
|
* @param {Object} [options.devBindings] - dev bindings for `target: app` and
|
|
107
141
|
* `target: pack` deps. App slots can't be defaultBound in the manifest; pack
|
|
108
142
|
* slots resolve their marketplace pin via installs dev doesn't have, so the
|
|
@@ -179,6 +213,15 @@ export default function informer(options = {}) {
|
|
|
179
213
|
const projectRoot = process.cwd();
|
|
180
214
|
const migrationsDir = resolve(projectRoot, 'migrations');
|
|
181
215
|
|
|
216
|
+
// One merged descriptor for the whole dev server. Built once
|
|
217
|
+
// rather than per consumer so "the channel hub, the handler bags
|
|
218
|
+
// and the browser all see the same mock" is structural — which is
|
|
219
|
+
// the property the embeddings opt-in depends on, since the flag
|
|
220
|
+
// the app reads and the flag that binds embed() must be the same
|
|
221
|
+
// flag. (transformIndexHtml is a separate hook and builds its own.)
|
|
222
|
+
const mock = buildMock(options);
|
|
223
|
+
const appId = readAppId(projectRoot);
|
|
224
|
+
|
|
182
225
|
// One API client for the whole dev-server setup — createClient just
|
|
183
226
|
// builds an auth header (no I/O), so it's hoisted out of the two
|
|
184
227
|
// branches that each used to rebuild an identical one.
|
|
@@ -257,6 +300,15 @@ export default function informer(options = {}) {
|
|
|
257
300
|
} catch (err) {
|
|
258
301
|
console.warn(`[informer] Could not read informer.yaml channels: ${err.message}`);
|
|
259
302
|
}
|
|
303
|
+
// And for channels/ files — the deploy 400s on a stray export or a
|
|
304
|
+
// path no page can subscribe to.
|
|
305
|
+
try {
|
|
306
|
+
for (const message of await validateChannelHandlers(projectRoot)) {
|
|
307
|
+
console.error(`[informer] ${message}`);
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
console.warn(`[informer] Could not read channels/: ${err.message}`);
|
|
311
|
+
}
|
|
260
312
|
|
|
261
313
|
// App Channels in dev. Handlers' broadcast() (and the `channels:`
|
|
262
314
|
// relay behind emit()) publish frames on this hub; each frame goes
|
|
@@ -265,10 +317,26 @@ export default function informer(options = {}) {
|
|
|
265
317
|
// import.meta.hot. Riding the HMR socket means no second websocket,
|
|
266
318
|
// no `ws: true` on the /api proxy and no socket credential to mint
|
|
267
319
|
// — the dev server already owns a live connection to every page.
|
|
268
|
-
|
|
320
|
+
// Every page receives every frame; the subscribe loop mounted below
|
|
321
|
+
// (join / joined / leave / send against channels/ files) is what
|
|
322
|
+
// tells a page which channels it may dispatch.
|
|
323
|
+
const channels = createDevChannels({ appId: mock.report.id });
|
|
269
324
|
channels.emitter.on(BROADCAST_EVENT, (frame) => {
|
|
270
325
|
server.ws.send({ type: 'custom', event: DEV_CHANNEL_EVENT, data: frame });
|
|
271
326
|
});
|
|
327
|
+
server.middlewares.use(DEV_CHANNEL_API, createDevChannelHandlers(server, {
|
|
328
|
+
serverOrigin,
|
|
329
|
+
authHeader,
|
|
330
|
+
devWorkspaceId,
|
|
331
|
+
projectRoot,
|
|
332
|
+
devBindings: options.devBindings || {},
|
|
333
|
+
appToken,
|
|
334
|
+
channels,
|
|
335
|
+
user: mock.user,
|
|
336
|
+
roles: (options.mock && options.mock.roles) || [],
|
|
337
|
+
platform: mock.platform,
|
|
338
|
+
appId
|
|
339
|
+
}));
|
|
272
340
|
|
|
273
341
|
// Generate .d.ts types for bound `target: app` / `target: pack`
|
|
274
342
|
// deps from their published OpenAPI docs, so server/ handlers get
|
|
@@ -343,6 +411,11 @@ export default function informer(options = {}) {
|
|
|
343
411
|
server.middlewares.use('/_uploads', createUploadsMiddleware(streamStore));
|
|
344
412
|
server.middlewares.use('/_downloads', createDownloadsMiddleware(streamStore));
|
|
345
413
|
|
|
414
|
+
// The merged platform descriptor (dev defaults + mock.platform) is
|
|
415
|
+
// what the handler bags see too, not only the browser: an app that
|
|
416
|
+
// opts into `embeddings` there must find embed() bound, not throwing.
|
|
417
|
+
const platform = mock.platform;
|
|
418
|
+
|
|
346
419
|
// Mount server-side route handlers if a server/ directory exists
|
|
347
420
|
const serverDir = resolve(projectRoot, 'server');
|
|
348
421
|
|
|
@@ -353,7 +426,7 @@ export default function informer(options = {}) {
|
|
|
353
426
|
devWorkspaceId,
|
|
354
427
|
projectRoot,
|
|
355
428
|
roles: (options.mock && options.mock.roles) || [],
|
|
356
|
-
user:
|
|
429
|
+
user: mock.user,
|
|
357
430
|
// Dev-only bindings for `target: app` / `target: pack`
|
|
358
431
|
// slots (app: overrides the manifest defaultBinding; pack:
|
|
359
432
|
// names the locally-installed pack app). Shape:
|
|
@@ -361,7 +434,9 @@ export default function informer(options = {}) {
|
|
|
361
434
|
devBindings: options.devBindings || {},
|
|
362
435
|
appToken,
|
|
363
436
|
channels,
|
|
364
|
-
streamStore
|
|
437
|
+
streamStore,
|
|
438
|
+
platform,
|
|
439
|
+
appId
|
|
365
440
|
});
|
|
366
441
|
server.middlewares.use('/api/_server', serverRoutes);
|
|
367
442
|
// Occupied-spot precedence (App API v2, matches production
|
|
@@ -393,7 +468,9 @@ export default function informer(options = {}) {
|
|
|
393
468
|
projectRoot,
|
|
394
469
|
devBindings: options.devBindings || {},
|
|
395
470
|
appToken,
|
|
396
|
-
channels
|
|
471
|
+
channels,
|
|
472
|
+
platform,
|
|
473
|
+
appId
|
|
397
474
|
});
|
|
398
475
|
server.middlewares.use('/api/_agent', agentDev);
|
|
399
476
|
}
|
|
@@ -421,6 +498,7 @@ export default function informer(options = {}) {
|
|
|
421
498
|
var __streams = ${streamsClientSource()};
|
|
422
499
|
window.__INFORMER__.upload = __streams.upload;
|
|
423
500
|
window.__INFORMER__.downloadUrl = __streams.downloadUrl;
|
|
501
|
+
window.__INFORMER__.streams = __streams.streams;
|
|
424
502
|
})();
|
|
425
503
|
</script>
|
|
426
504
|
${renderDevChannelScript({ hub: Boolean(serverOrigin) })}`;
|