@entrinsik/vite-plugin-informer 2.3.0 → 2.5.0-beta.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/src/index.js CHANGED
@@ -1,8 +1,9 @@
1
- import dotenv from 'dotenv';
2
1
  import { existsSync } from 'node:fs';
3
2
  import { readFile } from 'node:fs/promises';
4
3
  import { resolve } from 'node:path';
5
4
  import { createClient } from './client.js';
5
+ import { loadDependencies, validateDependencies } from './dev-dependencies.js';
6
+ import { loadEnv, envWritePath } from './env.js';
6
7
  import { createMiddleware as createServerRoutes } from './server-routes.js';
7
8
  import { createAgentMiddleware } from './agent-dev.js';
8
9
  import { init, migrate } from './workspace.js';
@@ -23,12 +24,14 @@ export default function informer(options = {}) {
23
24
  let authHeader = null;
24
25
  let serverOrigin = null;
25
26
  let devWorkspaceId = null;
27
+ let activeMode = null;
26
28
 
27
29
  return {
28
30
  name: 'vite-plugin-informer',
29
31
 
30
- config(_, { command }) {
31
- dotenv.config();
32
+ config(_, { command, mode }) {
33
+ activeMode = mode;
34
+ loadEnv({ mode });
32
35
 
33
36
  isDev = command === 'serve';
34
37
 
@@ -57,7 +60,8 @@ export default function informer(options = {}) {
57
60
  changeOrigin: true,
58
61
  headers: {
59
62
  Authorization: authHeader
60
- }
63
+ },
64
+ ...options.proxy
61
65
  }
62
66
  }
63
67
  };
@@ -106,7 +110,7 @@ export default function informer(options = {}) {
106
110
  api,
107
111
  slug,
108
112
  migrationsDir,
109
- envPath: resolve(projectRoot, '.env')
113
+ envPath: envWritePath({ mode: activeMode })
110
114
  });
111
115
  devWorkspaceId = result.workspaceId;
112
116
  }
@@ -116,6 +120,19 @@ export default function informer(options = {}) {
116
120
  }
117
121
  }
118
122
 
123
+ // Surface manifest-level dependency declaration errors at boot,
124
+ // not at `npx informer publish` time. Matches the deploy.js
125
+ // validation so devs see the same wording pre-deploy.
126
+ try {
127
+ const deps = await loadDependencies(projectRoot);
128
+ const errors = validateDependencies(deps);
129
+ for (const message of errors) {
130
+ console.error(`[informer] informer.yaml: ${message}`);
131
+ }
132
+ } catch (err) {
133
+ console.warn(`[informer] Could not validate informer.yaml dependencies: ${err.message}`);
134
+ }
135
+
119
136
  // Mount server-side route handlers if a server/ directory exists
120
137
  const serverDir = resolve(projectRoot, 'server');
121
138
 
