@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/bin/deploy.js +3 -2
- package/bin/init.js +55 -27
- package/bin/publish.js +128 -0
- package/bin/workspace.js +11 -5
- package/package.json +3 -1
- package/src/agent-dev.js +43 -2
- package/src/assemble.js +87 -0
- package/src/changelog.js +39 -0
- package/src/deploy.js +20 -159
- package/src/dev-dependencies.js +363 -0
- package/src/env.js +86 -0
- package/src/index.js +22 -5
- package/src/publish.js +97 -0
- package/src/server-routes.js +80 -10
- package/src/workspace.js +2 -2
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
|
-
|
|
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:
|
|
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
|
+
}
|
package/src/server-routes.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
234
|
-
const cryptoHelper =
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
//
|
|
269
|
-
|
|
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
|
-
|
|
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
|
|
125
|
+
console.log(`Saved INFORMER_DEV_WORKSPACE=${workspaceId} to ${basename(envPath)}`);
|
|
126
126
|
|
|
127
127
|
return { workspaceId, ...result };
|
|
128
128
|
}
|