manifest 7.0.0 → 7.2.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/CONTRACT.md +25 -1
- package/LICENSE +21 -0
- package/README.md +64 -19
- package/dist/bin.cjs +410 -0
- package/dist/bin.d.cts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +383 -0
- package/dist/chunk-GC5H22R2.js +495 -0
- package/dist/{chunk-DP2C4E42.js → chunk-NHEF7GUP.js} +126 -348
- package/dist/index.cjs +264 -24
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/register.cjs +264 -24
- package/dist/register.js +2 -1
- package/docs/guide.md +70 -10
- package/docs/sdk-flow-diagram.png +0 -0
- package/package.json +8 -3
package/dist/bin.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
VERSION,
|
|
4
|
+
isObject
|
|
5
|
+
} from "./chunk-NHEF7GUP.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
import { existsSync, readFileSync } from "fs";
|
|
10
|
+
import { readFile } from "fs/promises";
|
|
11
|
+
import path from "path";
|
|
12
|
+
var DEFAULT_URL = "https://api.manifest.build";
|
|
13
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
14
|
+
var symbols = {
|
|
15
|
+
ok: "\u2705",
|
|
16
|
+
fail: "\u274C",
|
|
17
|
+
warn: "\u26A0\uFE0F",
|
|
18
|
+
skip: "\u2796"
|
|
19
|
+
};
|
|
20
|
+
function maskKey(key) {
|
|
21
|
+
const tail = key.slice(-4);
|
|
22
|
+
if (key.length <= tail.length + 2) return "\u2026";
|
|
23
|
+
const prefix = /^(?:mnfst(?:_[a-z]+)*_)/i.exec(key)?.[0] ?? key.slice(0, 4);
|
|
24
|
+
const head = prefix.length + tail.length < key.length ? prefix : "";
|
|
25
|
+
return `${head}\u2026${tail}`;
|
|
26
|
+
}
|
|
27
|
+
function normalizeBase(raw) {
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(raw);
|
|
30
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
31
|
+
return null;
|
|
32
|
+
if (!url.pathname.endsWith("/")) url.pathname += "/";
|
|
33
|
+
return url.toString();
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function readJson(response) {
|
|
39
|
+
try {
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
return text.length > 1e6 ? null : JSON.parse(text);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function projectName(body) {
|
|
47
|
+
if (!isObject(body)) return void 0;
|
|
48
|
+
const candidate = body.project ?? body.projectName ?? body.project_name ?? body.name;
|
|
49
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
50
|
+
return candidate.trim();
|
|
51
|
+
if (isObject(candidate) && typeof candidate.name === "string" && candidate.name.trim())
|
|
52
|
+
return candidate.name.trim();
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
function requestCount(body) {
|
|
56
|
+
if (!isObject(body)) return void 0;
|
|
57
|
+
const candidate = body.requests ?? body.requestCount ?? body.requestsCount ?? body.requests_count ?? body.request_count;
|
|
58
|
+
if (typeof candidate === "number") return candidate;
|
|
59
|
+
if (isObject(candidate) && typeof candidate.total === "number")
|
|
60
|
+
return candidate.total;
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
function headers(key) {
|
|
64
|
+
return {
|
|
65
|
+
authorization: `Bearer ${key}`,
|
|
66
|
+
"content-type": "application/json",
|
|
67
|
+
"user-agent": `mnfst-node/${VERSION}`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function probe(url, key, doFetch) {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
73
|
+
try {
|
|
74
|
+
const response = await doFetch(new URL("v1/hello", url), {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: headers(key),
|
|
77
|
+
// `probe` marks this a key check, not a boot: the server answers but
|
|
78
|
+
// records no install. Without it, running doctor from a laptop makes
|
|
79
|
+
// the dashboard claim the app is connected — while doctor is printing
|
|
80
|
+
// "manifest is not installed in this project" two lines above.
|
|
81
|
+
body: JSON.stringify({ probe: true }),
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
redirect: "error"
|
|
84
|
+
});
|
|
85
|
+
const body = await readJson(response);
|
|
86
|
+
if (response.status === 200)
|
|
87
|
+
return {
|
|
88
|
+
kind: "valid",
|
|
89
|
+
project: projectName(body),
|
|
90
|
+
requests: requestCount(body)
|
|
91
|
+
};
|
|
92
|
+
if (response.status === 401)
|
|
93
|
+
return { kind: "invalid", detail: "the key was rejected (401)" };
|
|
94
|
+
if (response.status === 403)
|
|
95
|
+
return {
|
|
96
|
+
kind: "invalid",
|
|
97
|
+
detail: isObject(body) && body.error === "project_disabled" ? "the key is valid but the project is disabled (403)" : "the key was rejected (403)"
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
kind: "error",
|
|
101
|
+
detail: `unexpected response (${response.status})`
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const timedOut = error instanceof Error && error.name === "AbortError";
|
|
105
|
+
return {
|
|
106
|
+
kind: "error",
|
|
107
|
+
detail: `${timedOut ? "timed out" : "could not be reached"} at ${url}`
|
|
108
|
+
};
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function installedVersion(cwd) {
|
|
114
|
+
try {
|
|
115
|
+
const entry = createRequire(path.join(cwd, "package.json")).resolve(
|
|
116
|
+
"manifest"
|
|
117
|
+
);
|
|
118
|
+
let dir = path.dirname(entry);
|
|
119
|
+
while (true) {
|
|
120
|
+
const manifest = path.join(dir, "package.json");
|
|
121
|
+
if (existsSync(manifest)) {
|
|
122
|
+
const pkg = JSON.parse(readFileSync(manifest, "utf8"));
|
|
123
|
+
if (pkg.name === "manifest" && pkg.version) return pkg.version;
|
|
124
|
+
}
|
|
125
|
+
const parent = path.dirname(dir);
|
|
126
|
+
if (parent === dir) return void 0;
|
|
127
|
+
dir = parent;
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function nearestPackage(cwd) {
|
|
134
|
+
let dir = path.resolve(cwd);
|
|
135
|
+
while (true) {
|
|
136
|
+
const file = path.join(dir, "package.json");
|
|
137
|
+
if (existsSync(file)) {
|
|
138
|
+
try {
|
|
139
|
+
return {
|
|
140
|
+
dir,
|
|
141
|
+
pkg: JSON.parse(await readFile(file, "utf8"))
|
|
142
|
+
};
|
|
143
|
+
} catch {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const parent = path.dirname(dir);
|
|
148
|
+
if (parent === dir) return void 0;
|
|
149
|
+
dir = parent;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
var instrumentationFiles = [
|
|
153
|
+
"instrumentation.ts",
|
|
154
|
+
"instrumentation.js",
|
|
155
|
+
"instrumentation.mjs",
|
|
156
|
+
"src/instrumentation.ts",
|
|
157
|
+
"src/instrumentation.js",
|
|
158
|
+
"src/instrumentation.mjs"
|
|
159
|
+
];
|
|
160
|
+
async function loadCheck(cwd) {
|
|
161
|
+
const label = "Loads before your app";
|
|
162
|
+
const found = await nearestPackage(cwd);
|
|
163
|
+
if (!found)
|
|
164
|
+
return {
|
|
165
|
+
label,
|
|
166
|
+
status: "warn",
|
|
167
|
+
detail: "no package.json found; cannot tell how Manifest loads"
|
|
168
|
+
};
|
|
169
|
+
const scripts = isObject(found.pkg.scripts) ? found.pkg.scripts : {};
|
|
170
|
+
const values = Object.values(scripts).filter(
|
|
171
|
+
(value) => typeof value === "string"
|
|
172
|
+
);
|
|
173
|
+
if (values.some((value) => value.includes("manifest/register")))
|
|
174
|
+
return {
|
|
175
|
+
label,
|
|
176
|
+
status: "ok",
|
|
177
|
+
detail: "a script preloads manifest/register"
|
|
178
|
+
};
|
|
179
|
+
const dependencies = {
|
|
180
|
+
...isObject(found.pkg.dependencies) ? found.pkg.dependencies : {},
|
|
181
|
+
...isObject(found.pkg.devDependencies) ? found.pkg.devDependencies : {}
|
|
182
|
+
};
|
|
183
|
+
if ("next" in dependencies) {
|
|
184
|
+
for (const relative of instrumentationFiles) {
|
|
185
|
+
const file = path.join(found.dir, relative);
|
|
186
|
+
if (!existsSync(file)) continue;
|
|
187
|
+
if (/manifest/.test(await readFile(file, "utf8")))
|
|
188
|
+
return {
|
|
189
|
+
label,
|
|
190
|
+
status: "ok",
|
|
191
|
+
detail: `${relative} installs Manifest before the app runs`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
label,
|
|
196
|
+
status: "fail",
|
|
197
|
+
detail: 'package.json "start" relies on Next.js with no NODE_OPTIONS and no instrumentation file \u2014 Manifest will not load'
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const start = scripts.start;
|
|
201
|
+
if (typeof start === "string" && start.trim())
|
|
202
|
+
return {
|
|
203
|
+
label,
|
|
204
|
+
status: "warn",
|
|
205
|
+
detail: "cannot tell from here whether manifest() runs; check Requests received"
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
label,
|
|
209
|
+
status: "warn",
|
|
210
|
+
detail: "no start script found; cannot confirm Manifest loads before your app"
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function sdkCheck(cwd, injected) {
|
|
214
|
+
const label = "SDK installed";
|
|
215
|
+
const version = injected ?? installedVersion(cwd);
|
|
216
|
+
if (!version)
|
|
217
|
+
return {
|
|
218
|
+
label,
|
|
219
|
+
status: "fail",
|
|
220
|
+
detail: "manifest is not installed in this project"
|
|
221
|
+
};
|
|
222
|
+
return { label, status: "ok", detail: `manifest ${version}` };
|
|
223
|
+
}
|
|
224
|
+
async function runDoctor(options = {}) {
|
|
225
|
+
const cwd = options.cwd ?? process.cwd();
|
|
226
|
+
const env = options.env ?? process.env;
|
|
227
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
228
|
+
const checks = [sdkCheck(cwd, options.sdkVersion)];
|
|
229
|
+
const rawUrl = options.url ?? env.MNFST_URL ?? DEFAULT_URL;
|
|
230
|
+
const url = normalizeBase(rawUrl);
|
|
231
|
+
const key = env.MNFST_KEY;
|
|
232
|
+
if (!key) {
|
|
233
|
+
checks.push({
|
|
234
|
+
label: "MNFST_KEY set",
|
|
235
|
+
status: "fail",
|
|
236
|
+
detail: "MNFST_KEY is not set"
|
|
237
|
+
});
|
|
238
|
+
} else {
|
|
239
|
+
checks.push({ label: "MNFST_KEY set", status: "ok", detail: maskKey(key) });
|
|
240
|
+
}
|
|
241
|
+
let requests;
|
|
242
|
+
if (!url) {
|
|
243
|
+
checks.push({
|
|
244
|
+
label: "Key valid",
|
|
245
|
+
status: "fail",
|
|
246
|
+
detail: `invalid Manifest URL: ${rawUrl}`
|
|
247
|
+
});
|
|
248
|
+
} else if (!key) {
|
|
249
|
+
checks.push({ label: "Key valid", status: "skip", detail: "" });
|
|
250
|
+
} else {
|
|
251
|
+
const result = await probe(url, key, doFetch);
|
|
252
|
+
if (result.kind === "valid") {
|
|
253
|
+
requests = result.requests;
|
|
254
|
+
checks.push({
|
|
255
|
+
label: "Key valid",
|
|
256
|
+
status: "ok",
|
|
257
|
+
detail: result.project ? `project "${result.project}"` : "the server accepted the key"
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
checks.push({
|
|
261
|
+
label: "Key valid",
|
|
262
|
+
status: "fail",
|
|
263
|
+
detail: result.detail
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
checks.push(await loadCheck(cwd));
|
|
268
|
+
if (requests !== void 0) {
|
|
269
|
+
checks.push({
|
|
270
|
+
label: "Requests received",
|
|
271
|
+
status: requests > 0 ? "ok" : "warn",
|
|
272
|
+
detail: requests > 0 ? `${requests} request${requests === 1 ? "" : "s"} received` : "no requests received yet"
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return { checks, ok: checks.every((check) => check.status !== "fail") };
|
|
276
|
+
}
|
|
277
|
+
function wrap(text, width) {
|
|
278
|
+
if (!text) return [""];
|
|
279
|
+
const lines = [];
|
|
280
|
+
let line = "";
|
|
281
|
+
for (const word of text.split(" ")) {
|
|
282
|
+
if (!line) line = word;
|
|
283
|
+
else if (line.length + 1 + word.length <= width) line += ` ${word}`;
|
|
284
|
+
else {
|
|
285
|
+
lines.push(line);
|
|
286
|
+
line = word;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (line) lines.push(line);
|
|
290
|
+
return lines;
|
|
291
|
+
}
|
|
292
|
+
function render(report) {
|
|
293
|
+
const width = Math.max(
|
|
294
|
+
20,
|
|
295
|
+
...report.checks.map((check) => check.label.length)
|
|
296
|
+
);
|
|
297
|
+
const lines = [];
|
|
298
|
+
for (const check of report.checks) {
|
|
299
|
+
const left = ` ${symbols[check.status]} ${check.label.padEnd(width)}`;
|
|
300
|
+
const detail = wrap(check.detail, Math.max(24, 96 - left.length - 2));
|
|
301
|
+
lines.push(`${left} ${detail[0] ?? ""}`.trimEnd());
|
|
302
|
+
for (const extra of detail.slice(1))
|
|
303
|
+
lines.push(`${" ".repeat(left.length + 2)}${extra}`);
|
|
304
|
+
}
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push("Runtime coverage");
|
|
307
|
+
lines.push(
|
|
308
|
+
" Node.js runtime fetch, http.request, https.request, http.get are patched"
|
|
309
|
+
);
|
|
310
|
+
lines.push(
|
|
311
|
+
" Edge runtime not supported \u2014 middleware.ts and Edge route handlers are never covered"
|
|
312
|
+
);
|
|
313
|
+
return `${lines.join("\n")}
|
|
314
|
+
`;
|
|
315
|
+
}
|
|
316
|
+
function parseArgs(argv) {
|
|
317
|
+
const args = { help: false, version: false };
|
|
318
|
+
for (let index = 0; index < argv.length; index++) {
|
|
319
|
+
const arg = argv[index];
|
|
320
|
+
if (arg === "-h" || arg === "--help") args.help = true;
|
|
321
|
+
else if (arg === "-v" || arg === "--version") args.version = true;
|
|
322
|
+
else if (arg === "--url") {
|
|
323
|
+
const value = argv[++index];
|
|
324
|
+
if (!value) throw new Error("--url needs a value");
|
|
325
|
+
args.url = value;
|
|
326
|
+
} else if (arg.startsWith("--url=")) args.url = arg.slice("--url=".length);
|
|
327
|
+
else if (arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
|
|
328
|
+
else if (args.command === void 0) args.command = arg;
|
|
329
|
+
else throw new Error(`unexpected argument: ${arg}`);
|
|
330
|
+
}
|
|
331
|
+
return args;
|
|
332
|
+
}
|
|
333
|
+
var HELP = `Usage: manifest <command> [options]
|
|
334
|
+
|
|
335
|
+
Commands
|
|
336
|
+
doctor Check that this project is wired up to Manifest
|
|
337
|
+
|
|
338
|
+
Options
|
|
339
|
+
--url <url> Manifest API base URL (default: $MNFST_URL)
|
|
340
|
+
-h, --help Show this help
|
|
341
|
+
-v, --version Print the SDK version
|
|
342
|
+
|
|
343
|
+
Examples
|
|
344
|
+
npx manifest doctor
|
|
345
|
+
npx manifest doctor --url=https://api.manifest.build
|
|
346
|
+
`;
|
|
347
|
+
async function main(argv = process.argv.slice(2)) {
|
|
348
|
+
let args;
|
|
349
|
+
try {
|
|
350
|
+
args = parseArgs(argv);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
353
|
+
console.error(HELP);
|
|
354
|
+
return 1;
|
|
355
|
+
}
|
|
356
|
+
if (args.version) {
|
|
357
|
+
console.log(VERSION);
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
if (args.help || !args.command) {
|
|
361
|
+
console.log(HELP);
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
if (args.command !== "doctor") {
|
|
365
|
+
console.error(`Unknown command: ${args.command}`);
|
|
366
|
+
console.error(HELP);
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
369
|
+
const report = await runDoctor({ url: args.url });
|
|
370
|
+
process.stdout.write(render(report));
|
|
371
|
+
return report.ok ? 0 : 1;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/bin.ts
|
|
375
|
+
main().then(
|
|
376
|
+
(code) => {
|
|
377
|
+
process.exitCode = code;
|
|
378
|
+
},
|
|
379
|
+
(error) => {
|
|
380
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
381
|
+
process.exitCode = 1;
|
|
382
|
+
}
|
|
383
|
+
);
|