@mapled/cli 0.14.3 → 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 +1 -1
- package/dist/commands.js +4 -4
- package/dist/doctor.d.ts +21 -1
- package/dist/doctor.js +117 -8
- package/dist/pin.d.ts +9 -3
- package/dist/pin.js +71 -5
- 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 +1 -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";
|
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
|
}
|
|
@@ -732,8 +732,8 @@ export async function doctor(ctx, flags) {
|
|
|
732
732
|
checks.push(dr.checkCli(ctx.version, status?.sdk["@mapled/cli"]));
|
|
733
733
|
// the live facts first: the revalidate route's answer matters once a page listens (#435)
|
|
734
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));
|
|
736
|
-
checks.push(dr.checkRoute("preview", await dr.findRoute(dir, "preview"), framework));
|
|
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));
|
|
737
737
|
// the code is read only where the check applies: a Next.js site (or one doctor can't place)
|
|
738
738
|
checks.push(dr.checkLive(live, framework));
|
|
739
739
|
if (status) {
|
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,6 +158,20 @@ 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>;
|
|
156
176
|
/** `cacheMark` and `fetchCache` — the revalidate route only (revalidateRouteFacts). */
|
|
157
177
|
export type RouteFile = {
|
|
@@ -160,7 +180,7 @@ export type RouteFile = {
|
|
|
160
180
|
cacheMark?: boolean;
|
|
161
181
|
fetchCache?: boolean;
|
|
162
182
|
};
|
|
163
|
-
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>;
|
|
164
184
|
/** GET the site's preview route without a token — the adapter answers 400. */
|
|
165
185
|
export declare function probePreview(origin: string, previewPath: string, f: Fetch): Promise<{
|
|
166
186
|
status: number | null;
|
package/dist/doctor.js
CHANGED
|
@@ -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;
|
|
@@ -472,7 +580,7 @@ export async function findRoute(dir, kind) {
|
|
|
472
580
|
return kind === "revalidate" ? { file: rel, usesAdapter, ...revalidateRouteFacts(text) } : { file: rel, usesAdapter };
|
|
473
581
|
}
|
|
474
582
|
}
|
|
475
|
-
return null;
|
|
583
|
+
return framework === "nuxt" ? moduleRoute(dir) : null;
|
|
476
584
|
}
|
|
477
585
|
/** GET the site's preview route without a token — the adapter answers 400. */
|
|
478
586
|
export async function probePreview(origin, previewPath, f) {
|
|
@@ -656,22 +764,23 @@ export function checkRoute(kind, found, framework, listens = false) {
|
|
|
656
764
|
: "Not needed: the client trades Mapled's preview link on the page itself.",
|
|
657
765
|
};
|
|
658
766
|
}
|
|
659
|
-
if (framework
|
|
767
|
+
if (!checksRoutesOf(framework)) {
|
|
660
768
|
return { key, label, status: "skipped", detail: `No ${kind} route check for ${framework} sites yet.` };
|
|
661
769
|
}
|
|
770
|
+
const adapter = adapterOf(framework, kind);
|
|
662
771
|
if (!found) {
|
|
663
772
|
return kind === "revalidate"
|
|
664
773
|
? {
|
|
665
774
|
key,
|
|
666
775
|
label,
|
|
667
776
|
status: "failed",
|
|
668
|
-
detail:
|
|
777
|
+
detail: `${adapter.file} is missing — mount ${handler} from ${adapter.server}.`,
|
|
669
778
|
}
|
|
670
779
|
: {
|
|
671
780
|
key,
|
|
672
781
|
label,
|
|
673
782
|
status: "warning",
|
|
674
|
-
detail:
|
|
783
|
+
detail: `${adapter.file} is missing — mount ${handler} from ${adapter.server} so Preview from Mapled works.`,
|
|
675
784
|
};
|
|
676
785
|
}
|
|
677
786
|
if (kind === "revalidate" && listens && !found.usesAdapter && !found.cacheMark) {
|
|
@@ -679,7 +788,7 @@ export function checkRoute(kind, found, framework, listens = false) {
|
|
|
679
788
|
key,
|
|
680
789
|
label,
|
|
681
790
|
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
|
|
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().`,
|
|
683
792
|
};
|
|
684
793
|
}
|
|
685
794
|
if (kind === "revalidate" && listens && found.fetchCache) {
|
|
@@ -691,7 +800,7 @@ export function checkRoute(kind, found, framework, listens = false) {
|
|
|
691
800
|
};
|
|
692
801
|
}
|
|
693
802
|
if (!found.usesAdapter) {
|
|
694
|
-
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}.` };
|
|
695
804
|
}
|
|
696
805
|
return { key, label, status: "passed", detail: found.file };
|
|
697
806
|
}
|
package/dist/pin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Schema, type SchemaCollection } from "./schema.js";
|
|
1
|
+
import { type Schema, type SchemaCollection, type SchemaComponentType } from "./schema.js";
|
|
2
2
|
/** mapled/schema.json — the repository's pin of the schema it was last
|
|
3
3
|
synced with (§21.5). `mapled schema diff` compares it with the live
|
|
4
4
|
schema and says which changes a site that READS the content must
|
|
@@ -9,6 +9,8 @@ export type SchemaPin = {
|
|
|
9
9
|
project: string;
|
|
10
10
|
hash: string;
|
|
11
11
|
collections: SchemaCollection[];
|
|
12
|
+
/** the component types (§45.4) — only when the project has some, so older pins read as before */
|
|
13
|
+
componentTypes?: SchemaComponentType[];
|
|
12
14
|
};
|
|
13
15
|
/** The schema reduced to what the pin keeps — the live answer of
|
|
14
16
|
GET /v1/agent/schema without ids, positions and counters. */
|
|
@@ -22,7 +24,8 @@ export declare function writePin(dir: string, pin: SchemaPin): Promise<string>;
|
|
|
22
24
|
export type Severity = "breaking" | "safe";
|
|
23
25
|
export type ChangeOp = "added" | "removed" | "changed" | "renamed";
|
|
24
26
|
export type SchemaChange = {
|
|
25
|
-
|
|
27
|
+
/** `component`: a component type (§45.4) — `collection` holds its key */
|
|
28
|
+
scope: "collection" | "field" | "subfield" | "component";
|
|
26
29
|
collection: string;
|
|
27
30
|
collectionName: string;
|
|
28
31
|
/** The field key; "group.sub" for a sub-field. */
|
|
@@ -37,7 +40,10 @@ export type SchemaChange = {
|
|
|
37
40
|
(§33.5) is safe while the old one is still an alias of the field —
|
|
38
41
|
the site keeps reading it — and the rest of the field is compared
|
|
39
42
|
under its new key; once the alias is removed it is a removed field. */
|
|
40
|
-
export declare function diffSchema(before: SchemaCollection[], after: SchemaCollection[]
|
|
43
|
+
export declare function diffSchema(before: SchemaCollection[], after: SchemaCollection[], componentTypes?: {
|
|
44
|
+
before?: SchemaComponentType[];
|
|
45
|
+
after?: SchemaComponentType[];
|
|
46
|
+
}): SchemaChange[];
|
|
41
47
|
/** The diff as terminal lines: one heading per collection, one line per
|
|
42
48
|
change, the severity in the last column. */
|
|
43
49
|
export declare function formatChanges(changes: SchemaChange[], paint?: (severity: Severity, text: string) => string): string[];
|
package/dist/pin.js
CHANGED
|
@@ -50,6 +50,10 @@ function pinField(f) {
|
|
|
50
50
|
// translation is part of the schema hash too (§28.2)
|
|
51
51
|
if (f.localized === true)
|
|
52
52
|
out.localized = true;
|
|
53
|
+
// and so are the component types a field allows (§45.4)
|
|
54
|
+
if (f.type === "components" && f.components) {
|
|
55
|
+
out.components = { allowed: [...f.components.allowed], min: f.components.min ?? null, max: f.components.max ?? null };
|
|
56
|
+
}
|
|
53
57
|
if (f.aliases && f.aliases.length > 0)
|
|
54
58
|
out.aliases = [...f.aliases];
|
|
55
59
|
return out;
|
|
@@ -65,7 +69,10 @@ export function pinSchema(schema, project) {
|
|
|
65
69
|
mode: c.mode,
|
|
66
70
|
fields: c.fields.map(pinField),
|
|
67
71
|
}));
|
|
68
|
-
|
|
72
|
+
const componentTypes = (schema.componentTypes ?? []).map((t) => ({ key: t.key, displayName: t.displayName, fields: t.fields.map(pinSubField) }));
|
|
73
|
+
if (componentTypes.length === 0)
|
|
74
|
+
return { project, hash: schemaHash({ collections }), collections };
|
|
75
|
+
return { project, hash: schemaHash({ collections, componentTypes }), collections, componentTypes };
|
|
69
76
|
}
|
|
70
77
|
export function parsePin(raw, file) {
|
|
71
78
|
let data;
|
|
@@ -84,7 +91,8 @@ export function parsePin(raw, file) {
|
|
|
84
91
|
collections.some((c) => !c || typeof c !== "object" || typeof c.key !== "string" || !Array.isArray(c.fields))) {
|
|
85
92
|
throw new CliError(`${file} isn't a schema pin written by \`mapled schema pull\` — delete it and pull again.`);
|
|
86
93
|
}
|
|
87
|
-
|
|
94
|
+
const componentTypes = Array.isArray(obj.componentTypes) ? obj.componentTypes : undefined;
|
|
95
|
+
return pinSchema({ collections: collections, ...(componentTypes ? { componentTypes } : {}) }, obj.project);
|
|
88
96
|
}
|
|
89
97
|
export async function readPin(dir) {
|
|
90
98
|
const file = path.join(dir, SCHEMA_FILE);
|
|
@@ -109,6 +117,9 @@ function describeType(f, names) {
|
|
|
109
117
|
const target = names.get(f.relation.target) ?? f.relation.target;
|
|
110
118
|
return `relation to ${target}${f.relation.cardinality === "many" ? " (many)" : ""}`;
|
|
111
119
|
}
|
|
120
|
+
if (f.type === "components" && "components" in f && f.components) {
|
|
121
|
+
return `components of ${f.components.allowed.join(", ")}`;
|
|
122
|
+
}
|
|
112
123
|
if (f.type === "group" && "group" in f && f.group) {
|
|
113
124
|
return `group of ${f.group.fields.length} field${f.group.fields.length === 1 ? "" : "s"}${f.group.repeatable ? ", repeatable" : ""}`;
|
|
114
125
|
}
|
|
@@ -155,7 +166,7 @@ function sameJson(a, b) {
|
|
|
155
166
|
(§33.5) is safe while the old one is still an alias of the field —
|
|
156
167
|
the site keeps reading it — and the rest of the field is compared
|
|
157
168
|
under its new key; once the alias is removed it is a removed field. */
|
|
158
|
-
export function diffSchema(before, after) {
|
|
169
|
+
export function diffSchema(before, after, componentTypes = {}) {
|
|
159
170
|
const changes = [];
|
|
160
171
|
const names = new Map();
|
|
161
172
|
for (const c of before)
|
|
@@ -252,6 +263,19 @@ export function diffSchema(before, after) {
|
|
|
252
263
|
});
|
|
253
264
|
}
|
|
254
265
|
}
|
|
266
|
+
// §45.4: a type the list may no longer hold leaves the site's union; a new one is an item the site doesn't render yet
|
|
267
|
+
if (bf.type === "components" && af.type === "components" && bf.components && af.components) {
|
|
268
|
+
for (const k of bf.components.allowed)
|
|
269
|
+
if (!af.components.allowed.includes(k))
|
|
270
|
+
push("changed", `no longer holds ${q(k)} components`, "breaking");
|
|
271
|
+
for (const k of af.components.allowed)
|
|
272
|
+
if (!bf.components.allowed.includes(k))
|
|
273
|
+
push("changed", `may hold ${q(k)} components now`, "safe");
|
|
274
|
+
const limit = (lo, hi) => [lo ? `at least ${lo}` : null, hi ? `up to ${hi}` : null].filter((x) => x !== null).join(", ") || "no item limit";
|
|
275
|
+
if ((bf.components.min ?? null) !== (af.components.min ?? null) || (bf.components.max ?? null) !== (af.components.max ?? null)) {
|
|
276
|
+
push("changed", `${limit(bf.components.min, bf.components.max)} → ${limit(af.components.min, af.components.max)} items`, "safe");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
255
279
|
if (!sameJson(bf.validation, af.validation))
|
|
256
280
|
push("changed", "validation changed", "safe");
|
|
257
281
|
if (!sameJson(bf.defaultValue, af.defaultValue))
|
|
@@ -282,6 +306,48 @@ export function diffSchema(before, after) {
|
|
|
282
306
|
severity: "safe",
|
|
283
307
|
});
|
|
284
308
|
}
|
|
309
|
+
// the component types (§45.4): an item of a type is typed by the type's sub-fields
|
|
310
|
+
const typesBefore = componentTypes.before ?? [];
|
|
311
|
+
const typesAfter = componentTypes.after ?? [];
|
|
312
|
+
const afterTypes = new Map(typesAfter.map((t) => [t.key, t]));
|
|
313
|
+
const beforeTypes = new Map(typesBefore.map((t) => [t.key, t]));
|
|
314
|
+
for (const bt of typesBefore) {
|
|
315
|
+
const at = afterTypes.get(bt.key);
|
|
316
|
+
const base = { collection: bt.key, collectionName: `${at?.displayName ?? bt.displayName} (component)` };
|
|
317
|
+
if (!at) {
|
|
318
|
+
changes.push({ ...base, scope: "component", op: "removed", detail: "component type removed", severity: "breaking" });
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (bt.displayName !== at.displayName)
|
|
322
|
+
changes.push({ ...base, scope: "component", op: "changed", detail: `name ${q(bt.displayName)} → ${q(at.displayName)}`, severity: "safe" });
|
|
323
|
+
const afterSubs = new Map(at.fields.map((s) => [s.key, s]));
|
|
324
|
+
const beforeSubs = new Map(bt.fields.map((s) => [s.key, s]));
|
|
325
|
+
for (const bs of bt.fields) {
|
|
326
|
+
const as = afterSubs.get(bs.key);
|
|
327
|
+
if (!as) {
|
|
328
|
+
changes.push({ ...base, scope: "component", field: bs.key, op: "removed", detail: `removed — ${describeType(bs, names)}`, severity: "breaking" });
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
diffCommon(bs, as, names, (op, detail, severity) => changes.push({ ...base, scope: "component", field: bs.key, op, detail, severity }));
|
|
332
|
+
}
|
|
333
|
+
for (const as of at.fields) {
|
|
334
|
+
if (beforeSubs.has(as.key))
|
|
335
|
+
continue;
|
|
336
|
+
changes.push({ ...base, scope: "component", field: as.key, op: "added", detail: `added — ${describeType(as, names)}, ${as.required ? "required" : "optional"}`, severity: "safe" });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const at of typesAfter) {
|
|
340
|
+
if (beforeTypes.has(at.key))
|
|
341
|
+
continue;
|
|
342
|
+
changes.push({
|
|
343
|
+
collection: at.key,
|
|
344
|
+
collectionName: `${at.displayName} (component)`,
|
|
345
|
+
scope: "component",
|
|
346
|
+
op: "added",
|
|
347
|
+
detail: `new component type — ${at.fields.length} field${at.fields.length === 1 ? "" : "s"}`,
|
|
348
|
+
severity: "safe",
|
|
349
|
+
});
|
|
350
|
+
}
|
|
285
351
|
return changes;
|
|
286
352
|
}
|
|
287
353
|
const GLYPH = { added: "+", removed: "-", changed: "~", renamed: ">" };
|
|
@@ -303,14 +369,14 @@ export function formatChanges(changes, paint = (_, t) => t) {
|
|
|
303
369
|
for (const key of order) {
|
|
304
370
|
const list = byCollection.get(key);
|
|
305
371
|
const name = list[0].collectionName;
|
|
306
|
-
const whole = list.find((c) => c.scope === "collection" && c.op !== "changed");
|
|
372
|
+
const whole = list.find((c) => (c.scope === "collection" || (c.scope === "component" && !c.field)) && c.op !== "changed");
|
|
307
373
|
if (whole) {
|
|
308
374
|
lines.push(`${GLYPH[whole.op]} ${name} (${key}) ${whole.detail.padEnd(detailWidth)} ${paint(whole.severity, whole.severity)}`);
|
|
309
375
|
continue;
|
|
310
376
|
}
|
|
311
377
|
lines.push(`${name} (${key})`);
|
|
312
378
|
for (const c of list) {
|
|
313
|
-
const label = c.scope === "collection" ? "(collection)" : c.field;
|
|
379
|
+
const label = c.scope === "collection" ? "(collection)" : c.scope === "component" && !c.field ? "(component)" : c.field;
|
|
314
380
|
lines.push(` ${GLYPH[c.op]} ${label.padEnd(width)} ${c.detail.padEnd(detailWidth)} ${paint(c.severity, c.severity)}`);
|
|
315
381
|
}
|
|
316
382
|
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -44,6 +44,20 @@ export type SchemaField = {
|
|
|
44
44
|
/** Translated (§28.2): the field holds a value per language; the site
|
|
45
45
|
reads the default language's. Absent on older servers. */
|
|
46
46
|
localized?: boolean;
|
|
47
|
+
/** Components (§45.4): the component types the field allows (keys of
|
|
48
|
+
`componentTypes`) and how many items it takes. Only on a components field. */
|
|
49
|
+
components?: {
|
|
50
|
+
allowed: string[];
|
|
51
|
+
min?: number | null;
|
|
52
|
+
max?: number | null;
|
|
53
|
+
} | null;
|
|
54
|
+
};
|
|
55
|
+
/** A component type (§45.4): the key an item names in `_type`, the name,
|
|
56
|
+
the sub-fields in a group's shape. */
|
|
57
|
+
export type SchemaComponentType = {
|
|
58
|
+
key: string;
|
|
59
|
+
displayName: string;
|
|
60
|
+
fields: SchemaSubField[];
|
|
47
61
|
};
|
|
48
62
|
export type SchemaCollection = {
|
|
49
63
|
key: string;
|
|
@@ -55,6 +69,8 @@ export type SchemaCollection = {
|
|
|
55
69
|
};
|
|
56
70
|
export type Schema = {
|
|
57
71
|
collections: SchemaCollection[];
|
|
72
|
+
/** the project's component types, in their order — absent on older servers */
|
|
73
|
+
componentTypes?: SchemaComponentType[];
|
|
58
74
|
};
|
|
59
75
|
/** Twelve hex characters over everything the generated types depend on. */
|
|
60
76
|
export declare function schemaHash(schema: Schema): string;
|
package/dist/schema.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
/** Twelve hex characters over everything the generated types depend on. */
|
|
3
3
|
export function schemaHash(schema) {
|
|
4
|
-
const
|
|
4
|
+
const collections = schema.collections.map((c) => ({
|
|
5
5
|
key: c.key,
|
|
6
6
|
displayName: c.displayName,
|
|
7
7
|
kind: c.kind,
|
|
@@ -33,7 +33,31 @@ export function schemaHash(schema) {
|
|
|
33
33
|
...(f.type === "computed" && f.computed ? { computed: { expression: f.computed.expression, resultType: f.computed.resultType } } : {}),
|
|
34
34
|
// present only for translated fields, so schemas without translations hash as before
|
|
35
35
|
...(f.localized === true ? { localized: true } : {}),
|
|
36
|
+
// present only for components fields (§45.4)
|
|
37
|
+
...(f.type === "components" && f.components
|
|
38
|
+
? { components: { allowed: [...f.components.allowed], min: f.components.min ?? null, max: f.components.max ?? null } }
|
|
39
|
+
: {}),
|
|
36
40
|
})),
|
|
37
41
|
}));
|
|
42
|
+
// the component types (§45.4) join the projection only when the project
|
|
43
|
+
// has some: the generated types name their sub-fields; a schema without
|
|
44
|
+
// them hashes as before
|
|
45
|
+
const projection = schema.componentTypes && schema.componentTypes.length > 0
|
|
46
|
+
? {
|
|
47
|
+
collections,
|
|
48
|
+
componentTypes: schema.componentTypes.map((t) => ({
|
|
49
|
+
key: t.key,
|
|
50
|
+
displayName: t.displayName,
|
|
51
|
+
fields: t.fields.map((s) => ({
|
|
52
|
+
key: s.key,
|
|
53
|
+
displayName: s.displayName,
|
|
54
|
+
type: s.type,
|
|
55
|
+
required: s.required,
|
|
56
|
+
options: s.options ?? null,
|
|
57
|
+
sensitive: s.sensitive === true,
|
|
58
|
+
})),
|
|
59
|
+
})),
|
|
60
|
+
}
|
|
61
|
+
: collections;
|
|
38
62
|
return createHash("sha256").update(JSON.stringify(projection)).digest("hex").slice(0, 12);
|
|
39
63
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Schema, type SchemaCollection } from "./schema.js";
|
|
1
|
+
import { type Schema, type SchemaCollection, type SchemaComponentType } from "./schema.js";
|
|
2
2
|
/** TypeScript types for the content a site reads through @mapled/next:
|
|
3
3
|
one interface per collection and single, plus keyed maps. Sensitive
|
|
4
4
|
fields never reach the site, so they are left out. */
|
|
@@ -16,9 +16,25 @@ export declare function singular(word: string): string;
|
|
|
16
16
|
export declare function pascal(words: string[]): string;
|
|
17
17
|
/** `blog-posts` → `BlogPost`; singles keep their name (`homepage` → `Homepage`). */
|
|
18
18
|
export declare function typeName(key: string, kind: "collection" | "single"): string;
|
|
19
|
+
/** The names the file declares itself — no collection or component type
|
|
20
|
+
may take one: `mapled-schemas` is a collection like any other, and a
|
|
21
|
+
component keyed `mapled` isn't the union of them all. */
|
|
22
|
+
export declare const RESERVED_NAMES: readonly ["MapledAssetId", "MapledRecordId", "MapledJson", "MapledLocation", "MapledComponent", "MapledCollections", "MapledSingles", "MapledCollectionKey", "MapledSingleKey", "MapledSchema"];
|
|
23
|
+
/** Every name the file declares, claimed once: the reserved ones first,
|
|
24
|
+
then the collections', the component types' and the group items' —
|
|
25
|
+
a name already claimed gets a numeric suffix, whoever asks second. */
|
|
26
|
+
export declare class DeclaredNames {
|
|
27
|
+
private readonly taken;
|
|
28
|
+
claim(name: string): string;
|
|
29
|
+
}
|
|
19
30
|
/** Unique type names: the singular PascalCase of the key; the raw
|
|
20
|
-
PascalCase when two keys meet there; a numeric suffix as a last resort
|
|
21
|
-
|
|
31
|
+
PascalCase when two keys meet there; a numeric suffix as a last resort
|
|
32
|
+
— and when the name is one the file declares itself (`RESERVED_NAMES`). */
|
|
33
|
+
export declare function typeNames(collections: SchemaCollection[], declared?: DeclaredNames): Map<string, string>;
|
|
34
|
+
/** Unique names of the component types (§45.4): the PascalCase of the
|
|
35
|
+
key with `Component` after it, a numeric suffix when that is claimed
|
|
36
|
+
already — by a collection, another type or the file itself. */
|
|
37
|
+
export declare function componentNames(types: SchemaComponentType[], declared: DeclaredNames): Map<string, string>;
|
|
22
38
|
export declare function generateTypes(schema: Schema, opts?: {
|
|
23
39
|
projectName?: string;
|
|
24
40
|
sdk?: string;
|
package/dist/types.js
CHANGED
|
@@ -118,8 +118,8 @@ function renderSubField(sub) {
|
|
|
118
118
|
/** The field's line, followed by one per previous key (§33.5): a
|
|
119
119
|
renamed field is still delivered under them, typed as the field, and
|
|
120
120
|
the editor strikes them through until the site stops reading them. */
|
|
121
|
-
function renderField(field, owner, names, extras) {
|
|
122
|
-
const { required, rendered } = renderedField(field, owner, names, extras);
|
|
121
|
+
function renderField(field, owner, names, extras, components, declared) {
|
|
122
|
+
const { required, rendered } = renderedField(field, owner, names, extras, components, declared);
|
|
123
123
|
const lines = [fieldLine(field.key, field.displayName, required, rendered)];
|
|
124
124
|
for (const alias of field.aliases ?? []) {
|
|
125
125
|
lines.push(` /** @deprecated Renamed to "${comment(field.key)}" — still delivered under this key until the alias is removed in Mapled. */\n` +
|
|
@@ -127,7 +127,7 @@ function renderField(field, owner, names, extras) {
|
|
|
127
127
|
}
|
|
128
128
|
return lines.join("\n");
|
|
129
129
|
}
|
|
130
|
-
function renderedField(field, owner, names, extras) {
|
|
130
|
+
function renderedField(field, owner, names, extras, components = new Map(), declared = new DeclaredNames()) {
|
|
131
131
|
if (field.type === "relation" && field.relation) {
|
|
132
132
|
const target = names.get(field.relation.target) ?? field.relation.target;
|
|
133
133
|
const many = field.relation.cardinality === "many";
|
|
@@ -137,7 +137,8 @@ function renderedField(field, owner, names, extras) {
|
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
if (field.type === "group" && field.group) {
|
|
140
|
-
|
|
140
|
+
// the group's interface is a declared name like any other: a component or a collection may have taken it
|
|
141
|
+
const itemName = declared.claim(`${owner}${pascal(wordsOf(field.key))}${field.group.repeatable ? "Item" : ""}`);
|
|
141
142
|
const kept = field.group.fields.filter((s) => s.sensitive !== true);
|
|
142
143
|
const hidden = field.group.fields.filter((s) => s.sensitive === true).map((s) => s.key);
|
|
143
144
|
const lines = kept.map(renderSubField);
|
|
@@ -153,38 +154,95 @@ function renderedField(field, owner, names, extras) {
|
|
|
153
154
|
if (field.type === "computed" && field.computed) {
|
|
154
155
|
return { required: false, rendered: computedType(field.computed.resultType) };
|
|
155
156
|
}
|
|
157
|
+
// components (§45.4): a list of the types the field allows, told apart by `_type`
|
|
158
|
+
if (field.type === "components" && field.components) {
|
|
159
|
+
const allowed = field.components.allowed.map((k) => components.get(k)).filter((n) => n !== undefined);
|
|
160
|
+
const union = allowed.length === 0 ? "never" : allowed.length === 1 ? allowed[0] : `(${allowed.join(" | ")})`;
|
|
161
|
+
const limits = [
|
|
162
|
+
...(field.components.min ? [`at least ${field.components.min}`] : []),
|
|
163
|
+
...(field.components.max ? [`up to ${field.components.max}`] : []),
|
|
164
|
+
];
|
|
165
|
+
const note = `components${limits.length > 0 ? `, ${limits.join(", ")} items` : ""}${field.localized === true ? ", translated" : ""}`;
|
|
166
|
+
return { required: field.required, rendered: { type: `${union}[]`, note } };
|
|
167
|
+
}
|
|
156
168
|
const scalar = scalarType(field.type, field.options);
|
|
157
169
|
// translated (§28.2): one language per read — `locale` on the read picks it
|
|
158
170
|
return { required: field.required, rendered: field.localized === true ? { ...scalar, note: `${scalar.note}, translated` } : scalar };
|
|
159
171
|
}
|
|
172
|
+
/** The names the file declares itself — no collection or component type
|
|
173
|
+
may take one: `mapled-schemas` is a collection like any other, and a
|
|
174
|
+
component keyed `mapled` isn't the union of them all. */
|
|
175
|
+
export const RESERVED_NAMES = [
|
|
176
|
+
"MapledAssetId",
|
|
177
|
+
"MapledRecordId",
|
|
178
|
+
"MapledJson",
|
|
179
|
+
"MapledLocation",
|
|
180
|
+
"MapledComponent",
|
|
181
|
+
"MapledCollections",
|
|
182
|
+
"MapledSingles",
|
|
183
|
+
"MapledCollectionKey",
|
|
184
|
+
"MapledSingleKey",
|
|
185
|
+
"MapledSchema",
|
|
186
|
+
];
|
|
187
|
+
/** Every name the file declares, claimed once: the reserved ones first,
|
|
188
|
+
then the collections', the component types' and the group items' —
|
|
189
|
+
a name already claimed gets a numeric suffix, whoever asks second. */
|
|
190
|
+
export class DeclaredNames {
|
|
191
|
+
taken = new Set(RESERVED_NAMES);
|
|
192
|
+
claim(name) {
|
|
193
|
+
let unique = name;
|
|
194
|
+
for (let n = 2; this.taken.has(unique); n++)
|
|
195
|
+
unique = `${name}${n}`;
|
|
196
|
+
this.taken.add(unique);
|
|
197
|
+
return unique;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
160
200
|
/** Unique type names: the singular PascalCase of the key; the raw
|
|
161
|
-
PascalCase when two keys meet there; a numeric suffix as a last resort
|
|
162
|
-
|
|
201
|
+
PascalCase when two keys meet there; a numeric suffix as a last resort
|
|
202
|
+
— and when the name is one the file declares itself (`RESERVED_NAMES`). */
|
|
203
|
+
export function typeNames(collections, declared = new DeclaredNames()) {
|
|
163
204
|
const preferred = collections.map((c) => typeName(c.key, c.kind));
|
|
164
205
|
const counts = new Map();
|
|
165
206
|
for (const name of preferred)
|
|
166
207
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
167
|
-
const taken = new Set();
|
|
168
208
|
const out = new Map();
|
|
169
209
|
collections.forEach((c, i) => {
|
|
170
210
|
let name = preferred[i];
|
|
171
211
|
if ((counts.get(name) ?? 0) > 1)
|
|
172
212
|
name = pascal(wordsOf(c.key));
|
|
173
|
-
|
|
174
|
-
for (let n = 2; taken.has(unique); n++)
|
|
175
|
-
unique = `${name}${n}`;
|
|
176
|
-
taken.add(unique);
|
|
177
|
-
out.set(c.key, unique);
|
|
213
|
+
out.set(c.key, declared.claim(name));
|
|
178
214
|
});
|
|
179
215
|
return out;
|
|
180
216
|
}
|
|
217
|
+
/** Unique names of the component types (§45.4): the PascalCase of the
|
|
218
|
+
key with `Component` after it, a numeric suffix when that is claimed
|
|
219
|
+
already — by a collection, another type or the file itself. */
|
|
220
|
+
export function componentNames(types, declared) {
|
|
221
|
+
const out = new Map();
|
|
222
|
+
for (const t of types)
|
|
223
|
+
out.set(t.key, declared.claim(`${pascal(wordsOf(t.key))}Component`));
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
/** One interface per component type: `_type` as its key, `_key` the
|
|
227
|
+
item's own id, then its sub-fields. */
|
|
228
|
+
function renderComponent(type, name) {
|
|
229
|
+
const lines = [
|
|
230
|
+
` /** The component's type */\n _type: ${JSON.stringify(type.key)};`,
|
|
231
|
+
" /** The item's own id — stays with it across saves */\n _key: string;",
|
|
232
|
+
...type.fields.map(renderSubField),
|
|
233
|
+
];
|
|
234
|
+
return `/** ${comment(type.displayName)} — a component (key "${comment(type.key)}") */\nexport interface ${name} {\n${lines.join("\n")}\n}`;
|
|
235
|
+
}
|
|
181
236
|
export function generateTypes(schema, opts = {}) {
|
|
182
237
|
// the package the site reads through: "@mapled/next" unless the framework says otherwise
|
|
183
238
|
const sdk = opts.sdk ?? "@mapled/next";
|
|
184
239
|
// a React app's assetUrl comes from the client its hooks are built on; the HTML runtime has none to call
|
|
185
240
|
const assets = sdk === "@mapled/react" || sdk === "@mapled/vanilla" ? "@mapled/client" : sdk;
|
|
186
241
|
const hash = schemaHash(schema);
|
|
187
|
-
const
|
|
242
|
+
const declared = new DeclaredNames();
|
|
243
|
+
const names = typeNames(schema.collections, declared);
|
|
244
|
+
const componentTypes = schema.componentTypes ?? [];
|
|
245
|
+
const components = componentNames(componentTypes, declared);
|
|
188
246
|
const blocks = [];
|
|
189
247
|
blocks.push([
|
|
190
248
|
`// Types of the published content this site reads through ${sdk}.`,
|
|
@@ -198,12 +256,19 @@ export function generateTypes(schema, opts = {}) {
|
|
|
198
256
|
"export type MapledJson = Record<string, unknown> | unknown[];",
|
|
199
257
|
"export type MapledLocation = { lat: number; lng: number };",
|
|
200
258
|
].join("\n"));
|
|
259
|
+
// the component types come first: a components field names them
|
|
260
|
+
for (const t of componentTypes)
|
|
261
|
+
blocks.push(renderComponent(t, components.get(t.key)));
|
|
262
|
+
if (componentTypes.length > 0) {
|
|
263
|
+
blocks.push("/** Any component of the project — switch on `_type` to render an item of a components field. */\n" +
|
|
264
|
+
`export type MapledComponent = ${componentTypes.map((t) => components.get(t.key)).join(" | ")};`);
|
|
265
|
+
}
|
|
201
266
|
for (const c of schema.collections) {
|
|
202
267
|
const name = names.get(c.key);
|
|
203
268
|
const extras = [];
|
|
204
269
|
const kept = c.fields.filter((f) => !f.sensitive);
|
|
205
270
|
const hidden = c.fields.filter((f) => f.sensitive).map((f) => f.key);
|
|
206
|
-
const lines = kept.map((f) => renderField(f, name, names, extras));
|
|
271
|
+
const lines = kept.map((f) => renderField(f, name, names, extras, components, declared));
|
|
207
272
|
if (hidden.length > 0)
|
|
208
273
|
lines.push(` // Sensitive, never delivered: ${comment(hidden.join(", "))}`);
|
|
209
274
|
const traits = [
|
package/dist/vanillaDigests.js
CHANGED
|
@@ -12,4 +12,5 @@ export const VANILLA_DIGESTS = {
|
|
|
12
12
|
"0.2.0": "sha384-HJ5XG9AhAlbKe6jPRVaef3whiVup/5WhprMwqC6RwWhpq6Pb0OsRLzbLmLJ8WH0f",
|
|
13
13
|
"0.2.1": "sha384-6apTHYxXrozzwFCqNU+70Z9pa/FDnhArNomcKQNWbeKRX4scjFiOc9d7wWKCOvTU",
|
|
14
14
|
"0.2.2": "sha384-lfhwQoPUtrbLcD2HXSLr2o1z+aUAjotB62i3LFMGxzBlK5lfCQPQPLOoWL+1DBMc",
|
|
15
|
+
"0.2.3": "sha384-SuKH/yk2hpNI+Ec2QGKpZ7jVxe+0Ep0x/fchL560g+XuzG5hnVCIhG9OKX6BKdpY",
|
|
15
16
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mapled/cli",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
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",
|