package/src/publish.js ADDED
@@ -0,0 +1,97 @@
1
+ import { mkdtemp, mkdir, copyFile, rm, readFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join, dirname } from 'node:path';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+
7
+ const execFileP = promisify(execFile);
8
+
9
+ class PublishError extends Error {
10
+ constructor(status, body, url) {
11
+ super(`Marketplace publish failed: ${status}${body ? ` — ${body}` : ''}${url ? ` (${url})` : ''}`);
12
+ this.name = 'PublishError';
13
+ this.status = status;
14
+ this.body = body;
15
+ this.url = url;
16
+ }
17
+ }
18
+
19
+ export { PublishError };
20
+
21
+ /**
22
+ * Stage the assembled files into a temp dir (preserving their library-relative
23
+ * layout) and tar.gz them. System `tar` keeps the archive format exactly what
24
+ * the server's node-tar expects; COPYFILE_DISABLE suppresses macOS AppleDouble
25
+ * (`._*`) junk from leaking into the archive.
26
+ *
27
+ * @param {Array<{abs: string, rel: string}>} files
28
+ * @returns {Promise<Buffer>}
29
+ */
30
+ async function buildArchive(files) {
31
+ const stage = await mkdtemp(join(tmpdir(), 'informer-publish-'));
32
+ const archivePath = `${stage}.tgz`;
33
+ try {
34
+ for (const { abs, rel } of files) {
35
+ const dest = join(stage, rel);
36
+ await mkdir(dirname(dest), { recursive: true });
37
+ await copyFile(abs, dest);
38
+ }
39
+ await execFileP('tar', ['-czf', archivePath, '-C', stage, '.'], {
40
+ env: { ...process.env, COPYFILE_DISABLE: '1' }
41
+ });
42
+ return await readFile(archivePath);
43
+ } finally {
44
+ await rm(stage, { recursive: true, force: true });
45
+ await rm(archivePath, { force: true });
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Publish an assembled app to the marketplace (License Manager `/packs/publish`).
51
+ *
52
+ * @param {Object} opts
53
+ * @param {string} opts.marketplaceUrl - base URL fronting the cloud-api
54
+ * @param {string} opts.token - vendor publish token (sent as Bearer)
55
+ * @param {Array<{abs: string, rel: string}>} opts.files - assembled app files
56
+ * @param {Object} opts.fields - publish metadata; non-string values are JSON-encoded
57
+ * (the endpoint parses categories/requires/metadata as JSON strings)
58
+ * @param {{abs: string, filename: string}} [opts.icon] - optional listing icon
59
+ * @returns {Promise<Object>} the publish response
60
+ */
61
+ export async function publish({ marketplaceUrl, token, files, fields, icon }) {
62
+ const archive = await buildArchive(files);
63
+
64
+ const form = new FormData();
65
+ for (const [key, value] of Object.entries(fields)) {
66
+ if (value === undefined || value === null || value === '') continue;
67
+ form.append(key, typeof value === 'string' ? value : JSON.stringify(value));
68
+ }
69
+ form.append(
70
+ 'archive',
71
+ new Blob([archive], { type: 'application/gzip' }),
72
+ `${fields.slug}-${fields.version}.tgz`
73
+ );
74
+ if (icon) {
75
+ const iconBuffer = await readFile(icon.abs);
76
+ form.append('icon', new Blob([iconBuffer]), icon.filename);
77
+ }
78
+
79
+ const url = `${marketplaceUrl.replace(/\/+$/, '')}/packs/publish`;
80
+ const res = await fetch(url, {
81
+ method: 'POST',
82
+ headers: { Authorization: `Bearer ${token}` },
83
+ body: form
84
+ });
85
+
86
+ if (!res.ok) {
87
+ let body = '';
88
+ try {
89
+ body = await res.text();
90
+ } catch {
91
+ body = '';
92
+ }
93
+ throw new PublishError(res.status, body, url);
94
+ }
95
+
96
+ return await res.json();
97
+ }
@@ -1,10 +1,14 @@
1
- import { createHmac } from 'node:crypto';
2
1
  import { readdir, stat } from 'node:fs/promises';
3
2
  import { join, relative, posix } from 'node:path';
4
3
  import { parse as parseUrl } from 'node:url';
4
+ import { loadDependencies, buildDevContext, loadAppEnv, buildDevCrypto, buildDevMessaging, normalizeFetchPath } from './dev-dependencies.js';
5
5
 
6
6
  const VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
7
7
 
8
+ // Strict base64 — see view-api.js for the rationale. Mirror kept identical
9
+ // to keep dev and prod behavior aligned.
10
+ const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
11
+
8
12
  /**
9
13
  * Convert a file path under server/ to a route path.
10
14
  *
@@ -171,7 +175,13 @@ export function createMiddleware(viteServer, { serverOrigin, authHeader, devWork
171
175
  // fetch() implementation — proxies API calls to the Informer server
172
176
  async function apiFetch(path, opts = {}) {
173
177
  const method = (opts.method || 'GET').toUpperCase();
174
- const url = `${serverOrigin}/api/${path.replace(/^\/?(?:api\/)?/, '')}`;
178
+ // Same strict canonicalization as prod (app-sandbox.js normalizeFetchPath);
179
+ // reject non-canonical shapes here instead of silently accepting them.
180
+ const apiPath = normalizeFetchPath(path);
181
+ if (!apiPath) {
182
+ return { status: 400, body: { error: `Invalid fetch path: ${String(path).slice(0, 80)}` } };
183
+ }
184
+ const url = `${serverOrigin}${apiPath}`;
175
185
  const fetchOpts = {
176
186
  method,
177
187
  headers: { Authorization: authHeader, 'Content-Type': 'application/json' }
@@ -230,13 +240,40 @@ export function createMiddleware(viteServer, { serverOrigin, authHeader, devWork
230
240
  try { body = JSON.parse(rawBody); } catch { body = rawBody; }
231
241
  }
232
242
 
233
- // crypto helper — mirrors the sandbox's crypto.hmac()
234
- const cryptoHelper = {
235
- hmac(algorithm, key, data, encoding) {
236
- return createHmac(algorithm, key).update(data).digest(encoding || 'hex');
243
+ // crypto helper — mirrors the prod sandbox crypto surface
244
+ const cryptoHelper = buildDevCrypto();
245
+
246
+ // log helper — mirrors sandbox log(message, data) + log.info/warn/error/debug
247
+ const logCall = (level, message, data) => {
248
+ const msg = typeof message === 'string' ? message : JSON.stringify(message);
249
+ const args = [`[app-log] [${level}] ${msg}`];
250
+ if (data) args.push(data);
251
+ console.log(...args);
252
+ };
253
+ const log = Object.assign(
254
+ (message, data) => logCall('info', message, data),
255
+ {
256
+ debug: (message, data) => logCall('debug', message, data),
257
+ info: (message, data) => logCall('info', message, data),
258
+ warn: (message, data) => logCall('warn', message, data),
259
+ error: (message, data) => logCall('error', message, data)
237
260
  }
261
+ );
262
+
263
+ // markdown helper — passthrough in dev (production uses `marked`)
264
+ const markdown = (text) => text;
265
+
266
+ // emit helper — no-op in dev (logs to console)
267
+ const emit = (event, payload) => {
268
+ console.log(`[app-event] emit("${event}",`, JSON.stringify(payload), ')');
269
+ return { ok: true };
238
270
  };
239
271
 
272
+ // notify/email — delivery is a console-logged no-op in dev, but the
273
+ // required-field validation mirrors prod so an app that passes here
274
+ // won't 500 in production.
275
+ const { notify, email } = buildDevMessaging('[app]');
276
+
240
277
  // Build request context
241
278
  const request = {
242
279
  method: req.method.toUpperCase(),
@@ -265,20 +302,39 @@ export function createMiddleware(viteServer, { serverOrigin, authHeader, devWork
265
302
  res.end(JSON.stringify(earlyBody));
266
303
  }
267
304
 
268
- // Call handler
269
- const result = await handler({ query, fetch: apiFetch, respond, crypto: cryptoHelper, env: {}, request });
305
+ // Build the dependency-injection context the same way the prod
306
+ // sandbox does, so handlers using `await context.myDep.method(...)`
307
+ // work locally. Loaded per request so edits to informer.yaml take
308
+ // effect without a dev-server restart.
309
+ const deps = await loadDependencies(projectRoot);
310
+ const context = buildDevContext({ deps, apiFetch });
311
+ const env = await loadAppEnv(projectRoot);
312
+
313
+ // Call handler — bag mirrors the prod sandbox (see app-sandbox.js).
314
+ const result = await handler({ request, context, query, fetch: apiFetch, respond, emit, notify, email, crypto: cryptoHelper, markdown, log, env });
270
315
 
271
316
  // If respond() was already called, the response is already sent
272
317
  if (responded) return;
273
318
 
274
- // Normalize response (mirrors app-sandbox.js buildInvokeScript logic)
275
- let status, responseBody, responseHeaders;
319
+ // Normalize response (mirrors app-sandbox.js buildInvokeScript logic).
320
+ // The encoding allow-list and contract checks are enforced inside the
321
+ // ivm in production; replicate them here so handlers see the same
322
+ // errors in dev (where there's no isolate to attribute the throw to).
323
+ // If you change this, mirror it in modules/app/routes/view-api.js.
324
+ let status, responseBody, responseHeaders, encoding;
276
325
 
277
326
  if (result === undefined || result === null) {
278
327
  status = 204;
279
328
  responseBody = null;
280
329
  responseHeaders = {};
281
330
  } else if (typeof result === 'object' && typeof result.status === 'number') {
331
+ encoding = typeof result.encoding === 'string' ? result.encoding : null;
332
+ if (encoding !== null && encoding !== 'base64') {
333
+ throw new Error(`Unknown response encoding: ${JSON.stringify(encoding)} (expected 'base64' or omitted)`);
334
+ }
335
+ if (encoding === 'base64' && typeof result.body !== 'string') {
336
+ throw new Error(`encoding: 'base64' requires body to be a base64-encoded string, got ${typeof result.body}`);
337
+ }
282
338
  status = result.status || 200;
283
339
  responseBody = result.body !== undefined ? result.body : null;
284
340
  responseHeaders = result.headers || {};
@@ -295,6 +351,20 @@ export function createMiddleware(viteServer, { serverOrigin, authHeader, devWork
295
351
 
296
352
  if (responseBody === null) {
297
353
  res.end();
354
+ } else if (encoding === 'base64' && typeof responseBody === 'string') {
355
+ if (!BASE64_RE.test(responseBody)) {
356
+ throw new Error('Handler returned malformed base64 body');
357
+ }
358
+ res.end(Buffer.from(responseBody, 'base64'));
359
+ } else if (typeof responseBody === 'string') {
360
+ // Pre-PR the dev middleware always JSON.stringify'd the body, so
361
+ // a handler returning { body: 'hello' } emitted "hello" (with
362
+ // quotes) — diverging from prod which passed strings verbatim.
363
+ // This branch fixes that parity.
364
+ if (!res.getHeader('content-type')) {
365
+ res.setHeader('Content-Type', 'application/json');
366
+ }
367
+ res.end(responseBody);
298
368
  } else {
299
369
  if (!res.getHeader('content-type')) {
300
370
  res.setHeader('Content-Type', 'application/json');
package/src/workspace.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readdir, readFile, writeFile, access } from 'node:fs/promises';
2
- import { join } from 'node:path';
2
+ import { join, basename } from 'node:path';
3
3
 
4
4
  /**
5
5
  * Execute raw SQL against a workspace datasource via the _sql route.
@@ -122,7 +122,7 @@ export async function init({ api, slug, migrationsDir, envPath }) {
122
122
  }
123
123
 
124
124
  await writeFile(envPath, envContent);
125
- console.log(`Saved INFORMER_DEV_WORKSPACE=${workspaceId} to .env`);
125
+ console.log(`Saved INFORMER_DEV_WORKSPACE=${workspaceId} to ${basename(envPath)}`);
126
126
 
127
127
  return { workspaceId, ...result };
128
128
  }