@entrinsik/vite-plugin-informer 2.7.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 +131 -0
- package/bin/ci.js +0 -0
- package/bin/workspace.js +15 -7
- package/package.json +1 -1
- package/src/agent-dev.js +26 -12
- package/src/assemble.js +5 -1
- package/src/compat.js +252 -0
- package/src/deploy.js +116 -43
- package/src/dev-bag.js +185 -0
- package/src/dev-channel-handlers.js +325 -0
- package/src/dev-channel-shim.js +361 -0
- package/src/dev-channels.js +283 -0
- package/src/dev-dependencies.js +44 -17
- package/src/dev-platform.js +44 -0
- package/src/dev-streams.js +660 -0
- package/src/env.js +17 -1
- package/src/index.js +156 -16
- package/src/server-routes.js +121 -196
- package/src/streams-client.js +200 -0
package/src/deploy.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createClient } from './client.js';
|
|
2
2
|
import { collectAppFiles } from './assemble.js';
|
|
3
|
+
import { describeServer, planDeploy } from './compat.js';
|
|
4
|
+
import { loadManifest } from './dev-dependencies.js';
|
|
3
5
|
import { readFile } from 'node:fs/promises';
|
|
4
6
|
import { basename, dirname } from 'node:path';
|
|
5
7
|
|
|
@@ -22,6 +24,20 @@ const CHUNK_THRESHOLD = 512 * 1024; // 512KB
|
|
|
22
24
|
*/
|
|
23
25
|
export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, description, icon, id }) {
|
|
24
26
|
const api = createClient({ baseUrl, apiKey, user, pass });
|
|
27
|
+
const projectRoot = dirname(distDir);
|
|
28
|
+
|
|
29
|
+
// 0. Assemble and version-check before touching the app. Both steps below
|
|
30
|
+
// are destructive — the snapshot rotates and _clear empties the library —
|
|
31
|
+
// so a missing dist/ or a refused version floor has to surface here,
|
|
32
|
+
// while the deployed app is still whole.
|
|
33
|
+
const collected = await collectAppFiles({ distDir, projectRoot });
|
|
34
|
+
const server = await describeServer(api);
|
|
35
|
+
const plan = planDeploy({ server, files: collected, manifest: await loadManifest(projectRoot) });
|
|
36
|
+
|
|
37
|
+
for (const warning of plan.warnings) {
|
|
38
|
+
console.warn(` ! ${warning}`);
|
|
39
|
+
}
|
|
40
|
+
if (plan.refusal) throw new Error(plan.refusal);
|
|
25
41
|
|
|
26
42
|
let entity = null;
|
|
27
43
|
let naturalId = null;
|
|
@@ -104,12 +120,14 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
|
|
|
104
120
|
console.log('Clearing existing files...');
|
|
105
121
|
await api.post(`${entityPath}/files/_clear`);
|
|
106
122
|
|
|
107
|
-
// 6. Upload the app-library file set: dist output at the
|
|
108
|
-
// informer.yaml / data-access.yaml and the
|
|
109
|
-
// source trees
|
|
110
|
-
//
|
|
123
|
+
// 6. Upload the app-library file set assembled in step 0: dist output at the
|
|
124
|
+
// library root, plus informer.yaml / data-access.yaml and the
|
|
125
|
+
// server/tools/mcp/migrations/webhooks/embeddings source trees, less any
|
|
126
|
+
// tree this server is too old to keep out of browsers (see compat.js).
|
|
127
|
+
// Sourced from the shared collectAppFiles() so a deploy and a marketplace
|
|
128
|
+
// publish package byte-identical contents.
|
|
111
129
|
console.log('Uploading files...');
|
|
112
|
-
const files =
|
|
130
|
+
const files = plan.files;
|
|
113
131
|
|
|
114
132
|
for (const { abs, rel } of files) {
|
|
115
133
|
const content = await readFile(abs);
|
|
@@ -136,45 +154,9 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
|
|
|
136
154
|
}
|
|
137
155
|
}
|
|
138
156
|
|
|
139
|
-
// 12. Deploy: run migrations + scan/bundle server routes + webhooks + tools + agents
|
|
157
|
+
// 12. Deploy: run migrations + scan/bundle server routes + webhooks + embeddings + tools + agents
|
|
140
158
|
if (apiPrefix === 'apps') {
|
|
141
|
-
|
|
142
|
-
console.log('Deploying...');
|
|
143
|
-
const result = await api.post(`${entityPath}/_deploy`);
|
|
144
|
-
if (result) {
|
|
145
|
-
if (result.migrated && result.migrated.length > 0) {
|
|
146
|
-
console.log(` Ran ${result.migrated.length} migration(s): ${result.migrated.join(', ')}`);
|
|
147
|
-
}
|
|
148
|
-
if (result.routes && result.routes.length > 0) {
|
|
149
|
-
console.log(` Registered ${result.routes.length} server route(s):`);
|
|
150
|
-
for (const r of result.routes) {
|
|
151
|
-
console.log(` ${r}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (result.webhooks && result.webhooks.length > 0) {
|
|
155
|
-
console.log(` Registered ${result.webhooks.length} webhook(s):`);
|
|
156
|
-
for (const r of result.webhooks) {
|
|
157
|
-
console.log(` ${r}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (result.tools && result.tools.length > 0) {
|
|
161
|
-
console.log(` Registered ${result.tools.length} tool(s): ${result.tools.join(', ')}`);
|
|
162
|
-
}
|
|
163
|
-
if (result.mcpTools && result.mcpTools.length > 0) {
|
|
164
|
-
console.log(` Registered ${result.mcpTools.length} MCP tool(s): ${result.mcpTools.join(', ')}`);
|
|
165
|
-
}
|
|
166
|
-
if (result.agents && result.agents.length > 0) {
|
|
167
|
-
console.log(` Deployed ${result.agents.length} agent(s): ${result.agents.join(', ')}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
} catch (err) {
|
|
171
|
-
if (err.status === 404) {
|
|
172
|
-
// Server may not support _deploy yet — ignore
|
|
173
|
-
} else {
|
|
174
|
-
const detail = err.body || err.message;
|
|
175
|
-
console.error(` Deploy failed: ${detail}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
159
|
+
await runServerDeploy(api, entityPath);
|
|
178
160
|
}
|
|
179
161
|
|
|
180
162
|
// 13. Print URL and return UUID for saving
|
|
@@ -188,6 +170,97 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
|
|
|
188
170
|
return { id: entity.id, url: entityUrl };
|
|
189
171
|
}
|
|
190
172
|
|
|
173
|
+
/**
|
|
174
|
+
* POST `_deploy`: run migrations, scan/bundle server routes, webhooks, tools and
|
|
175
|
+
* agents, and claim the app as platform-managed.
|
|
176
|
+
*
|
|
177
|
+
* Exported for its own tests. This is the step whose failure modes are silent —
|
|
178
|
+
* everything it does is invisible in the CLI output when it doesn't happen.
|
|
179
|
+
*
|
|
180
|
+
* @param {{post: (path: string, body?: unknown) => Promise<unknown>}} api
|
|
181
|
+
* @param {string} entityPath e.g. `apps/my-app`
|
|
182
|
+
*/
|
|
183
|
+
export async function runServerDeploy(api, entityPath) {
|
|
184
|
+
try {
|
|
185
|
+
console.log('Deploying...');
|
|
186
|
+
// `managed: true` claims the app as platform-managed (origin
|
|
187
|
+
// 'deployed'), which locks builder editing so a UI edit can't drift
|
|
188
|
+
// from this project. Opt-in: the server leaves an app editable when the
|
|
189
|
+
// flag is absent, so the GO admin panel's Redeploy and the builder's
|
|
190
|
+
// Save can share this route without converting user-built apps. See
|
|
191
|
+
// app/routes/deploy.js.
|
|
192
|
+
const result = await api.post(`${entityPath}/_deploy`, { managed: true });
|
|
193
|
+
if (result === null) {
|
|
194
|
+
// api.post turns a 404 into null instead of throwing, so this is the
|
|
195
|
+
// only place it can be caught. Files uploaded, but migrations, server
|
|
196
|
+
// routes, webhooks and the managed claim did NOT run — and a missing
|
|
197
|
+
// route is far more often a wrong app id or a proxy path rewrite than
|
|
198
|
+
// a server too old to have _deploy. Falling through here prints
|
|
199
|
+
// "Published ..." and exits 0 on a half-finished deploy.
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Deploy endpoint not found at ${entityPath}/_deploy. The app may not exist on this server, `
|
|
202
|
+
+ `the path may be rewritten by a proxy, or the server may predate _deploy. `
|
|
203
|
+
+ `Files were uploaded, but migrations, server routes and the managed claim did not run.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (result.migrated && result.migrated.length > 0) {
|
|
207
|
+
console.log(` Ran ${result.migrated.length} migration(s): ${result.migrated.join(', ')}`);
|
|
208
|
+
}
|
|
209
|
+
if (result.routes && result.routes.length > 0) {
|
|
210
|
+
console.log(` Registered ${result.routes.length} server route(s):`);
|
|
211
|
+
for (const r of result.routes) {
|
|
212
|
+
console.log(` ${r}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (result.webhooks && result.webhooks.length > 0) {
|
|
216
|
+
console.log(` Registered ${result.webhooks.length} webhook(s):`);
|
|
217
|
+
for (const r of result.webhooks) {
|
|
218
|
+
console.log(` ${r}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (result.channelHandlers && result.channelHandlers.length > 0) {
|
|
222
|
+
console.log(` Registered ${result.channelHandlers.length} channel handler(s):`);
|
|
223
|
+
for (const c of result.channelHandlers) {
|
|
224
|
+
console.log(` ${c}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (result.channels && result.channels.length > 0) {
|
|
228
|
+
console.log(` Relaying events to ${result.channels.length} channel(s): ${result.channels.join(', ')}`);
|
|
229
|
+
}
|
|
230
|
+
if (result.tools && result.tools.length > 0) {
|
|
231
|
+
console.log(` Registered ${result.tools.length} tool(s): ${result.tools.join(', ')}`);
|
|
232
|
+
}
|
|
233
|
+
if (result.mcpTools && result.mcpTools.length > 0) {
|
|
234
|
+
console.log(` Registered ${result.mcpTools.length} MCP tool(s): ${result.mcpTools.join(', ')}`);
|
|
235
|
+
}
|
|
236
|
+
if (result.agents && result.agents.length > 0) {
|
|
237
|
+
console.log(` Deployed ${result.agents.length} agent(s): ${result.agents.join(', ')}`);
|
|
238
|
+
}
|
|
239
|
+
// Advisory findings the server accumulated. The deploy still
|
|
240
|
+
// succeeded — but silently dropping these leaves authors debugging
|
|
241
|
+
// mystery responses the server already diagnosed for them.
|
|
242
|
+
if (result.routeShadowWarnings && result.routeShadowWarnings.length > 0) {
|
|
243
|
+
console.warn(` ⚠ ${result.routeShadowWarnings.length} route shadow warning(s):`);
|
|
244
|
+
for (const w of result.routeShadowWarnings) {
|
|
245
|
+
console.warn(` ${w}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (result.partialFailures && result.partialFailures.length > 0) {
|
|
249
|
+
console.warn(` ⚠ ${result.partialFailures.length} non-fatal deploy phase failure(s):`);
|
|
250
|
+
for (const f of result.partialFailures) {
|
|
251
|
+
console.warn(` [${f.phase}] ${f.error}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
// Rethrow: a failed deploy is a failed publish. Logging and falling
|
|
257
|
+
// through prints "Published ... files" and exits 0, so CI goes green on
|
|
258
|
+
// an app whose migrations never ran. bin/deploy.js turns this into exit 1.
|
|
259
|
+
const detail = err.body || err.message;
|
|
260
|
+
throw new Error(`Deploy failed: ${detail}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
191
264
|
function formatSize(bytes) {
|
|
192
265
|
if (bytes < 1024) return `${bytes} B`;
|
|
193
266
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
package/src/dev-bag.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { loadManifest, manifestBlock, buildDevContext, buildDevCrypto, buildDevMessaging, normalizeFetchPath } from './dev-dependencies.js';
|
|
2
|
+
import { createDevChannels, createDevEmit } from './dev-channels.js';
|
|
3
|
+
import { devPlatform } from './dev-platform.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The part of the dev handler bag every surface shares — `server/` routes and
|
|
7
|
+
* `channels/` handlers alike: the platform services (`query`, `fetch`, the
|
|
8
|
+
* typed dependency `context`, `emit`, `broadcast`, `notify`, `email`,
|
|
9
|
+
* `crypto`, `markdown`, `log`, `env`, `platform`). Each surface adds only its
|
|
10
|
+
* own inbound members (`request` + `respond` for HTTP, `channel` + `payload`
|
|
11
|
+
* for channels), the same split the prod sandbox's buildInvokeScript makes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// embed() is present on a real install even without the embeddings
|
|
15
|
+
// capability, where it throws a written explanation. Mirroring that here
|
|
16
|
+
// keeps the dev failure the same lesson as the deployed one, instead of a
|
|
17
|
+
// bare "embed is not a function" that reads like a missing binding.
|
|
18
|
+
export const embed = async () => {
|
|
19
|
+
throw new Error('embed() is not available in the dev mirror: the embeddings capability needs a real Informer (platform.capabilities.embeddings is false)');
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Cap dev proxy calls at 30s so a hung upstream fails loudly instead of
|
|
23
|
+
// hanging the handler.
|
|
24
|
+
const FETCH_TIMEOUT_MS = 30000;
|
|
25
|
+
|
|
26
|
+
/** log(message, data) + log.debug/info/warn/error — the sandbox log surface, to the terminal. */
|
|
27
|
+
export function buildDevLog(prefix = '[app-log]') {
|
|
28
|
+
const logCall = (level, message, data) => {
|
|
29
|
+
const msg = typeof message === 'string' ? message : JSON.stringify(message);
|
|
30
|
+
const args = [`${prefix} [${level}] ${msg}`];
|
|
31
|
+
if (data) args.push(data);
|
|
32
|
+
console.log(...args);
|
|
33
|
+
};
|
|
34
|
+
return Object.assign(
|
|
35
|
+
(message, data) => logCall('info', message, data),
|
|
36
|
+
{
|
|
37
|
+
debug: (message, data) => logCall('debug', message, data),
|
|
38
|
+
info: (message, data) => logCall('info', message, data),
|
|
39
|
+
warn: (message, data) => logCall('warn', message, data),
|
|
40
|
+
error: (message, data) => logCall('error', message, data)
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The viewer identity dev handlers see — the same object the page gets as window.__INFORMER__.user. */
|
|
46
|
+
export function buildDevUser(user) {
|
|
47
|
+
return {
|
|
48
|
+
username: (user && user.username) || 'dev',
|
|
49
|
+
displayName: (user && user.displayName) || 'Local Developer',
|
|
50
|
+
email: (user && user.email) || null,
|
|
51
|
+
timezone: (user && user.timezone) || null
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the shared services once per dev server and the per-invocation bag
|
|
57
|
+
* on demand.
|
|
58
|
+
*
|
|
59
|
+
* @param {Object} opts
|
|
60
|
+
* @param {string} opts.serverOrigin - Informer server origin
|
|
61
|
+
* @param {string} opts.authHeader - Basic or Bearer auth header for /api calls
|
|
62
|
+
* @param {string|null} opts.devWorkspaceId - workspace datasource id for query()
|
|
63
|
+
* @param {string} opts.projectRoot - app project root
|
|
64
|
+
* @param {Object} [opts.devBindings] - dev bindings for `target: app` / `target: pack` deps
|
|
65
|
+
* @param {string|null} [opts.appToken] - INFORMER_APP_TOKEN for cross-app request()
|
|
66
|
+
* @param {ReturnType<typeof createDevChannels>} [opts.channels] - the dev channels hub
|
|
67
|
+
* @param {string} [opts.logPrefix] - prefix for notify/email/emit console lines
|
|
68
|
+
* @returns {{ query: Function, apiFetch: Function, build: () => Promise<{ manifest: Object, bag: Object }> }}
|
|
69
|
+
*/
|
|
70
|
+
export function createDevBagBuilder({ serverOrigin, authHeader, devWorkspaceId, projectRoot, devBindings = {}, appToken = null, channels = createDevChannels(), logPrefix = '[app]' }) {
|
|
71
|
+
// query() implementation — proxies to the workspace _sql endpoint
|
|
72
|
+
async function query(sql, params) {
|
|
73
|
+
if (!devWorkspaceId) {
|
|
74
|
+
throw new Error('query() requires INFORMER_DEV_WORKSPACE. Run: npx informer-workspace init');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const resp = await globalThis.fetch(`${serverOrigin}/api/datasources/${devWorkspaceId}/_sql`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json', Authorization: authHeader },
|
|
80
|
+
body: JSON.stringify({ sql, params: params || [] })
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!resp.ok) {
|
|
84
|
+
const err = await resp.json().catch(() => ({}));
|
|
85
|
+
const detail = err.message || resp.statusText;
|
|
86
|
+
throw new Error(`query() failed: ${resp.status} ${detail} (${serverOrigin}/api/datasources/${devWorkspaceId}/_sql)`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const data = await resp.json();
|
|
90
|
+
return data.rows;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// fetch() implementation — proxies API calls to the Informer server
|
|
94
|
+
async function fetchAs(auth, path, opts = {}) {
|
|
95
|
+
const method = (opts.method || 'GET').toUpperCase();
|
|
96
|
+
// Same strict canonicalization as prod (app-sandbox.js normalizeFetchPath);
|
|
97
|
+
// reject non-canonical shapes here instead of silently accepting them.
|
|
98
|
+
const apiPath = normalizeFetchPath(path);
|
|
99
|
+
if (!apiPath) {
|
|
100
|
+
return { status: 400, body: { error: `Invalid fetch path: ${String(path).slice(0, 80)}` }, contentType: 'application/json' };
|
|
101
|
+
}
|
|
102
|
+
const url = `${serverOrigin}${apiPath}`;
|
|
103
|
+
const fetchOpts = {
|
|
104
|
+
method,
|
|
105
|
+
headers: { Authorization: auth, 'Content-Type': 'application/json', ...(opts.headers || {}) }
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (opts.body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
109
|
+
fetchOpts.body = JSON.stringify(opts.body);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Read the stream exactly once — `.json()` consumes/locks the body, so a
|
|
113
|
+
// `.text()` fallback would throw "Body is unusable" on any non-JSON
|
|
114
|
+
// response (auth-bounce HTML, proxy error page). Parse in memory
|
|
115
|
+
// instead — same as prod's unwrapInject.
|
|
116
|
+
let status, contentType, text;
|
|
117
|
+
try {
|
|
118
|
+
const resp = await globalThis.fetch(url, { ...fetchOpts, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
119
|
+
status = resp.status;
|
|
120
|
+
contentType = resp.headers.get('content-type') || '';
|
|
121
|
+
text = await resp.text();
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// Transport failure or timeout — fetch throws (TypeError 'fetch failed'
|
|
124
|
+
// with the real reason on err.cause, or a TimeoutError). Return a
|
|
125
|
+
// synthetic 502 so the dependency layer names it (dep + url + cause)
|
|
126
|
+
// rather than a bare unhandled "fetch failed".
|
|
127
|
+
const reason = (err.cause && err.cause.message) || err.message;
|
|
128
|
+
return { status: 502, body: { message: `fetch to ${url} failed: ${reason}` }, contentType: 'application/json' };
|
|
129
|
+
}
|
|
130
|
+
let body;
|
|
131
|
+
try { body = JSON.parse(text); } catch { body = text; }
|
|
132
|
+
return { status, body, contentType };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function apiFetch(path, opts = {}) {
|
|
136
|
+
return await fetchAs(authHeader, path, opts);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Cross-app request() targets /api/apps/<id>/view/_/<path>, whose auth accepts
|
|
140
|
+
// only the token/session strategies — NOT basic auth. In API-key mode the
|
|
141
|
+
// INFORMER_API_KEY Bearer already satisfies that, so reuse it; under basic
|
|
142
|
+
// auth a separate API token (INFORMER_APP_TOKEN) is required, and without one
|
|
143
|
+
// the app proxy's request() throws a pointed error instead of a bare 401.
|
|
144
|
+
// appFetch also stamps x-informer-app-depth:1 so the target runs one hop deep
|
|
145
|
+
// and enforces the same one-hop guard it does in production.
|
|
146
|
+
const appAuth = appToken
|
|
147
|
+
? `Bearer ${appToken}`
|
|
148
|
+
: (authHeader && authHeader.startsWith('Bearer ') ? authHeader : null);
|
|
149
|
+
const appFetch = appAuth
|
|
150
|
+
? async (path, opts = {}) => await fetchAs(appAuth, path, { ...opts, headers: { ...(opts.headers || {}), 'x-informer-app-depth': '1' } })
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
// notify/email — delivery is a console-logged no-op in dev, but the
|
|
154
|
+
// required-field validation mirrors prod so an app that passes here
|
|
155
|
+
// won't 500 in production.
|
|
156
|
+
const { notify, email } = buildDevMessaging(logPrefix);
|
|
157
|
+
const log = buildDevLog();
|
|
158
|
+
// markdown helper — passthrough in dev (production uses `marked`)
|
|
159
|
+
const markdown = (text) => text;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The shared bag for one invocation. The manifest is parsed per call
|
|
163
|
+
* (deps, env and channels all come from that one read) so edits to
|
|
164
|
+
* informer.yaml take effect without a dev-server restart.
|
|
165
|
+
*/
|
|
166
|
+
async function build() {
|
|
167
|
+
const manifest = await loadManifest(projectRoot);
|
|
168
|
+
const deps = manifestBlock(manifest, 'dependencies');
|
|
169
|
+
const context = buildDevContext({ deps, apiFetch, devBindings, appFetch });
|
|
170
|
+
const env = manifestBlock(manifest, 'env');
|
|
171
|
+
|
|
172
|
+
// emit() writes no app_event row in dev, but still relays a listed
|
|
173
|
+
// event to its `channels:` channel; broadcast() publishes a live
|
|
174
|
+
// frame to the page (see dev-channels.js).
|
|
175
|
+
const emit = createDevEmit({ channels, manifestChannels: manifestBlock(manifest, 'channels'), logPrefix: '[app-event]' });
|
|
176
|
+
const { broadcast } = channels;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
manifest,
|
|
180
|
+
bag: { context, query, fetch: apiFetch, emit, broadcast, notify, email, embed, crypto: buildDevCrypto(), markdown, log, env, platform: devPlatform() }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { query, apiFetch, build };
|
|
185
|
+
}
|