@mapled/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/dist/api.d.ts +21 -0
- package/dist/api.js +72 -0
- package/dist/args.d.ts +6 -0
- package/dist/args.js +52 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +23 -0
- package/dist/commands.d.ts +27 -0
- package/dist/commands.js +211 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +77 -0
- package/dist/credentials.d.ts +34 -0
- package/dist/credentials.js +65 -0
- package/dist/doctor.d.ts +106 -0
- package/dist/doctor.js +422 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +99 -0
- package/dist/oauth.d.ts +45 -0
- package/dist/oauth.js +192 -0
- package/dist/output.d.ts +8 -0
- package/dist/output.js +30 -0
- package/dist/schema.d.ts +44 -0
- package/dist/schema.js +35 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.js +218 -0
- package/package.json +36 -0
package/dist/types.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { schemaHash } from "./schema.js";
|
|
2
|
+
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
3
|
+
const UNCOUNTABLE = new Set([
|
|
4
|
+
"news",
|
|
5
|
+
"series",
|
|
6
|
+
"species",
|
|
7
|
+
"media",
|
|
8
|
+
"data",
|
|
9
|
+
"status",
|
|
10
|
+
"analytics",
|
|
11
|
+
"press",
|
|
12
|
+
"staff",
|
|
13
|
+
"software",
|
|
14
|
+
"hardware",
|
|
15
|
+
"sheep",
|
|
16
|
+
"fish",
|
|
17
|
+
]);
|
|
18
|
+
/** A conservative singular: regular English plurals only. */
|
|
19
|
+
export function singular(word) {
|
|
20
|
+
const lower = word.toLowerCase();
|
|
21
|
+
if (lower.length < 4 || UNCOUNTABLE.has(lower))
|
|
22
|
+
return word;
|
|
23
|
+
if (/[^aeiou]ies$/.test(lower))
|
|
24
|
+
return word.slice(0, -3) + "y";
|
|
25
|
+
if (/(sses|shes|ches|xes|zes)$/.test(lower))
|
|
26
|
+
return word.slice(0, -2);
|
|
27
|
+
if (/(ss|us|is)$/.test(lower))
|
|
28
|
+
return word;
|
|
29
|
+
if (lower.endsWith("s"))
|
|
30
|
+
return word.slice(0, -1);
|
|
31
|
+
return word;
|
|
32
|
+
}
|
|
33
|
+
export function pascal(words) {
|
|
34
|
+
const name = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
35
|
+
if (!name)
|
|
36
|
+
return "Item";
|
|
37
|
+
return /^[0-9]/.test(name) ? `_${name}` : name;
|
|
38
|
+
}
|
|
39
|
+
function wordsOf(key) {
|
|
40
|
+
return key.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
/** `blog-posts` → `BlogPost`; singles keep their name (`homepage` → `Homepage`). */
|
|
43
|
+
export function typeName(key, kind) {
|
|
44
|
+
const words = wordsOf(key);
|
|
45
|
+
if (kind === "collection" && words.length > 0) {
|
|
46
|
+
words[words.length - 1] = singular(words[words.length - 1]);
|
|
47
|
+
}
|
|
48
|
+
return pascal(words);
|
|
49
|
+
}
|
|
50
|
+
function prop(key) {
|
|
51
|
+
return IDENT.test(key) ? key : JSON.stringify(key);
|
|
52
|
+
}
|
|
53
|
+
/** Doc-comment text is user content: it can never close the comment early. */
|
|
54
|
+
function comment(text) {
|
|
55
|
+
return text.replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ").trim();
|
|
56
|
+
}
|
|
57
|
+
function literalUnion(options) {
|
|
58
|
+
if (!options || options.length === 0)
|
|
59
|
+
return "string";
|
|
60
|
+
return options.map((o) => JSON.stringify(o)).join(" | ");
|
|
61
|
+
}
|
|
62
|
+
/** What the site receives for a field of this type. */
|
|
63
|
+
function scalarType(type, options) {
|
|
64
|
+
switch (type) {
|
|
65
|
+
case "short_text":
|
|
66
|
+
case "long_text":
|
|
67
|
+
case "slug":
|
|
68
|
+
case "url":
|
|
69
|
+
case "email":
|
|
70
|
+
return { type: "string", note: type.replace("_", " ") };
|
|
71
|
+
case "rich_text":
|
|
72
|
+
return { type: "string", note: "rich text, Markdown" };
|
|
73
|
+
case "color":
|
|
74
|
+
return { type: "string", note: "color, #rrggbb" };
|
|
75
|
+
case "number":
|
|
76
|
+
return { type: "number", note: "number" };
|
|
77
|
+
case "boolean":
|
|
78
|
+
return { type: "boolean", note: "boolean" };
|
|
79
|
+
case "date":
|
|
80
|
+
return { type: "string", note: "date, YYYY-MM-DD" };
|
|
81
|
+
case "datetime":
|
|
82
|
+
return { type: "string", note: "date and time, ISO 8601" };
|
|
83
|
+
case "image":
|
|
84
|
+
return { type: "MapledAssetId", note: "image" };
|
|
85
|
+
case "file":
|
|
86
|
+
return { type: "MapledAssetId", note: "file" };
|
|
87
|
+
case "enum":
|
|
88
|
+
return { type: literalUnion(options), note: "choice" };
|
|
89
|
+
case "json":
|
|
90
|
+
return { type: "MapledJson", note: "JSON" };
|
|
91
|
+
case "location":
|
|
92
|
+
return { type: "MapledLocation", note: "location" };
|
|
93
|
+
default:
|
|
94
|
+
return { type: "unknown", note: type };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function fieldLine(key, displayName, required, rendered) {
|
|
98
|
+
const doc = ` /** ${comment(displayName)} — ${rendered.note} */`;
|
|
99
|
+
const line = required ? ` ${prop(key)}: ${rendered.type};` : ` ${prop(key)}?: ${rendered.type} | null;`;
|
|
100
|
+
return `${doc}\n${line}`;
|
|
101
|
+
}
|
|
102
|
+
function renderSubField(sub) {
|
|
103
|
+
return fieldLine(sub.key, sub.displayName, sub.required, scalarType(sub.type, sub.options));
|
|
104
|
+
}
|
|
105
|
+
function renderField(field, owner, names, extras) {
|
|
106
|
+
if (field.type === "relation" && field.relation) {
|
|
107
|
+
const target = names.get(field.relation.target) ?? field.relation.target;
|
|
108
|
+
const many = field.relation.cardinality === "many";
|
|
109
|
+
return fieldLine(field.key, field.displayName, field.required, {
|
|
110
|
+
type: many ? "MapledRecordId[]" : "MapledRecordId",
|
|
111
|
+
note: `relation to ${comment(target)}${many ? " (many)" : ""}`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (field.type === "group" && field.group) {
|
|
115
|
+
const itemName = `${owner}${pascal(wordsOf(field.key))}${field.group.repeatable ? "Item" : ""}`;
|
|
116
|
+
const kept = field.group.fields.filter((s) => s.sensitive !== true);
|
|
117
|
+
const hidden = field.group.fields.filter((s) => s.sensitive === true).map((s) => s.key);
|
|
118
|
+
const lines = kept.map(renderSubField);
|
|
119
|
+
if (hidden.length > 0)
|
|
120
|
+
lines.push(` // Sensitive, never delivered: ${comment(hidden.join(", "))}`);
|
|
121
|
+
extras.push(`/** ${comment(field.displayName)} — ${field.group.repeatable ? "an item" : "the group"} of ${owner} */\n` +
|
|
122
|
+
`export interface ${itemName} {\n${lines.join("\n")}\n}`);
|
|
123
|
+
const note = field.group.repeatable
|
|
124
|
+
? `group${field.group.maxItems ? `, up to ${field.group.maxItems} items` : ""}`
|
|
125
|
+
: "group";
|
|
126
|
+
return fieldLine(field.key, field.displayName, field.required, {
|
|
127
|
+
type: field.group.repeatable ? `${itemName}[]` : itemName,
|
|
128
|
+
note,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return fieldLine(field.key, field.displayName, field.required, scalarType(field.type, field.options));
|
|
132
|
+
}
|
|
133
|
+
/** Unique type names: the singular PascalCase of the key; the raw
|
|
134
|
+
PascalCase when two keys meet there; a numeric suffix as a last resort. */
|
|
135
|
+
export function typeNames(collections) {
|
|
136
|
+
const preferred = collections.map((c) => typeName(c.key, c.kind));
|
|
137
|
+
const counts = new Map();
|
|
138
|
+
for (const name of preferred)
|
|
139
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
140
|
+
const taken = new Set();
|
|
141
|
+
const out = new Map();
|
|
142
|
+
collections.forEach((c, i) => {
|
|
143
|
+
let name = preferred[i];
|
|
144
|
+
if ((counts.get(name) ?? 0) > 1)
|
|
145
|
+
name = pascal(wordsOf(c.key));
|
|
146
|
+
let unique = name;
|
|
147
|
+
for (let n = 2; taken.has(unique); n++)
|
|
148
|
+
unique = `${name}${n}`;
|
|
149
|
+
taken.add(unique);
|
|
150
|
+
out.set(c.key, unique);
|
|
151
|
+
});
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
export function generateTypes(schema, opts = {}) {
|
|
155
|
+
const hash = schemaHash(schema);
|
|
156
|
+
const names = typeNames(schema.collections);
|
|
157
|
+
const blocks = [];
|
|
158
|
+
blocks.push([
|
|
159
|
+
"// Types of the published content this site reads through @mapled/next.",
|
|
160
|
+
"// Sensitive fields never reach the site and are left out.",
|
|
161
|
+
"",
|
|
162
|
+
'/** An asset id — pass it to assetUrl(id, { width }) from "@mapled/next". */',
|
|
163
|
+
"export type MapledAssetId = string;",
|
|
164
|
+
'/** The id of a record in another collection — attach it with { expand: ["field"] }. */',
|
|
165
|
+
"export type MapledRecordId = string;",
|
|
166
|
+
"/** A JSON value: an object or a list, up to 32 KB. */",
|
|
167
|
+
"export type MapledJson = Record<string, unknown> | unknown[];",
|
|
168
|
+
"export type MapledLocation = { lat: number; lng: number };",
|
|
169
|
+
].join("\n"));
|
|
170
|
+
for (const c of schema.collections) {
|
|
171
|
+
const name = names.get(c.key);
|
|
172
|
+
const extras = [];
|
|
173
|
+
const kept = c.fields.filter((f) => !f.sensitive);
|
|
174
|
+
const hidden = c.fields.filter((f) => f.sensitive).map((f) => f.key);
|
|
175
|
+
const lines = kept.map((f) => renderField(f, name, names, extras));
|
|
176
|
+
if (hidden.length > 0)
|
|
177
|
+
lines.push(` // Sensitive, never delivered: ${comment(hidden.join(", "))}`);
|
|
178
|
+
const traits = [
|
|
179
|
+
c.kind,
|
|
180
|
+
...(c.mode && c.mode !== "editorial" ? [c.mode] : []),
|
|
181
|
+
...(c.accessClass && c.accessClass !== "public" ? [`${c.accessClass} access`] : []),
|
|
182
|
+
];
|
|
183
|
+
blocks.push(...extras);
|
|
184
|
+
blocks.push(`/** ${comment(c.displayName)} (${traits.join(", ")}) — key "${comment(c.key)}" */\n` +
|
|
185
|
+
`export interface ${name} {\n${lines.join("\n")}\n}`);
|
|
186
|
+
}
|
|
187
|
+
const collections = schema.collections.filter((c) => c.kind === "collection");
|
|
188
|
+
const singles = schema.collections.filter((c) => c.kind === "single");
|
|
189
|
+
const mapOf = (list) => list.length === 0 ? "{}" : `{\n${list.map((c) => ` ${prop(c.key)}: ${names.get(c.key)};`).join("\n")}\n}`;
|
|
190
|
+
blocks.push([
|
|
191
|
+
'/** Collections by key — getRecords<MapledCollections["articles"]>("articles"). */',
|
|
192
|
+
`export interface MapledCollections ${mapOf(collections)}`,
|
|
193
|
+
'/** Singles by key — getSingle<MapledSingles["homepage"]>("homepage"). */',
|
|
194
|
+
`export interface MapledSingles ${mapOf(singles)}`,
|
|
195
|
+
"export type MapledCollectionKey = keyof MapledCollections;",
|
|
196
|
+
"export type MapledSingleKey = keyof MapledSingles;",
|
|
197
|
+
].join("\n"));
|
|
198
|
+
const body = blocks.join("\n\n") + "\n";
|
|
199
|
+
const header = [
|
|
200
|
+
"// Generated by `mapled types generate` — do not edit by hand; run it again after the schema changes.",
|
|
201
|
+
`// Project: ${comment(opts.projectName ?? "Mapled")} • schema ${hash}`,
|
|
202
|
+
].join("\n");
|
|
203
|
+
return { text: `${header}\n\n${body}`, body, hash, collections: collections.length, singles: singles.length };
|
|
204
|
+
}
|
|
205
|
+
/** Splits a generated file into its header and body, with the hash the
|
|
206
|
+
header names when it has one. */
|
|
207
|
+
export function splitGenerated(text) {
|
|
208
|
+
const lines = text.split("\n");
|
|
209
|
+
let i = 0;
|
|
210
|
+
while (i < lines.length && lines[i].startsWith("//"))
|
|
211
|
+
i++;
|
|
212
|
+
const header = lines.slice(0, i).join("\n");
|
|
213
|
+
while (i < lines.length && lines[i].trim() === "")
|
|
214
|
+
i++;
|
|
215
|
+
const body = lines.slice(i).join("\n");
|
|
216
|
+
const hash = /schema ([0-9a-f]{12})/.exec(header)?.[1] ?? null;
|
|
217
|
+
return { header, body, hash };
|
|
218
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mapled/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mapled CLI — sign in, link a project, generate TypeScript types for its content and check the site's integration.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://mapled.io",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mapled",
|
|
9
|
+
"cms",
|
|
10
|
+
"headless-cms",
|
|
11
|
+
"cli",
|
|
12
|
+
"typescript",
|
|
13
|
+
"codegen"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"mapled": "dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
29
|
+
"prepare": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^24.0.0",
|
|
33
|
+
"typescript": "^5.9.0",
|
|
34
|
+
"vitest": "^3.2.0"
|
|
35
|
+
}
|
|
36
|
+
}
|