@mapled/cli 0.14.2 → 0.14.4
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 +6 -3
- package/dist/args.js +1 -1
- package/dist/commands.d.ts +6 -4
- package/dist/commands.js +30 -13
- package/dist/doctor.d.ts +27 -2
- package/dist/doctor.js +141 -11
- package/dist/index.js +1 -1
- package/dist/integrationHash.d.ts +6 -1
- package/dist/integrationHash.js +2 -0
- package/dist/live.d.ts +10 -0
- package/dist/live.js +13 -0
- package/dist/manifest.d.ts +18 -1
- package/dist/manifest.js +42 -1
- package/dist/pin.d.ts +9 -3
- package/dist/pin.js +71 -5
- package/dist/report.d.ts +1 -0
- package/dist/scan.d.ts +10 -1
- package/dist/scan.js +22 -2
- package/dist/schema.d.ts +16 -0
- package/dist/schema.js +25 -1
- package/dist/types.d.ts +19 -3
- package/dist/types.js +79 -14
- package/dist/vanillaDigests.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Only public values live here — the project id, where the generated types go, t
|
|
|
50
50
|
npx @mapled/cli types generate
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Reads the project's schema and writes `mapled-types.ts` (the path from `mapled.json`, or `--out <file>`): one interface per collection and single, keyed maps, a `MapledSchema` that ties them together, and doc comments with the field types. Sensitive fields never reach the site and are left out.
|
|
53
|
+
Reads the project's schema and writes `mapled-types.ts` (the path from `mapled.json`, or `--out <file>`): one interface per collection and single, keyed maps, a `MapledSchema` that ties them together, and doc comments with the field types. Sensitive fields never reach the site and are left out. A group field is typed by an interface of its items; the project's component types become one interface each (`HeroComponent`, with `_type: "hero"` and the item's `_key`), a `components` field a list of the union of the types it allows, and `MapledComponent` the union of them all — switch on `_type` to render an item.
|
|
54
54
|
|
|
55
55
|
```ts
|
|
56
56
|
import type { MapledSchema } from "./mapled-types";
|
|
@@ -130,6 +130,8 @@ Scanned 2 HTML files — 2 pages, 11 bindings.
|
|
|
130
130
|
|
|
131
131
|
For a Next.js site the scan also records its live updates: `capabilities.realtime` is `true`, with the route in `releaseRoute`, when `<MapledLive />` or `watchRelease` asks — at the default path or one written in the code — a route whose exported `GET` answers with `createReleaseHandler`, or, on the Pages Router, an API route whose default export answers with `createPagesReleaseHandler` (with `<MapledLive />` from `@mapled/next/live/pages` in `pages/_app`), and `false` otherwise — `MAPLED.md` then tells the next agent whether open tabs follow Publish. Capabilities aren't part of the integration hash; `doctor`'s Site manifest line says when the pushed ones differ.
|
|
132
132
|
|
|
133
|
+
On a site with more than one language the scan records how the routes carry it: `locales` is `{ "routing": "prefix", "param": "locale" }` when the routes start with a segment named for the language (`/[locale]/blog/[slug]`) — Mapled then builds each language's address of a record from it (the editor's routes, Preview), a translated slug being the address in that language. Write `{ "routing": "prefix-except-default" }` by hand when the default language lives at the root and the others under `/ru/…`, `/de/…` (without `param` Mapled puts the language before the page); leave `locales` out when every language shares one address. It is part of the integration hash.
|
|
134
|
+
|
|
133
135
|
`--write` creates or updates `mapled/manifest.json`. Bindings the scan can't see (written by your AI agent, or by hand) are kept and listed; `--prune` drops them. Commit the file.
|
|
134
136
|
|
|
135
137
|
## Validate and push the manifest
|
|
@@ -140,7 +142,7 @@ npx @mapled/cli bindings push
|
|
|
140
142
|
npx @mapled/cli bindings pull
|
|
141
143
|
```
|
|
142
144
|
|
|
143
|
-
`manifest validate` checks `mapled/manifest.json` the way Mapled will: the shape and limits, duplicate keys, and — against the schema pin or the live schema — collections and fields that don't exist and targets that don't fit the field type, plus files the repository doesn't have. Problems exit 1; warnings only tell you what Mapled would grade down.
|
|
145
|
+
`manifest validate` checks `mapled/manifest.json` the way Mapled will: the shape and limits, duplicate keys, `capabilities` and `locales` (`routing` and `param` only, as Mapled's schema takes them — and whether a page route has the `param` segment), and — against the schema pin or the live schema — collections and fields that don't exist and targets that don't fit the field type, plus files the repository doesn't have. Problems exit 1; warnings only tell you what Mapled would grade down.
|
|
144
146
|
|
|
145
147
|
`bindings push` sends the manifest to Mapled (the same call your AI agent's `push_site_manifest` makes). Mapled grades every binding — healthy, type mismatch, outdated, missing on site — and the result shows up in **Structure → Bindings**. The push also records the integration hash: one marker over the schema and these bindings, what the site is synced with from now on. `--dry-run` validates and stops. Pushing bindings changes structure, so it needs the builder plan.
|
|
146
148
|
|
|
@@ -254,9 +256,10 @@ Still hardcoded
|
|
|
254
256
|
```bash
|
|
255
257
|
npx @mapled/cli preview
|
|
256
258
|
npx @mapled/cli preview articles hello-world
|
|
259
|
+
npx @mapled/cli preview articles hello-world --locale ru
|
|
257
260
|
```
|
|
258
261
|
|
|
259
|
-
Prints a Preview link — the one the **Preview** button in Mapled opens: the site in draft mode, with every saved change that isn't published yet. Name a collection and a record's slug (or id) and the link lands on the record's page, the one the pushed site manifest says shows it; a collection alone opens the page that lists it, a single its page, nothing at all the home page. Mapled makes the link after checking that the site answers and mounts its preview route; it works once, for five minutes. A plain HTML site whose pages are built with the content in them has no page for a record it wasn't built with: for such a record the command says so instead of printing a link — its page appears once the site is built with it. `--open` opens it in your browser instead of printing it; `--json` prints the link with the page and when it expires.
|
|
262
|
+
Prints a Preview link — the one the **Preview** button in Mapled opens: the site in draft mode, with every saved change that isn't published yet. Name a collection and a record's slug (or id) and the link lands on the record's page, the one the pushed site manifest says shows it; a collection alone opens the page that lists it, a single its page, nothing at all the home page. Mapled makes the link after checking that the site answers and mounts its preview route; it works once, for five minutes. A plain HTML site whose pages are built with the content in them has no page for a record it wasn't built with: for such a record the command says so instead of printing a link — its page appears once the site is built with it. `--locale <code>` opens the page in that language — the address Mapled builds from the manifest's `locales` (a translated slug is the address in its language); a language the project doesn't have is refused. `--open` opens it in your browser instead of printing it; `--json` prints the link with the page, the language when one was asked, and when it expires.
|
|
260
263
|
|
|
261
264
|
## Options
|
|
262
265
|
|
package/dist/args.js
CHANGED
|
@@ -2,7 +2,7 @@ import { CliError } from "./errors.js";
|
|
|
2
2
|
/** Minimal argument parsing: the leading words name the command;
|
|
3
3
|
`--flag`, `--flag=value` and `--flag value` (for the flags that take
|
|
4
4
|
one) follow. Nothing here is user content beyond the command line. */
|
|
5
|
-
const VALUE_FLAGS = new Set(["api", "project", "out", "key"]);
|
|
5
|
+
const VALUE_FLAGS = new Set(["api", "project", "out", "key", "locale"]);
|
|
6
6
|
export function parseArgs(argv) {
|
|
7
7
|
const words = [];
|
|
8
8
|
const flags = {};
|
package/dist/commands.d.ts
CHANGED
|
@@ -38,10 +38,12 @@ export declare function formsList(ctx: Ctx, flags: Flags): Promise<void>;
|
|
|
38
38
|
applied setup run (S-42) and what the agent reported; `--verify` runs
|
|
39
39
|
the server checks again first, as "Run checks again" does. */
|
|
40
40
|
export declare function integrationReport(ctx: Ctx, flags: Flags): Promise<number>;
|
|
41
|
-
/** `mapled preview [<collection> [<slug>]]` — a Preview
|
|
42
|
-
launcher, the one the Preview button opens: the token
|
|
43
|
-
CLI only passes it on. `--
|
|
44
|
-
|
|
41
|
+
/** `mapled preview [<collection> [<slug>]] [--locale <code>]` — a Preview
|
|
42
|
+
link from Mapled's launcher, the one the Preview button opens: the token
|
|
43
|
+
is Mapled's, the CLI only passes it on. `--locale` opens the page in that
|
|
44
|
+
language (§28.4: Mapled builds the address from the manifest's `locales`;
|
|
45
|
+
a language the project doesn't have is refused by Mapled). `--open` hands
|
|
46
|
+
it to the browser instead of printing it (the link works once). */
|
|
45
47
|
export declare function preview(ctx: Ctx, flags: Flags, words: string[]): Promise<void>;
|
|
46
48
|
export declare function scan(ctx: Ctx, flags: Flags): Promise<void>;
|
|
47
49
|
export declare function doctor(ctx: Ctx, flags: Flags): Promise<number>;
|
package/dist/commands.js
CHANGED
|
@@ -179,7 +179,7 @@ export async function schemaPull(ctx, flags) {
|
|
|
179
179
|
ctx.out(`${SCHEMA_FILE} is up to date (schema ${pin.hash}).`);
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
|
-
const changes = diffSchema(existing.pin.collections, pin.collections);
|
|
182
|
+
const changes = diffSchema(existing.pin.collections, pin.collections, { before: existing.pin.componentTypes, after: pin.componentTypes });
|
|
183
183
|
await writePin(found.dir, pin);
|
|
184
184
|
ctx.out(`Updated ${SCHEMA_FILE} (schema ${existing.pin.hash} → ${pin.hash}): ${summarizeChanges(changes).toLowerCase()}.`);
|
|
185
185
|
ctx.out("");
|
|
@@ -199,7 +199,7 @@ export async function schemaDiff(ctx, flags) {
|
|
|
199
199
|
throw new CliError(`No ${SCHEMA_FILE} yet. Run \`mapled schema pull\` to record the schema this site is built against.`);
|
|
200
200
|
}
|
|
201
201
|
const live = pinSchema(await session.get("/v1/agent/schema"), found.config.project);
|
|
202
|
-
const changes = diffSchema(existing.pin.collections, live.collections);
|
|
202
|
+
const changes = diffSchema(existing.pin.collections, live.collections, { before: existing.pin.componentTypes, after: live.componentTypes });
|
|
203
203
|
if (flags.json) {
|
|
204
204
|
ctx.out(JSON.stringify({ from: existing.pin.hash, to: live.hash, changes }, null, 2));
|
|
205
205
|
}
|
|
@@ -360,7 +360,7 @@ export async function bindingsPull(ctx, flags) {
|
|
|
360
360
|
if (local && !flags.force) {
|
|
361
361
|
const diff = compareManifests(local.manifest, stored.manifest);
|
|
362
362
|
// capabilities a scan just recorded are as much the repository's as its bindings
|
|
363
|
-
if (!diff.same || diff.capabilitiesDiffer) {
|
|
363
|
+
if (!diff.same || diff.capabilitiesDiffer || diff.localesDiffer) {
|
|
364
364
|
const parts = [];
|
|
365
365
|
if (diff.onlyLocal.length > 0)
|
|
366
366
|
parts.push(`${diff.onlyLocal.length} not pushed`);
|
|
@@ -370,6 +370,8 @@ export async function bindingsPull(ctx, flags) {
|
|
|
370
370
|
parts.push(`${diff.onlyRemote.length} only in Mapled`);
|
|
371
371
|
if (diff.capabilitiesDiffer)
|
|
372
372
|
parts.push("capabilities changed");
|
|
373
|
+
if (diff.localesDiffer)
|
|
374
|
+
parts.push("routes by language changed");
|
|
373
375
|
throw new CliError(`${MANIFEST_FILE} differs from the pushed manifest (v${stored.version}): ${parts.join(", ")}. Pass --force to overwrite it, or push yours with \`mapled bindings push\`.`);
|
|
374
376
|
}
|
|
375
377
|
}
|
|
@@ -478,21 +480,28 @@ export async function integrationReport(ctx, flags) {
|
|
|
478
480
|
ctx.out(line);
|
|
479
481
|
return report.checks.some((c) => c.status === "failed") ? 1 : 0;
|
|
480
482
|
}
|
|
481
|
-
/** `mapled preview [<collection> [<slug>]]` — a Preview
|
|
482
|
-
launcher, the one the Preview button opens: the token
|
|
483
|
-
CLI only passes it on. `--
|
|
484
|
-
|
|
483
|
+
/** `mapled preview [<collection> [<slug>]] [--locale <code>]` — a Preview
|
|
484
|
+
link from Mapled's launcher, the one the Preview button opens: the token
|
|
485
|
+
is Mapled's, the CLI only passes it on. `--locale` opens the page in that
|
|
486
|
+
language (§28.4: Mapled builds the address from the manifest's `locales`;
|
|
487
|
+
a language the project doesn't have is refused by Mapled). `--open` hands
|
|
488
|
+
it to the browser instead of printing it (the link works once). */
|
|
485
489
|
export async function preview(ctx, flags, words) {
|
|
486
490
|
const [collection, record, ...rest] = words;
|
|
487
491
|
if (rest.length > 0) {
|
|
488
492
|
throw new CliError("Name a collection and, optionally, one record's slug: `mapled preview articles hello-world`.", 2);
|
|
489
493
|
}
|
|
494
|
+
const locale = stringFlag(flags, "locale");
|
|
495
|
+
if (locale !== undefined && !/^[A-Za-z0-9_-]{1,40}$/.test(locale)) {
|
|
496
|
+
throw new CliError("--locale takes a language code such as ru or pt-BR.", 2);
|
|
497
|
+
}
|
|
490
498
|
const { session } = await openSession(ctx, flags);
|
|
491
499
|
let answer;
|
|
492
500
|
try {
|
|
493
501
|
answer = await session.send("POST", "/v1/agent/preview", {
|
|
494
502
|
...(collection ? { collection } : {}),
|
|
495
503
|
...(record ? { record } : {}),
|
|
504
|
+
...(locale ? { locale } : {}),
|
|
496
505
|
});
|
|
497
506
|
}
|
|
498
507
|
catch (err) {
|
|
@@ -516,11 +525,13 @@ export async function preview(ctx, flags, words) {
|
|
|
516
525
|
if (typeof answer.url !== "string" || !/^https?:\/\/[!-~]+$/.test(answer.url)) {
|
|
517
526
|
throw new CliError("Mapled answered with a link this CLI can't print. Update @mapled/cli and try again.");
|
|
518
527
|
}
|
|
528
|
+
// the language Mapled opened the page in (the answer names it only when one was asked)
|
|
529
|
+
const inLanguage = typeof answer.locale === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(answer.locale) ? answer.locale : null;
|
|
519
530
|
if (flags.json) {
|
|
520
|
-
ctx.out(JSON.stringify({ url: answer.url, path: answer.path, expiresAt: answer.expiresAt ?? null }, null, 2));
|
|
531
|
+
ctx.out(JSON.stringify({ url: answer.url, path: answer.path, expiresAt: answer.expiresAt ?? null, ...(inLanguage ? { locale: inLanguage } : {}) }, null, 2));
|
|
521
532
|
return;
|
|
522
533
|
}
|
|
523
|
-
const where = answer.path ? clean(answer.path, 500) : "the site's home page";
|
|
534
|
+
const where = (answer.path ? clean(answer.path, 500) : "the site's home page") + (inLanguage ? ` in ${inLanguage}` : "");
|
|
524
535
|
const lifetime = linkLifetime(answer.expiresAt);
|
|
525
536
|
const project = clean(session.conn.projectName, 120);
|
|
526
537
|
if (flags.open && (await ctx.openBrowser(answer.url))) {
|
|
@@ -626,6 +637,8 @@ export async function scan(ctx, flags) {
|
|
|
626
637
|
parts.push(`${merged.unchanged.length} unchanged`);
|
|
627
638
|
if (merged.capabilitiesChanged)
|
|
628
639
|
parts.push("capabilities updated");
|
|
640
|
+
if (merged.localesChanged)
|
|
641
|
+
parts.push("routes by language updated");
|
|
629
642
|
ctx.out(`Wrote ${MANIFEST_FILE} — ${manifestSummary(merged.manifest)}${parts.length > 0 && existing ? ` (${parts.join(", ")})` : ""}.${keptNote}${droppedNote}`);
|
|
630
643
|
ctx.out("Next: `mapled bindings push`.");
|
|
631
644
|
}
|
|
@@ -641,10 +654,12 @@ export async function scan(ctx, flags) {
|
|
|
641
654
|
parts.push(`${merged.unchanged.length} unchanged`);
|
|
642
655
|
if (merged.capabilitiesChanged)
|
|
643
656
|
parts.push("capabilities changed");
|
|
657
|
+
if (merged.localesChanged)
|
|
658
|
+
parts.push("routes by language changed");
|
|
644
659
|
if (merged.kept.length > 0)
|
|
645
660
|
parts.push(`${merged.kept.length} not found in the code (${merged.kept.slice(0, 5).join(", ")}${merged.kept.length > 5 ? ", …" : ""})`);
|
|
646
661
|
ctx.out(`Compared with ${MANIFEST_FILE}: ${parts.join(", ")}.`);
|
|
647
|
-
if (merged.added.length > 0 || merged.changed.length > 0 || merged.capabilitiesChanged) {
|
|
662
|
+
if (merged.added.length > 0 || merged.changed.length > 0 || merged.capabilitiesChanged || merged.localesChanged) {
|
|
648
663
|
ctx.out(`Run \`mapled scan --write\` to update ${MANIFEST_FILE}, then \`mapled bindings push\`.`);
|
|
649
664
|
}
|
|
650
665
|
else {
|
|
@@ -715,10 +730,12 @@ export async function doctor(ctx, flags) {
|
|
|
715
730
|
// plain HTML installs nothing: the runtime comes with a script tag
|
|
716
731
|
checks.push(framework === "plain-html" ? dr.checkScriptTag(await dr.findScriptTag(dir, status?.site?.url), status?.sdk[sdk]) : dr.checkSdk(await dr.installedVersion(dir, sdk), status?.sdk[sdk], sdk));
|
|
717
732
|
checks.push(dr.checkCli(ctx.version, status?.sdk["@mapled/cli"]));
|
|
718
|
-
|
|
719
|
-
|
|
733
|
+
// the live facts first: the revalidate route's answer matters once a page listens (#435)
|
|
734
|
+
const live = !framework || framework === "nextjs" ? await findLive(dir) : null;
|
|
735
|
+
checks.push(dr.checkRoute("revalidate", await dr.findRoute(dir, "revalidate", framework), framework, (live?.listeners.length ?? 0) > 0));
|
|
736
|
+
checks.push(dr.checkRoute("preview", await dr.findRoute(dir, "preview", framework), framework));
|
|
720
737
|
// the code is read only where the check applies: a Next.js site (or one doctor can't place)
|
|
721
|
-
checks.push(dr.checkLive(
|
|
738
|
+
checks.push(dr.checkLive(live, framework));
|
|
722
739
|
if (status) {
|
|
723
740
|
checks.push(dr.checkReads(status.delivery.lastReadAt));
|
|
724
741
|
checks.push(dr.checkWebhook(status.webhook, { local: browser, remote: status.site }));
|
package/dist/doctor.d.ts
CHANGED
|
@@ -101,6 +101,12 @@ export declare function installedVersion(dir: string, pkg: string): Promise<Pack
|
|
|
101
101
|
export declare const rendersInBrowser: (framework: string | null | undefined) => boolean;
|
|
102
102
|
/** The first-party package a site of this framework reads through. */
|
|
103
103
|
export declare const sdkOf: (framework: string | null | undefined) => string;
|
|
104
|
+
/** The frameworks whose revalidate and preview routes doctor reads: Next.js
|
|
105
|
+
(`app/api/mapled/…/route.ts`, `pages/api/mapled/…`), Astro
|
|
106
|
+
(`src/pages/api/mapled/….ts` — the same paths ROUTE_FILES already names
|
|
107
|
+
for a Pages Router site under `src/`) and Nuxt (`server/api/mapled/…`,
|
|
108
|
+
or the module in `nuxt.config.ts`, which mounts them — `moduleRoute`). */
|
|
109
|
+
export declare const checksRoutesOf: (framework: string | null | undefined) => boolean;
|
|
104
110
|
/** How a plain HTML site loads "@mapled/vanilla": a script tag in its
|
|
105
111
|
pages — from a CDN, where the version is in the address, or a copy of
|
|
106
112
|
the single file next to the pages, where it is in the file's first
|
|
@@ -152,12 +158,29 @@ export declare function detectFramework(dir: string): Promise<string | undefined
|
|
|
152
158
|
/** -1, 0 or 1 comparing dotted numeric versions; anything else compares by number. */
|
|
153
159
|
export declare function compareVersions(a: string, b: string): number;
|
|
154
160
|
export type RouteKind = "revalidate" | "preview";
|
|
161
|
+
/** The Nuxt module mounts every route itself: a site whose `nuxt.config`
|
|
162
|
+
lists "@mapled/nuxt/module" has them without a file. What `findRoute`
|
|
163
|
+
answers for such a site, when no route file of its own is found. */
|
|
164
|
+
export declare const NUXT_CONFIG_FILES: string[];
|
|
165
|
+
export declare const NUXT_MODULE = "@mapled/nuxt/module";
|
|
166
|
+
export declare function moduleRoute(dir: string): Promise<RouteFile | null>;
|
|
167
|
+
/** Whether a nuxt.config's text lists the module, routes on, in its
|
|
168
|
+
`modules` array: as the string "@mapled/nuxt/module" or as the
|
|
169
|
+
identifier a default import of it binds, bare or as the first element
|
|
170
|
+
of a tuple whose options don't say `routes: false`. Comments don't
|
|
171
|
+
count; neither does an import that never reaches `modules`.
|
|
172
|
+
Conservative: a config that can't be read this way claims nothing,
|
|
173
|
+
and doctor asks for the route files. */
|
|
174
|
+
export declare function mountsModuleRoutes(text: string): boolean;
|
|
155
175
|
export declare const ROUTE_HANDLER: Record<RouteKind, string>;
|
|
176
|
+
/** `cacheMark` and `fetchCache` — the revalidate route only (revalidateRouteFacts). */
|
|
156
177
|
export type RouteFile = {
|
|
157
178
|
file: string;
|
|
158
179
|
usesAdapter: boolean;
|
|
180
|
+
cacheMark?: boolean;
|
|
181
|
+
fetchCache?: boolean;
|
|
159
182
|
};
|
|
160
|
-
export declare function findRoute(dir: string, kind: RouteKind): Promise<RouteFile | null>;
|
|
183
|
+
export declare function findRoute(dir: string, kind: RouteKind, framework?: string | null): Promise<RouteFile | null>;
|
|
161
184
|
/** GET the site's preview route without a token — the adapter answers 400. */
|
|
162
185
|
export declare function probePreview(origin: string, previewPath: string, f: Fetch): Promise<{
|
|
163
186
|
status: number | null;
|
|
@@ -180,7 +203,9 @@ export declare function checkEnv(names: Map<string, string>, env: NodeJS.Process
|
|
|
180
203
|
export declare function checkSecrets(facts: GitFacts): Check;
|
|
181
204
|
export declare function checkSdk(pkg: PackageVersion, latest: string | undefined, name?: string): Check;
|
|
182
205
|
export declare function checkCli(current: string, latest: string | undefined): Check;
|
|
183
|
-
|
|
206
|
+
/** `listens`: a page follows Publish (<MapledLive /> or watchRelease) — then
|
|
207
|
+
the revalidate route's answer is what the release route trusts (#435). */
|
|
208
|
+
export declare function checkRoute(kind: RouteKind, found: RouteFile | null, framework: string | undefined, listens?: boolean): Check;
|
|
184
209
|
/** Live updates (§18.8): whether open tabs follow Publish, and when the
|
|
185
210
|
code listens, whether a route answers it. `facts` is null when the
|
|
186
211
|
code wasn't read (a stack this check doesn't cover). */
|
package/dist/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { parseDocument } from "./html5.js";
|
|
7
7
|
import { integrationHash } from "./integrationHash.js";
|
|
8
|
-
import { BASE_PATH_SINCE, fullAddress, liveState, pagesRouteFileFor, RELEASE_ROUTE, routeFileFor, runsIn } from "./live.js";
|
|
8
|
+
import { BASE_PATH_SINCE, fullAddress, liveState, pagesRouteFileFor, RELEASE_ROUTE, revalidateRouteFacts, routeFileFor, runsIn } from "./live.js";
|
|
9
9
|
import { compareManifests, manifestSummary, MANIFEST_FILE } from "./manifest.js";
|
|
10
10
|
import { SCHEMA_FILE } from "./pin.js";
|
|
11
11
|
import { htmlPage } from "./scanHtml.js";
|
|
@@ -113,7 +113,19 @@ export async function installedVersion(dir, pkg) {
|
|
|
113
113
|
keys are the ones Mapled knows by (lib/siteStack.ts of the API). */
|
|
114
114
|
export const rendersInBrowser = (framework) => framework === "react-spa" || framework === "plain-html";
|
|
115
115
|
/** The first-party package a site of this framework reads through. */
|
|
116
|
-
export const sdkOf = (framework) => !framework || framework === "nextjs" ? "@mapled/next" : framework === "react-spa" ? "@mapled/react" : framework === "plain-html" ? "@mapled/vanilla" : "@mapled/client";
|
|
116
|
+
export const sdkOf = (framework) => !framework || framework === "nextjs" ? "@mapled/next" : framework === "react-spa" ? "@mapled/react" : framework === "plain-html" ? "@mapled/vanilla" : framework === "astro" ? "@mapled/astro" : framework === "nuxt" ? "@mapled/nuxt" : "@mapled/client";
|
|
117
|
+
/** The frameworks whose revalidate and preview routes doctor reads: Next.js
|
|
118
|
+
(`app/api/mapled/…/route.ts`, `pages/api/mapled/…`), Astro
|
|
119
|
+
(`src/pages/api/mapled/….ts` — the same paths ROUTE_FILES already names
|
|
120
|
+
for a Pages Router site under `src/`) and Nuxt (`server/api/mapled/…`,
|
|
121
|
+
or the module in `nuxt.config.ts`, which mounts them — `moduleRoute`). */
|
|
122
|
+
export const checksRoutesOf = (framework) => !framework || framework === "nextjs" || framework === "astro" || framework === "nuxt";
|
|
123
|
+
/** where the adapter's handlers come from, and where the route file goes, by framework */
|
|
124
|
+
const adapterOf = (framework, kind) => framework === "astro"
|
|
125
|
+
? { server: '"@mapled/astro/server"', file: `src/pages/api/mapled/${kind}.ts` }
|
|
126
|
+
: framework === "nuxt"
|
|
127
|
+
? { server: '"@mapled/nuxt/server"', file: `server/api/mapled/${kind}.${kind === "revalidate" ? "post" : "get"}.ts` }
|
|
128
|
+
: { server: '"@mapled/next/server"', file: `app/api/mapled/${kind}/route.ts` };
|
|
117
129
|
const SKIPPED_DIRS = new Set(["node_modules", ".git", ".next", ".vercel", ".netlify", "vendor"]);
|
|
118
130
|
export async function findScriptTag(dir, site) {
|
|
119
131
|
// pages first, breadth first, and not forever: a site's pages sit near its root
|
|
@@ -444,20 +456,116 @@ const ROUTE_FILES = {
|
|
|
444
456
|
"src/app/api/mapled/revalidate/route",
|
|
445
457
|
"pages/api/mapled/revalidate",
|
|
446
458
|
"src/pages/api/mapled/revalidate",
|
|
459
|
+
// Nuxt: a Nitro route, with or without the method in its name
|
|
460
|
+
"server/api/mapled/revalidate.post",
|
|
461
|
+
"server/api/mapled/revalidate",
|
|
447
462
|
],
|
|
448
463
|
preview: [
|
|
449
464
|
"app/api/mapled/preview/route",
|
|
450
465
|
"src/app/api/mapled/preview/route",
|
|
451
466
|
"pages/api/mapled/preview",
|
|
452
467
|
"src/pages/api/mapled/preview",
|
|
468
|
+
"server/api/mapled/preview.get",
|
|
469
|
+
"server/api/mapled/preview",
|
|
453
470
|
],
|
|
454
471
|
};
|
|
472
|
+
/** The Nuxt module mounts every route itself: a site whose `nuxt.config`
|
|
473
|
+
lists "@mapled/nuxt/module" has them without a file. What `findRoute`
|
|
474
|
+
answers for such a site, when no route file of its own is found. */
|
|
475
|
+
export const NUXT_CONFIG_FILES = ["nuxt.config.ts", "nuxt.config.mts", "nuxt.config.js", "nuxt.config.mjs"];
|
|
476
|
+
export const NUXT_MODULE = "@mapled/nuxt/module";
|
|
477
|
+
export async function moduleRoute(dir) {
|
|
478
|
+
for (const file of NUXT_CONFIG_FILES) {
|
|
479
|
+
let text;
|
|
480
|
+
try {
|
|
481
|
+
text = await readFile(path.join(dir, file), "utf8");
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
return mountsModuleRoutes(text) ? { file: `${file} (${NUXT_MODULE} mounts it)`, usesAdapter: true, cacheMark: true } : null;
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
/** Whether a nuxt.config's text lists the module, routes on, in its
|
|
491
|
+
`modules` array: as the string "@mapled/nuxt/module" or as the
|
|
492
|
+
identifier a default import of it binds, bare or as the first element
|
|
493
|
+
of a tuple whose options don't say `routes: false`. Comments don't
|
|
494
|
+
count; neither does an import that never reaches `modules`.
|
|
495
|
+
Conservative: a config that can't be read this way claims nothing,
|
|
496
|
+
and doctor asks for the route files. */
|
|
497
|
+
export function mountsModuleRoutes(text) {
|
|
498
|
+
// comments go: block comments, and line comments that aren't the `//` of a URL
|
|
499
|
+
const code = text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
500
|
+
// the names a default import of the module binds
|
|
501
|
+
const names = [...code.matchAll(/import\s+([A-Za-z_$][\w$]*)\s*(?:,\s*\{[^}]*\})?\s*from\s*["'`]@mapled\/nuxt\/module["'`]/g)].map((m) => m[1]);
|
|
502
|
+
const open = code.search(/\bmodules\s*:\s*\[/);
|
|
503
|
+
if (open < 0)
|
|
504
|
+
return false;
|
|
505
|
+
const array = balanced(code, code.indexOf("[", open));
|
|
506
|
+
if (array === null)
|
|
507
|
+
return false;
|
|
508
|
+
// each element of the array, at depth one
|
|
509
|
+
for (const element of elementsOf(array)) {
|
|
510
|
+
const asString = /^\s*["'`]@mapled\/nuxt\/module["'`]\s*$/.test(element);
|
|
511
|
+
const asName = names.some((n) => new RegExp(`^\\s*${n.replace(/\$/g, "\\$")}\\s*$`).test(element));
|
|
512
|
+
if (asString || asName)
|
|
513
|
+
return true;
|
|
514
|
+
const tuple = element.trim().startsWith("[") ? balanced(element, element.indexOf("[")) : null;
|
|
515
|
+
if (tuple === null)
|
|
516
|
+
continue;
|
|
517
|
+
const [first, ...options] = elementsOf(tuple);
|
|
518
|
+
const firstIs = first !== undefined && (/^\s*["'`]@mapled\/nuxt\/module["'`]\s*$/.test(first) || names.some((n) => first.trim() === n));
|
|
519
|
+
if (!firstIs)
|
|
520
|
+
continue;
|
|
521
|
+
return !/\broutes\s*:\s*false\b/.test(options.join(","));
|
|
522
|
+
}
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
/** the text inside the bracket at `at` and its match, or null when unbalanced */
|
|
526
|
+
function balanced(code, at) {
|
|
527
|
+
const pairs = { "[": "]", "{": "}", "(": ")" };
|
|
528
|
+
const stack = [];
|
|
529
|
+
for (let i = at; i < code.length; i++) {
|
|
530
|
+
const c = code[i];
|
|
531
|
+
if (c in pairs)
|
|
532
|
+
stack.push(pairs[c]);
|
|
533
|
+
else if (c === "]" || c === "}" || c === ")") {
|
|
534
|
+
if (stack.pop() !== c)
|
|
535
|
+
return null;
|
|
536
|
+
if (stack.length === 0)
|
|
537
|
+
return code.slice(at + 1, i);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
/** the comma-separated elements of a bracket's inside, at its own depth */
|
|
543
|
+
function elementsOf(inside) {
|
|
544
|
+
const out = [];
|
|
545
|
+
let depth = 0;
|
|
546
|
+
let from = 0;
|
|
547
|
+
for (let i = 0; i < inside.length; i++) {
|
|
548
|
+
const c = inside[i];
|
|
549
|
+
if (c === "[" || c === "{" || c === "(")
|
|
550
|
+
depth += 1;
|
|
551
|
+
else if (c === "]" || c === "}" || c === ")")
|
|
552
|
+
depth -= 1;
|
|
553
|
+
else if (c === "," && depth === 0) {
|
|
554
|
+
out.push(inside.slice(from, i));
|
|
555
|
+
from = i + 1;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const last = inside.slice(from);
|
|
559
|
+
if (last.trim())
|
|
560
|
+
out.push(last);
|
|
561
|
+
return out;
|
|
562
|
+
}
|
|
455
563
|
const ROUTE_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".jsx"];
|
|
456
564
|
export const ROUTE_HANDLER = {
|
|
457
565
|
revalidate: "createRevalidateHandler",
|
|
458
566
|
preview: "createPreviewHandler",
|
|
459
567
|
};
|
|
460
|
-
export async function findRoute(dir, kind) {
|
|
568
|
+
export async function findRoute(dir, kind, framework) {
|
|
461
569
|
for (const base of ROUTE_FILES[kind]) {
|
|
462
570
|
for (const ext of ROUTE_EXTENSIONS) {
|
|
463
571
|
const rel = base + ext;
|
|
@@ -468,10 +576,11 @@ export async function findRoute(dir, kind) {
|
|
|
468
576
|
catch {
|
|
469
577
|
continue;
|
|
470
578
|
}
|
|
471
|
-
|
|
579
|
+
const usesAdapter = text.includes(ROUTE_HANDLER[kind]);
|
|
580
|
+
return kind === "revalidate" ? { file: rel, usesAdapter, ...revalidateRouteFacts(text) } : { file: rel, usesAdapter };
|
|
472
581
|
}
|
|
473
582
|
}
|
|
474
|
-
return null;
|
|
583
|
+
return framework === "nuxt" ? moduleRoute(dir) : null;
|
|
475
584
|
}
|
|
476
585
|
/** GET the site's preview route without a token — the adapter answers 400. */
|
|
477
586
|
export async function probePreview(origin, previewPath, f) {
|
|
@@ -639,7 +748,9 @@ export function checkCli(current, latest) {
|
|
|
639
748
|
}
|
|
640
749
|
return { key: "cli", label, status: "passed", detail: latest ? `${current} (current)` : current };
|
|
641
750
|
}
|
|
642
|
-
|
|
751
|
+
/** `listens`: a page follows Publish (<MapledLive /> or watchRelease) — then
|
|
752
|
+
the revalidate route's answer is what the release route trusts (#435). */
|
|
753
|
+
export function checkRoute(kind, found, framework, listens = false) {
|
|
643
754
|
const key = `route_${kind}`;
|
|
644
755
|
const label = kind === "revalidate" ? "Revalidation route" : "Preview route";
|
|
645
756
|
const handler = ROUTE_HANDLER[kind];
|
|
@@ -653,26 +764,43 @@ export function checkRoute(kind, found, framework) {
|
|
|
653
764
|
: "Not needed: the client trades Mapled's preview link on the page itself.",
|
|
654
765
|
};
|
|
655
766
|
}
|
|
656
|
-
if (framework
|
|
767
|
+
if (!checksRoutesOf(framework)) {
|
|
657
768
|
return { key, label, status: "skipped", detail: `No ${kind} route check for ${framework} sites yet.` };
|
|
658
769
|
}
|
|
770
|
+
const adapter = adapterOf(framework, kind);
|
|
659
771
|
if (!found) {
|
|
660
772
|
return kind === "revalidate"
|
|
661
773
|
? {
|
|
662
774
|
key,
|
|
663
775
|
label,
|
|
664
776
|
status: "failed",
|
|
665
|
-
detail:
|
|
777
|
+
detail: `${adapter.file} is missing — mount ${handler} from ${adapter.server}.`,
|
|
666
778
|
}
|
|
667
779
|
: {
|
|
668
780
|
key,
|
|
669
781
|
label,
|
|
670
782
|
status: "warning",
|
|
671
|
-
detail:
|
|
783
|
+
detail: `${adapter.file} is missing — mount ${handler} from ${adapter.server} so Preview from Mapled works.`,
|
|
672
784
|
};
|
|
673
785
|
}
|
|
786
|
+
if (kind === "revalidate" && listens && !found.usesAdapter && !found.cacheMark) {
|
|
787
|
+
return {
|
|
788
|
+
key,
|
|
789
|
+
label,
|
|
790
|
+
status: "warning",
|
|
791
|
+
detail: `${found.file} doesn't use ${handler} and doesn't answer with cacheMark() — the release route then names no release, so open tabs don't follow Publish. Use ${handler} from ${adapter.server}, or answer with the header x-mapled-cache: await cacheMark().`,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
if (kind === "revalidate" && listens && found.fetchCache) {
|
|
795
|
+
return {
|
|
796
|
+
key,
|
|
797
|
+
label,
|
|
798
|
+
status: "warning",
|
|
799
|
+
detail: `${found.file} may set fetchCache to something other than "auto" — with "force-no-store" every answer names a new cache mark, so the release route may name no release and open tabs don't follow Publish. Leave fetchCache out of the route.`,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
674
802
|
if (!found.usesAdapter) {
|
|
675
|
-
return { key, label, status: "warning", detail: `${found.file} exists but doesn't use ${handler} from
|
|
803
|
+
return { key, label, status: "warning", detail: `${found.file} exists but doesn't use ${handler} from ${adapter.server}.` };
|
|
676
804
|
}
|
|
677
805
|
return { key, label, status: "passed", detail: found.file };
|
|
678
806
|
}
|
|
@@ -1021,7 +1149,7 @@ export function checkManifestFile(local, remote) {
|
|
|
1021
1149
|
if (remote === null)
|
|
1022
1150
|
return { key: "manifest", label, status: "warning", detail: `${MANIFEST_FILE} isn't pushed yet — run \`mapled bindings push\`.` };
|
|
1023
1151
|
const diff = compareManifests(local.manifest, remote.manifest);
|
|
1024
|
-
if (diff.same && !diff.capabilitiesDiffer) {
|
|
1152
|
+
if (diff.same && !diff.capabilitiesDiffer && !diff.localesDiffer) {
|
|
1025
1153
|
return { key: "manifest", label, status: "passed", detail: `${MANIFEST_FILE} is pushed as manifest v${remote.version} (${manifestSummary(local.manifest)})` };
|
|
1026
1154
|
}
|
|
1027
1155
|
const parts = [];
|
|
@@ -1034,6 +1162,8 @@ export function checkManifestFile(local, remote) {
|
|
|
1034
1162
|
// capabilities aren't in the integration hash, so this is the line that notices them
|
|
1035
1163
|
if (diff.capabilitiesDiffer)
|
|
1036
1164
|
parts.push("capabilities changed");
|
|
1165
|
+
if (diff.localesDiffer)
|
|
1166
|
+
parts.push("routes by language changed");
|
|
1037
1167
|
return {
|
|
1038
1168
|
key: "manifest",
|
|
1039
1169
|
label,
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ Usage
|
|
|
28
28
|
mapled forms list [--json] List the project's forms and where the site posts to
|
|
29
29
|
mapled doctor [--json] Check the integration end to end
|
|
30
30
|
mapled integration report [--json] [--verify] Show the last setup run's verification checklist and report
|
|
31
|
-
mapled preview [<collection> [<slug>]] [--open]
|
|
31
|
+
mapled preview [<collection> [<slug>]] [--locale <code>] [--open] Print a one-time Preview link that shows the drafts on the site
|
|
32
32
|
|
|
33
33
|
Options
|
|
34
34
|
--version, -v Print the version
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The integration hash (§21.4): one marker over everything the site's
|
|
2
2
|
integration was written against — the schema (its hash, the twelve hex
|
|
3
3
|
characters in mapled-types.ts and mapled/schema.json) and the site
|
|
4
|
-
manifest (bindings, pages, framework, notes). Mapled records it on
|
|
4
|
+
manifest (bindings, pages, framework, notes, routes by language). Mapled records it on
|
|
5
5
|
every manifest push and computes it again on read; the repository
|
|
6
6
|
computes the same value from mapled/schema.json and
|
|
7
7
|
mapled/manifest.json, and `mapled doctor` compares the two in one
|
|
@@ -31,5 +31,10 @@ export type HashedManifest = {
|
|
|
31
31
|
}[] | null;
|
|
32
32
|
bindings: HashedBinding[];
|
|
33
33
|
notes?: string[] | null;
|
|
34
|
+
/** routes by language (§28.4); a manifest without it hashes as before */
|
|
35
|
+
locales?: {
|
|
36
|
+
routing: string;
|
|
37
|
+
param?: string | null;
|
|
38
|
+
} | null;
|
|
34
39
|
};
|
|
35
40
|
export declare function integrationHash(schemaHash: string, manifest: HashedManifest): string;
|
package/dist/integrationHash.js
CHANGED
|
@@ -21,6 +21,8 @@ export function integrationHash(schemaHash, manifest) {
|
|
|
21
21
|
}))
|
|
22
22
|
.sort((a, b) => byText(a.key, b.key)),
|
|
23
23
|
notes: manifest.notes ?? [],
|
|
24
|
+
// only when declared: the hashes of every manifest without it stay what they were
|
|
25
|
+
...(manifest.locales ? { locales: { routing: manifest.locales.routing, param: manifest.locales.param || null } } : {}),
|
|
24
26
|
};
|
|
25
27
|
return createHash("sha256").update(JSON.stringify(projection)).digest("hex").slice(0, 12);
|
|
26
28
|
}
|
package/dist/live.d.ts
CHANGED
|
@@ -208,6 +208,16 @@ export declare function liveInFile(file: string, text: string, router?: "app" |
|
|
|
208
208
|
segment?: SegmentSetting;
|
|
209
209
|
runtime?: string | null;
|
|
210
210
|
};
|
|
211
|
+
/** What the revalidate (webhook) route does that live updates depend on
|
|
212
|
+
(§18.8, #435): whether it answers with the cache's mark (`cacheMark()`,
|
|
213
|
+
which createRevalidateHandler does for you), and whether it may export
|
|
214
|
+
`fetchCache` as anything but "auto" — "force-no-store" makes every
|
|
215
|
+
answer name a new mark, so the release route may name no release.
|
|
216
|
+
Read the way the release route's segment config is read. */
|
|
217
|
+
export declare function revalidateRouteFacts(text: string): {
|
|
218
|
+
cacheMark: boolean;
|
|
219
|
+
fetchCache: boolean;
|
|
220
|
+
};
|
|
211
221
|
/** The route segment settings that change how Next serves the release
|
|
212
222
|
route or reads what the handler reads, in the order doctor names them:
|
|
213
223
|
`dynamic` can make it a static copy built without the tab's request,
|
package/dist/live.js
CHANGED
|
@@ -945,6 +945,19 @@ export function liveInFile(file, text, router = "app") {
|
|
|
945
945
|
const segment = sets("dynamic", SEGMENT_KEEPS.dynamic) ? "dynamic" : forced() ? "force-dynamic" : sets("fetchCache", SEGMENT_KEEPS.fetchCache) ? "fetchCache" : undefined;
|
|
946
946
|
return { listeners, handler: releaseHandlerOf(tokens, APP_HANDLER(names), "GET", read), params, ...(segment ? { segment } : {}) };
|
|
947
947
|
}
|
|
948
|
+
/** What the revalidate (webhook) route does that live updates depend on
|
|
949
|
+
(§18.8, #435): whether it answers with the cache's mark (`cacheMark()`,
|
|
950
|
+
which createRevalidateHandler does for you), and whether it may export
|
|
951
|
+
`fetchCache` as anything but "auto" — "force-no-store" makes every
|
|
952
|
+
answer name a new mark, so the release route may name no release.
|
|
953
|
+
Read the way the release route's segment config is read. */
|
|
954
|
+
export function revalidateRouteFacts(text) {
|
|
955
|
+
const tokens = tokenize(text);
|
|
956
|
+
const cacheMark = tokens.some((t, k) => t.kind === "ident" && t.value === "cacheMark" && isPunct(tokens[k + 1], "("));
|
|
957
|
+
const listing = tokens.some((u, k) => isWord(u, "exports") || (isWord(u, "export") && isPunct(tokens[k + 1], "*")));
|
|
958
|
+
const fetchCache = (listing || tokens.some((u) => isName(u, "fetchCache"))) && setsExport(tokens, readingOf(tokens).b.after, "fetchCache", SEGMENT_KEEPS.fetchCache);
|
|
959
|
+
return { cacheMark, fetchCache };
|
|
960
|
+
}
|
|
948
961
|
/** Whether every release handler the file makes keeps a window of its own
|
|
949
962
|
for the pointer's read (keepsWindow), with the factory only ever called
|
|
950
963
|
where it's named: `const make = createReleaseHandler`, a namespace
|
package/dist/manifest.d.ts
CHANGED
|
@@ -30,6 +30,18 @@ export type ManifestCapabilities = {
|
|
|
30
30
|
realtime?: boolean;
|
|
31
31
|
releaseRoute?: string;
|
|
32
32
|
};
|
|
33
|
+
/** How the site's routes carry the language (§28.4): `prefix` — every
|
|
34
|
+
language under its own (`/en/…`, `/ru/…`), `prefix-except-default` —
|
|
35
|
+
the default language at the root, the others under theirs; `param`
|
|
36
|
+
names the dynamic segment that holds it (`/[locale]/blog/[slug]`),
|
|
37
|
+
without one Mapled puts the language before the page. Part of the
|
|
38
|
+
integration hash: Mapled builds each language's address of a record
|
|
39
|
+
from it (the editor's routes, Preview). */
|
|
40
|
+
export type ManifestLocales = {
|
|
41
|
+
routing: "prefix" | "prefix-except-default";
|
|
42
|
+
param?: string;
|
|
43
|
+
};
|
|
44
|
+
export declare const LOCALE_ROUTINGS: readonly ["prefix", "prefix-except-default"];
|
|
33
45
|
export type Manifest = {
|
|
34
46
|
framework?: string;
|
|
35
47
|
integrationMode?: string;
|
|
@@ -37,6 +49,7 @@ export type Manifest = {
|
|
|
37
49
|
bindings: ManifestBinding[];
|
|
38
50
|
notes?: string[];
|
|
39
51
|
capabilities?: ManifestCapabilities;
|
|
52
|
+
locales?: ManifestLocales;
|
|
40
53
|
};
|
|
41
54
|
export declare const KEY_PATTERN: RegExp;
|
|
42
55
|
export declare const LIMITS: {
|
|
@@ -93,6 +106,8 @@ export declare function readManifestFile(dir: string): Promise<{
|
|
|
93
106
|
the order they came in. */
|
|
94
107
|
export declare function writeManifestFile(dir: string, manifest: Manifest): Promise<string>;
|
|
95
108
|
export declare function orderManifest(manifest: Manifest): Manifest;
|
|
109
|
+
/** Whether two manifests route languages the same way (missing on both is the same). */
|
|
110
|
+
export declare function sameLocales(a: ManifestLocales | undefined, b: ManifestLocales | undefined): boolean;
|
|
96
111
|
/** Whether two manifests declare the same capabilities (missing and empty are the same). */
|
|
97
112
|
export declare function sameCapabilities(a: ManifestCapabilities | undefined, b: ManifestCapabilities | undefined): boolean;
|
|
98
113
|
export declare function orderBinding(b: ManifestBinding): ManifestBinding;
|
|
@@ -100,12 +115,14 @@ export declare function plural(n: number, word: string, pluralWord?: string): st
|
|
|
100
115
|
export declare function manifestSummary(manifest: Manifest): string;
|
|
101
116
|
/** Bindings that mean the same thing to Mapled (what a push stores). */
|
|
102
117
|
export declare function sameBinding(a: ManifestBinding, b: ManifestBinding): boolean;
|
|
103
|
-
/** `same` is about the bindings; `capabilitiesDiffer` says whether the
|
|
118
|
+
/** `same` is about the bindings; `capabilitiesDiffer` says whether the
|
|
119
|
+
capabilities do, `localesDiffer` whether the routes by language do. */
|
|
104
120
|
export type ManifestComparison = {
|
|
105
121
|
same: boolean;
|
|
106
122
|
onlyLocal: string[];
|
|
107
123
|
onlyRemote: string[];
|
|
108
124
|
changed: string[];
|
|
109
125
|
capabilitiesDiffer: boolean;
|
|
126
|
+
localesDiffer: boolean;
|
|
110
127
|
};
|
|
111
128
|
export declare function compareManifests(local: Manifest, remote: Manifest): ManifestComparison;
|