@mapled/cli 0.14.1 → 0.14.3
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 +5 -2
- package/dist/args.js +1 -1
- package/dist/commands.d.ts +6 -4
- package/dist/commands.js +32 -10
- package/dist/doctor.d.ts +6 -1
- package/dist/doctor.js +25 -4
- package/dist/html5.d.ts +13 -2
- package/dist/html5.js +9 -0
- 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/report.d.ts +5 -0
- package/dist/scan.d.ts +10 -1
- package/dist/scan.js +22 -2
- package/dist/scanHtml.d.ts +2 -1
- package/dist/scanHtml.js +11 -4
- package/dist/types.js +3 -1
- package/dist/vanillaDigests.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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. `--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
|
@@ -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) {
|
|
@@ -507,15 +516,22 @@ export async function preview(ctx, flags, words) {
|
|
|
507
516
|
if (answer.status === "not_set_up") {
|
|
508
517
|
throw new CliError(`${clean(answer.origin)} has no preview route — mount createPreviewHandler from "@mapled/next/server" at /api/mapled/preview and redeploy.`);
|
|
509
518
|
}
|
|
519
|
+
if (answer.status === "no_route") {
|
|
520
|
+
// a site with its content baked in at build time has no page for a record it wasn't built with (§22.7 п. 3)
|
|
521
|
+
const where = answer.path ? `at ${clean(answer.path, 500)}` : "for this record";
|
|
522
|
+
throw new CliError(`${clean(answer.origin)} has no page ${where} — if the record is new, its page appears once the site is built with it. Then try again.`);
|
|
523
|
+
}
|
|
510
524
|
// the link is printed as it came (masking would drop the token): printable ASCII only
|
|
511
525
|
if (typeof answer.url !== "string" || !/^https?:\/\/[!-~]+$/.test(answer.url)) {
|
|
512
526
|
throw new CliError("Mapled answered with a link this CLI can't print. Update @mapled/cli and try again.");
|
|
513
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;
|
|
514
530
|
if (flags.json) {
|
|
515
|
-
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));
|
|
516
532
|
return;
|
|
517
533
|
}
|
|
518
|
-
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}` : "");
|
|
519
535
|
const lifetime = linkLifetime(answer.expiresAt);
|
|
520
536
|
const project = clean(session.conn.projectName, 120);
|
|
521
537
|
if (flags.open && (await ctx.openBrowser(answer.url))) {
|
|
@@ -621,6 +637,8 @@ export async function scan(ctx, flags) {
|
|
|
621
637
|
parts.push(`${merged.unchanged.length} unchanged`);
|
|
622
638
|
if (merged.capabilitiesChanged)
|
|
623
639
|
parts.push("capabilities updated");
|
|
640
|
+
if (merged.localesChanged)
|
|
641
|
+
parts.push("routes by language updated");
|
|
624
642
|
ctx.out(`Wrote ${MANIFEST_FILE} — ${manifestSummary(merged.manifest)}${parts.length > 0 && existing ? ` (${parts.join(", ")})` : ""}.${keptNote}${droppedNote}`);
|
|
625
643
|
ctx.out("Next: `mapled bindings push`.");
|
|
626
644
|
}
|
|
@@ -636,10 +654,12 @@ export async function scan(ctx, flags) {
|
|
|
636
654
|
parts.push(`${merged.unchanged.length} unchanged`);
|
|
637
655
|
if (merged.capabilitiesChanged)
|
|
638
656
|
parts.push("capabilities changed");
|
|
657
|
+
if (merged.localesChanged)
|
|
658
|
+
parts.push("routes by language changed");
|
|
639
659
|
if (merged.kept.length > 0)
|
|
640
660
|
parts.push(`${merged.kept.length} not found in the code (${merged.kept.slice(0, 5).join(", ")}${merged.kept.length > 5 ? ", …" : ""})`);
|
|
641
661
|
ctx.out(`Compared with ${MANIFEST_FILE}: ${parts.join(", ")}.`);
|
|
642
|
-
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) {
|
|
643
663
|
ctx.out(`Run \`mapled scan --write\` to update ${MANIFEST_FILE}, then \`mapled bindings push\`.`);
|
|
644
664
|
}
|
|
645
665
|
else {
|
|
@@ -710,10 +730,12 @@ export async function doctor(ctx, flags) {
|
|
|
710
730
|
// plain HTML installs nothing: the runtime comes with a script tag
|
|
711
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));
|
|
712
732
|
checks.push(dr.checkCli(ctx.version, status?.sdk["@mapled/cli"]));
|
|
713
|
-
|
|
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, (live?.listeners.length ?? 0) > 0));
|
|
714
736
|
checks.push(dr.checkRoute("preview", await dr.findRoute(dir, "preview"), framework));
|
|
715
737
|
// the code is read only where the check applies: a Next.js site (or one doctor can't place)
|
|
716
|
-
checks.push(dr.checkLive(
|
|
738
|
+
checks.push(dr.checkLive(live, framework));
|
|
717
739
|
if (status) {
|
|
718
740
|
checks.push(dr.checkReads(status.delivery.lastReadAt));
|
|
719
741
|
checks.push(dr.checkWebhook(status.webhook, { local: browser, remote: status.site }));
|
package/dist/doctor.d.ts
CHANGED
|
@@ -153,9 +153,12 @@ export declare function detectFramework(dir: string): Promise<string | undefined
|
|
|
153
153
|
export declare function compareVersions(a: string, b: string): number;
|
|
154
154
|
export type RouteKind = "revalidate" | "preview";
|
|
155
155
|
export declare const ROUTE_HANDLER: Record<RouteKind, string>;
|
|
156
|
+
/** `cacheMark` and `fetchCache` — the revalidate route only (revalidateRouteFacts). */
|
|
156
157
|
export type RouteFile = {
|
|
157
158
|
file: string;
|
|
158
159
|
usesAdapter: boolean;
|
|
160
|
+
cacheMark?: boolean;
|
|
161
|
+
fetchCache?: boolean;
|
|
159
162
|
};
|
|
160
163
|
export declare function findRoute(dir: string, kind: RouteKind): Promise<RouteFile | null>;
|
|
161
164
|
/** GET the site's preview route without a token — the adapter answers 400. */
|
|
@@ -180,7 +183,9 @@ export declare function checkEnv(names: Map<string, string>, env: NodeJS.Process
|
|
|
180
183
|
export declare function checkSecrets(facts: GitFacts): Check;
|
|
181
184
|
export declare function checkSdk(pkg: PackageVersion, latest: string | undefined, name?: string): Check;
|
|
182
185
|
export declare function checkCli(current: string, latest: string | undefined): Check;
|
|
183
|
-
|
|
186
|
+
/** `listens`: a page follows Publish (<MapledLive /> or watchRelease) — then
|
|
187
|
+
the revalidate route's answer is what the release route trusts (#435). */
|
|
188
|
+
export declare function checkRoute(kind: RouteKind, found: RouteFile | null, framework: string | undefined, listens?: boolean): Check;
|
|
184
189
|
/** Live updates (§18.8): whether open tabs follow Publish, and when the
|
|
185
190
|
code listens, whether a route answers it. `facts` is null when the
|
|
186
191
|
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";
|
|
@@ -468,7 +468,8 @@ export async function findRoute(dir, kind) {
|
|
|
468
468
|
catch {
|
|
469
469
|
continue;
|
|
470
470
|
}
|
|
471
|
-
|
|
471
|
+
const usesAdapter = text.includes(ROUTE_HANDLER[kind]);
|
|
472
|
+
return kind === "revalidate" ? { file: rel, usesAdapter, ...revalidateRouteFacts(text) } : { file: rel, usesAdapter };
|
|
472
473
|
}
|
|
473
474
|
}
|
|
474
475
|
return null;
|
|
@@ -639,7 +640,9 @@ export function checkCli(current, latest) {
|
|
|
639
640
|
}
|
|
640
641
|
return { key: "cli", label, status: "passed", detail: latest ? `${current} (current)` : current };
|
|
641
642
|
}
|
|
642
|
-
|
|
643
|
+
/** `listens`: a page follows Publish (<MapledLive /> or watchRelease) — then
|
|
644
|
+
the revalidate route's answer is what the release route trusts (#435). */
|
|
645
|
+
export function checkRoute(kind, found, framework, listens = false) {
|
|
643
646
|
const key = `route_${kind}`;
|
|
644
647
|
const label = kind === "revalidate" ? "Revalidation route" : "Preview route";
|
|
645
648
|
const handler = ROUTE_HANDLER[kind];
|
|
@@ -671,6 +674,22 @@ export function checkRoute(kind, found, framework) {
|
|
|
671
674
|
detail: `app/api/mapled/preview/route.ts is missing — mount ${handler} from "@mapled/next/server" so Preview from Mapled works.`,
|
|
672
675
|
};
|
|
673
676
|
}
|
|
677
|
+
if (kind === "revalidate" && listens && !found.usesAdapter && !found.cacheMark) {
|
|
678
|
+
return {
|
|
679
|
+
key,
|
|
680
|
+
label,
|
|
681
|
+
status: "warning",
|
|
682
|
+
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 "@mapled/next/server", or answer with the header x-mapled-cache: await cacheMark().`,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (kind === "revalidate" && listens && found.fetchCache) {
|
|
686
|
+
return {
|
|
687
|
+
key,
|
|
688
|
+
label,
|
|
689
|
+
status: "warning",
|
|
690
|
+
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.`,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
674
693
|
if (!found.usesAdapter) {
|
|
675
694
|
return { key, label, status: "warning", detail: `${found.file} exists but doesn't use ${handler} from "@mapled/next/server".` };
|
|
676
695
|
}
|
|
@@ -1021,7 +1040,7 @@ export function checkManifestFile(local, remote) {
|
|
|
1021
1040
|
if (remote === null)
|
|
1022
1041
|
return { key: "manifest", label, status: "warning", detail: `${MANIFEST_FILE} isn't pushed yet — run \`mapled bindings push\`.` };
|
|
1023
1042
|
const diff = compareManifests(local.manifest, remote.manifest);
|
|
1024
|
-
if (diff.same && !diff.capabilitiesDiffer) {
|
|
1043
|
+
if (diff.same && !diff.capabilitiesDiffer && !diff.localesDiffer) {
|
|
1025
1044
|
return { key: "manifest", label, status: "passed", detail: `${MANIFEST_FILE} is pushed as manifest v${remote.version} (${manifestSummary(local.manifest)})` };
|
|
1026
1045
|
}
|
|
1027
1046
|
const parts = [];
|
|
@@ -1034,6 +1053,8 @@ export function checkManifestFile(local, remote) {
|
|
|
1034
1053
|
// capabilities aren't in the integration hash, so this is the line that notices them
|
|
1035
1054
|
if (diff.capabilitiesDiffer)
|
|
1036
1055
|
parts.push("capabilities changed");
|
|
1056
|
+
if (diff.localesDiffer)
|
|
1057
|
+
parts.push("routes by language changed");
|
|
1037
1058
|
return {
|
|
1038
1059
|
key: "manifest",
|
|
1039
1060
|
label,
|
package/dist/html5.d.ts
CHANGED
|
@@ -18,11 +18,21 @@ import { type Token, type TreeAdapter, type TreeAdapterTypeMap } from "parse5";
|
|
|
18
18
|
before a <table>: indexOf), the adoption agency (a misnested </b>:
|
|
19
19
|
splice from the front) and repeated <html>/<body> tags (every
|
|
20
20
|
attribute so far) walk them — 160,000 "a<br>" in a table: 11 s.
|
|
21
|
-
The worst page within the cap is the depth times its start tags.
|
|
21
|
+
The worst page within the cap is the depth times its start tags.
|
|
22
|
+
|
|
23
|
+
parse5 can't read a numeric character reference of 309 or more decimal
|
|
24
|
+
digits, or of 256 or more hex ones, at all: BadReference. */
|
|
22
25
|
/** How deep elements may be open at once — Chrome's own limit of the tree it builds. */
|
|
23
26
|
export declare const MAX_OPEN = 512;
|
|
24
27
|
export declare class TooDeep extends Error {
|
|
25
28
|
}
|
|
29
|
+
/** A numeric character reference parse5 can't read: its decoder (entities)
|
|
30
|
+
multiplies by 10 or 16 to the power of the digits — Infinity past 308
|
|
31
|
+
decimal or 255 hex ones — and 0 × Infinity is NaN, on which parse5
|
|
32
|
+
throws RangeError. Browsers read it as a character. The same guard is
|
|
33
|
+
in the preview proxy's reader (apps/api/src/lib/previewProxyHtml.ts). */
|
|
34
|
+
export declare class BadReference extends Error {
|
|
35
|
+
}
|
|
26
36
|
export type Html5Attr = Token.Attribute;
|
|
27
37
|
/** A node of the tree: an element (tagName), text, a comment, a doctype,
|
|
28
38
|
the document or a template's content — children linked both ways. Its
|
|
@@ -61,6 +71,7 @@ type Map_ = TreeAdapterTypeMap<Html5Node, Html5Node, Html5Node, Html5Node, Html5
|
|
|
61
71
|
/** parse5's default tree adapter, method for method, on linked children. */
|
|
62
72
|
export declare const linked: TreeAdapter<Map_>;
|
|
63
73
|
/** The document, with scripting on — as the runtime sees it (a <noscript>
|
|
64
|
-
is text). Throws TooDeep past MAX_OPEN open elements
|
|
74
|
+
is text). Throws TooDeep past MAX_OPEN open elements, BadReference at a
|
|
75
|
+
numeric character reference parse5 can't read. */
|
|
65
76
|
export declare function parseDocument(text: string): Html5Node;
|
|
66
77
|
export {};
|
package/dist/html5.js
CHANGED
|
@@ -7853,6 +7853,9 @@ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
7853
7853
|
var MAX_OPEN = 512;
|
|
7854
7854
|
var TooDeep = class extends Error {
|
|
7855
7855
|
};
|
|
7856
|
+
var BadReference = class extends Error {
|
|
7857
|
+
};
|
|
7858
|
+
var isCodePoint = (cp) => Number.isInteger(cp) && cp >= 0 && cp <= 1114111;
|
|
7856
7859
|
var Html5Node = class {
|
|
7857
7860
|
constructor(nodeName, extra = {}) {
|
|
7858
7861
|
this.nodeName = nodeName;
|
|
@@ -7991,6 +7994,11 @@ var Guarded = class extends Parser {
|
|
|
7991
7994
|
names.add(this.currentAttr.name);
|
|
7992
7995
|
tag.attrs.push(this.currentAttr);
|
|
7993
7996
|
};
|
|
7997
|
+
const reference = tokenizer._flushCodePointConsumedAsCharacterReference;
|
|
7998
|
+
tokenizer._flushCodePointConsumedAsCharacterReference = function(cp) {
|
|
7999
|
+
if (!isCodePoint(cp)) throw new BadReference();
|
|
8000
|
+
reference.call(this, cp);
|
|
8001
|
+
};
|
|
7994
8002
|
}
|
|
7995
8003
|
onItemPush(...args) {
|
|
7996
8004
|
if (this.openElements.stackTop >= MAX_OPEN) throw new TooDeep();
|
|
@@ -8001,6 +8009,7 @@ function parseDocument(text) {
|
|
|
8001
8009
|
return Guarded.parse(text, { scriptingEnabled: true, treeAdapter: linked });
|
|
8002
8010
|
}
|
|
8003
8011
|
export {
|
|
8012
|
+
BadReference,
|
|
8004
8013
|
Html5Node,
|
|
8005
8014
|
MAX_OPEN,
|
|
8006
8015
|
TooDeep,
|
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;
|
package/dist/manifest.js
CHANGED
|
@@ -22,6 +22,7 @@ export const BINDING_TARGETS = [
|
|
|
22
22
|
"form_field",
|
|
23
23
|
"other",
|
|
24
24
|
];
|
|
25
|
+
export const LOCALE_ROUTINGS = ["prefix", "prefix-except-default"];
|
|
25
26
|
export const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/\[\]-]{0,119}$/;
|
|
26
27
|
export const LIMITS = { bindings: 500, pages: 200, notes: 20 };
|
|
27
28
|
/** Field types a target renders without a transform — the API's table
|
|
@@ -91,7 +92,8 @@ export function typeLabel(type) {
|
|
|
91
92
|
}
|
|
92
93
|
const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
93
94
|
const isText = (v, max, min = 1) => typeof v === "string" && v.length >= min && v.length <= max;
|
|
94
|
-
const TOP_KEYS = new Set(["framework", "integrationMode", "pages", "bindings", "notes", "capabilities"]);
|
|
95
|
+
const TOP_KEYS = new Set(["framework", "integrationMode", "pages", "bindings", "notes", "capabilities", "locales"]);
|
|
96
|
+
const LOCALE_PARAM = /^[A-Za-z0-9_-]{1,60}$/;
|
|
95
97
|
export const CAPABILITY_KEYS = ["realtime", "releaseRoute"];
|
|
96
98
|
const BINDING_KEYS = new Set(["key", "page", "component", "file", "collection", "field", "target", "required"]);
|
|
97
99
|
/** Parses the file's text into a manifest with only the known fields,
|
|
@@ -271,6 +273,31 @@ export function parseManifest(raw, file) {
|
|
|
271
273
|
manifest.capabilities = kept;
|
|
272
274
|
}
|
|
273
275
|
}
|
|
276
|
+
if (data.locales !== undefined) {
|
|
277
|
+
const loc = data.locales;
|
|
278
|
+
if (!isObject(loc)) {
|
|
279
|
+
error("locales", `"locales" must be an object — { routing, param }.`);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
let ok = true;
|
|
283
|
+
for (const key of Object.keys(loc)) {
|
|
284
|
+
if (key !== "routing" && key !== "param") {
|
|
285
|
+
error(`locales.${key}`, `locales.${key} isn't part of the manifest — only routing and param; Mapled refuses the push.`);
|
|
286
|
+
ok = false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!LOCALE_ROUTINGS.includes(loc.routing)) {
|
|
290
|
+
error("locales.routing", `locales.routing must be "prefix" (every language under its own, /en/…) or "prefix-except-default" (the default language at the root).`);
|
|
291
|
+
ok = false;
|
|
292
|
+
}
|
|
293
|
+
if (loc.param !== undefined && !(typeof loc.param === "string" && LOCALE_PARAM.test(loc.param))) {
|
|
294
|
+
error("locales.param", `locales.param must name the dynamic segment that carries the language, e.g. "locale" for /[locale]/blog/[slug].`);
|
|
295
|
+
ok = false;
|
|
296
|
+
}
|
|
297
|
+
if (ok)
|
|
298
|
+
manifest.locales = { routing: loc.routing, ...(typeof loc.param === "string" ? { param: loc.param } : {}) };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
274
301
|
return { manifest, problems };
|
|
275
302
|
}
|
|
276
303
|
export function indexSchema(schema) {
|
|
@@ -302,6 +329,14 @@ export async function checkManifest(manifest, opts) {
|
|
|
302
329
|
const warn = (p, message) => problems.push({ level: "warning", path: p, message });
|
|
303
330
|
const index = opts.schema ? indexSchema(opts.schema) : null;
|
|
304
331
|
const routes = new Set((manifest.pages ?? []).map((p) => p.route));
|
|
332
|
+
// routes by language (§28.4): a segment the manifest names must be on some page
|
|
333
|
+
if (manifest.locales?.param !== undefined) {
|
|
334
|
+
const marker = `[${manifest.locales.param}]`;
|
|
335
|
+
const all = [...routes, ...manifest.bindings.map((b) => b.page)];
|
|
336
|
+
if (all.length > 0 && !all.some((r) => r.split("?")[0].split("/").includes(marker))) {
|
|
337
|
+
warn("locales.param", `locales.param names "${marker}", but no page route has that segment — the language then goes nowhere; leave param out for Mapled to put the language before the page.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
305
340
|
for (const [i, p] of (manifest.pages ?? []).entries()) {
|
|
306
341
|
if (p.file && !(await opts.fileExists(p.file))) {
|
|
307
342
|
warn(`pages[${i}].file`, `pages[${i}] (${p.route}): ${p.file} doesn't exist in the repository.`);
|
|
@@ -381,8 +416,13 @@ export function orderManifest(manifest) {
|
|
|
381
416
|
bindings: manifest.bindings.map(orderBinding),
|
|
382
417
|
...(manifest.notes && manifest.notes.length > 0 ? { notes: [...manifest.notes] } : {}),
|
|
383
418
|
...(manifest.capabilities ? { capabilities: orderCapabilities(manifest.capabilities) } : {}),
|
|
419
|
+
...(manifest.locales ? { locales: { routing: manifest.locales.routing, ...(manifest.locales.param !== undefined ? { param: manifest.locales.param } : {}) } } : {}),
|
|
384
420
|
};
|
|
385
421
|
}
|
|
422
|
+
/** Whether two manifests route languages the same way (missing on both is the same). */
|
|
423
|
+
export function sameLocales(a, b) {
|
|
424
|
+
return (a?.routing ?? null) === (b?.routing ?? null) && (a?.param ?? null) === (b?.param ?? null);
|
|
425
|
+
}
|
|
386
426
|
function orderCapabilities(c) {
|
|
387
427
|
return {
|
|
388
428
|
...(c.realtime !== undefined ? { realtime: c.realtime } : {}),
|
|
@@ -434,5 +474,6 @@ export function compareManifests(local, remote) {
|
|
|
434
474
|
onlyRemote,
|
|
435
475
|
changed,
|
|
436
476
|
capabilitiesDiffer: !sameCapabilities(local.capabilities, remote.capabilities),
|
|
477
|
+
localesDiffer: !sameLocales(local.locales, remote.locales),
|
|
437
478
|
};
|
|
438
479
|
}
|
package/dist/report.d.ts
CHANGED
|
@@ -114,6 +114,7 @@ export type PreviewAnswer = {
|
|
|
114
114
|
url: string;
|
|
115
115
|
expiresAt?: string;
|
|
116
116
|
path: string | null;
|
|
117
|
+
locale?: string;
|
|
117
118
|
} | {
|
|
118
119
|
status: "not_deployed";
|
|
119
120
|
origin: string;
|
|
@@ -122,6 +123,10 @@ export type PreviewAnswer = {
|
|
|
122
123
|
status: "not_set_up";
|
|
123
124
|
origin: string;
|
|
124
125
|
path: string | null;
|
|
126
|
+
} | {
|
|
127
|
+
status: "no_route";
|
|
128
|
+
origin: string;
|
|
129
|
+
path: string | null;
|
|
125
130
|
};
|
|
126
131
|
/** "5 minutes" until the link stops working, or null when unknown. */
|
|
127
132
|
export declare function linkLifetime(expiresAt: string | undefined, now?: number): string | null;
|
package/dist/scan.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as TS from "typescript";
|
|
2
|
-
import { type Manifest, type ManifestBinding, type ManifestCapabilities, type ManifestPage } from "./manifest.js";
|
|
2
|
+
import { type Manifest, type ManifestBinding, type ManifestCapabilities, type ManifestLocales, type ManifestPage } from "./manifest.js";
|
|
3
3
|
import type { Schema } from "./schema.js";
|
|
4
4
|
/** `mapled scan` (§19.5, §21.4 — the standalone scanner of wave 2): reads
|
|
5
5
|
the site's source and writes down what the code reads from Mapled —
|
|
@@ -39,7 +39,14 @@ export type ScanResult = {
|
|
|
39
39
|
notes: string[];
|
|
40
40
|
/** what the code says the site can do (live updates of a Next.js site); undefined keeps the manifest's */
|
|
41
41
|
capabilities?: ManifestCapabilities;
|
|
42
|
+
/** how the routes carry the language (§28.4), when a leading dynamic segment names it; undefined keeps the manifest's */
|
|
43
|
+
locales?: ManifestLocales;
|
|
42
44
|
};
|
|
45
|
+
/** Routes by language from the file conventions (§28.4): a dynamic segment
|
|
46
|
+
named for the language at the front of a route (`/[locale]/…`,
|
|
47
|
+
`/[lang]/…`) — every language then lives under its own prefix, and
|
|
48
|
+
the segment is the manifest's `param`. Null when no route starts so. */
|
|
49
|
+
export declare function detectLocales(routes: Iterable<string>): ManifestLocales | null;
|
|
43
50
|
export declare const SDK_METHODS: Set<string>;
|
|
44
51
|
/** The repository's TypeScript, when it has one; the CLI ships none. */
|
|
45
52
|
export declare function loadTypeScript(dir: string): TypeScriptModule | null;
|
|
@@ -90,6 +97,8 @@ export type MergeResult = {
|
|
|
90
97
|
dropped: string[];
|
|
91
98
|
/** the scan changed what an existing manifest says the site can do */
|
|
92
99
|
capabilitiesChanged: boolean;
|
|
100
|
+
/** the scan changed how an existing manifest routes languages */
|
|
101
|
+
localesChanged: boolean;
|
|
93
102
|
};
|
|
94
103
|
/** The scan's bindings replace the ones with the same key; bindings the
|
|
95
104
|
scanner can't see (an agent's, hand-written) stay unless pruned. */
|
package/dist/scan.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { sameCapabilities, targetFor } from "./manifest.js";
|
|
4
|
+
import { sameCapabilities, sameLocales, targetFor } from "./manifest.js";
|
|
5
|
+
/** The segment names a site gives the language at the front of its routes. */
|
|
6
|
+
const LOCALE_SEGMENTS = new Set(["locale", "lang", "lng", "language"]);
|
|
7
|
+
/** Routes by language from the file conventions (§28.4): a dynamic segment
|
|
8
|
+
named for the language at the front of a route (`/[locale]/…`,
|
|
9
|
+
`/[lang]/…`) — every language then lives under its own prefix, and
|
|
10
|
+
the segment is the manifest's `param`. Null when no route starts so. */
|
|
11
|
+
export function detectLocales(routes) {
|
|
12
|
+
for (const route of routes) {
|
|
13
|
+
const first = route.split("/")[1];
|
|
14
|
+
const m = first ? /^\[([A-Za-z0-9_-]{1,60})\]$/.exec(first) : null;
|
|
15
|
+
if (m && LOCALE_SEGMENTS.has(m[1].toLowerCase()))
|
|
16
|
+
return { routing: "prefix", param: m[1] };
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
5
20
|
const SKIP_DIRS = new Set([
|
|
6
21
|
"node_modules",
|
|
7
22
|
".git",
|
|
@@ -1398,6 +1413,7 @@ export async function scanRepository(dir, opts) {
|
|
|
1398
1413
|
if (!opts.ts) {
|
|
1399
1414
|
notes.push("TypeScript isn't installed in this repository, so fields weren't traced (collections only) — npm install -D typescript and scan again.");
|
|
1400
1415
|
}
|
|
1416
|
+
const locales = detectLocales(routes.pages.values());
|
|
1401
1417
|
return {
|
|
1402
1418
|
framework,
|
|
1403
1419
|
parser: opts.ts ? "typescript" : "tokens",
|
|
@@ -1405,6 +1421,7 @@ export async function scanRepository(dir, opts) {
|
|
|
1405
1421
|
pages: uniquePages,
|
|
1406
1422
|
bindings: sortedBindings,
|
|
1407
1423
|
notes,
|
|
1424
|
+
...(locales ? { locales } : {}),
|
|
1408
1425
|
};
|
|
1409
1426
|
}
|
|
1410
1427
|
/** The scan's bindings replace the ones with the same key; bindings the
|
|
@@ -1451,7 +1468,10 @@ export function mergeManifest(existing, scan, opts) {
|
|
|
1451
1468
|
bindings: out,
|
|
1452
1469
|
...(existing?.notes && existing.notes.length > 0 ? { notes: existing.notes } : {}),
|
|
1453
1470
|
...(scan.capabilities ? { capabilities: scan.capabilities } : existing?.capabilities ? { capabilities: existing.capabilities } : {}),
|
|
1471
|
+
// the scan's routes by language, else the manifest's (written by hand or by the agent)
|
|
1472
|
+
...(scan.locales ? { locales: scan.locales } : existing?.locales ? { locales: existing.locales } : {}),
|
|
1454
1473
|
};
|
|
1455
1474
|
const capabilitiesChanged = existing !== null && !sameCapabilities(existing.capabilities, manifest.capabilities);
|
|
1456
|
-
|
|
1475
|
+
const localesChanged = existing !== null && !sameLocales(existing.locales, manifest.locales);
|
|
1476
|
+
return { manifest, added, changed, unchanged, kept, dropped, capabilitiesChanged, localesChanged };
|
|
1457
1477
|
}
|
package/dist/scanHtml.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ export type HtmlElement = {
|
|
|
20
20
|
};
|
|
21
21
|
/** The elements of a page and their attributes — parse5's tree (html5.ts),
|
|
22
22
|
a <template>'s children being its content. Built without recursion:
|
|
23
|
-
a page nests up to MAX_OPEN elements, deeper throws TooDeep
|
|
23
|
+
a page nests up to MAX_OPEN elements, deeper throws TooDeep; a
|
|
24
|
+
numeric character reference parse5 can't read throws BadReference. */
|
|
24
25
|
export declare function parseHtml(text: string): HtmlElement;
|
|
25
26
|
/** Where a file is served and the key its bindings start with:
|
|
26
27
|
index.html → "/" (index), blog/index.html → "/blog/" (blog/index),
|
package/dist/scanHtml.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { MAX_OPEN, parseDocument, TooDeep } from "./html5.js";
|
|
4
|
+
import { BadReference, MAX_OPEN, parseDocument, TooDeep } from "./html5.js";
|
|
5
5
|
import { targetFor } from "./manifest.js";
|
|
6
6
|
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
7
7
|
const MAX_DEPTH = MAX_OPEN;
|
|
8
8
|
/** The elements of a page and their attributes — parse5's tree (html5.ts),
|
|
9
9
|
a <template>'s children being its content. Built without recursion:
|
|
10
|
-
a page nests up to MAX_OPEN elements, deeper throws TooDeep
|
|
10
|
+
a page nests up to MAX_OPEN elements, deeper throws TooDeep; a
|
|
11
|
+
numeric character reference parse5 can't read throws BadReference. */
|
|
11
12
|
export function parseHtml(text) {
|
|
12
13
|
const root = { name: "#document", attrs: new Map(), children: [] };
|
|
13
14
|
const pending = [[parseDocument(text), root]];
|
|
@@ -148,6 +149,7 @@ export async function scanHtml(dir, opts) {
|
|
|
148
149
|
const noTemplate = [];
|
|
149
150
|
const unusableRoutes = [];
|
|
150
151
|
const tooDeep = [];
|
|
152
|
+
const badReferences = [];
|
|
151
153
|
const notGroups = [];
|
|
152
154
|
const seenRoutes = new Set();
|
|
153
155
|
const pageKeys = new Set();
|
|
@@ -170,9 +172,12 @@ export async function scanHtml(dir, opts) {
|
|
|
170
172
|
doc = parseHtml(text);
|
|
171
173
|
}
|
|
172
174
|
catch (e) {
|
|
173
|
-
if (
|
|
175
|
+
if (e instanceof TooDeep)
|
|
176
|
+
tooDeep.push(file);
|
|
177
|
+
else if (e instanceof BadReference)
|
|
178
|
+
badReferences.push(file);
|
|
179
|
+
else
|
|
174
180
|
throw e;
|
|
175
|
-
tooDeep.push(file);
|
|
176
181
|
continue;
|
|
177
182
|
}
|
|
178
183
|
seenRoutes.add(served);
|
|
@@ -422,6 +427,8 @@ export async function scanHtml(dir, opts) {
|
|
|
422
427
|
notes.push(`A list or an each without a <template> isn't filled: ${list(noTemplate)}.`);
|
|
423
428
|
if (tooDeep.length > 0)
|
|
424
429
|
notes.push(`Elements nest deeper than ${MAX_OPEN} levels, so the scan didn't read ${count(tooDeep) === 1 ? "this page" : "these pages"}: ${list(tooDeep)}.`);
|
|
430
|
+
if (badReferences.length > 0)
|
|
431
|
+
notes.push(`A numeric character reference (&#…;) is too long to read, so the scan didn't read ${count(badReferences) === 1 ? "this page" : "these pages"}: ${list(badReferences)}.`);
|
|
425
432
|
if (unusableRoutes.length > 0)
|
|
426
433
|
notes.push(`Preview can't open ${count(unusableRoutes) === 1 ? "this address" : "these addresses"} — rename the file or the slug parameter: ${list(unusableRoutes)}.`);
|
|
427
434
|
// keys within the manifest's 120 characters, and still one per binding
|
package/dist/types.js
CHANGED
|
@@ -153,7 +153,9 @@ function renderedField(field, owner, names, extras) {
|
|
|
153
153
|
if (field.type === "computed" && field.computed) {
|
|
154
154
|
return { required: false, rendered: computedType(field.computed.resultType) };
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
const scalar = scalarType(field.type, field.options);
|
|
157
|
+
// translated (§28.2): one language per read — `locale` on the read picks it
|
|
158
|
+
return { required: field.required, rendered: field.localized === true ? { ...scalar, note: `${scalar.note}, translated` } : scalar };
|
|
157
159
|
}
|
|
158
160
|
/** Unique type names: the singular PascalCase of the key; the raw
|
|
159
161
|
PascalCase when two keys meet there; a numeric suffix as a last resort. */
|
package/dist/vanillaDigests.js
CHANGED
|
@@ -8,4 +8,8 @@
|
|
|
8
8
|
export const VANILLA_DIGESTS = {
|
|
9
9
|
"0.1.0": "sha384-mbuMcQAueviAbkTzgvxc8oDtspuYH6lFLUgE+gocGexnI8ehgSgdjnNMNL7JPk4q",
|
|
10
10
|
"0.1.1": "sha384-2vgQ4g9re/eiYxEa/H3WKpWI5C9L4wCB/62c8Kq+LOfZDzmqc2ozqMGC8mZoOUqk",
|
|
11
|
+
"0.1.2": "sha384-bhIJoFuEGouAZtHbYeNZeN0m6fqFVWpQw+YkSMJhfk/7m5bUwS/qVWZ3xczw/1du",
|
|
12
|
+
"0.2.0": "sha384-HJ5XG9AhAlbKe6jPRVaef3whiVup/5WhprMwqC6RwWhpq6Pb0OsRLzbLmLJ8WH0f",
|
|
13
|
+
"0.2.1": "sha384-6apTHYxXrozzwFCqNU+70Z9pa/FDnhArNomcKQNWbeKRX4scjFiOc9d7wWKCOvTU",
|
|
14
|
+
"0.2.2": "sha384-lfhwQoPUtrbLcD2HXSLr2o1z+aUAjotB62i3LFMGxzBlK5lfCQPQPLOoWL+1DBMc",
|
|
11
15
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mapled/cli",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.3",
|
|
4
4
|
"description": "Mapled CLI — sign in, link a project, generate TypeScript types for its content, create forms, preview drafts and check the site's integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://mapled.io",
|