@kdrgny/justjson 1.2.0 → 1.4.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 +8 -4
- package/dist/cli.js +867 -205
- package/dist/editor/assets/index-B7tRIQcy.js +632 -0
- package/dist/editor/assets/index-GwsCiKhf.css +1 -0
- package/dist/editor/index.html +3 -3
- package/package.json +1 -1
- package/dist/editor/assets/index-BoAqd15W.js +0 -612
- package/dist/editor/assets/index-CP_v5575.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -4054,6 +4054,20 @@ var coerce = {
|
|
|
4054
4054
|
var NEVER = INVALID;
|
|
4055
4055
|
|
|
4056
4056
|
// ../core/dist/index.js
|
|
4057
|
+
var JustJsonError = class extends Error {
|
|
4058
|
+
constructor(message) {
|
|
4059
|
+
super(message);
|
|
4060
|
+
this.name = new.target.name;
|
|
4061
|
+
}
|
|
4062
|
+
};
|
|
4063
|
+
var NotFoundError = class extends JustJsonError {
|
|
4064
|
+
};
|
|
4065
|
+
var UnsafeSlugError = class extends JustJsonError {
|
|
4066
|
+
};
|
|
4067
|
+
var PathEscapeError = class extends JustJsonError {
|
|
4068
|
+
};
|
|
4069
|
+
var SchemaError = class extends JustJsonError {
|
|
4070
|
+
};
|
|
4057
4071
|
var fieldTypes = [
|
|
4058
4072
|
"text",
|
|
4059
4073
|
"richtext",
|
|
@@ -4062,23 +4076,34 @@ var fieldTypes = [
|
|
|
4062
4076
|
"date",
|
|
4063
4077
|
"select",
|
|
4064
4078
|
"relation",
|
|
4065
|
-
"image"
|
|
4079
|
+
"image",
|
|
4080
|
+
"url",
|
|
4081
|
+
"email",
|
|
4082
|
+
"list",
|
|
4083
|
+
"color",
|
|
4084
|
+
"group"
|
|
4066
4085
|
];
|
|
4067
|
-
var zField = external_exports.
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
});
|
|
4086
|
+
var zField = external_exports.lazy(
|
|
4087
|
+
() => external_exports.object({
|
|
4088
|
+
key: external_exports.string().min(1),
|
|
4089
|
+
label: external_exports.string().optional(),
|
|
4090
|
+
type: external_exports.enum(fieldTypes),
|
|
4091
|
+
required: external_exports.boolean().optional(),
|
|
4092
|
+
options: external_exports.array(external_exports.string()).optional(),
|
|
4093
|
+
to: external_exports.string().optional(),
|
|
4094
|
+
fields: external_exports.array(zField).optional()
|
|
4095
|
+
}).superRefine((field, ctx) => {
|
|
4096
|
+
if (field.type === "select" && (!field.options || field.options.length === 0)) {
|
|
4097
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: "a select field requires options" });
|
|
4098
|
+
}
|
|
4099
|
+
if (field.type === "relation" && !field.to) {
|
|
4100
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: 'a relation field requires "to"' });
|
|
4101
|
+
}
|
|
4102
|
+
if (field.type === "group" && (!field.fields || field.fields.length === 0)) {
|
|
4103
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: "a group field requires fields" });
|
|
4104
|
+
}
|
|
4105
|
+
})
|
|
4106
|
+
);
|
|
4082
4107
|
var zCollection = external_exports.object({
|
|
4083
4108
|
name: external_exports.string().min(1),
|
|
4084
4109
|
label: external_exports.string().optional(),
|
|
@@ -4102,30 +4127,38 @@ var zSchema = external_exports.object({
|
|
|
4102
4127
|
const containers = [...schema.collections, ...schema.singletons];
|
|
4103
4128
|
for (const c of containers) {
|
|
4104
4129
|
if (names.has(c.name)) {
|
|
4105
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `
|
|
4130
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `duplicate name: ${c.name}` });
|
|
4106
4131
|
}
|
|
4107
4132
|
names.add(c.name);
|
|
4108
4133
|
if (c.path.includes("..") || c.path.startsWith("/") || c.path.includes("\\")) {
|
|
4109
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `
|
|
4134
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `unsafe path: ${c.path}` });
|
|
4110
4135
|
}
|
|
4111
4136
|
if (paths.has(c.path)) {
|
|
4112
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `
|
|
4137
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `duplicate path: ${c.path}` });
|
|
4113
4138
|
}
|
|
4114
4139
|
paths.add(c.path);
|
|
4115
4140
|
const keys = /* @__PURE__ */ new Set();
|
|
4116
4141
|
for (const f of c.fields) {
|
|
4117
4142
|
if (keys.has(f.key)) {
|
|
4118
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `
|
|
4143
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: `duplicate field key: ${f.key}` });
|
|
4119
4144
|
}
|
|
4120
4145
|
keys.add(f.key);
|
|
4121
4146
|
if (f.type === "relation" && f.to && !collectionNames.has(f.to)) {
|
|
4122
|
-
ctx.addIssue({
|
|
4147
|
+
ctx.addIssue({
|
|
4148
|
+
code: external_exports.ZodIssueCode.custom,
|
|
4149
|
+
message: `relation target does not exist: ${f.to}`
|
|
4150
|
+
});
|
|
4123
4151
|
}
|
|
4124
4152
|
}
|
|
4125
4153
|
}
|
|
4126
4154
|
});
|
|
4127
4155
|
function parseSchema(input) {
|
|
4128
|
-
|
|
4156
|
+
const result = zSchema.safeParse(input);
|
|
4157
|
+
if (result.success) return result.data;
|
|
4158
|
+
const lines = result.error.issues.map(
|
|
4159
|
+
(i) => i.path.length ? `${i.path.join(".")}: ${i.message}` : i.message
|
|
4160
|
+
);
|
|
4161
|
+
throw new SchemaError(lines.join("\n"));
|
|
4129
4162
|
}
|
|
4130
4163
|
function serializeSchema(schema) {
|
|
4131
4164
|
return `${JSON.stringify(schema, null, 2)}
|
|
@@ -4149,7 +4182,7 @@ var TR = {
|
|
|
4149
4182
|
function slugify(input) {
|
|
4150
4183
|
const mapped = input.replace(/[çğıöşüÇĞİÖŞÜI]/g, (c) => TR[c] ?? c);
|
|
4151
4184
|
const slug = mapped.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4152
|
-
return slug || "
|
|
4185
|
+
return slug || "content";
|
|
4153
4186
|
}
|
|
4154
4187
|
var TITLE_KEYS = ["title", "name", "baslik", "ba\u015Fl\u0131k", "ad", "label", "heading", "isim"];
|
|
4155
4188
|
function entryTitle(fields, data) {
|
|
@@ -4171,7 +4204,7 @@ function schemaPath(contentDir) {
|
|
|
4171
4204
|
}
|
|
4172
4205
|
function assertSafeSlug(slug) {
|
|
4173
4206
|
if (slug.length === 0 || slug.includes("/") || slug.includes("\\") || slug.includes("..")) {
|
|
4174
|
-
throw new
|
|
4207
|
+
throw new UnsafeSlugError(`Unsafe slug: ${slug}`);
|
|
4175
4208
|
}
|
|
4176
4209
|
}
|
|
4177
4210
|
async function loadSchema(adapter, contentDir = "content") {
|
|
@@ -4193,12 +4226,12 @@ var ContentStore = class {
|
|
|
4193
4226
|
contentDir;
|
|
4194
4227
|
collection(name) {
|
|
4195
4228
|
const col = this.schema.collections.find((c) => c.name === name);
|
|
4196
|
-
if (!col) throw new
|
|
4229
|
+
if (!col) throw new NotFoundError(`Bilinmeyen koleksiyon: ${name}`);
|
|
4197
4230
|
return col;
|
|
4198
4231
|
}
|
|
4199
4232
|
singleton(name) {
|
|
4200
4233
|
const s = this.schema.singletons.find((x) => x.name === name);
|
|
4201
|
-
if (!s) throw new
|
|
4234
|
+
if (!s) throw new NotFoundError(`Bilinmeyen singleton: ${name}`);
|
|
4202
4235
|
return s;
|
|
4203
4236
|
}
|
|
4204
4237
|
entryPath(col, slug) {
|
|
@@ -4251,6 +4284,206 @@ var ContentStore = class {
|
|
|
4251
4284
|
`);
|
|
4252
4285
|
}
|
|
4253
4286
|
};
|
|
4287
|
+
function isEmpty(value) {
|
|
4288
|
+
return value === void 0 || value === null || value === "";
|
|
4289
|
+
}
|
|
4290
|
+
var URL_RE = /^[a-z][a-z0-9+.-]*:\/\/.+/i;
|
|
4291
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
4292
|
+
var COLOR_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
4293
|
+
function typeError(field, value) {
|
|
4294
|
+
switch (field.type) {
|
|
4295
|
+
case "text":
|
|
4296
|
+
case "richtext":
|
|
4297
|
+
case "date":
|
|
4298
|
+
case "image":
|
|
4299
|
+
return typeof value === "string" ? null : "expected text";
|
|
4300
|
+
case "url":
|
|
4301
|
+
if (typeof value !== "string") return "expected text";
|
|
4302
|
+
return URL_RE.test(value) ? null : "expected a valid URL";
|
|
4303
|
+
case "email":
|
|
4304
|
+
if (typeof value !== "string") return "expected text";
|
|
4305
|
+
return EMAIL_RE.test(value) ? null : "expected a valid email address";
|
|
4306
|
+
case "color":
|
|
4307
|
+
if (typeof value !== "string") return "expected text";
|
|
4308
|
+
return COLOR_RE.test(value) ? null : "expected a hex color (e.g. #ff0000)";
|
|
4309
|
+
case "list":
|
|
4310
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
|
|
4311
|
+
return "expected a list of text";
|
|
4312
|
+
}
|
|
4313
|
+
return null;
|
|
4314
|
+
case "relation":
|
|
4315
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
|
|
4316
|
+
return "expected a list of slugs";
|
|
4317
|
+
}
|
|
4318
|
+
return null;
|
|
4319
|
+
case "group":
|
|
4320
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
4321
|
+
return "expected an object";
|
|
4322
|
+
}
|
|
4323
|
+
return null;
|
|
4324
|
+
case "number":
|
|
4325
|
+
return typeof value === "number" ? null : "expected a number";
|
|
4326
|
+
case "boolean":
|
|
4327
|
+
return typeof value === "boolean" ? null : "expected true or false";
|
|
4328
|
+
case "select":
|
|
4329
|
+
if (typeof value !== "string") return "expected text";
|
|
4330
|
+
return field.options?.includes(value) ? null : "not one of the options";
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
function validateEntry(fields, data) {
|
|
4334
|
+
const issues = [];
|
|
4335
|
+
const known = new Set(fields.map((f) => f.key));
|
|
4336
|
+
for (const field of fields) {
|
|
4337
|
+
const value = data[field.key];
|
|
4338
|
+
if (isEmpty(value)) {
|
|
4339
|
+
if (field.required) {
|
|
4340
|
+
issues.push({
|
|
4341
|
+
key: field.key,
|
|
4342
|
+
level: "warning",
|
|
4343
|
+
kind: "required",
|
|
4344
|
+
message: "required field is empty"
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4347
|
+
continue;
|
|
4348
|
+
}
|
|
4349
|
+
const err = typeError(field, value);
|
|
4350
|
+
if (err) issues.push({ key: field.key, level: "error", kind: "type", message: err });
|
|
4351
|
+
}
|
|
4352
|
+
for (const key of Object.keys(data)) {
|
|
4353
|
+
if (key.startsWith("_")) continue;
|
|
4354
|
+
if (!known.has(key)) {
|
|
4355
|
+
issues.push({
|
|
4356
|
+
key,
|
|
4357
|
+
level: "warning",
|
|
4358
|
+
kind: "unknown-key",
|
|
4359
|
+
message: "key is not in the schema"
|
|
4360
|
+
});
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
return { ok: issues.every((i) => i.level !== "error"), issues };
|
|
4364
|
+
}
|
|
4365
|
+
function basename(path) {
|
|
4366
|
+
return path.split(/[/\\]/).pop() ?? path;
|
|
4367
|
+
}
|
|
4368
|
+
function imageFields(fields) {
|
|
4369
|
+
return fields.filter((f) => f.type === "image");
|
|
4370
|
+
}
|
|
4371
|
+
function relationFields(fields) {
|
|
4372
|
+
return fields.filter((f) => f.type === "relation");
|
|
4373
|
+
}
|
|
4374
|
+
function validateProject(schema, content) {
|
|
4375
|
+
const issues = [];
|
|
4376
|
+
const mediaNames = content.media ? new Set(content.media.map(basename)) : null;
|
|
4377
|
+
const schemaCollectionNames = new Set(schema.collections.map((c) => c.name));
|
|
4378
|
+
for (const name of Object.keys(content.collections)) {
|
|
4379
|
+
if (!schemaCollectionNames.has(name)) {
|
|
4380
|
+
issues.push({
|
|
4381
|
+
level: "warning",
|
|
4382
|
+
kind: "schema-content-mismatch",
|
|
4383
|
+
collection: name,
|
|
4384
|
+
message: `Collection folder '${name}' is not in the schema`
|
|
4385
|
+
});
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
const slugsByCollection = {};
|
|
4389
|
+
for (const [name, entries] of Object.entries(content.collections)) {
|
|
4390
|
+
slugsByCollection[name] = new Set(entries.map((e) => e.slug));
|
|
4391
|
+
}
|
|
4392
|
+
for (const col of schema.collections) {
|
|
4393
|
+
const entries = content.collections[col.name] ?? [];
|
|
4394
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4395
|
+
const reported = /* @__PURE__ */ new Set();
|
|
4396
|
+
for (const { slug } of entries) {
|
|
4397
|
+
if (seen.has(slug) && !reported.has(slug)) {
|
|
4398
|
+
reported.add(slug);
|
|
4399
|
+
issues.push({
|
|
4400
|
+
level: "error",
|
|
4401
|
+
kind: "duplicate-slug",
|
|
4402
|
+
collection: col.name,
|
|
4403
|
+
slug,
|
|
4404
|
+
message: `Duplicate slug in '${col.name}': ${slug}`
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
seen.add(slug);
|
|
4408
|
+
}
|
|
4409
|
+
const rels = relationFields(col.fields);
|
|
4410
|
+
const imgs = imageFields(col.fields);
|
|
4411
|
+
for (const { slug, data } of entries) {
|
|
4412
|
+
for (const issue of validateEntry(col.fields, data).issues) {
|
|
4413
|
+
issues.push({
|
|
4414
|
+
level: issue.level,
|
|
4415
|
+
kind: issue.kind,
|
|
4416
|
+
collection: col.name,
|
|
4417
|
+
slug,
|
|
4418
|
+
field: issue.key,
|
|
4419
|
+
message: issue.message
|
|
4420
|
+
});
|
|
4421
|
+
}
|
|
4422
|
+
for (const field of rels) {
|
|
4423
|
+
const value = data[field.key];
|
|
4424
|
+
if (!Array.isArray(value)) continue;
|
|
4425
|
+
const target = field.to ? slugsByCollection[field.to] : void 0;
|
|
4426
|
+
for (const ref of value) {
|
|
4427
|
+
if (typeof ref !== "string") continue;
|
|
4428
|
+
if (!target || !target.has(ref)) {
|
|
4429
|
+
issues.push({
|
|
4430
|
+
level: "error",
|
|
4431
|
+
kind: "broken-relation",
|
|
4432
|
+
collection: col.name,
|
|
4433
|
+
slug,
|
|
4434
|
+
field: field.key,
|
|
4435
|
+
message: field.to ? `Broken relation: '${ref}' does not exist in '${field.to}'` : `Relation field '${field.key}' has no target collection`
|
|
4436
|
+
});
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
if (mediaNames) {
|
|
4441
|
+
for (const field of imgs) {
|
|
4442
|
+
const value = data[field.key];
|
|
4443
|
+
if (typeof value !== "string" || value === "") continue;
|
|
4444
|
+
if (!mediaNames.has(basename(value))) {
|
|
4445
|
+
issues.push({
|
|
4446
|
+
level: "error",
|
|
4447
|
+
kind: "missing-media",
|
|
4448
|
+
collection: col.name,
|
|
4449
|
+
slug,
|
|
4450
|
+
field: field.key,
|
|
4451
|
+
message: `Missing media: ${value}`
|
|
4452
|
+
});
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
for (const s of schema.singletons) {
|
|
4459
|
+
const data = content.singletons[s.name] ?? {};
|
|
4460
|
+
for (const issue of validateEntry(s.fields, data).issues) {
|
|
4461
|
+
issues.push({
|
|
4462
|
+
level: issue.level,
|
|
4463
|
+
kind: issue.kind,
|
|
4464
|
+
singleton: s.name,
|
|
4465
|
+
field: issue.key,
|
|
4466
|
+
message: issue.message
|
|
4467
|
+
});
|
|
4468
|
+
}
|
|
4469
|
+
if (mediaNames) {
|
|
4470
|
+
for (const field of imageFields(s.fields)) {
|
|
4471
|
+
const value = data[field.key];
|
|
4472
|
+
if (typeof value !== "string" || value === "") continue;
|
|
4473
|
+
if (!mediaNames.has(basename(value))) {
|
|
4474
|
+
issues.push({
|
|
4475
|
+
level: "error",
|
|
4476
|
+
kind: "missing-media",
|
|
4477
|
+
singleton: s.name,
|
|
4478
|
+
field: field.key,
|
|
4479
|
+
message: `Missing media: ${value}`
|
|
4480
|
+
});
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
return issues;
|
|
4486
|
+
}
|
|
4254
4487
|
function pascalCase(name) {
|
|
4255
4488
|
return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
4256
4489
|
}
|
|
@@ -4263,7 +4496,14 @@ function fieldTsType(field) {
|
|
|
4263
4496
|
case "select":
|
|
4264
4497
|
return (field.options ?? []).map((o) => `'${o}'`).join(" | ") || "string";
|
|
4265
4498
|
case "relation":
|
|
4499
|
+
case "list":
|
|
4266
4500
|
return "string[]";
|
|
4501
|
+
case "group": {
|
|
4502
|
+
const subs = field.fields ?? [];
|
|
4503
|
+
if (subs.length === 0) return "Record<string, unknown>";
|
|
4504
|
+
const inner = subs.map((f) => `${f.key}${f.required ? "" : "?"}: ${fieldTsType(f)}`).join("; ");
|
|
4505
|
+
return `{ ${inner} }`;
|
|
4506
|
+
}
|
|
4267
4507
|
default:
|
|
4268
4508
|
return "string";
|
|
4269
4509
|
}
|
|
@@ -4278,7 +4518,7 @@ ${lines.join("\n")}
|
|
|
4278
4518
|
}`;
|
|
4279
4519
|
}
|
|
4280
4520
|
function generateTypes(schema) {
|
|
4281
|
-
const blocks = ["//
|
|
4521
|
+
const blocks = ["// Generated by JustJSON \u2014 do not edit by hand."];
|
|
4282
4522
|
for (const col of schema.collections) {
|
|
4283
4523
|
const name = pascalCase(col.name);
|
|
4284
4524
|
blocks.push(emitInterface(name, col.fields));
|
|
@@ -4290,6 +4530,71 @@ function generateTypes(schema) {
|
|
|
4290
4530
|
return `${blocks.join("\n\n")}
|
|
4291
4531
|
`;
|
|
4292
4532
|
}
|
|
4533
|
+
function pascalCase2(name) {
|
|
4534
|
+
return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
4535
|
+
}
|
|
4536
|
+
var IMPORTS = `import { readFile, readdir } from 'node:fs/promises'
|
|
4537
|
+
import { dirname, join } from 'node:path'
|
|
4538
|
+
import { fileURLToPath } from 'node:url'`;
|
|
4539
|
+
var READERS = `export type WithSlug<T> = T & { slug: string }
|
|
4540
|
+
|
|
4541
|
+
export interface LoadOptions {
|
|
4542
|
+
/** true ise _status: 'draft' olan kay\u0131tlar da dahil edilir (varsay\u0131lan: yaln\u0131zca yay\u0131nlananlar). */
|
|
4543
|
+
drafts?: boolean
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
async function readCollection<T>(dir: string, opts?: LoadOptions): Promise<WithSlug<T>[]> {
|
|
4547
|
+
let files: string[]
|
|
4548
|
+
try {
|
|
4549
|
+
files = (await readdir(join(contentDir, dir))).filter((f) => f.endsWith('.json'))
|
|
4550
|
+
} catch {
|
|
4551
|
+
return []
|
|
4552
|
+
}
|
|
4553
|
+
const out: WithSlug<T>[] = []
|
|
4554
|
+
for (const file of files.sort()) {
|
|
4555
|
+
const raw = await readFile(join(contentDir, dir, file), 'utf8')
|
|
4556
|
+
const data = JSON.parse(raw) as T & { _status?: string }
|
|
4557
|
+
if (!opts?.drafts && data._status === 'draft') continue
|
|
4558
|
+
out.push({ ...data, slug: file.slice(0, -'.json'.length) })
|
|
4559
|
+
}
|
|
4560
|
+
return out
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
async function readSingleton<T>(file: string): Promise<T | null> {
|
|
4564
|
+
try {
|
|
4565
|
+
return JSON.parse(await readFile(join(contentDir, file), 'utf8')) as T
|
|
4566
|
+
} catch {
|
|
4567
|
+
return null
|
|
4568
|
+
}
|
|
4569
|
+
}`;
|
|
4570
|
+
function generateLoader(schema, contentDir) {
|
|
4571
|
+
const typeNames = [
|
|
4572
|
+
...schema.collections.map((c) => pascalCase2(c.name)),
|
|
4573
|
+
...schema.singletons.map((s) => pascalCase2(s.name))
|
|
4574
|
+
];
|
|
4575
|
+
const header = [
|
|
4576
|
+
"// Generated by JustJSON \u2014 do not edit by hand.",
|
|
4577
|
+
IMPORTS,
|
|
4578
|
+
`import type { ${typeNames.join(", ")} } from './types'`
|
|
4579
|
+
].join("\n");
|
|
4580
|
+
const q = (s) => `'${s.replace(/'/g, "\\'")}'`;
|
|
4581
|
+
const contentDirLine = `const contentDir = join(dirname(fileURLToPath(import.meta.url)), ${q(contentDir)})`;
|
|
4582
|
+
const loaders = [];
|
|
4583
|
+
for (const col of schema.collections) {
|
|
4584
|
+
const name = pascalCase2(col.name);
|
|
4585
|
+
loaders.push(
|
|
4586
|
+
`export const load${name} = (opts?: LoadOptions): Promise<WithSlug<${name}>[]> => readCollection<${name}>(${q(col.path)}, opts)`
|
|
4587
|
+
);
|
|
4588
|
+
}
|
|
4589
|
+
for (const s of schema.singletons) {
|
|
4590
|
+
const name = pascalCase2(s.name);
|
|
4591
|
+
loaders.push(
|
|
4592
|
+
`export const load${name} = (): Promise<${name} | null> => readSingleton<${name}>(${q(s.path)})`
|
|
4593
|
+
);
|
|
4594
|
+
}
|
|
4595
|
+
return `${[header, contentDirLine, READERS, loaders.join("\n")].join("\n\n")}
|
|
4596
|
+
`;
|
|
4597
|
+
}
|
|
4293
4598
|
function json(data) {
|
|
4294
4599
|
return `${JSON.stringify(data, null, 2)}
|
|
4295
4600
|
`;
|
|
@@ -4342,7 +4647,7 @@ function fieldsFromRows(rows) {
|
|
|
4342
4647
|
return order.map((key) => ({ key, label: key, type: types.get(key) }));
|
|
4343
4648
|
}
|
|
4344
4649
|
function inferProject(data) {
|
|
4345
|
-
if (!isPlainObject(data)) throw new Error("
|
|
4650
|
+
if (!isPlainObject(data)) throw new Error("The imported JSON must be an object.");
|
|
4346
4651
|
const collections = [];
|
|
4347
4652
|
const singletons = [];
|
|
4348
4653
|
const entries = {};
|
|
@@ -4351,7 +4656,7 @@ function inferProject(data) {
|
|
|
4351
4656
|
const generalData = {};
|
|
4352
4657
|
const usedNames = /* @__PURE__ */ new Set();
|
|
4353
4658
|
const uniqueName = (base) => {
|
|
4354
|
-
let name = base || "
|
|
4659
|
+
let name = base || "field";
|
|
4355
4660
|
let n = 2;
|
|
4356
4661
|
while (usedNames.has(name)) name = `${base}-${n++}`;
|
|
4357
4662
|
usedNames.add(name);
|
|
@@ -4400,8 +4705,8 @@ function inferProject(data) {
|
|
|
4400
4705
|
}
|
|
4401
4706
|
}
|
|
4402
4707
|
if (generalFields.length > 0) {
|
|
4403
|
-
const name = uniqueName("
|
|
4404
|
-
singletons.push({ name, label: "
|
|
4708
|
+
const name = uniqueName("general");
|
|
4709
|
+
singletons.push({ name, label: "General", path: `${name}.json`, fields: generalFields });
|
|
4405
4710
|
singletonData[name] = generalData;
|
|
4406
4711
|
}
|
|
4407
4712
|
return { schema: { version: 1, collections, singletons }, entries, singletons: singletonData };
|
|
@@ -4422,7 +4727,7 @@ var FsAdapter = class {
|
|
|
4422
4727
|
const rootAbs = resolve(this.root);
|
|
4423
4728
|
const full = resolve(rootAbs, path);
|
|
4424
4729
|
if (full !== rootAbs && !full.startsWith(rootAbs + sep)) {
|
|
4425
|
-
throw new
|
|
4730
|
+
throw new PathEscapeError(`Path escapes the project root: ${path}`);
|
|
4426
4731
|
}
|
|
4427
4732
|
return full;
|
|
4428
4733
|
}
|
|
@@ -4481,7 +4786,7 @@ async function resolveContentDir(root2) {
|
|
|
4481
4786
|
// src/commands/export.ts
|
|
4482
4787
|
async function collectExportZip(adapter, contentDir) {
|
|
4483
4788
|
const schema = await loadSchema(adapter, contentDir);
|
|
4484
|
-
if (!schema) throw new Error("
|
|
4789
|
+
if (!schema) throw new Error("No schema found. Run `justjson init` first.");
|
|
4485
4790
|
const store = new ContentStore(adapter, schema, contentDir);
|
|
4486
4791
|
const entries = {};
|
|
4487
4792
|
for (const col of schema.collections) {
|
|
@@ -4518,82 +4823,142 @@ async function exportZip(root2, outFile = "justjson-export.zip") {
|
|
|
4518
4823
|
// src/templates/blog.json
|
|
4519
4824
|
var blog_default = {
|
|
4520
4825
|
title: "Blog",
|
|
4521
|
-
description: "
|
|
4826
|
+
description: "Posts with rich-text content, plus a Settings singleton for the site name.",
|
|
4522
4827
|
schema: {
|
|
4523
4828
|
version: 1,
|
|
4524
4829
|
collections: [
|
|
4525
4830
|
{
|
|
4526
4831
|
name: "posts",
|
|
4527
|
-
label: "
|
|
4832
|
+
label: "Posts",
|
|
4528
4833
|
path: "posts",
|
|
4529
4834
|
fields: [
|
|
4530
|
-
{
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4835
|
+
{
|
|
4836
|
+
key: "title",
|
|
4837
|
+
label: "Title",
|
|
4838
|
+
type: "text",
|
|
4839
|
+
required: true
|
|
4840
|
+
},
|
|
4841
|
+
{
|
|
4842
|
+
key: "slug",
|
|
4843
|
+
label: "Slug",
|
|
4844
|
+
type: "text",
|
|
4845
|
+
required: true
|
|
4846
|
+
},
|
|
4847
|
+
{
|
|
4848
|
+
key: "date",
|
|
4849
|
+
label: "Date",
|
|
4850
|
+
type: "date"
|
|
4851
|
+
},
|
|
4852
|
+
{
|
|
4853
|
+
key: "body",
|
|
4854
|
+
label: "Body",
|
|
4855
|
+
type: "richtext"
|
|
4856
|
+
}
|
|
4534
4857
|
]
|
|
4535
4858
|
}
|
|
4536
4859
|
],
|
|
4537
4860
|
singletons: [
|
|
4538
4861
|
{
|
|
4539
4862
|
name: "settings",
|
|
4540
|
-
label: "
|
|
4863
|
+
label: "Settings",
|
|
4541
4864
|
path: "settings.json",
|
|
4542
|
-
fields: [
|
|
4865
|
+
fields: [
|
|
4866
|
+
{
|
|
4867
|
+
key: "title",
|
|
4868
|
+
label: "Site name",
|
|
4869
|
+
type: "text"
|
|
4870
|
+
}
|
|
4871
|
+
]
|
|
4543
4872
|
}
|
|
4544
4873
|
]
|
|
4545
4874
|
},
|
|
4546
4875
|
samples: {
|
|
4547
|
-
posts: [
|
|
4876
|
+
posts: [
|
|
4877
|
+
{
|
|
4878
|
+
title: "First post",
|
|
4879
|
+
slug: "first-post",
|
|
4880
|
+
body: "Hello."
|
|
4881
|
+
}
|
|
4882
|
+
]
|
|
4548
4883
|
}
|
|
4549
4884
|
};
|
|
4550
4885
|
|
|
4551
4886
|
// src/templates/catalog.json
|
|
4552
4887
|
var catalog_default = {
|
|
4553
|
-
title: "
|
|
4554
|
-
description: "
|
|
4888
|
+
title: "Product catalog",
|
|
4889
|
+
description: "Products with price, category and an image, plus Store settings.",
|
|
4555
4890
|
schema: {
|
|
4556
4891
|
version: 1,
|
|
4557
4892
|
collections: [
|
|
4558
4893
|
{
|
|
4559
|
-
name: "
|
|
4560
|
-
label: "
|
|
4561
|
-
path: "
|
|
4894
|
+
name: "products",
|
|
4895
|
+
label: "Products",
|
|
4896
|
+
path: "products",
|
|
4562
4897
|
fields: [
|
|
4563
|
-
{ key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
|
|
4564
|
-
{ key: "slug", label: "Slug", type: "text", required: true },
|
|
4565
|
-
{ key: "fiyat", label: "Fiyat", type: "number" },
|
|
4566
4898
|
{
|
|
4567
|
-
key: "
|
|
4568
|
-
label: "
|
|
4899
|
+
key: "title",
|
|
4900
|
+
label: "Title",
|
|
4901
|
+
type: "text",
|
|
4902
|
+
required: true
|
|
4903
|
+
},
|
|
4904
|
+
{
|
|
4905
|
+
key: "slug",
|
|
4906
|
+
label: "Slug",
|
|
4907
|
+
type: "text",
|
|
4908
|
+
required: true
|
|
4909
|
+
},
|
|
4910
|
+
{
|
|
4911
|
+
key: "price",
|
|
4912
|
+
label: "Price",
|
|
4913
|
+
type: "number"
|
|
4914
|
+
},
|
|
4915
|
+
{
|
|
4916
|
+
key: "category",
|
|
4917
|
+
label: "Category",
|
|
4569
4918
|
type: "select",
|
|
4570
|
-
options: ["
|
|
4919
|
+
options: ["Home", "Clothing", "Accessories"]
|
|
4920
|
+
},
|
|
4921
|
+
{
|
|
4922
|
+
key: "cover",
|
|
4923
|
+
label: "Image",
|
|
4924
|
+
type: "image"
|
|
4571
4925
|
},
|
|
4572
|
-
{
|
|
4573
|
-
|
|
4926
|
+
{
|
|
4927
|
+
key: "description",
|
|
4928
|
+
label: "Description",
|
|
4929
|
+
type: "richtext"
|
|
4930
|
+
}
|
|
4574
4931
|
]
|
|
4575
4932
|
}
|
|
4576
4933
|
],
|
|
4577
4934
|
singletons: [
|
|
4578
4935
|
{
|
|
4579
|
-
name: "
|
|
4580
|
-
label: "
|
|
4581
|
-
path: "
|
|
4936
|
+
name: "store",
|
|
4937
|
+
label: "Store",
|
|
4938
|
+
path: "store.json",
|
|
4582
4939
|
fields: [
|
|
4583
|
-
{
|
|
4584
|
-
|
|
4940
|
+
{
|
|
4941
|
+
key: "title",
|
|
4942
|
+
label: "Store name",
|
|
4943
|
+
type: "text"
|
|
4944
|
+
},
|
|
4945
|
+
{
|
|
4946
|
+
key: "tagline",
|
|
4947
|
+
label: "Tagline",
|
|
4948
|
+
type: "text"
|
|
4949
|
+
}
|
|
4585
4950
|
]
|
|
4586
4951
|
}
|
|
4587
4952
|
]
|
|
4588
4953
|
},
|
|
4589
4954
|
samples: {
|
|
4590
|
-
|
|
4955
|
+
products: [
|
|
4591
4956
|
{
|
|
4592
|
-
title: "
|
|
4593
|
-
slug: "
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4957
|
+
title: "Ceramic mug",
|
|
4958
|
+
slug: "ceramic-mug",
|
|
4959
|
+
price: 18,
|
|
4960
|
+
category: "Home",
|
|
4961
|
+
description: "Hand-made ceramic mug."
|
|
4597
4962
|
}
|
|
4598
4963
|
]
|
|
4599
4964
|
}
|
|
@@ -4602,25 +4967,43 @@ var catalog_default = {
|
|
|
4602
4967
|
// src/templates/changelog.json
|
|
4603
4968
|
var changelog_default = {
|
|
4604
4969
|
title: "Changelog",
|
|
4605
|
-
description: "
|
|
4970
|
+
description: "Release notes: version, date, type and a rich-text body.",
|
|
4606
4971
|
schema: {
|
|
4607
4972
|
version: 1,
|
|
4608
4973
|
collections: [
|
|
4609
4974
|
{
|
|
4610
4975
|
name: "releases",
|
|
4611
|
-
label: "
|
|
4976
|
+
label: "Releases",
|
|
4612
4977
|
path: "releases",
|
|
4613
4978
|
fields: [
|
|
4614
|
-
{
|
|
4615
|
-
|
|
4616
|
-
|
|
4979
|
+
{
|
|
4980
|
+
key: "version",
|
|
4981
|
+
label: "Version",
|
|
4982
|
+
type: "text",
|
|
4983
|
+
required: true
|
|
4984
|
+
},
|
|
4985
|
+
{
|
|
4986
|
+
key: "slug",
|
|
4987
|
+
label: "Slug",
|
|
4988
|
+
type: "text",
|
|
4989
|
+
required: true
|
|
4990
|
+
},
|
|
4991
|
+
{
|
|
4992
|
+
key: "date",
|
|
4993
|
+
label: "Date",
|
|
4994
|
+
type: "date"
|
|
4995
|
+
},
|
|
4617
4996
|
{
|
|
4618
4997
|
key: "type",
|
|
4619
|
-
label: "
|
|
4998
|
+
label: "Type",
|
|
4620
4999
|
type: "select",
|
|
4621
|
-
options: ["
|
|
5000
|
+
options: ["Added", "Changed", "Fixed"]
|
|
4622
5001
|
},
|
|
4623
|
-
{
|
|
5002
|
+
{
|
|
5003
|
+
key: "body",
|
|
5004
|
+
label: "Notes",
|
|
5005
|
+
type: "richtext"
|
|
5006
|
+
}
|
|
4624
5007
|
]
|
|
4625
5008
|
}
|
|
4626
5009
|
],
|
|
@@ -4631,8 +5014,8 @@ var changelog_default = {
|
|
|
4631
5014
|
{
|
|
4632
5015
|
version: "1.0.0",
|
|
4633
5016
|
slug: "1-0-0",
|
|
4634
|
-
type: "
|
|
4635
|
-
body: "
|
|
5017
|
+
type: "Added",
|
|
5018
|
+
body: "First release."
|
|
4636
5019
|
}
|
|
4637
5020
|
]
|
|
4638
5021
|
}
|
|
@@ -4641,119 +5024,217 @@ var changelog_default = {
|
|
|
4641
5024
|
// src/templates/cv.json
|
|
4642
5025
|
var cv_default = {
|
|
4643
5026
|
title: "CV",
|
|
4644
|
-
description: "
|
|
5027
|
+
description: "An Experience collection and a Profile singleton for your personal details.",
|
|
4645
5028
|
schema: {
|
|
4646
5029
|
version: 1,
|
|
4647
5030
|
collections: [
|
|
4648
5031
|
{
|
|
4649
5032
|
name: "experience",
|
|
4650
|
-
label: "
|
|
5033
|
+
label: "Experience",
|
|
4651
5034
|
path: "experience",
|
|
4652
5035
|
fields: [
|
|
4653
|
-
{
|
|
4654
|
-
|
|
4655
|
-
|
|
5036
|
+
{
|
|
5037
|
+
key: "role",
|
|
5038
|
+
label: "Role",
|
|
5039
|
+
type: "text",
|
|
5040
|
+
required: true
|
|
5041
|
+
},
|
|
5042
|
+
{
|
|
5043
|
+
key: "company",
|
|
5044
|
+
label: "Company",
|
|
5045
|
+
type: "text",
|
|
5046
|
+
required: true
|
|
5047
|
+
},
|
|
5048
|
+
{
|
|
5049
|
+
key: "summary",
|
|
5050
|
+
label: "Summary",
|
|
5051
|
+
type: "richtext"
|
|
5052
|
+
}
|
|
4656
5053
|
]
|
|
4657
5054
|
}
|
|
4658
5055
|
],
|
|
4659
5056
|
singletons: [
|
|
4660
5057
|
{
|
|
4661
5058
|
name: "profile",
|
|
4662
|
-
label: "
|
|
5059
|
+
label: "Profile",
|
|
4663
5060
|
path: "profile.json",
|
|
4664
5061
|
fields: [
|
|
4665
|
-
{
|
|
4666
|
-
|
|
5062
|
+
{
|
|
5063
|
+
key: "name",
|
|
5064
|
+
label: "Name",
|
|
5065
|
+
type: "text",
|
|
5066
|
+
required: true
|
|
5067
|
+
},
|
|
5068
|
+
{
|
|
5069
|
+
key: "headline",
|
|
5070
|
+
label: "Headline",
|
|
5071
|
+
type: "text"
|
|
5072
|
+
}
|
|
4667
5073
|
]
|
|
4668
5074
|
}
|
|
4669
5075
|
]
|
|
4670
5076
|
},
|
|
4671
5077
|
samples: {
|
|
4672
|
-
experience: [
|
|
5078
|
+
experience: [
|
|
5079
|
+
{
|
|
5080
|
+
role: "Developer",
|
|
5081
|
+
company: "Acme Inc.",
|
|
5082
|
+
summary: "..."
|
|
5083
|
+
}
|
|
5084
|
+
]
|
|
4673
5085
|
}
|
|
4674
5086
|
};
|
|
4675
5087
|
|
|
4676
5088
|
// src/templates/docs.json
|
|
4677
5089
|
var docs_default = {
|
|
4678
|
-
title: "
|
|
4679
|
-
description: "
|
|
5090
|
+
title: "Documentation",
|
|
5091
|
+
description: "Orderable pages with rich-text content, plus a Settings singleton for the site title.",
|
|
4680
5092
|
schema: {
|
|
4681
5093
|
version: 1,
|
|
4682
5094
|
collections: [
|
|
4683
5095
|
{
|
|
4684
5096
|
name: "pages",
|
|
4685
|
-
label: "
|
|
5097
|
+
label: "Pages",
|
|
4686
5098
|
path: "pages",
|
|
4687
5099
|
fields: [
|
|
4688
|
-
{
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
5100
|
+
{
|
|
5101
|
+
key: "title",
|
|
5102
|
+
label: "Title",
|
|
5103
|
+
type: "text",
|
|
5104
|
+
required: true
|
|
5105
|
+
},
|
|
5106
|
+
{
|
|
5107
|
+
key: "slug",
|
|
5108
|
+
label: "Slug",
|
|
5109
|
+
type: "text",
|
|
5110
|
+
required: true
|
|
5111
|
+
},
|
|
5112
|
+
{
|
|
5113
|
+
key: "order",
|
|
5114
|
+
label: "Order",
|
|
5115
|
+
type: "number"
|
|
5116
|
+
},
|
|
5117
|
+
{
|
|
5118
|
+
key: "body",
|
|
5119
|
+
label: "Body",
|
|
5120
|
+
type: "richtext"
|
|
5121
|
+
}
|
|
4692
5122
|
]
|
|
4693
5123
|
}
|
|
4694
5124
|
],
|
|
4695
5125
|
singletons: [
|
|
4696
5126
|
{
|
|
4697
5127
|
name: "settings",
|
|
4698
|
-
label: "
|
|
5128
|
+
label: "Settings",
|
|
4699
5129
|
path: "settings.json",
|
|
4700
5130
|
fields: [
|
|
4701
|
-
{
|
|
4702
|
-
|
|
5131
|
+
{
|
|
5132
|
+
key: "title",
|
|
5133
|
+
label: "Site name",
|
|
5134
|
+
type: "text"
|
|
5135
|
+
},
|
|
5136
|
+
{
|
|
5137
|
+
key: "tagline",
|
|
5138
|
+
label: "Tagline",
|
|
5139
|
+
type: "text"
|
|
5140
|
+
}
|
|
4703
5141
|
]
|
|
4704
5142
|
}
|
|
4705
5143
|
]
|
|
4706
5144
|
},
|
|
4707
5145
|
samples: {
|
|
4708
5146
|
pages: [
|
|
4709
|
-
{
|
|
5147
|
+
{
|
|
5148
|
+
title: "Getting started",
|
|
5149
|
+
slug: "getting-started",
|
|
5150
|
+
order: 1,
|
|
5151
|
+
body: "Welcome aboard."
|
|
5152
|
+
}
|
|
4710
5153
|
]
|
|
4711
5154
|
}
|
|
4712
5155
|
};
|
|
4713
5156
|
|
|
4714
5157
|
// src/templates/event.json
|
|
4715
5158
|
var event_default = {
|
|
4716
|
-
title: "
|
|
4717
|
-
description: "
|
|
5159
|
+
title: "Event schedule",
|
|
5160
|
+
description: "An event agenda of sessions with date, time and speaker.",
|
|
4718
5161
|
schema: {
|
|
4719
5162
|
version: 1,
|
|
4720
5163
|
collections: [
|
|
4721
5164
|
{
|
|
4722
|
-
name: "
|
|
4723
|
-
label: "
|
|
4724
|
-
path: "
|
|
5165
|
+
name: "sessions",
|
|
5166
|
+
label: "Sessions",
|
|
5167
|
+
path: "sessions",
|
|
4725
5168
|
fields: [
|
|
4726
|
-
{
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
{
|
|
5169
|
+
{
|
|
5170
|
+
key: "title",
|
|
5171
|
+
label: "Title",
|
|
5172
|
+
type: "text",
|
|
5173
|
+
required: true
|
|
5174
|
+
},
|
|
5175
|
+
{
|
|
5176
|
+
key: "slug",
|
|
5177
|
+
label: "Slug",
|
|
5178
|
+
type: "text",
|
|
5179
|
+
required: true
|
|
5180
|
+
},
|
|
5181
|
+
{
|
|
5182
|
+
key: "date",
|
|
5183
|
+
label: "Date",
|
|
5184
|
+
type: "date"
|
|
5185
|
+
},
|
|
5186
|
+
{
|
|
5187
|
+
key: "time",
|
|
5188
|
+
label: "Time",
|
|
5189
|
+
type: "text"
|
|
5190
|
+
},
|
|
5191
|
+
{
|
|
5192
|
+
key: "speaker",
|
|
5193
|
+
label: "Speaker",
|
|
5194
|
+
type: "text"
|
|
5195
|
+
},
|
|
5196
|
+
{
|
|
5197
|
+
key: "room",
|
|
5198
|
+
label: "Room",
|
|
5199
|
+
type: "text"
|
|
5200
|
+
},
|
|
5201
|
+
{
|
|
5202
|
+
key: "description",
|
|
5203
|
+
label: "Description",
|
|
5204
|
+
type: "richtext"
|
|
5205
|
+
}
|
|
4733
5206
|
]
|
|
4734
5207
|
}
|
|
4735
5208
|
],
|
|
4736
5209
|
singletons: [
|
|
4737
5210
|
{
|
|
4738
|
-
name: "
|
|
4739
|
-
label: "
|
|
4740
|
-
path: "
|
|
5211
|
+
name: "event",
|
|
5212
|
+
label: "Event",
|
|
5213
|
+
path: "event.json",
|
|
4741
5214
|
fields: [
|
|
4742
|
-
{
|
|
4743
|
-
|
|
5215
|
+
{
|
|
5216
|
+
key: "title",
|
|
5217
|
+
label: "Event name",
|
|
5218
|
+
type: "text"
|
|
5219
|
+
},
|
|
5220
|
+
{
|
|
5221
|
+
key: "tagline",
|
|
5222
|
+
label: "Tagline",
|
|
5223
|
+
type: "text"
|
|
5224
|
+
}
|
|
4744
5225
|
]
|
|
4745
5226
|
}
|
|
4746
5227
|
]
|
|
4747
5228
|
},
|
|
4748
5229
|
samples: {
|
|
4749
|
-
|
|
5230
|
+
sessions: [
|
|
4750
5231
|
{
|
|
4751
|
-
title: "
|
|
4752
|
-
slug: "
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
5232
|
+
title: "Opening keynote",
|
|
5233
|
+
slug: "opening-keynote",
|
|
5234
|
+
time: "09:00",
|
|
5235
|
+
speaker: "Ada Lovelace",
|
|
5236
|
+
room: "Main hall",
|
|
5237
|
+
description: "Welcome to the event."
|
|
4757
5238
|
}
|
|
4758
5239
|
]
|
|
4759
5240
|
}
|
|
@@ -4761,41 +5242,84 @@ var event_default = {
|
|
|
4761
5242
|
|
|
4762
5243
|
// src/templates/portfolio.json
|
|
4763
5244
|
var portfolio_default = {
|
|
4764
|
-
title: "
|
|
4765
|
-
description: "
|
|
5245
|
+
title: "Portfolio",
|
|
5246
|
+
description: "Projects with a cover image, tag and rich-text body, plus an About page.",
|
|
4766
5247
|
schema: {
|
|
4767
5248
|
version: 1,
|
|
4768
5249
|
collections: [
|
|
4769
5250
|
{
|
|
4770
5251
|
name: "projects",
|
|
4771
|
-
label: "
|
|
5252
|
+
label: "Projects",
|
|
4772
5253
|
path: "projects",
|
|
4773
5254
|
fields: [
|
|
4774
|
-
{
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
5255
|
+
{
|
|
5256
|
+
key: "title",
|
|
5257
|
+
label: "Title",
|
|
5258
|
+
type: "text",
|
|
5259
|
+
required: true
|
|
5260
|
+
},
|
|
5261
|
+
{
|
|
5262
|
+
key: "slug",
|
|
5263
|
+
label: "Slug",
|
|
5264
|
+
type: "text",
|
|
5265
|
+
required: true
|
|
5266
|
+
},
|
|
5267
|
+
{
|
|
5268
|
+
key: "summary",
|
|
5269
|
+
label: "Summary",
|
|
5270
|
+
type: "text"
|
|
5271
|
+
},
|
|
5272
|
+
{
|
|
5273
|
+
key: "cover",
|
|
5274
|
+
label: "Cover",
|
|
5275
|
+
type: "image"
|
|
5276
|
+
},
|
|
5277
|
+
{
|
|
5278
|
+
key: "url",
|
|
5279
|
+
label: "Link",
|
|
5280
|
+
type: "url"
|
|
5281
|
+
},
|
|
4779
5282
|
{
|
|
4780
5283
|
key: "kind",
|
|
4781
|
-
label: "
|
|
5284
|
+
label: "Kind",
|
|
4782
5285
|
type: "select",
|
|
4783
|
-
options: ["Web", "
|
|
5286
|
+
options: ["Web", "Mobile", "Design"]
|
|
5287
|
+
},
|
|
5288
|
+
{
|
|
5289
|
+
key: "body",
|
|
5290
|
+
label: "Body",
|
|
5291
|
+
type: "richtext"
|
|
4784
5292
|
},
|
|
4785
|
-
{
|
|
4786
|
-
|
|
5293
|
+
{
|
|
5294
|
+
key: "date",
|
|
5295
|
+
label: "Date",
|
|
5296
|
+
type: "date"
|
|
5297
|
+
}
|
|
4787
5298
|
]
|
|
4788
5299
|
}
|
|
4789
5300
|
],
|
|
4790
5301
|
singletons: [
|
|
4791
5302
|
{
|
|
4792
5303
|
name: "about",
|
|
4793
|
-
label: "
|
|
5304
|
+
label: "About",
|
|
4794
5305
|
path: "about.json",
|
|
4795
5306
|
fields: [
|
|
4796
|
-
{
|
|
4797
|
-
|
|
4798
|
-
|
|
5307
|
+
{
|
|
5308
|
+
key: "name",
|
|
5309
|
+
label: "Name",
|
|
5310
|
+
type: "text",
|
|
5311
|
+
required: true
|
|
5312
|
+
},
|
|
5313
|
+
{
|
|
5314
|
+
key: "avatar",
|
|
5315
|
+
label: "Avatar",
|
|
5316
|
+
type: "image"
|
|
5317
|
+
},
|
|
5318
|
+
{
|
|
5319
|
+
key: "bio",
|
|
5320
|
+
label: "Bio",
|
|
5321
|
+
type: "richtext"
|
|
5322
|
+
}
|
|
4799
5323
|
]
|
|
4800
5324
|
}
|
|
4801
5325
|
]
|
|
@@ -4803,11 +5327,11 @@ var portfolio_default = {
|
|
|
4803
5327
|
samples: {
|
|
4804
5328
|
projects: [
|
|
4805
5329
|
{
|
|
4806
|
-
title: "
|
|
4807
|
-
slug: "
|
|
4808
|
-
summary: "
|
|
5330
|
+
title: "Sample project",
|
|
5331
|
+
slug: "sample-project",
|
|
5332
|
+
summary: "A short description.",
|
|
4809
5333
|
kind: "Web",
|
|
4810
|
-
body: "
|
|
5334
|
+
body: "Project details go here."
|
|
4811
5335
|
}
|
|
4812
5336
|
]
|
|
4813
5337
|
}
|
|
@@ -4815,44 +5339,80 @@ var portfolio_default = {
|
|
|
4815
5339
|
|
|
4816
5340
|
// src/templates/recipe.json
|
|
4817
5341
|
var recipe_default = {
|
|
4818
|
-
title: "
|
|
4819
|
-
description: "
|
|
5342
|
+
title: "Recipe box",
|
|
5343
|
+
description: "Recipes with ingredients, steps and a cover image, plus Kitchen settings.",
|
|
4820
5344
|
schema: {
|
|
4821
5345
|
version: 1,
|
|
4822
5346
|
collections: [
|
|
4823
5347
|
{
|
|
4824
|
-
name: "
|
|
4825
|
-
label: "
|
|
4826
|
-
path: "
|
|
5348
|
+
name: "recipes",
|
|
5349
|
+
label: "Recipes",
|
|
5350
|
+
path: "recipes",
|
|
4827
5351
|
fields: [
|
|
4828
|
-
{
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
{
|
|
5352
|
+
{
|
|
5353
|
+
key: "title",
|
|
5354
|
+
label: "Title",
|
|
5355
|
+
type: "text",
|
|
5356
|
+
required: true
|
|
5357
|
+
},
|
|
5358
|
+
{
|
|
5359
|
+
key: "slug",
|
|
5360
|
+
label: "Slug",
|
|
5361
|
+
type: "text",
|
|
5362
|
+
required: true
|
|
5363
|
+
},
|
|
5364
|
+
{
|
|
5365
|
+
key: "time",
|
|
5366
|
+
label: "Time",
|
|
5367
|
+
type: "text"
|
|
5368
|
+
},
|
|
5369
|
+
{
|
|
5370
|
+
key: "servings",
|
|
5371
|
+
label: "Servings",
|
|
5372
|
+
type: "number"
|
|
5373
|
+
},
|
|
5374
|
+
{
|
|
5375
|
+
key: "cover",
|
|
5376
|
+
label: "Cover",
|
|
5377
|
+
type: "image"
|
|
5378
|
+
},
|
|
5379
|
+
{
|
|
5380
|
+
key: "ingredients",
|
|
5381
|
+
label: "Ingredients",
|
|
5382
|
+
type: "richtext"
|
|
5383
|
+
},
|
|
5384
|
+
{
|
|
5385
|
+
key: "steps",
|
|
5386
|
+
label: "Steps",
|
|
5387
|
+
type: "richtext"
|
|
5388
|
+
}
|
|
4835
5389
|
]
|
|
4836
5390
|
}
|
|
4837
5391
|
],
|
|
4838
5392
|
singletons: [
|
|
4839
5393
|
{
|
|
4840
|
-
name: "
|
|
4841
|
-
label: "
|
|
4842
|
-
path: "
|
|
4843
|
-
fields: [
|
|
5394
|
+
name: "kitchen",
|
|
5395
|
+
label: "Kitchen",
|
|
5396
|
+
path: "kitchen.json",
|
|
5397
|
+
fields: [
|
|
5398
|
+
{
|
|
5399
|
+
key: "title",
|
|
5400
|
+
label: "Site name",
|
|
5401
|
+
type: "text"
|
|
5402
|
+
}
|
|
5403
|
+
]
|
|
4844
5404
|
}
|
|
4845
5405
|
]
|
|
4846
5406
|
},
|
|
4847
5407
|
samples: {
|
|
4848
|
-
|
|
5408
|
+
recipes: [
|
|
4849
5409
|
{
|
|
4850
|
-
title: "
|
|
4851
|
-
slug: "
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
5410
|
+
title: "Weeknight pasta",
|
|
5411
|
+
slug: "weeknight-pasta",
|
|
5412
|
+
time: "20 min",
|
|
5413
|
+
servings: 2,
|
|
5414
|
+
ingredients: "- 200g pasta\n- 2 cloves garlic\n- Olive oil",
|
|
5415
|
+
steps: "Boil the pasta. Fry the garlic in olive oil. Toss together."
|
|
4856
5416
|
}
|
|
4857
5417
|
]
|
|
4858
5418
|
}
|
|
@@ -4896,7 +5456,7 @@ async function applyTemplate(adapter, contentDir, template) {
|
|
|
4896
5456
|
const store = new ContentStore(adapter, schema, contentDir);
|
|
4897
5457
|
for (const [collection, rows] of Object.entries(template.samples)) {
|
|
4898
5458
|
for (const row of rows) {
|
|
4899
|
-
const slug = slugify(typeof row.slug === "string" ? row.slug : String(row.title ?? "
|
|
5459
|
+
const slug = slugify(typeof row.slug === "string" ? row.slug : String(row.title ?? "content"));
|
|
4900
5460
|
await store.writeEntry(collection, slug, row);
|
|
4901
5461
|
}
|
|
4902
5462
|
}
|
|
@@ -4908,26 +5468,108 @@ async function initProject(root2, templateName) {
|
|
|
4908
5468
|
const adapter = new FsAdapter(root2);
|
|
4909
5469
|
const contentDir = await resolveContentDir(root2);
|
|
4910
5470
|
if (await loadSchema(adapter, contentDir)) {
|
|
4911
|
-
throw new Error("
|
|
5471
|
+
throw new Error("This folder already has a schema (content/_schema.json).");
|
|
4912
5472
|
}
|
|
4913
5473
|
await applyTemplate(adapter, contentDir, template);
|
|
4914
5474
|
}
|
|
4915
5475
|
|
|
4916
5476
|
// src/commands/types.ts
|
|
4917
5477
|
import { join as join2 } from "path";
|
|
4918
|
-
async function generateTypesFile(root2, outPath = "types.ts") {
|
|
5478
|
+
async function generateTypesFile(root2, outPath = "types.ts", loaderPath = "content.ts") {
|
|
4919
5479
|
const adapter = new FsAdapter(root2);
|
|
4920
5480
|
const contentDir = await resolveContentDir(root2);
|
|
4921
5481
|
const schema = await loadSchema(adapter, contentDir);
|
|
4922
|
-
if (!schema) throw new Error("
|
|
5482
|
+
if (!schema) throw new Error("No schema found. Run `justjson init` first.");
|
|
4923
5483
|
await adapter.write(outPath, generateTypes(schema));
|
|
5484
|
+
await adapter.write(loaderPath, generateLoader(schema, contentDir));
|
|
4924
5485
|
return join2(root2, outPath);
|
|
4925
5486
|
}
|
|
4926
5487
|
|
|
5488
|
+
// src/commands/validate.ts
|
|
5489
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
5490
|
+
import { join as join3 } from "path";
|
|
5491
|
+
async function listDir(path) {
|
|
5492
|
+
try {
|
|
5493
|
+
const entries = await readdir2(path, { withFileTypes: true });
|
|
5494
|
+
return entries.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
|
|
5495
|
+
} catch {
|
|
5496
|
+
return [];
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
async function loadProjectContent(root2) {
|
|
5500
|
+
const adapter = new FsAdapter(root2);
|
|
5501
|
+
const contentDir = await resolveContentDir(root2);
|
|
5502
|
+
const schema = await loadSchema(adapter, contentDir);
|
|
5503
|
+
if (!schema) return null;
|
|
5504
|
+
const store = new ContentStore(adapter, schema, contentDir);
|
|
5505
|
+
const collections = {};
|
|
5506
|
+
for (const col of schema.collections) {
|
|
5507
|
+
const slugs = await store.listEntries(col.name);
|
|
5508
|
+
const entries = [];
|
|
5509
|
+
for (const slug of slugs) {
|
|
5510
|
+
entries.push({ slug, data: await store.readEntry(col.name, slug) ?? {} });
|
|
5511
|
+
}
|
|
5512
|
+
collections[col.name] = entries;
|
|
5513
|
+
}
|
|
5514
|
+
const known = new Set(schema.collections.map((c) => c.name));
|
|
5515
|
+
for (const { name, isDir } of await listDir(join3(root2, contentDir))) {
|
|
5516
|
+
if (!isDir || name === "media" || known.has(name) || collections[name]) continue;
|
|
5517
|
+
const files = await listDir(join3(root2, contentDir, name));
|
|
5518
|
+
collections[name] = files.filter((f) => !f.isDir && f.name.endsWith(".json")).map((f) => ({ slug: f.name.slice(0, -".json".length), data: {} }));
|
|
5519
|
+
}
|
|
5520
|
+
const singletons = {};
|
|
5521
|
+
for (const s of schema.singletons) {
|
|
5522
|
+
singletons[s.name] = await store.readSingleton(s.name);
|
|
5523
|
+
}
|
|
5524
|
+
const media = (await listDir(join3(root2, contentDir, "media"))).filter((f) => !f.isDir).map((f) => `${contentDir}/media/${f.name}`);
|
|
5525
|
+
return { schema, content: { collections, singletons, media } };
|
|
5526
|
+
}
|
|
5527
|
+
async function validateProjectAt(root2) {
|
|
5528
|
+
const loaded = await loadProjectContent(root2);
|
|
5529
|
+
if (!loaded) return null;
|
|
5530
|
+
return validateProject(loaded.schema, loaded.content);
|
|
5531
|
+
}
|
|
5532
|
+
function summarize(issues) {
|
|
5533
|
+
let errors = 0;
|
|
5534
|
+
let warnings = 0;
|
|
5535
|
+
for (const i of issues) {
|
|
5536
|
+
if (i.level === "error") errors++;
|
|
5537
|
+
else warnings++;
|
|
5538
|
+
}
|
|
5539
|
+
return { errors, warnings };
|
|
5540
|
+
}
|
|
5541
|
+
function shouldFail(issues, strict) {
|
|
5542
|
+
const { errors, warnings } = summarize(issues);
|
|
5543
|
+
return errors > 0 || strict && warnings > 0;
|
|
5544
|
+
}
|
|
5545
|
+
function location(issue) {
|
|
5546
|
+
const parts = [];
|
|
5547
|
+
if (issue.collection) parts.push(issue.collection);
|
|
5548
|
+
if (issue.singleton) parts.push(issue.singleton);
|
|
5549
|
+
if (issue.slug) parts.push(issue.slug);
|
|
5550
|
+
if (issue.field) parts.push(issue.field);
|
|
5551
|
+
return parts.join(":") || "(project)";
|
|
5552
|
+
}
|
|
5553
|
+
function formatText(issues) {
|
|
5554
|
+
if (issues.length === 0) return "\u2713 No issues \u2014 content matches the schema.";
|
|
5555
|
+
const lines = issues.map((i) => {
|
|
5556
|
+
const tag = i.level === "error" ? "ERROR" : "WARN ";
|
|
5557
|
+
return ` ${tag} ${location(i)} ${i.message}`;
|
|
5558
|
+
});
|
|
5559
|
+
const { errors, warnings } = summarize(issues);
|
|
5560
|
+
lines.push("");
|
|
5561
|
+
lines.push(`${errors} error(s), ${warnings} warning(s)`);
|
|
5562
|
+
return lines.join("\n");
|
|
5563
|
+
}
|
|
5564
|
+
function formatJson(issues) {
|
|
5565
|
+
const { errors, warnings } = summarize(issues);
|
|
5566
|
+
return JSON.stringify({ ok: errors === 0, errors, warnings, issues }, null, 2);
|
|
5567
|
+
}
|
|
5568
|
+
|
|
4927
5569
|
// src/server.ts
|
|
4928
5570
|
import { exec } from "child_process";
|
|
4929
5571
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
4930
|
-
import { basename, dirname as dirname2, extname, join as
|
|
5572
|
+
import { basename as basename2, dirname as dirname2, extname, join as join4, normalize } from "path";
|
|
4931
5573
|
import { fileURLToPath } from "url";
|
|
4932
5574
|
import { serve } from "@hono/node-server";
|
|
4933
5575
|
import { Hono } from "hono";
|
|
@@ -4948,16 +5590,17 @@ async function callGemini(apiKey, model, system, prompt) {
|
|
|
4948
5590
|
})
|
|
4949
5591
|
});
|
|
4950
5592
|
const data = await res.json();
|
|
4951
|
-
if (!res.ok) throw new Error(data.error?.message ?? `Gemini
|
|
5593
|
+
if (!res.ok) throw new Error(data.error?.message ?? `Gemini request failed (${res.status})`);
|
|
4952
5594
|
const text = data.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
|
|
4953
|
-
if (!text) throw new Error("Gemini
|
|
5595
|
+
if (!text) throw new Error("Gemini returned an empty response");
|
|
4954
5596
|
return text;
|
|
4955
5597
|
}
|
|
4956
5598
|
async function listGeminiModels(apiKey) {
|
|
4957
5599
|
const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}&pageSize=1000`;
|
|
4958
5600
|
const res = await fetch(url);
|
|
4959
5601
|
const data = await res.json();
|
|
4960
|
-
if (!res.ok)
|
|
5602
|
+
if (!res.ok)
|
|
5603
|
+
throw new Error(data.error?.message ?? `Could not fetch the model list (${res.status})`);
|
|
4961
5604
|
return (data.models ?? []).filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((m) => ({
|
|
4962
5605
|
id: (m.name ?? "").replace(/^models\//, ""),
|
|
4963
5606
|
label: m.displayName ?? (m.name ?? "").replace(/^models\//, "")
|
|
@@ -4966,16 +5609,17 @@ async function listGeminiModels(apiKey) {
|
|
|
4966
5609
|
);
|
|
4967
5610
|
}
|
|
4968
5611
|
async function listOpenAiCompatibleModels(baseUrl, apiKey) {
|
|
4969
|
-
if (!baseUrl) throw new Error("
|
|
5612
|
+
if (!baseUrl) throw new Error("This provider needs a base URL");
|
|
4970
5613
|
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
|
|
4971
5614
|
headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {}
|
|
4972
5615
|
});
|
|
4973
5616
|
const data = await res.json();
|
|
4974
|
-
if (!res.ok)
|
|
5617
|
+
if (!res.ok)
|
|
5618
|
+
throw new Error(data.error?.message ?? `Could not fetch the model list (${res.status})`);
|
|
4975
5619
|
return (data.data ?? []).map((m) => ({ id: m.id ?? "", label: m.id ?? "" })).filter((m) => m.id);
|
|
4976
5620
|
}
|
|
4977
5621
|
async function callOpenAiCompatible(baseUrl, apiKey, model, system, prompt) {
|
|
4978
|
-
if (!baseUrl) throw new Error("
|
|
5622
|
+
if (!baseUrl) throw new Error("This provider needs a base URL");
|
|
4979
5623
|
const messages = [
|
|
4980
5624
|
...system ? [{ role: "system", content: system }] : [],
|
|
4981
5625
|
{ role: "user", content: prompt }
|
|
@@ -4986,9 +5630,9 @@ async function callOpenAiCompatible(baseUrl, apiKey, model, system, prompt) {
|
|
|
4986
5630
|
body: JSON.stringify({ model, messages })
|
|
4987
5631
|
});
|
|
4988
5632
|
const data = await res.json();
|
|
4989
|
-
if (!res.ok) throw new Error(data.error?.message ??
|
|
5633
|
+
if (!res.ok) throw new Error(data.error?.message ?? `Request failed (${res.status})`);
|
|
4990
5634
|
const text = data.choices?.[0]?.message?.content ?? "";
|
|
4991
|
-
if (!text) throw new Error("
|
|
5635
|
+
if (!text) throw new Error("The provider returned an empty response");
|
|
4992
5636
|
return text;
|
|
4993
5637
|
}
|
|
4994
5638
|
var MIME = {
|
|
@@ -5009,15 +5653,16 @@ async function createServer(root2) {
|
|
|
5009
5653
|
let store = new ContentStore(adapter, schema, contentDir);
|
|
5010
5654
|
const app = new Hono();
|
|
5011
5655
|
app.onError((err, c) => {
|
|
5012
|
-
|
|
5013
|
-
if (
|
|
5014
|
-
|
|
5015
|
-
|
|
5656
|
+
if (err instanceof NotFoundError) return c.json({ error: err.message }, 404);
|
|
5657
|
+
if (err instanceof UnsafeSlugError || err instanceof PathEscapeError) {
|
|
5658
|
+
return c.json({ error: err.message }, 400);
|
|
5659
|
+
}
|
|
5660
|
+
return c.json({ error: "server error" }, 500);
|
|
5016
5661
|
});
|
|
5017
5662
|
app.get(
|
|
5018
5663
|
"/api/_project",
|
|
5019
5664
|
(c) => c.json({
|
|
5020
|
-
name:
|
|
5665
|
+
name: basename2(root2) || "project",
|
|
5021
5666
|
path: root2,
|
|
5022
5667
|
contentDir,
|
|
5023
5668
|
collections: schema.collections.length,
|
|
@@ -5028,9 +5673,9 @@ async function createServer(root2) {
|
|
|
5028
5673
|
app.post("/api/_init", async (c) => {
|
|
5029
5674
|
const { template: id } = await c.req.json();
|
|
5030
5675
|
const t = id ? getTemplate(id) : void 0;
|
|
5031
|
-
if (!t) return c.json({ error: "
|
|
5676
|
+
if (!t) return c.json({ error: "Unknown template" }, 404);
|
|
5032
5677
|
if (schema.collections.length > 0 || schema.singletons.length > 0) {
|
|
5033
|
-
return c.json({ error: "
|
|
5678
|
+
return c.json({ error: "This folder already has a schema." }, 400);
|
|
5034
5679
|
}
|
|
5035
5680
|
schema = await applyTemplate(adapter, contentDir, t);
|
|
5036
5681
|
store = new ContentStore(adapter, schema, contentDir);
|
|
@@ -5038,7 +5683,7 @@ async function createServer(root2) {
|
|
|
5038
5683
|
});
|
|
5039
5684
|
app.post("/api/_import", async (c) => {
|
|
5040
5685
|
if (schema.collections.length > 0 || schema.singletons.length > 0) {
|
|
5041
|
-
return c.json({ error: "
|
|
5686
|
+
return c.json({ error: "This folder already has a schema." }, 400);
|
|
5042
5687
|
}
|
|
5043
5688
|
const { raw } = await c.req.json();
|
|
5044
5689
|
let next;
|
|
@@ -5053,7 +5698,7 @@ async function createServer(root2) {
|
|
|
5053
5698
|
entries = inferred.entries;
|
|
5054
5699
|
singletonData = inferred.singletons;
|
|
5055
5700
|
} catch (e) {
|
|
5056
|
-
return c.json({ error:
|
|
5701
|
+
return c.json({ error: `Import failed: ${e.message}` }, 400);
|
|
5057
5702
|
}
|
|
5058
5703
|
}
|
|
5059
5704
|
await saveSchema(adapter, next, contentDir);
|
|
@@ -5061,7 +5706,7 @@ async function createServer(root2) {
|
|
|
5061
5706
|
store = new ContentStore(adapter, schema, contentDir);
|
|
5062
5707
|
for (const [collection, rows] of Object.entries(entries)) {
|
|
5063
5708
|
for (const row of rows) {
|
|
5064
|
-
const slug = slugify(String(row.slug ?? row.title ?? "
|
|
5709
|
+
const slug = slugify(String(row.slug ?? row.title ?? "content")) || "content";
|
|
5065
5710
|
await store.writeEntry(collection, slug, row);
|
|
5066
5711
|
}
|
|
5067
5712
|
}
|
|
@@ -5103,11 +5748,11 @@ async function createServer(root2) {
|
|
|
5103
5748
|
});
|
|
5104
5749
|
app.post("/api/_media", async (c) => {
|
|
5105
5750
|
const body = await c.req.json();
|
|
5106
|
-
if (!body.dataBase64) return c.json({ error: "
|
|
5751
|
+
if (!body.dataBase64) return c.json({ error: "no data" }, 400);
|
|
5107
5752
|
const base = slugify((body.filename ?? "gorsel").replace(/\.[^.]+$/, "")) || "gorsel";
|
|
5108
5753
|
const name = `${base}-${Date.now().toString(36)}.webp`;
|
|
5109
5754
|
const rel = `${contentDir}/media/${name}`;
|
|
5110
|
-
const abs =
|
|
5755
|
+
const abs = join4(root2, rel);
|
|
5111
5756
|
await mkdir2(dirname2(abs), { recursive: true });
|
|
5112
5757
|
await writeFile3(abs, Buffer.from(body.dataBase64, "base64"));
|
|
5113
5758
|
return c.json({ path: rel });
|
|
@@ -5115,7 +5760,7 @@ async function createServer(root2) {
|
|
|
5115
5760
|
app.post("/api/_ai/models", async (c) => {
|
|
5116
5761
|
const body = await c.req.json();
|
|
5117
5762
|
const { provider, apiKey } = body;
|
|
5118
|
-
if (!provider) return c.json({ error: "provider
|
|
5763
|
+
if (!provider) return c.json({ error: "provider is required" }, 400);
|
|
5119
5764
|
try {
|
|
5120
5765
|
const models = provider === "gemini" ? await listGeminiModels(apiKey ?? "") : await listOpenAiCompatibleModels(
|
|
5121
5766
|
body.baseUrl || PROVIDER_BASE_URLS[provider] || "",
|
|
@@ -5130,7 +5775,7 @@ async function createServer(root2) {
|
|
|
5130
5775
|
const body = await c.req.json();
|
|
5131
5776
|
const { provider, apiKey, model, system, prompt } = body;
|
|
5132
5777
|
if (!provider || !apiKey || !model || !prompt) {
|
|
5133
|
-
return c.json({ error: "provider, model, apiKey
|
|
5778
|
+
return c.json({ error: "provider, model, apiKey and prompt are required" }, 400);
|
|
5134
5779
|
}
|
|
5135
5780
|
try {
|
|
5136
5781
|
const text = provider === "gemini" ? await callGemini(apiKey, model, system, prompt) : await callOpenAiCompatible(
|
|
@@ -5151,10 +5796,10 @@ async function createServer(root2) {
|
|
|
5151
5796
|
return c.text("forbidden", 400);
|
|
5152
5797
|
}
|
|
5153
5798
|
try {
|
|
5154
|
-
const bytes = await readFile2(
|
|
5799
|
+
const bytes = await readFile2(join4(root2, contentDir, "media", file));
|
|
5155
5800
|
return new Response(bytes, { headers: { "content-type": "image/webp" } });
|
|
5156
5801
|
} catch {
|
|
5157
|
-
return c.text("
|
|
5802
|
+
return c.text("not found", 404);
|
|
5158
5803
|
}
|
|
5159
5804
|
});
|
|
5160
5805
|
app.get("/api/:collection", async (c) => {
|
|
@@ -5163,7 +5808,7 @@ async function createServer(root2) {
|
|
|
5163
5808
|
});
|
|
5164
5809
|
app.get("/api/:collection/:slug", async (c) => {
|
|
5165
5810
|
const data = await store.readEntry(c.req.param("collection"), c.req.param("slug"));
|
|
5166
|
-
return data ? c.json(data) : c.json({ error: "
|
|
5811
|
+
return data ? c.json(data) : c.json({ error: "not found" }, 404);
|
|
5167
5812
|
});
|
|
5168
5813
|
app.put("/api/:collection/:slug", async (c) => {
|
|
5169
5814
|
const slug = slugify(c.req.param("slug"));
|
|
@@ -5178,17 +5823,17 @@ async function createServer(root2) {
|
|
|
5178
5823
|
app.get("/*", async (c) => {
|
|
5179
5824
|
const urlPath = c.req.path === "/" ? "/index.html" : c.req.path;
|
|
5180
5825
|
const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
|
|
5181
|
-
const file =
|
|
5826
|
+
const file = join4(editorDir, rel);
|
|
5182
5827
|
if (!file.startsWith(editorDir)) return c.text("forbidden", 403);
|
|
5183
5828
|
const type = MIME[extname(file)] ?? "application/octet-stream";
|
|
5184
5829
|
try {
|
|
5185
5830
|
return new Response(await readFile2(file), { headers: { "content-type": type } });
|
|
5186
5831
|
} catch {
|
|
5187
5832
|
try {
|
|
5188
|
-
const html = await readFile2(
|
|
5833
|
+
const html = await readFile2(join4(editorDir, "index.html"));
|
|
5189
5834
|
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
5190
5835
|
} catch {
|
|
5191
|
-
return c.text("
|
|
5836
|
+
return c.text("Editor UI not found (run the justjson build first).", 404);
|
|
5192
5837
|
}
|
|
5193
5838
|
}
|
|
5194
5839
|
});
|
|
@@ -5203,27 +5848,44 @@ async function startServer(root2, port) {
|
|
|
5203
5848
|
const app = await createServer(root2);
|
|
5204
5849
|
serve({ fetch: app.fetch, port });
|
|
5205
5850
|
const url = `http://localhost:${port}`;
|
|
5206
|
-
console.log(`JustJSON
|
|
5851
|
+
console.log(`JustJSON is running at ${url}`);
|
|
5207
5852
|
openBrowser(url);
|
|
5208
5853
|
}
|
|
5209
5854
|
|
|
5855
|
+
// src/version.ts
|
|
5856
|
+
import { readFileSync } from "fs";
|
|
5857
|
+
function readVersion() {
|
|
5858
|
+
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
5859
|
+
return JSON.parse(raw).version;
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5210
5862
|
// src/cli.ts
|
|
5211
5863
|
var program = new Command();
|
|
5212
5864
|
var root = process.cwd();
|
|
5213
|
-
program.name("justjson").description("
|
|
5214
|
-
program.command("init").description("
|
|
5865
|
+
program.name("justjson").description("A tiny local-first CMS that keeps your content as plain JSON").version(readVersion());
|
|
5866
|
+
program.command("init").description("Scaffold a project from a template").argument("[template]", `template name (${listTemplates().join(", ")})`, "blog").action(async (template) => {
|
|
5215
5867
|
await initProject(root, template);
|
|
5216
|
-
console.log(`'${template}' template
|
|
5868
|
+
console.log(`Scaffolded from the '${template}' template.`);
|
|
5217
5869
|
});
|
|
5218
|
-
program.command("types").description("
|
|
5870
|
+
program.command("types").description("Generate types.ts and a typed content.ts loader from your schema").action(async () => {
|
|
5219
5871
|
const out = await generateTypesFile(root);
|
|
5220
|
-
console.log(`
|
|
5872
|
+
console.log(`Wrote: ${out}`);
|
|
5221
5873
|
});
|
|
5222
|
-
program.command("export").description("
|
|
5874
|
+
program.command("export").description("Export schema, content and types as a ZIP").action(async () => {
|
|
5223
5875
|
const out = await exportZip(root);
|
|
5224
|
-
console.log(`
|
|
5876
|
+
console.log(`Wrote: ${out}`);
|
|
5877
|
+
});
|
|
5878
|
+
program.command("validate").description("Check your content against the schema (great for CI)").option("--json", "machine-readable JSON output").option("--strict", "treat warnings as errors too").action(async (opts) => {
|
|
5879
|
+
const issues = await validateProjectAt(root);
|
|
5880
|
+
if (issues === null) {
|
|
5881
|
+
console.error("No schema found. Run `justjson init` first.");
|
|
5882
|
+
process.exitCode = 1;
|
|
5883
|
+
return;
|
|
5884
|
+
}
|
|
5885
|
+
console.log(opts.json ? formatJson(issues) : formatText(issues));
|
|
5886
|
+
if (shouldFail(issues, Boolean(opts.strict))) process.exitCode = 1;
|
|
5225
5887
|
});
|
|
5226
|
-
program.command("serve", { isDefault: true }).description("
|
|
5888
|
+
program.command("serve", { isDefault: true }).description("Start the local editor server").option("-p, --port <port>", "port", "5180").action(async (opts) => {
|
|
5227
5889
|
await startServer(root, Number(opts.port));
|
|
5228
5890
|
});
|
|
5229
5891
|
program.parseAsync();
|