@mapled/cli 0.7.1 → 0.9.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/dist/commands.js +20 -8
- package/dist/doctor.d.ts +38 -5
- package/dist/doctor.js +174 -23
- package/dist/types.d.ts +1 -0
- package/dist/types.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ Mapled doctor — Dolphin Landing
|
|
|
175
175
|
✓ Environment MAPLED_KEY and MAPLED_WEBHOOK_SECRET in .env.local
|
|
176
176
|
✓ Secrets in git No env files or Mapled secrets are tracked.
|
|
177
177
|
✓ @mapled/next 0.8.0 (current)
|
|
178
|
-
✓ mapled CLI 0.
|
|
178
|
+
✓ mapled CLI 0.9.0 (current)
|
|
179
179
|
✓ Revalidation route app/api/mapled/revalidate/route.ts
|
|
180
180
|
⚠ Preview route app/api/mapled/preview/route.ts is missing — mount createPreviewHandler from "@mapled/next/server" so Preview from Mapled works.
|
|
181
181
|
✓ Site reads content Last read 3m ago
|
|
@@ -188,6 +188,8 @@ Mapled doctor — Dolphin Landing
|
|
|
188
188
|
|
|
189
189
|
The repository side: the link, the sign-in, the generated types, the schema pin, the site manifest (and whether it matches what was pushed), the integration hash — one line saying whether the pin and the manifest here describe the same integration Mapled holds, and if not, what moved (the schema, the bindings, or edits made in Mapled) and which command catches up — `MAPLED.md` (and whether it is what Mapled would render now), `MAPLED_KEY` and `MAPLED_WEBHOOK_SECRET` in your env files (names only — values are never read out), env files or Mapled secrets tracked by git, the installed `@mapled/next` and CLI versions, the revalidation and preview routes of a Next.js site. The Mapled side, from the same status the AI agent's `check_integration` tool reads: whether the site has read content with the delivery key, the publish webhook and its last delivery, the preview route on the deployed site, the bindings health.
|
|
190
190
|
|
|
191
|
+
A site rendered in the visitor's browser — a React single-page app (Vite and the like), plain HTML — is checked for what it has: the package line is `@mapled/react`, Environment wants `VITE_MAPLED_KEY` and no webhook secret, and the routes and the webhook read ○ «Not needed» — such a site shows a publish on its next load and trades Mapled's preview link on the page itself. Plain HTML installs nothing: the line is `@mapled/vanilla`, read from the script tag in the pages (or from a copy of the file next to them) — ⚠ when the address has no version or an older one — and Generated types and Environment read ○: the fields are named in attributes, the delivery key sits in the page. doctor takes the framework from `mapled.json`, or works it out from `package.json` (no `package.json` and an `index.html` is plain HTML). Whatever the framework, a server key or a signing secret under a public prefix (`VITE_…`, `NEXT_PUBLIC_…`) is a ✗: it would ship to every visitor.
|
|
192
|
+
|
|
191
193
|
Exit code 1 when something is marked ✗; `--json` prints the checks for scripts and agents.
|
|
192
194
|
|
|
193
195
|
## Options
|
package/dist/commands.js
CHANGED
|
@@ -142,7 +142,8 @@ async function openSession(ctx, flags) {
|
|
|
142
142
|
export async function generate(ctx, flags) {
|
|
143
143
|
const { found, session } = await openSession(ctx, flags);
|
|
144
144
|
const schema = await session.get("/v1/agent/schema");
|
|
145
|
-
const
|
|
145
|
+
const framework = found.config.framework ?? (await dr.detectFramework(found.dir));
|
|
146
|
+
const generated = generateTypes(schema, { projectName: session.conn.projectName, sdk: dr.sdkOf(framework) });
|
|
146
147
|
const out = stringFlag(flags, "out") ?? found.config.types ?? DEFAULT_TYPES_PATH;
|
|
147
148
|
const file = path.resolve(found.dir, out);
|
|
148
149
|
await writeFile(file, generated.text);
|
|
@@ -580,9 +581,10 @@ export async function doctor(ctx, flags) {
|
|
|
580
581
|
const checks = [];
|
|
581
582
|
checks.push(dr.checkLink(found ? { path: path.relative(ctx.cwd, found.path) || CONFIG_FILE } : null, status?.project.name ?? conn?.projectName ?? null));
|
|
582
583
|
checks.push(dr.checkAuth({ conn: conn ?? null, status, error: authError }));
|
|
584
|
+
const framework = found?.config.framework ?? (await dr.detectFramework(dir));
|
|
583
585
|
const typesPath = found?.config.types ?? DEFAULT_TYPES_PATH;
|
|
584
586
|
const existing = await readFile(path.join(dir, typesPath), "utf8").catch(() => null);
|
|
585
|
-
checks.push(dr.checkTypes(existing, schema ? generateTypes(schema, { projectName: status?.project.name }) : null, typesPath));
|
|
587
|
+
checks.push(dr.checkTypes(existing, schema ? generateTypes(schema, { projectName: status?.project.name, sdk: dr.sdkOf(framework) }) : null, typesPath, framework));
|
|
586
588
|
const pin = found ? await readPin(dir).catch(() => null) : null;
|
|
587
589
|
const livePin = schema && found ? pinSchema(schema, found.config.project) : null;
|
|
588
590
|
const pinChanges = pin && livePin ? diffSchema(pin.pin.collections, livePin.collections) : null;
|
|
@@ -600,16 +602,19 @@ export async function doctor(ctx, flags) {
|
|
|
600
602
|
checks.push(dr.checkManifestFile(localManifest ? { manifest: localManifest.manifest, problems: localManifest.problems } : null, remoteManifest));
|
|
601
603
|
checks.push(dr.checkIntegration({ pin: pin?.pin ?? null, manifest: localManifest?.manifest ?? null }, conn && status ? { integration: status.integration, manifest: remoteManifest } : null));
|
|
602
604
|
checks.push(checkMapledMd(await readFile(path.join(dir, MD_FILE), "utf8").catch(() => null), guide));
|
|
603
|
-
|
|
605
|
+
// a site rendered in the browser has no webhook, no routes and no secret to check
|
|
606
|
+
const browser = dr.rendersInBrowser(framework);
|
|
607
|
+
const sdk = dr.sdkOf(framework);
|
|
608
|
+
checks.push(dr.checkEnv(await dr.envNames(dir), ctx.env, framework));
|
|
604
609
|
checks.push(dr.checkSecrets(await dr.gitFacts(dir)));
|
|
605
|
-
|
|
610
|
+
// plain HTML installs nothing: the runtime comes with a script tag
|
|
611
|
+
checks.push(framework === "plain-html" ? dr.checkScriptTag(await dr.findScriptTag(dir), status?.sdk[sdk]) : dr.checkSdk(await dr.installedVersion(dir, sdk), status?.sdk[sdk], sdk));
|
|
606
612
|
checks.push(dr.checkCli(ctx.version, status?.sdk["@mapled/cli"]));
|
|
607
|
-
const framework = found?.config.framework ?? (await dr.detectFramework(dir));
|
|
608
613
|
checks.push(dr.checkRoute("revalidate", await dr.findRoute(dir, "revalidate"), framework));
|
|
609
614
|
checks.push(dr.checkRoute("preview", await dr.findRoute(dir, "preview"), framework));
|
|
610
615
|
if (status) {
|
|
611
616
|
checks.push(dr.checkReads(status.delivery.lastReadAt));
|
|
612
|
-
checks.push(dr.checkWebhook(status.webhook));
|
|
617
|
+
checks.push(dr.checkWebhook(status.webhook, { local: browser, remote: status.site }));
|
|
613
618
|
let origin = null;
|
|
614
619
|
try {
|
|
615
620
|
origin = status.webhook.url ? new URL(status.webhook.url).origin : null;
|
|
@@ -617,8 +622,15 @@ export async function doctor(ctx, flags) {
|
|
|
617
622
|
catch {
|
|
618
623
|
origin = null;
|
|
619
624
|
}
|
|
620
|
-
|
|
621
|
-
|
|
625
|
+
if (browser) {
|
|
626
|
+
// no route to probe: the page itself, where Mapled knows the site to live
|
|
627
|
+
origin = origin ?? status.site?.url ?? null;
|
|
628
|
+
checks.push(dr.checkPreviewOnSite(origin, status.previewPath, origin ? await dr.probeSite(origin, ctx.fetch) : null, true));
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
const probe = origin ? await dr.probePreview(origin, status.previewPath, ctx.fetch) : null;
|
|
632
|
+
checks.push(dr.checkPreviewOnSite(origin, status.previewPath, probe));
|
|
633
|
+
}
|
|
622
634
|
checks.push(dr.checkBindings(status.bindings));
|
|
623
635
|
}
|
|
624
636
|
else {
|
package/dist/doctor.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export type IntegrationStatus = {
|
|
|
51
51
|
};
|
|
52
52
|
/** the integration hash (§21.4); missing from an API older than the hash */
|
|
53
53
|
integration?: IntegrationState;
|
|
54
|
+
/** what Mapled takes the site for, and where Preview opens it; missing from an older API */
|
|
55
|
+
site?: {
|
|
56
|
+
framework: string;
|
|
57
|
+
rendering: "server" | "browser";
|
|
58
|
+
url: string | null;
|
|
59
|
+
};
|
|
54
60
|
previewPath: string;
|
|
55
61
|
sdk: Record<string, string>;
|
|
56
62
|
};
|
|
@@ -87,6 +93,25 @@ export type PackageVersion = {
|
|
|
87
93
|
declared: string | null;
|
|
88
94
|
};
|
|
89
95
|
export declare function installedVersion(dir: string, pkg: string): Promise<PackageVersion>;
|
|
96
|
+
/** A site rendered in the visitor's browser — a React single-page app,
|
|
97
|
+
plain HTML — reads Mapled from the page: a publish shows on the next
|
|
98
|
+
load, so it has no webhook, no routes to mount and no secret. The two
|
|
99
|
+
keys are the ones Mapled knows by (lib/siteStack.ts of the API). */
|
|
100
|
+
export declare const rendersInBrowser: (framework: string | null | undefined) => boolean;
|
|
101
|
+
/** The first-party package a site of this framework reads through. */
|
|
102
|
+
export declare const sdkOf: (framework: string | null | undefined) => string;
|
|
103
|
+
/** How a plain HTML site loads "@mapled/vanilla": a script tag in its
|
|
104
|
+
pages — from a CDN, where the version is in the address, or a copy of
|
|
105
|
+
the single file next to the pages, where it is in the file's first
|
|
106
|
+
line. */
|
|
107
|
+
export type ScriptTag = {
|
|
108
|
+
/** the page or the copy the version was read from */
|
|
109
|
+
file: string;
|
|
110
|
+
/** null — a CDN address without a version: it follows every release */
|
|
111
|
+
version: string | null;
|
|
112
|
+
};
|
|
113
|
+
export declare function findScriptTag(dir: string): Promise<ScriptTag | null>;
|
|
114
|
+
export declare function checkScriptTag(tag: ScriptTag | null, latest: string | undefined): Check;
|
|
90
115
|
export declare function detectFramework(dir: string): Promise<string | undefined>;
|
|
91
116
|
/** -1, 0 or 1 comparing dotted numeric versions; anything else compares by number. */
|
|
92
117
|
export declare function compareVersions(a: string, b: string): number;
|
|
@@ -101,6 +126,11 @@ export declare function findRoute(dir: string, kind: RouteKind): Promise<RouteFi
|
|
|
101
126
|
export declare function probePreview(origin: string, previewPath: string, f: Fetch): Promise<{
|
|
102
127
|
status: number | null;
|
|
103
128
|
}>;
|
|
129
|
+
/** A page of a site rendered in the browser: any answer means it is
|
|
130
|
+
deployed (a single-page app's host may say 404 and serve the app anyway). */
|
|
131
|
+
export declare function probeSite(origin: string, f: Fetch): Promise<{
|
|
132
|
+
status: number | null;
|
|
133
|
+
}>;
|
|
104
134
|
export declare function checkLink(found: {
|
|
105
135
|
path: string;
|
|
106
136
|
} | null, projectName: string | null): Check;
|
|
@@ -109,17 +139,20 @@ export declare function checkAuth(state: {
|
|
|
109
139
|
status: IntegrationStatus | null;
|
|
110
140
|
error: string | null;
|
|
111
141
|
}): Check;
|
|
112
|
-
export declare function checkTypes(existing: string | null, generated: Generated | null, typesPath: string): Check;
|
|
113
|
-
export declare function checkEnv(names: Map<string, string>, env: NodeJS.ProcessEnv): Check;
|
|
142
|
+
export declare function checkTypes(existing: string | null, generated: Generated | null, typesPath: string, framework?: string | null): Check;
|
|
143
|
+
export declare function checkEnv(names: Map<string, string>, env: NodeJS.ProcessEnv, framework?: string | null): Check;
|
|
114
144
|
export declare function checkSecrets(facts: GitFacts): Check;
|
|
115
|
-
export declare function checkSdk(pkg: PackageVersion, latest: string | undefined): Check;
|
|
145
|
+
export declare function checkSdk(pkg: PackageVersion, latest: string | undefined, name?: string): Check;
|
|
116
146
|
export declare function checkCli(current: string, latest: string | undefined): Check;
|
|
117
147
|
export declare function checkRoute(kind: RouteKind, found: RouteFile | null, framework: string | undefined): Check;
|
|
118
148
|
export declare function checkReads(lastReadAt: string | null): Check;
|
|
119
|
-
export declare function checkWebhook(webhook: IntegrationStatus["webhook"]
|
|
149
|
+
export declare function checkWebhook(webhook: IntegrationStatus["webhook"], site?: {
|
|
150
|
+
local: boolean;
|
|
151
|
+
remote: IntegrationStatus["site"];
|
|
152
|
+
}): Check;
|
|
120
153
|
export declare function checkPreviewOnSite(origin: string | null, previewPath: string, probe: {
|
|
121
154
|
status: number | null;
|
|
122
|
-
} | null): Check;
|
|
155
|
+
} | null, browser?: boolean): Check;
|
|
123
156
|
export declare function checkBindings(bindings: IntegrationStatus["bindings"]): Check;
|
|
124
157
|
export declare function checkSchemaPin(pin: SchemaPin | null, liveHash: string | null, changes: SchemaChange[] | null): Check;
|
|
125
158
|
/** The pushed manifest as `GET /v1/agent/manifest` answers it. */
|
package/dist/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { integrationHash } from "./integrationHash.js";
|
|
@@ -102,26 +102,101 @@ export async function installedVersion(dir, pkg) {
|
|
|
102
102
|
}
|
|
103
103
|
return { installed, declared };
|
|
104
104
|
}
|
|
105
|
+
/** A site rendered in the visitor's browser — a React single-page app,
|
|
106
|
+
plain HTML — reads Mapled from the page: a publish shows on the next
|
|
107
|
+
load, so it has no webhook, no routes to mount and no secret. The two
|
|
108
|
+
keys are the ones Mapled knows by (lib/siteStack.ts of the API). */
|
|
109
|
+
export const rendersInBrowser = (framework) => framework === "react-spa" || framework === "plain-html";
|
|
110
|
+
/** The first-party package a site of this framework reads through. */
|
|
111
|
+
export const sdkOf = (framework) => !framework || framework === "nextjs" ? "@mapled/next" : framework === "react-spa" ? "@mapled/react" : framework === "plain-html" ? "@mapled/vanilla" : "@mapled/client";
|
|
112
|
+
const SKIPPED_DIRS = new Set(["node_modules", ".git", ".next", ".vercel", ".netlify", "vendor"]);
|
|
113
|
+
export async function findScriptTag(dir) {
|
|
114
|
+
// pages first, breadth first, and not forever: a site's pages sit near its root
|
|
115
|
+
const queue = [{ abs: dir, rel: "", depth: 0 }];
|
|
116
|
+
const copies = [];
|
|
117
|
+
let unpinned = null;
|
|
118
|
+
let seen = 0;
|
|
119
|
+
while (queue.length > 0 && seen < 2000) {
|
|
120
|
+
const at = queue.shift();
|
|
121
|
+
const entries = await readdir(at.abs, { withFileTypes: true }).catch(() => []);
|
|
122
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
123
|
+
seen += 1;
|
|
124
|
+
const rel = at.rel ? `${at.rel}/${entry.name}` : entry.name;
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
if (at.depth < 3 && !SKIPPED_DIRS.has(entry.name) && !entry.name.startsWith("."))
|
|
127
|
+
queue.push({ abs: path.join(at.abs, entry.name), rel, depth: at.depth + 1 });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (/^mapled(\.min)?\.js$/.test(entry.name))
|
|
131
|
+
copies.push(rel);
|
|
132
|
+
if (!/\.html?$/i.test(entry.name))
|
|
133
|
+
continue;
|
|
134
|
+
const text = await readFile(path.join(at.abs, entry.name), "utf8").catch(() => "");
|
|
135
|
+
const pinned = /@mapled\/vanilla@(\d+\.\d+\.\d+)/.exec(text);
|
|
136
|
+
if (pinned)
|
|
137
|
+
return { file: rel, version: pinned[1] };
|
|
138
|
+
if (unpinned === null && /@mapled\/vanilla(@latest)?\//.test(text))
|
|
139
|
+
unpinned = rel;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const rel of copies) {
|
|
143
|
+
const head = (await readFile(path.join(dir, rel), "utf8").catch(() => "")).slice(0, 200);
|
|
144
|
+
const banner = /^\/\*! @mapled\/vanilla (\d+\.\d+\.\d+) /.exec(head);
|
|
145
|
+
if (banner)
|
|
146
|
+
return { file: rel, version: banner[1] };
|
|
147
|
+
}
|
|
148
|
+
return unpinned === null ? null : { file: unpinned, version: null };
|
|
149
|
+
}
|
|
150
|
+
export function checkScriptTag(tag, latest) {
|
|
151
|
+
const key = "sdk";
|
|
152
|
+
const label = "@mapled/vanilla";
|
|
153
|
+
const at = latest ?? "x.y.z";
|
|
154
|
+
if (!tag) {
|
|
155
|
+
return {
|
|
156
|
+
key,
|
|
157
|
+
label,
|
|
158
|
+
status: "failed",
|
|
159
|
+
detail: `No page loads it yet. Put <script src="https://cdn.jsdelivr.net/npm/@mapled/vanilla@${at}/dist/mapled.js" data-mapled-key="…" defer></script> into the pages.`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (tag.version === null) {
|
|
163
|
+
return { key, label, status: "warning", detail: `${tag.file} loads it without a version — that address follows every release. Pin it: @mapled/vanilla@${at}.` };
|
|
164
|
+
}
|
|
165
|
+
if (latest && compareVersions(tag.version, latest) < 0) {
|
|
166
|
+
return { key, label, status: "warning", detail: `${tag.version} in ${tag.file}, ${latest} available — change the version in the script tag.` };
|
|
167
|
+
}
|
|
168
|
+
return { key, label, status: "passed", detail: latest ? `${tag.version} in ${tag.file} (current)` : `${tag.version} in ${tag.file}` };
|
|
169
|
+
}
|
|
105
170
|
export async function detectFramework(dir) {
|
|
171
|
+
const hasIndexHtml = () => access(path.join(dir, "index.html")).then(() => true, () => false);
|
|
172
|
+
let deps;
|
|
106
173
|
try {
|
|
107
174
|
const raw = await readFile(path.join(dir, "package.json"), "utf8");
|
|
108
175
|
const p = JSON.parse(raw);
|
|
109
|
-
|
|
110
|
-
if (deps.next)
|
|
111
|
-
return "nextjs";
|
|
112
|
-
if (deps.astro)
|
|
113
|
-
return "astro";
|
|
114
|
-
if (deps.nuxt)
|
|
115
|
-
return "nuxt";
|
|
116
|
-
if (deps["@sveltejs/kit"])
|
|
117
|
-
return "sveltekit";
|
|
118
|
-
if (deps["@remix-run/react"] || deps["react-router"])
|
|
119
|
-
return "remix";
|
|
120
|
-
return undefined;
|
|
176
|
+
deps = { ...p.devDependencies, ...p.dependencies };
|
|
121
177
|
}
|
|
122
178
|
catch {
|
|
123
|
-
|
|
124
|
-
|
|
179
|
+
// no package.json at all: pages written by hand
|
|
180
|
+
return (await hasIndexHtml()) ? "plain-html" : undefined;
|
|
181
|
+
}
|
|
182
|
+
if (deps.next)
|
|
183
|
+
return "nextjs";
|
|
184
|
+
if (deps.astro)
|
|
185
|
+
return "astro";
|
|
186
|
+
if (deps.nuxt)
|
|
187
|
+
return "nuxt";
|
|
188
|
+
if (deps["@sveltejs/kit"])
|
|
189
|
+
return "sveltekit";
|
|
190
|
+
// react-router alone is a library a single-page app routes with; its framework mode brings @react-router/dev
|
|
191
|
+
if (deps["@remix-run/react"] || deps["@react-router/dev"])
|
|
192
|
+
return "remix";
|
|
193
|
+
if (deps.react && (deps.vite || deps["@vitejs/plugin-react"] || deps["@vitejs/plugin-react-swc"] || deps["react-scripts"] || deps["@mapled/react"])) {
|
|
194
|
+
return "react-spa";
|
|
195
|
+
}
|
|
196
|
+
if (!deps.react && !deps.vue && !deps.svelte && !deps["solid-js"] && !deps.express && !deps.fastify && (await hasIndexHtml())) {
|
|
197
|
+
return "plain-html";
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
125
200
|
}
|
|
126
201
|
/** -1, 0 or 1 comparing dotted numeric versions; anything else compares by number. */
|
|
127
202
|
export function compareVersions(a, b) {
|
|
@@ -186,6 +261,17 @@ export async function probePreview(origin, previewPath, f) {
|
|
|
186
261
|
}
|
|
187
262
|
}
|
|
188
263
|
/* ---- the checks ---- */
|
|
264
|
+
/** A page of a site rendered in the browser: any answer means it is
|
|
265
|
+
deployed (a single-page app's host may say 404 and serve the app anyway). */
|
|
266
|
+
export async function probeSite(origin, f) {
|
|
267
|
+
try {
|
|
268
|
+
const res = await f(`${origin}/`, { method: "GET", redirect: "manual", signal: AbortSignal.timeout(5000) });
|
|
269
|
+
return { status: res.status };
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return { status: null };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
189
275
|
export function checkLink(found, projectName) {
|
|
190
276
|
const label = "Project link";
|
|
191
277
|
if (!found)
|
|
@@ -206,8 +292,11 @@ export function checkAuth(state) {
|
|
|
206
292
|
detail: `${state.status.connection.name} • ${state.status.project.name}`,
|
|
207
293
|
};
|
|
208
294
|
}
|
|
209
|
-
export function checkTypes(existing, generated, typesPath) {
|
|
295
|
+
export function checkTypes(existing, generated, typesPath, framework) {
|
|
210
296
|
const label = "Generated types";
|
|
297
|
+
if (framework === "plain-html" && existing === null) {
|
|
298
|
+
return { key: "types", label, status: "skipped", detail: "Not needed: plain HTML names its fields in attributes, not in TypeScript." };
|
|
299
|
+
}
|
|
211
300
|
if (!generated)
|
|
212
301
|
return { key: "types", label, status: "skipped", detail: "Sign in to compare with the schema." };
|
|
213
302
|
if (existing === null) {
|
|
@@ -218,9 +307,36 @@ export function checkTypes(existing, generated, typesPath) {
|
|
|
218
307
|
}
|
|
219
308
|
return { key: "types", label, status: "warning", detail: `${typesPath} is out of date — run \`mapled types generate\`.` };
|
|
220
309
|
}
|
|
221
|
-
|
|
310
|
+
/** Prefixes that put an env var into the browser's bundle. */
|
|
311
|
+
const PUBLIC_ENV = /^(VITE_|REACT_APP_|NEXT_PUBLIC_|NUXT_PUBLIC_|PUBLIC_|GATSBY_)/;
|
|
312
|
+
export function checkEnv(names, env, framework) {
|
|
222
313
|
const label = "Environment";
|
|
223
314
|
const where = (name) => names.get(name) ?? (env[name] ? "the environment" : null);
|
|
315
|
+
// whatever the framework: a server key or a signing secret under a public prefix ships to every visitor
|
|
316
|
+
const exposed = [...names.keys()].filter((n) => PUBLIC_ENV.test(n) && /MAPLED/.test(n) && /(SERVER_KEY|SECRET)/.test(n));
|
|
317
|
+
if (exposed.length > 0) {
|
|
318
|
+
return {
|
|
319
|
+
key: "env",
|
|
320
|
+
label,
|
|
321
|
+
status: "failed",
|
|
322
|
+
detail: `${exposed.join(", ")} in ${names.get(exposed[0])} is bundled into the browser. Only the delivery key may be: rename it and rotate the value in Integrations.`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (framework === "plain-html") {
|
|
326
|
+
return { key: "env", label, status: "skipped", detail: "No env files: the delivery key sits in the page — the one key that is safe there." };
|
|
327
|
+
}
|
|
328
|
+
if (framework === "react-spa") {
|
|
329
|
+
const name = ["VITE_MAPLED_KEY", "REACT_APP_MAPLED_KEY"].find((n) => where(n));
|
|
330
|
+
if (!name) {
|
|
331
|
+
return {
|
|
332
|
+
key: "env",
|
|
333
|
+
label,
|
|
334
|
+
status: "failed",
|
|
335
|
+
detail: "VITE_MAPLED_KEY isn't set. Put the delivery key (Integrations → Your site) in .env — it is the one key that is safe in a browser.",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return { key: "env", label, status: "passed", detail: `${name} in ${where(name)} — no webhook secret needed: the site reads in the browser` };
|
|
339
|
+
}
|
|
224
340
|
const key = where("MAPLED_KEY");
|
|
225
341
|
const secret = where("MAPLED_WEBHOOK_SECRET");
|
|
226
342
|
if (!key) {
|
|
@@ -268,10 +384,10 @@ export function checkSecrets(facts) {
|
|
|
268
384
|
}
|
|
269
385
|
return { key: "secrets", label, status: "passed", detail: "No env files or Mapled secrets are tracked." };
|
|
270
386
|
}
|
|
271
|
-
export function checkSdk(pkg, latest) {
|
|
272
|
-
const label =
|
|
387
|
+
export function checkSdk(pkg, latest, name = "@mapled/next") {
|
|
388
|
+
const label = name;
|
|
273
389
|
if (!pkg.installed && !pkg.declared) {
|
|
274
|
-
return { key: "sdk", label, status: "failed", detail:
|
|
390
|
+
return { key: "sdk", label, status: "failed", detail: `Not installed. Run \`npm install ${name}\`.` };
|
|
275
391
|
}
|
|
276
392
|
if (!pkg.installed) {
|
|
277
393
|
return { key: "sdk", label, status: "warning", detail: `${pkg.declared} is declared but not installed — run npm install.` };
|
|
@@ -283,7 +399,7 @@ export function checkSdk(pkg, latest) {
|
|
|
283
399
|
key: "sdk",
|
|
284
400
|
label,
|
|
285
401
|
status: "warning",
|
|
286
|
-
detail: `${pkg.installed} installed, ${latest} available — npm install @
|
|
402
|
+
detail: `${pkg.installed} installed, ${latest} available — npm install ${name}@latest.`,
|
|
287
403
|
};
|
|
288
404
|
}
|
|
289
405
|
return { key: "sdk", label, status: "passed", detail: `${pkg.installed} (current)` };
|
|
@@ -299,6 +415,16 @@ export function checkRoute(kind, found, framework) {
|
|
|
299
415
|
const key = `route_${kind}`;
|
|
300
416
|
const label = kind === "revalidate" ? "Revalidation route" : "Preview route";
|
|
301
417
|
const handler = ROUTE_HANDLER[kind];
|
|
418
|
+
if (rendersInBrowser(framework)) {
|
|
419
|
+
return {
|
|
420
|
+
key,
|
|
421
|
+
label,
|
|
422
|
+
status: "skipped",
|
|
423
|
+
detail: kind === "revalidate"
|
|
424
|
+
? "Not needed: the site reads in the browser, so a publish shows on the next load."
|
|
425
|
+
: "Not needed: the client trades Mapled's preview link on the page itself.",
|
|
426
|
+
};
|
|
427
|
+
}
|
|
302
428
|
if (framework && framework !== "nextjs") {
|
|
303
429
|
return { key, label, status: "skipped", detail: `No ${kind} route check for ${framework} sites yet.` };
|
|
304
430
|
}
|
|
@@ -334,8 +460,20 @@ export function checkReads(lastReadAt) {
|
|
|
334
460
|
}
|
|
335
461
|
return { key: "reads", label, status: "passed", detail: `Last read ${ago(lastReadAt)}` };
|
|
336
462
|
}
|
|
337
|
-
export function checkWebhook(webhook) {
|
|
463
|
+
export function checkWebhook(webhook, site) {
|
|
338
464
|
const label = "Publish webhook";
|
|
465
|
+
if (!webhook.url && site?.local) {
|
|
466
|
+
// the repository renders in the browser; does Mapled know?
|
|
467
|
+
if (site.remote && site.remote.rendering !== "browser") {
|
|
468
|
+
return {
|
|
469
|
+
key: "webhook",
|
|
470
|
+
label,
|
|
471
|
+
status: "warning",
|
|
472
|
+
detail: `Mapled takes this site for ${site.remote.framework} and will ask for a webhook: run \`mapled scan --write\` and \`mapled bindings push\` so it knows the site renders in the browser.`,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return { key: "webhook", label, status: "skipped", detail: "Not needed: the site reads in the browser, so a publish shows on the next load." };
|
|
476
|
+
}
|
|
339
477
|
if (!webhook.url) {
|
|
340
478
|
return {
|
|
341
479
|
key: "webhook",
|
|
@@ -377,8 +515,21 @@ export function checkWebhook(webhook) {
|
|
|
377
515
|
detail: `The last delivery to ${host} ${reason}. Check the route and MAPLED_WEBHOOK_SECRET, then send a test ping.`,
|
|
378
516
|
};
|
|
379
517
|
}
|
|
380
|
-
export function checkPreviewOnSite(origin, previewPath, probe) {
|
|
518
|
+
export function checkPreviewOnSite(origin, previewPath, probe, browser = false) {
|
|
381
519
|
const label = "Preview on the site";
|
|
520
|
+
if (browser) {
|
|
521
|
+
if (!origin || !probe) {
|
|
522
|
+
return {
|
|
523
|
+
key: "preview_site",
|
|
524
|
+
label,
|
|
525
|
+
status: "warning",
|
|
526
|
+
detail: "Mapled doesn't know where the site is deployed — set Production domain in Project settings, or ask your agent (set_site_url).",
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
return probe.status === null
|
|
530
|
+
? { key: "preview_site", label, status: "warning", detail: `${origin} is unreachable from here.` }
|
|
531
|
+
: { key: "preview_site", label, status: "passed", detail: `Opens at ${origin} — the client trades the link in the browser` };
|
|
532
|
+
}
|
|
382
533
|
if (!origin || !probe) {
|
|
383
534
|
return { key: "preview_site", label, status: "skipped", detail: "Needs the webhook URL to know where the site lives." };
|
|
384
535
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare function typeName(key: string, kind: "collection" | "single"): st
|
|
|
21
21
|
export declare function typeNames(collections: SchemaCollection[]): Map<string, string>;
|
|
22
22
|
export declare function generateTypes(schema: Schema, opts?: {
|
|
23
23
|
projectName?: string;
|
|
24
|
+
sdk?: string;
|
|
24
25
|
}): Generated;
|
|
25
26
|
/** Splits a generated file into its header and body, with the hash the
|
|
26
27
|
header names when it has one. */
|
package/dist/types.js
CHANGED
|
@@ -177,14 +177,18 @@ export function typeNames(collections) {
|
|
|
177
177
|
return out;
|
|
178
178
|
}
|
|
179
179
|
export function generateTypes(schema, opts = {}) {
|
|
180
|
+
// the package the site reads through: "@mapled/next" unless the framework says otherwise
|
|
181
|
+
const sdk = opts.sdk ?? "@mapled/next";
|
|
182
|
+
// a React app's assetUrl comes from the client its hooks are built on; the HTML runtime has none to call
|
|
183
|
+
const assets = sdk === "@mapled/react" || sdk === "@mapled/vanilla" ? "@mapled/client" : sdk;
|
|
180
184
|
const hash = schemaHash(schema);
|
|
181
185
|
const names = typeNames(schema.collections);
|
|
182
186
|
const blocks = [];
|
|
183
187
|
blocks.push([
|
|
184
|
-
|
|
188
|
+
`// Types of the published content this site reads through ${sdk}.`,
|
|
185
189
|
"// Sensitive fields never reach the site and are left out.",
|
|
186
190
|
"",
|
|
187
|
-
|
|
191
|
+
`/** An asset id — pass it to assetUrl(id, { width }) from "${assets}". */`,
|
|
188
192
|
"export type MapledAssetId = string;",
|
|
189
193
|
'/** The id of a record in another collection — attach it with { expand: ["field"] }. */',
|
|
190
194
|
"export type MapledRecordId = string;",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mapled/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Mapled CLI — sign in, link a project, generate TypeScript types for its content, create forms and check the site's integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://mapled.io",
|