@entrinsik/vite-plugin-informer 2.10.0 → 2.11.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 +3 -1
- package/bin/workspace.js +15 -7
- package/package.json +1 -1
- package/src/dev-bag.js +185 -0
- package/src/dev-channel-handlers.js +325 -0
- package/src/dev-channel-shim.js +248 -37
- package/src/dev-channels.js +119 -27
- package/src/dev-platform.js +3 -0
- package/src/env.js +17 -1
- package/src/index.js +28 -3
- package/src/server-routes.js +78 -211
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';
|
|
@@ -96,8 +97,9 @@ async function writeAppDepTypes (projectRoot, dts) {
|
|
|
96
97
|
*
|
|
97
98
|
* - Proxies /api requests to the Informer server with Basic auth
|
|
98
99
|
* - Runs server/ route handlers locally via ssrLoadModule (if server/ dir exists)
|
|
99
|
-
* -
|
|
100
|
-
* `channel()` mock
|
|
100
|
+
* - Runs channels/ handlers locally (join / joined / leave / send) behind
|
|
101
|
+
* the `channel()` mock injected with the window.__INFORMER__ context in
|
|
102
|
+
* dev mode, whose frames arrive over Vite's dev websocket
|
|
101
103
|
* - Sets base to './' so built assets use relative paths
|
|
102
104
|
*
|
|
103
105
|
* @param {Object} [options]
|
|
@@ -257,6 +259,15 @@ export default function informer(options = {}) {
|
|
|
257
259
|
} catch (err) {
|
|
258
260
|
console.warn(`[informer] Could not read informer.yaml channels: ${err.message}`);
|
|
259
261
|
}
|
|
262
|
+
// And for channels/ files — the deploy 400s on a stray export or a
|
|
263
|
+
// path no page can subscribe to.
|
|
264
|
+
try {
|
|
265
|
+
for (const message of await validateChannelHandlers(projectRoot)) {
|
|
266
|
+
console.error(`[informer] ${message}`);
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.warn(`[informer] Could not read channels/: ${err.message}`);
|
|
270
|
+
}
|
|
260
271
|
|
|
261
272
|
// App Channels in dev. Handlers' broadcast() (and the `channels:`
|
|
262
273
|
// relay behind emit()) publish frames on this hub; each frame goes
|
|
@@ -265,10 +276,24 @@ export default function informer(options = {}) {
|
|
|
265
276
|
// import.meta.hot. Riding the HMR socket means no second websocket,
|
|
266
277
|
// no `ws: true` on the /api proxy and no socket credential to mint
|
|
267
278
|
// — the dev server already owns a live connection to every page.
|
|
279
|
+
// Every page receives every frame; the subscribe loop mounted below
|
|
280
|
+
// (join / joined / leave / send against channels/ files) is what
|
|
281
|
+
// tells a page which channels it may dispatch.
|
|
268
282
|
const channels = createDevChannels({ appId: buildMock(options).report.id });
|
|
269
283
|
channels.emitter.on(BROADCAST_EVENT, (frame) => {
|
|
270
284
|
server.ws.send({ type: 'custom', event: DEV_CHANNEL_EVENT, data: frame });
|
|
271
285
|
});
|
|
286
|
+
server.middlewares.use(DEV_CHANNEL_API, createDevChannelHandlers(server, {
|
|
287
|
+
serverOrigin,
|
|
288
|
+
authHeader,
|
|
289
|
+
devWorkspaceId,
|
|
290
|
+
projectRoot,
|
|
291
|
+
devBindings: options.devBindings || {},
|
|
292
|
+
appToken,
|
|
293
|
+
channels,
|
|
294
|
+
user: buildMock(options).user,
|
|
295
|
+
roles: (options.mock && options.mock.roles) || []
|
|
296
|
+
}));
|
|
272
297
|
|
|
273
298
|
// Generate .d.ts types for bound `target: app` / `target: pack`
|
|
274
299
|
// deps from their published OpenAPI docs, so server/ handlers get
|
package/src/server-routes.js
CHANGED
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from 'node:path';
|
|
3
3
|
import { parse as parseUrl } from 'node:url';
|
|
4
|
-
import {
|
|
5
|
-
import { createDevChannels
|
|
6
|
-
import { devPlatform } from './dev-platform.js';
|
|
4
|
+
import { createDevBagBuilder, buildDevUser } from './dev-bag.js';
|
|
5
|
+
import { createDevChannels } from './dev-channels.js';
|
|
7
6
|
import { createStreamStore, createStreamServices, isStreamRef, serveDownload as serveDevDownload } from './dev-streams.js';
|
|
8
7
|
|
|
9
|
-
// embed() is present on a real install even without the embeddings
|
|
10
|
-
// capability, where it throws a written explanation. Mirroring that here
|
|
11
|
-
// keeps the dev failure the same lesson as the deployed one, instead of a
|
|
12
|
-
// bare "embed is not a function" that reads like a missing binding.
|
|
13
|
-
const embed = async () => {
|
|
14
|
-
throw new Error('embed() is not available in the dev mirror: the embeddings capability needs a real Informer (platform.capabilities.embeddings is false)');
|
|
15
|
-
};
|
|
16
|
-
|
|
17
8
|
const VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
18
9
|
|
|
19
|
-
// Cap dev proxy calls at 30s so a hung upstream fails loudly instead of
|
|
20
|
-
// hanging the handler.
|
|
21
|
-
const FETCH_TIMEOUT_MS = 30000;
|
|
22
|
-
|
|
23
10
|
// Strict base64 — see view-api.js for the rationale. Mirror kept identical to
|
|
24
11
|
// keep dev and prod behavior aligned, down to the scan: the quantified
|
|
25
12
|
// /^[A-Za-z0-9+/]+={0,2}$/ throws "RangeError: Maximum call stack size
|
|
@@ -37,17 +24,19 @@ function isBase64(s) {
|
|
|
37
24
|
}
|
|
38
25
|
|
|
39
26
|
/**
|
|
40
|
-
* Convert a file path under
|
|
27
|
+
* Convert a file path under a handler directory to a route path (mirrors the
|
|
28
|
+
* server's route-scanner.js filePathToRoute).
|
|
41
29
|
*
|
|
42
30
|
* Examples:
|
|
43
|
-
* server/orders/index.js
|
|
44
|
-
* server/orders/[id].js
|
|
31
|
+
* server/orders/index.js -> /orders
|
|
32
|
+
* server/orders/[id].js -> /orders/:id
|
|
45
33
|
* server/orders/[id]/approve.js -> /orders/:id/approve
|
|
46
|
-
* server/index.js
|
|
34
|
+
* server/index.js -> /
|
|
35
|
+
* channels/rooms/[room].js -> /rooms/:room (dirPrefix 'channels')
|
|
47
36
|
*/
|
|
48
|
-
function filePathToRoute(filePath) {
|
|
37
|
+
function filePathToRoute(filePath, dirPrefix = 'server') {
|
|
49
38
|
let route = filePath
|
|
50
|
-
.replace(
|
|
39
|
+
.replace(new RegExp(`^${dirPrefix}/`), '')
|
|
51
40
|
.replace(/\.js$/, '');
|
|
52
41
|
|
|
53
42
|
route = route.replace(/\[([^\]]+)\]/g, ':$1');
|
|
@@ -143,7 +132,7 @@ async function scanRoutes(serverDir) {
|
|
|
143
132
|
}));
|
|
144
133
|
}
|
|
145
134
|
|
|
146
|
-
export { filePathToRoute, walkJsFiles };
|
|
135
|
+
export { filePathToRoute, matchRoute, walkJsFiles };
|
|
147
136
|
|
|
148
137
|
async function walkJsFiles(dir, basePath) {
|
|
149
138
|
const results = [];
|
|
@@ -169,6 +158,54 @@ async function walkJsFiles(dir, basePath) {
|
|
|
169
158
|
return results;
|
|
170
159
|
}
|
|
171
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Write a handler's `{ status, headers?, body?, encoding? }` response
|
|
163
|
+
* descriptor (mirrors app-sandbox.js buildInvokeScript / respondCallback).
|
|
164
|
+
* The encoding allow-list and contract checks are enforced inside the ivm in
|
|
165
|
+
* production; replicated here so handlers see the same errors in dev (where
|
|
166
|
+
* there's no isolate to attribute the throw to). If you change this, mirror
|
|
167
|
+
* it in modules/app/routes/view-api.js.
|
|
168
|
+
*/
|
|
169
|
+
function sendDescriptor(res, result) {
|
|
170
|
+
const encoding = typeof result.encoding === 'string' ? result.encoding : null;
|
|
171
|
+
if (encoding !== null && encoding !== 'base64') {
|
|
172
|
+
throw new Error(`Unknown response encoding: ${JSON.stringify(encoding)} (expected 'base64' or omitted)`);
|
|
173
|
+
}
|
|
174
|
+
if (encoding === 'base64' && typeof result.body !== 'string') {
|
|
175
|
+
throw new Error(`encoding: 'base64' requires body to be a base64-encoded string, got ${typeof result.body}`);
|
|
176
|
+
}
|
|
177
|
+
const responseBody = result.body !== undefined ? result.body : null;
|
|
178
|
+
|
|
179
|
+
res.statusCode = result.status || 200;
|
|
180
|
+
for (const [key, value] of Object.entries(result.headers || {})) {
|
|
181
|
+
res.setHeader(key, value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (responseBody === null) {
|
|
185
|
+
res.end();
|
|
186
|
+
} else if (encoding === 'base64') {
|
|
187
|
+
if (!isBase64(responseBody)) {
|
|
188
|
+
throw new Error('Handler returned malformed base64 body');
|
|
189
|
+
}
|
|
190
|
+
res.end(Buffer.from(responseBody, 'base64'));
|
|
191
|
+
} else if (typeof responseBody === 'string') {
|
|
192
|
+
// Strings go out verbatim, as prod passes them.
|
|
193
|
+
if (!res.getHeader('content-type')) {
|
|
194
|
+
res.setHeader('Content-Type', 'application/json');
|
|
195
|
+
}
|
|
196
|
+
res.end(responseBody);
|
|
197
|
+
} else {
|
|
198
|
+
if (!res.getHeader('content-type')) {
|
|
199
|
+
res.setHeader('Content-Type', 'application/json');
|
|
200
|
+
}
|
|
201
|
+
res.end(JSON.stringify(responseBody));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isDescriptor(value) {
|
|
206
|
+
return value !== null && typeof value === 'object' && typeof value.status === 'number';
|
|
207
|
+
}
|
|
208
|
+
|
|
172
209
|
/**
|
|
173
210
|
* Create Connect middleware for dev-mode server route execution.
|
|
174
211
|
*
|
|
@@ -188,99 +225,13 @@ async function walkJsFiles(dir, basePath) {
|
|
|
188
225
|
* @returns {Function} Connect middleware
|
|
189
226
|
*/
|
|
190
227
|
export function createMiddleware(viteServer, { serverOrigin, authHeader, devWorkspaceId, projectRoot, roles, user, devBindings, appToken, channels = createDevChannels(), streamStore }) {
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
const devUser = {
|
|
195
|
-
username: (user && user.username) || 'dev',
|
|
196
|
-
displayName: (user && user.displayName) || 'Local Developer',
|
|
197
|
-
email: (user && user.email) || null,
|
|
198
|
-
timezone: (user && user.timezone) || null
|
|
199
|
-
};
|
|
228
|
+
// `@user/${request.user.username}` on the server names the channel the
|
|
229
|
+
// page subscribes to.
|
|
230
|
+
const devUser = buildDevUser(user);
|
|
200
231
|
const serverDir = join(projectRoot, 'server');
|
|
201
232
|
const streams = streamStore || createStreamStore();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
async function query(sql, params) {
|
|
205
|
-
if (!devWorkspaceId) {
|
|
206
|
-
throw new Error('query() requires INFORMER_DEV_WORKSPACE. Run: npx informer-workspace init');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const resp = await globalThis.fetch(`${serverOrigin}/api/datasources/${devWorkspaceId}/_sql`, {
|
|
210
|
-
method: 'POST',
|
|
211
|
-
headers: { 'Content-Type': 'application/json', Authorization: authHeader },
|
|
212
|
-
body: JSON.stringify({ sql, params: params || [] })
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
if (!resp.ok) {
|
|
216
|
-
const err = await resp.json().catch(() => ({}));
|
|
217
|
-
const detail = err.message || resp.statusText;
|
|
218
|
-
throw new Error(`query() failed: ${resp.status} ${detail} (${serverOrigin}/api/datasources/${devWorkspaceId}/_sql)`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const data = await resp.json();
|
|
222
|
-
return data.rows;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// fetch() implementation — proxies API calls to the Informer server
|
|
226
|
-
async function fetchAs(auth, path, opts = {}) {
|
|
227
|
-
const method = (opts.method || 'GET').toUpperCase();
|
|
228
|
-
// Same strict canonicalization as prod (app-sandbox.js normalizeFetchPath);
|
|
229
|
-
// reject non-canonical shapes here instead of silently accepting them.
|
|
230
|
-
const apiPath = normalizeFetchPath(path);
|
|
231
|
-
if (!apiPath) {
|
|
232
|
-
return { status: 400, body: { error: `Invalid fetch path: ${String(path).slice(0, 80)}` }, contentType: 'application/json' };
|
|
233
|
-
}
|
|
234
|
-
const url = `${serverOrigin}${apiPath}`;
|
|
235
|
-
const fetchOpts = {
|
|
236
|
-
method,
|
|
237
|
-
headers: { Authorization: auth, 'Content-Type': 'application/json', ...(opts.headers || {}) }
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
if (opts.body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
241
|
-
fetchOpts.body = JSON.stringify(opts.body);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Read the stream exactly once — `.json()` consumes/locks the body, so a
|
|
245
|
-
// `.text()` fallback would throw "Body is unusable" on any non-JSON
|
|
246
|
-
// response (auth-bounce HTML, proxy error page). Parse in memory
|
|
247
|
-
// instead — same as prod's unwrapInject.
|
|
248
|
-
let status, contentType, text;
|
|
249
|
-
try {
|
|
250
|
-
const resp = await globalThis.fetch(url, { ...fetchOpts, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
251
|
-
status = resp.status;
|
|
252
|
-
contentType = resp.headers.get('content-type') || '';
|
|
253
|
-
text = await resp.text();
|
|
254
|
-
} catch (err) {
|
|
255
|
-
// Transport failure or timeout — fetch throws (TypeError 'fetch failed'
|
|
256
|
-
// with the real reason on err.cause, or a TimeoutError). Return a
|
|
257
|
-
// synthetic 502 so the dependency layer names it (dep + url + cause)
|
|
258
|
-
// rather than a bare unhandled "fetch failed".
|
|
259
|
-
const reason = (err.cause && err.cause.message) || err.message;
|
|
260
|
-
return { status: 502, body: { message: `fetch to ${url} failed: ${reason}` }, contentType: 'application/json' };
|
|
261
|
-
}
|
|
262
|
-
let body;
|
|
263
|
-
try { body = JSON.parse(text); } catch { body = text; }
|
|
264
|
-
return { status, body, contentType };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function apiFetch(path, opts = {}) {
|
|
268
|
-
return await fetchAs(authHeader, path, opts);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Cross-app request() targets /api/apps/<id>/view/_/<path>, whose auth accepts
|
|
272
|
-
// only the token/session strategies — NOT basic auth. In API-key mode the
|
|
273
|
-
// INFORMER_API_KEY Bearer already satisfies that, so reuse it; under basic
|
|
274
|
-
// auth a separate API token (INFORMER_APP_TOKEN) is required, and without one
|
|
275
|
-
// the app proxy's request() throws a pointed error instead of a bare 401.
|
|
276
|
-
// appFetch also stamps x-informer-app-depth:1 so the target runs one hop deep
|
|
277
|
-
// and enforces the same one-hop guard it does in production.
|
|
278
|
-
const appAuth = appToken
|
|
279
|
-
? `Bearer ${appToken}`
|
|
280
|
-
: (authHeader && authHeader.startsWith('Bearer ') ? authHeader : null);
|
|
281
|
-
const appFetch = appAuth
|
|
282
|
-
? async (path, opts = {}) => await fetchAs(appAuth, path, { ...opts, headers: { ...(opts.headers || {}), 'x-informer-app-depth': '1' } })
|
|
283
|
-
: null;
|
|
233
|
+
const bagBuilder = createDevBagBuilder({ serverOrigin, authHeader, devWorkspaceId, projectRoot, devBindings, appToken, channels, logPrefix: '[app]' });
|
|
234
|
+
const { query } = bagBuilder;
|
|
284
235
|
|
|
285
236
|
return async function serverRoutesMiddleware(req, res, next) {
|
|
286
237
|
try {
|
|
@@ -325,34 +276,6 @@ export function createMiddleware(viteServer, { serverOrigin, authHeader, devWork
|
|
|
325
276
|
try { body = JSON.parse(rawBody); } catch { body = rawBody; }
|
|
326
277
|
}
|
|
327
278
|
|
|
328
|
-
// crypto helper — mirrors the prod sandbox crypto surface
|
|
329
|
-
const cryptoHelper = buildDevCrypto();
|
|
330
|
-
|
|
331
|
-
// log helper — mirrors sandbox log(message, data) + log.info/warn/error/debug
|
|
332
|
-
const logCall = (level, message, data) => {
|
|
333
|
-
const msg = typeof message === 'string' ? message : JSON.stringify(message);
|
|
334
|
-
const args = [`[app-log] [${level}] ${msg}`];
|
|
335
|
-
if (data) args.push(data);
|
|
336
|
-
console.log(...args);
|
|
337
|
-
};
|
|
338
|
-
const log = Object.assign(
|
|
339
|
-
(message, data) => logCall('info', message, data),
|
|
340
|
-
{
|
|
341
|
-
debug: (message, data) => logCall('debug', message, data),
|
|
342
|
-
info: (message, data) => logCall('info', message, data),
|
|
343
|
-
warn: (message, data) => logCall('warn', message, data),
|
|
344
|
-
error: (message, data) => logCall('error', message, data)
|
|
345
|
-
}
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
// markdown helper — passthrough in dev (production uses `marked`)
|
|
349
|
-
const markdown = (text) => text;
|
|
350
|
-
|
|
351
|
-
// notify/email — delivery is a console-logged no-op in dev, but the
|
|
352
|
-
// required-field validation mirrors prod so an app that passes here
|
|
353
|
-
// won't 500 in production.
|
|
354
|
-
const { notify, email } = buildDevMessaging('[app]');
|
|
355
|
-
|
|
356
279
|
// Build request context
|
|
357
280
|
const request = {
|
|
358
281
|
method: req.method.toUpperCase(),
|
|
@@ -387,8 +310,11 @@ export function createMiddleware(viteServer, { serverOrigin, authHeader, devWork
|
|
|
387
310
|
serveDevDownload(streams, item, res);
|
|
388
311
|
}
|
|
389
312
|
|
|
390
|
-
// respond() — sends early response, handler continues in
|
|
391
|
-
//
|
|
313
|
+
// respond() — sends the early response, the handler continues in
|
|
314
|
+
// the background. A `{ status, headers?, body?, encoding? }`
|
|
315
|
+
// descriptor is honored as such (app-sandbox.js respondCallback);
|
|
316
|
+
// anything else is wrapped as 200 JSON; a download handle streams
|
|
317
|
+
// the staged bytes.
|
|
392
318
|
let responded = false;
|
|
393
319
|
async function respond(earlyBody) {
|
|
394
320
|
if (responded) return;
|
|
@@ -401,29 +327,16 @@ export function createMiddleware(viteServer, { serverOrigin, authHeader, devWork
|
|
|
401
327
|
return serveDownloadHandle(earlyBody);
|
|
402
328
|
}
|
|
403
329
|
responded = true;
|
|
330
|
+
if (isDescriptor(earlyBody)) return sendDescriptor(res, earlyBody);
|
|
404
331
|
res.statusCode = 200;
|
|
405
332
|
res.setHeader('Content-Type', 'application/json');
|
|
406
333
|
res.end(JSON.stringify(earlyBody));
|
|
407
334
|
}
|
|
408
335
|
|
|
409
|
-
|
|
410
|
-
// sandbox does, so handlers using `await context.myDep.method(...)`
|
|
411
|
-
// work locally. The manifest is parsed once per request (deps, env
|
|
412
|
-
// and channels all come from that one read) so edits to
|
|
413
|
-
// informer.yaml take effect without a dev-server restart.
|
|
414
|
-
const manifest = await loadManifest(projectRoot);
|
|
415
|
-
const deps = manifestBlock(manifest, 'dependencies');
|
|
416
|
-
const context = buildDevContext({ deps, apiFetch, devBindings, appFetch });
|
|
417
|
-
const env = manifestBlock(manifest, 'env');
|
|
418
|
-
|
|
419
|
-
// emit() writes no app_event row in dev, but still relays a listed
|
|
420
|
-
// event to its `channels:` channel; broadcast() publishes a live
|
|
421
|
-
// frame to the page (see dev-channels.js).
|
|
422
|
-
const emit = createDevEmit({ channels, manifestChannels: manifestBlock(manifest, 'channels'), logPrefix: '[app-event]' });
|
|
423
|
-
const { broadcast } = channels;
|
|
336
|
+
const { bag } = await bagBuilder.build();
|
|
424
337
|
|
|
425
338
|
// Call handler — bag mirrors the prod sandbox (see app-sandbox.js).
|
|
426
|
-
const result = await handler({
|
|
339
|
+
const result = await handler({ ...bag, request, query: streamQuery, respond, uploads: streamServices.uploads, downloads: streamServices.downloads });
|
|
427
340
|
|
|
428
341
|
// Seal any download the handler left open (prod finalizes the same way).
|
|
429
342
|
await streamServices.finalize();
|
|
@@ -434,60 +347,14 @@ export function createMiddleware(viteServer, { serverOrigin, authHeader, devWork
|
|
|
434
347
|
// A returned download handle streams as the response.
|
|
435
348
|
if (isStreamRef(result, 'download')) return serveDownloadHandle(result);
|
|
436
349
|
|
|
437
|
-
// Normalize
|
|
438
|
-
// The encoding allow-list and contract checks are enforced inside the
|
|
439
|
-
// ivm in production; replicate them here so handlers see the same
|
|
440
|
-
// errors in dev (where there's no isolate to attribute the throw to).
|
|
441
|
-
// If you change this, mirror it in modules/app/routes/view-api.js.
|
|
442
|
-
let status, responseBody, responseHeaders, encoding;
|
|
443
|
-
|
|
350
|
+
// Normalize the return value (mirrors app-sandbox.js buildInvokeScript).
|
|
444
351
|
if (result === undefined || result === null) {
|
|
445
|
-
|
|
446
|
-
responseBody = null;
|
|
447
|
-
responseHeaders = {};
|
|
448
|
-
} else if (typeof result === 'object' && typeof result.status === 'number') {
|
|
449
|
-
encoding = typeof result.encoding === 'string' ? result.encoding : null;
|
|
450
|
-
if (encoding !== null && encoding !== 'base64') {
|
|
451
|
-
throw new Error(`Unknown response encoding: ${JSON.stringify(encoding)} (expected 'base64' or omitted)`);
|
|
452
|
-
}
|
|
453
|
-
if (encoding === 'base64' && typeof result.body !== 'string') {
|
|
454
|
-
throw new Error(`encoding: 'base64' requires body to be a base64-encoded string, got ${typeof result.body}`);
|
|
455
|
-
}
|
|
456
|
-
status = result.status || 200;
|
|
457
|
-
responseBody = result.body !== undefined ? result.body : null;
|
|
458
|
-
responseHeaders = result.headers || {};
|
|
459
|
-
} else {
|
|
460
|
-
status = 200;
|
|
461
|
-
responseBody = result;
|
|
462
|
-
responseHeaders = { 'content-type': 'application/json' };
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
res.statusCode = status;
|
|
466
|
-
for (const [key, value] of Object.entries(responseHeaders)) {
|
|
467
|
-
res.setHeader(key, value);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (responseBody === null) {
|
|
352
|
+
res.statusCode = 204;
|
|
471
353
|
res.end();
|
|
472
|
-
} else if (
|
|
473
|
-
|
|
474
|
-
throw new Error('Handler returned malformed base64 body');
|
|
475
|
-
}
|
|
476
|
-
res.end(Buffer.from(responseBody, 'base64'));
|
|
477
|
-
} else if (typeof responseBody === 'string') {
|
|
478
|
-
// Pre-PR the dev middleware always JSON.stringify'd the body, so
|
|
479
|
-
// a handler returning { body: 'hello' } emitted "hello" (with
|
|
480
|
-
// quotes) — diverging from prod which passed strings verbatim.
|
|
481
|
-
// This branch fixes that parity.
|
|
482
|
-
if (!res.getHeader('content-type')) {
|
|
483
|
-
res.setHeader('Content-Type', 'application/json');
|
|
484
|
-
}
|
|
485
|
-
res.end(responseBody);
|
|
354
|
+
} else if (isDescriptor(result)) {
|
|
355
|
+
sendDescriptor(res, result);
|
|
486
356
|
} else {
|
|
487
|
-
|
|
488
|
-
res.setHeader('Content-Type', 'application/json');
|
|
489
|
-
}
|
|
490
|
-
res.end(JSON.stringify(responseBody));
|
|
357
|
+
sendDescriptor(res, { status: 200, headers: { 'content-type': 'application/json' }, body: result });
|
|
491
358
|
}
|
|
492
359
|
} catch (err) {
|
|
493
360
|
viteServer.ssrFixStacktrace(err);
|