@kdrgny/justjson 1.1.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.
Files changed (28) hide show
  1. package/README.md +8 -4
  2. package/dist/cli.js +1026 -125
  3. package/dist/editor/assets/index-B7tRIQcy.js +632 -0
  4. package/dist/editor/assets/index-GwsCiKhf.css +1 -0
  5. package/dist/editor/assets/public-sans-latin-ext-wght-normal-MQgHevqp.woff2 +0 -0
  6. package/dist/editor/assets/public-sans-latin-wght-normal-DdeTHZLK.woff2 +0 -0
  7. package/dist/editor/assets/public-sans-vietnamese-wght-normal-CtlIRbEm.woff2 +0 -0
  8. package/dist/editor/assets/space-mono-latin-400-normal-Rg4St2Dn.woff2 +0 -0
  9. package/dist/editor/assets/space-mono-latin-400-normal-_3DlpgIW.woff +0 -0
  10. package/dist/editor/assets/space-mono-latin-700-normal-D7A851RN.woff +0 -0
  11. package/dist/editor/assets/space-mono-latin-700-normal-mWgeinG7.woff2 +0 -0
  12. package/dist/editor/assets/space-mono-latin-ext-400-normal-D4cJI_B-.woff +0 -0
  13. package/dist/editor/assets/space-mono-latin-ext-400-normal-DTLbW2xa.woff2 +0 -0
  14. package/dist/editor/assets/space-mono-latin-ext-700-normal-B2s3bDs2.woff2 +0 -0
  15. package/dist/editor/assets/space-mono-latin-ext-700-normal-B_E7P90g.woff +0 -0
  16. package/dist/editor/assets/space-mono-vietnamese-400-normal-B0PMp_xB.woff +0 -0
  17. package/dist/editor/assets/space-mono-vietnamese-400-normal-BNOj0Qhp.woff2 +0 -0
  18. package/dist/editor/assets/space-mono-vietnamese-700-normal-D-KrLuLr.woff +0 -0
  19. package/dist/editor/assets/space-mono-vietnamese-700-normal-DWQgDHuA.woff2 +0 -0
  20. package/dist/editor/index.html +3 -3
  21. package/package.json +3 -14
  22. package/dist/editor/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  23. package/dist/editor/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  24. package/dist/editor/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  25. package/dist/editor/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  26. package/dist/editor/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  27. package/dist/editor/assets/index-B-aBBk0p.css +0 -1
  28. package/dist/editor/assets/index-DhyPvr9S.js +0 -574
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.object({
4068
- key: external_exports.string().min(1),
4069
- label: external_exports.string().optional(),
4070
- type: external_exports.enum(fieldTypes),
4071
- required: external_exports.boolean().optional(),
4072
- options: external_exports.array(external_exports.string()).optional(),
4073
- to: external_exports.string().optional()
4074
- }).superRefine((field, ctx) => {
4075
- if (field.type === "select" && (!field.options || field.options.length === 0)) {
4076
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: "select alan\u0131 options gerektirir" });
4077
- }
4078
- if (field.type === "relation" && !field.to) {
4079
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: 'relation alan\u0131 "to" gerektirir' });
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: `tekrar eden ad: ${c.name}` });
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: `g\xFCvensiz path: ${c.path}` });
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: `tekrar eden path: ${c.path}` });
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: `tekrar eden field key: ${f.key}` });
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({ code: external_exports.ZodIssueCode.custom, message: `relation hedefi yok: ${f.to}` });
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
- return zSchema.parse(input);
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 || "icerik";
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 Error(`G\xFCvensiz slug: ${slug}`);
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 Error(`Bilinmeyen koleksiyon: ${name}`);
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 Error(`Bilinmeyen singleton: ${name}`);
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 = ["// JustJSON taraf\u0131ndan \xFCretildi \u2014 elle d\xFCzenlemeyin."];
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("\u0130\xE7e aktar\u0131lan JSON bir nesne olmal\u0131.");
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 || "alan";
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("genel");
4404
- singletons.push({ name, label: "Genel", path: `${name}.json`, fields: generalFields });
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 Error(`Yol k\xF6k d\u0131\u015F\u0131na \xE7\u0131k\u0131yor: ${path}`);
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("\u015Eema bulunamad\u0131. \xD6nce `justjson init` \xE7al\u0131\u015Ft\u0131r\u0131n.");
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,58 +4823,187 @@ 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: "Rich-text i\xE7erikli yaz\u0131lar ve site ad\u0131 i\xE7in bir Ayarlar.",
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: "Yaz\u0131lar",
4832
+ label: "Posts",
4528
4833
  path: "posts",
4529
4834
  fields: [
4530
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4531
- { key: "slug", label: "Slug", type: "text", required: true },
4532
- { key: "date", label: "Tarih", type: "date" },
4533
- { key: "body", label: "\u0130\xE7erik", type: "richtext" }
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: "Ayarlar",
4863
+ label: "Settings",
4541
4864
  path: "settings.json",
4542
- fields: [{ key: "title", label: "Site ad\u0131", type: "text" }]
4865
+ fields: [
4866
+ {
4867
+ key: "title",
4868
+ label: "Site name",
4869
+ type: "text"
4870
+ }
4871
+ ]
4872
+ }
4873
+ ]
4874
+ },
4875
+ samples: {
4876
+ posts: [
4877
+ {
4878
+ title: "First post",
4879
+ slug: "first-post",
4880
+ body: "Hello."
4881
+ }
4882
+ ]
4883
+ }
4884
+ };
4885
+
4886
+ // src/templates/catalog.json
4887
+ var catalog_default = {
4888
+ title: "Product catalog",
4889
+ description: "Products with price, category and an image, plus Store settings.",
4890
+ schema: {
4891
+ version: 1,
4892
+ collections: [
4893
+ {
4894
+ name: "products",
4895
+ label: "Products",
4896
+ path: "products",
4897
+ fields: [
4898
+ {
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",
4918
+ type: "select",
4919
+ options: ["Home", "Clothing", "Accessories"]
4920
+ },
4921
+ {
4922
+ key: "cover",
4923
+ label: "Image",
4924
+ type: "image"
4925
+ },
4926
+ {
4927
+ key: "description",
4928
+ label: "Description",
4929
+ type: "richtext"
4930
+ }
4931
+ ]
4932
+ }
4933
+ ],
4934
+ singletons: [
4935
+ {
4936
+ name: "store",
4937
+ label: "Store",
4938
+ path: "store.json",
4939
+ fields: [
4940
+ {
4941
+ key: "title",
4942
+ label: "Store name",
4943
+ type: "text"
4944
+ },
4945
+ {
4946
+ key: "tagline",
4947
+ label: "Tagline",
4948
+ type: "text"
4949
+ }
4950
+ ]
4543
4951
  }
4544
4952
  ]
4545
4953
  },
4546
4954
  samples: {
4547
- posts: [{ title: "\u0130lk yaz\u0131", slug: "ilk-yazi", body: "Merhaba." }]
4955
+ products: [
4956
+ {
4957
+ title: "Ceramic mug",
4958
+ slug: "ceramic-mug",
4959
+ price: 18,
4960
+ category: "Home",
4961
+ description: "Hand-made ceramic mug."
4962
+ }
4963
+ ]
4548
4964
  }
4549
4965
  };
4550
4966
 
4551
4967
  // src/templates/changelog.json
4552
4968
  var changelog_default = {
4553
4969
  title: "Changelog",
4554
- description: "S\xFCr\xFCm notlar\u0131: versiyon, tarih, t\xFCr ve rich-text a\xE7\u0131klama.",
4970
+ description: "Release notes: version, date, type and a rich-text body.",
4555
4971
  schema: {
4556
4972
  version: 1,
4557
4973
  collections: [
4558
4974
  {
4559
4975
  name: "releases",
4560
- label: "S\xFCr\xFCmler",
4976
+ label: "Releases",
4561
4977
  path: "releases",
4562
4978
  fields: [
4563
- { key: "version", label: "Versiyon", type: "text", required: true },
4564
- { key: "slug", label: "Slug", type: "text", required: true },
4565
- { key: "date", label: "Tarih", type: "date" },
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
+ },
4566
4996
  {
4567
4997
  key: "type",
4568
- label: "T\xFCr",
4998
+ label: "Type",
4569
4999
  type: "select",
4570
- options: ["Eklendi", "De\u011Fi\u015Fti", "D\xFCzeltildi"]
5000
+ options: ["Added", "Changed", "Fixed"]
4571
5001
  },
4572
- { key: "body", label: "Notlar", type: "richtext" }
5002
+ {
5003
+ key: "body",
5004
+ label: "Notes",
5005
+ type: "richtext"
5006
+ }
4573
5007
  ]
4574
5008
  }
4575
5009
  ],
@@ -4580,8 +5014,8 @@ var changelog_default = {
4580
5014
  {
4581
5015
  version: "1.0.0",
4582
5016
  slug: "1-0-0",
4583
- type: "Eklendi",
4584
- body: "\u0130lk s\xFCr\xFCm."
5017
+ type: "Added",
5018
+ body: "First release."
4585
5019
  }
4586
5020
  ]
4587
5021
  }
@@ -4590,113 +5024,302 @@ var changelog_default = {
4590
5024
  // src/templates/cv.json
4591
5025
  var cv_default = {
4592
5026
  title: "CV",
4593
- description: "Deneyim koleksiyonu ve ki\u015Fisel bilgiler i\xE7in bir Profil.",
5027
+ description: "An Experience collection and a Profile singleton for your personal details.",
4594
5028
  schema: {
4595
5029
  version: 1,
4596
5030
  collections: [
4597
5031
  {
4598
5032
  name: "experience",
4599
- label: "Deneyim",
5033
+ label: "Experience",
4600
5034
  path: "experience",
4601
5035
  fields: [
4602
- { key: "role", label: "Pozisyon", type: "text", required: true },
4603
- { key: "company", label: "\u015Eirket", type: "text", required: true },
4604
- { key: "summary", label: "\xD6zet", type: "richtext" }
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
+ }
4605
5053
  ]
4606
5054
  }
4607
5055
  ],
4608
5056
  singletons: [
4609
5057
  {
4610
5058
  name: "profile",
4611
- label: "Profil",
5059
+ label: "Profile",
4612
5060
  path: "profile.json",
4613
5061
  fields: [
4614
- { key: "name", label: "Ad", type: "text", required: true },
4615
- { key: "headline", label: "Ba\u015Fl\u0131k", type: "text" }
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
+ }
4616
5073
  ]
4617
5074
  }
4618
5075
  ]
4619
5076
  },
4620
5077
  samples: {
4621
- experience: [{ role: "Yaz\u0131l\u0131mc\u0131", company: "\xD6rnek A.\u015E.", summary: "..." }]
5078
+ experience: [
5079
+ {
5080
+ role: "Developer",
5081
+ company: "Acme Inc.",
5082
+ summary: "..."
5083
+ }
5084
+ ]
4622
5085
  }
4623
5086
  };
4624
5087
 
4625
5088
  // src/templates/docs.json
4626
5089
  var docs_default = {
4627
- title: "Dok\xFCmantasyon",
4628
- description: "S\u0131ralanabilir sayfalar ve rich-text i\xE7erik; site ba\u015Fl\u0131\u011F\u0131 i\xE7in bir Ayarlar.",
5090
+ title: "Documentation",
5091
+ description: "Orderable pages with rich-text content, plus a Settings singleton for the site title.",
4629
5092
  schema: {
4630
5093
  version: 1,
4631
5094
  collections: [
4632
5095
  {
4633
5096
  name: "pages",
4634
- label: "Sayfalar",
5097
+ label: "Pages",
4635
5098
  path: "pages",
4636
5099
  fields: [
4637
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4638
- { key: "slug", label: "Slug", type: "text", required: true },
4639
- { key: "order", label: "S\u0131ra", type: "number" },
4640
- { key: "body", label: "\u0130\xE7erik", type: "richtext" }
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
+ }
4641
5122
  ]
4642
5123
  }
4643
5124
  ],
4644
5125
  singletons: [
4645
5126
  {
4646
5127
  name: "settings",
4647
- label: "Ayarlar",
5128
+ label: "Settings",
4648
5129
  path: "settings.json",
4649
5130
  fields: [
4650
- { key: "title", label: "Site ad\u0131", type: "text" },
4651
- { key: "tagline", label: "Slogan", type: "text" }
5131
+ {
5132
+ key: "title",
5133
+ label: "Site name",
5134
+ type: "text"
5135
+ },
5136
+ {
5137
+ key: "tagline",
5138
+ label: "Tagline",
5139
+ type: "text"
5140
+ }
4652
5141
  ]
4653
5142
  }
4654
5143
  ]
4655
5144
  },
4656
5145
  samples: {
4657
5146
  pages: [
4658
- { title: "Ba\u015Flarken", slug: "baslarken", order: 1, body: "Kuruluma ho\u015F geldin." }
5147
+ {
5148
+ title: "Getting started",
5149
+ slug: "getting-started",
5150
+ order: 1,
5151
+ body: "Welcome aboard."
5152
+ }
5153
+ ]
5154
+ }
5155
+ };
5156
+
5157
+ // src/templates/event.json
5158
+ var event_default = {
5159
+ title: "Event schedule",
5160
+ description: "An event agenda of sessions with date, time and speaker.",
5161
+ schema: {
5162
+ version: 1,
5163
+ collections: [
5164
+ {
5165
+ name: "sessions",
5166
+ label: "Sessions",
5167
+ path: "sessions",
5168
+ fields: [
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
+ }
5206
+ ]
5207
+ }
5208
+ ],
5209
+ singletons: [
5210
+ {
5211
+ name: "event",
5212
+ label: "Event",
5213
+ path: "event.json",
5214
+ fields: [
5215
+ {
5216
+ key: "title",
5217
+ label: "Event name",
5218
+ type: "text"
5219
+ },
5220
+ {
5221
+ key: "tagline",
5222
+ label: "Tagline",
5223
+ type: "text"
5224
+ }
5225
+ ]
5226
+ }
5227
+ ]
5228
+ },
5229
+ samples: {
5230
+ sessions: [
5231
+ {
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."
5238
+ }
4659
5239
  ]
4660
5240
  }
4661
5241
  };
4662
5242
 
4663
5243
  // src/templates/portfolio.json
4664
5244
  var portfolio_default = {
4665
- title: "Portfolyo",
4666
- description: "Kapak g\xF6rseli, etiket ve rich-text i\xE7erikli projeler; bir de Hakk\u0131nda sayfas\u0131.",
5245
+ title: "Portfolio",
5246
+ description: "Projects with a cover image, tag and rich-text body, plus an About page.",
4667
5247
  schema: {
4668
5248
  version: 1,
4669
5249
  collections: [
4670
5250
  {
4671
5251
  name: "projects",
4672
- label: "Projeler",
5252
+ label: "Projects",
4673
5253
  path: "projects",
4674
5254
  fields: [
4675
- { key: "title", label: "Ba\u015Fl\u0131k", type: "text", required: true },
4676
- { key: "slug", label: "Slug", type: "text", required: true },
4677
- { key: "summary", label: "\xD6zet", type: "text" },
4678
- { key: "cover", label: "Kapak", type: "image" },
4679
- { key: "url", label: "Ba\u011Flant\u0131", type: "text" },
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
+ },
4680
5282
  {
4681
5283
  key: "kind",
4682
- label: "T\xFCr",
5284
+ label: "Kind",
4683
5285
  type: "select",
4684
- options: ["Web", "Mobil", "Tasar\u0131m"]
5286
+ options: ["Web", "Mobile", "Design"]
5287
+ },
5288
+ {
5289
+ key: "body",
5290
+ label: "Body",
5291
+ type: "richtext"
4685
5292
  },
4686
- { key: "body", label: "\u0130\xE7erik", type: "richtext" },
4687
- { key: "date", label: "Tarih", type: "date" }
5293
+ {
5294
+ key: "date",
5295
+ label: "Date",
5296
+ type: "date"
5297
+ }
4688
5298
  ]
4689
5299
  }
4690
5300
  ],
4691
5301
  singletons: [
4692
5302
  {
4693
5303
  name: "about",
4694
- label: "Hakk\u0131nda",
5304
+ label: "About",
4695
5305
  path: "about.json",
4696
5306
  fields: [
4697
- { key: "name", label: "Ad", type: "text", required: true },
4698
- { key: "avatar", label: "Avatar", type: "image" },
4699
- { key: "bio", label: "Biyografi", type: "richtext" }
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
+ }
4700
5323
  ]
4701
5324
  }
4702
5325
  ]
@@ -4704,11 +5327,92 @@ var portfolio_default = {
4704
5327
  samples: {
4705
5328
  projects: [
4706
5329
  {
4707
- title: "\xD6rnek Proje",
4708
- slug: "ornek-proje",
4709
- summary: "K\u0131sa bir a\xE7\u0131klama.",
5330
+ title: "Sample project",
5331
+ slug: "sample-project",
5332
+ summary: "A short description.",
4710
5333
  kind: "Web",
4711
- body: "Proje detaylar\u0131 burada."
5334
+ body: "Project details go here."
5335
+ }
5336
+ ]
5337
+ }
5338
+ };
5339
+
5340
+ // src/templates/recipe.json
5341
+ var recipe_default = {
5342
+ title: "Recipe box",
5343
+ description: "Recipes with ingredients, steps and a cover image, plus Kitchen settings.",
5344
+ schema: {
5345
+ version: 1,
5346
+ collections: [
5347
+ {
5348
+ name: "recipes",
5349
+ label: "Recipes",
5350
+ path: "recipes",
5351
+ fields: [
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
+ }
5389
+ ]
5390
+ }
5391
+ ],
5392
+ singletons: [
5393
+ {
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
+ ]
5404
+ }
5405
+ ]
5406
+ },
5407
+ samples: {
5408
+ recipes: [
5409
+ {
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."
4712
5416
  }
4713
5417
  ]
4714
5418
  }
@@ -4720,7 +5424,10 @@ var templates = {
4720
5424
  cv: cv_default,
4721
5425
  portfolio: portfolio_default,
4722
5426
  docs: docs_default,
4723
- changelog: changelog_default
5427
+ changelog: changelog_default,
5428
+ recipe: recipe_default,
5429
+ event: event_default,
5430
+ catalog: catalog_default
4724
5431
  };
4725
5432
  function listTemplates() {
4726
5433
  return Object.keys(templates);
@@ -4749,7 +5456,7 @@ async function applyTemplate(adapter, contentDir, template) {
4749
5456
  const store = new ContentStore(adapter, schema, contentDir);
4750
5457
  for (const [collection, rows] of Object.entries(template.samples)) {
4751
5458
  for (const row of rows) {
4752
- const slug = slugify(typeof row.slug === "string" ? row.slug : String(row.title ?? "icerik"));
5459
+ const slug = slugify(typeof row.slug === "string" ? row.slug : String(row.title ?? "content"));
4753
5460
  await store.writeEntry(collection, slug, row);
4754
5461
  }
4755
5462
  }
@@ -4761,30 +5468,173 @@ async function initProject(root2, templateName) {
4761
5468
  const adapter = new FsAdapter(root2);
4762
5469
  const contentDir = await resolveContentDir(root2);
4763
5470
  if (await loadSchema(adapter, contentDir)) {
4764
- throw new Error("Bu klas\xF6rde zaten bir \u015Fema var (content/_schema.json).");
5471
+ throw new Error("This folder already has a schema (content/_schema.json).");
4765
5472
  }
4766
5473
  await applyTemplate(adapter, contentDir, template);
4767
5474
  }
4768
5475
 
4769
5476
  // src/commands/types.ts
4770
5477
  import { join as join2 } from "path";
4771
- async function generateTypesFile(root2, outPath = "types.ts") {
5478
+ async function generateTypesFile(root2, outPath = "types.ts", loaderPath = "content.ts") {
4772
5479
  const adapter = new FsAdapter(root2);
4773
5480
  const contentDir = await resolveContentDir(root2);
4774
5481
  const schema = await loadSchema(adapter, contentDir);
4775
- if (!schema) throw new Error("\u015Eema bulunamad\u0131. \xD6nce `justjson init` \xE7al\u0131\u015Ft\u0131r\u0131n.");
5482
+ if (!schema) throw new Error("No schema found. Run `justjson init` first.");
4776
5483
  await adapter.write(outPath, generateTypes(schema));
5484
+ await adapter.write(loaderPath, generateLoader(schema, contentDir));
4777
5485
  return join2(root2, outPath);
4778
5486
  }
4779
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
+
4780
5569
  // src/server.ts
4781
5570
  import { exec } from "child_process";
4782
5571
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
4783
- import { basename, dirname as dirname2, extname, join as join3, normalize } from "path";
5572
+ import { basename as basename2, dirname as dirname2, extname, join as join4, normalize } from "path";
4784
5573
  import { fileURLToPath } from "url";
4785
5574
  import { serve } from "@hono/node-server";
4786
5575
  import { Hono } from "hono";
4787
5576
  var editorDir = fileURLToPath(new URL("./editor", import.meta.url));
5577
+ var PROVIDER_BASE_URLS = {
5578
+ groq: "https://api.groq.com/openai/v1",
5579
+ openrouter: "https://openrouter.ai/api/v1"
5580
+ };
5581
+ async function callGemini(apiKey, model, system, prompt) {
5582
+ const cleanModel = model.trim().replace(/^models\//, "");
5583
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(cleanModel)}:generateContent?key=${encodeURIComponent(apiKey)}`;
5584
+ const res = await fetch(url, {
5585
+ method: "POST",
5586
+ headers: { "content-type": "application/json" },
5587
+ body: JSON.stringify({
5588
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
5589
+ ...system ? { systemInstruction: { parts: [{ text: system }] } } : {}
5590
+ })
5591
+ });
5592
+ const data = await res.json();
5593
+ if (!res.ok) throw new Error(data.error?.message ?? `Gemini request failed (${res.status})`);
5594
+ const text = data.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
5595
+ if (!text) throw new Error("Gemini returned an empty response");
5596
+ return text;
5597
+ }
5598
+ async function listGeminiModels(apiKey) {
5599
+ const url = `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}&pageSize=1000`;
5600
+ const res = await fetch(url);
5601
+ const data = await res.json();
5602
+ if (!res.ok)
5603
+ throw new Error(data.error?.message ?? `Could not fetch the model list (${res.status})`);
5604
+ return (data.models ?? []).filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((m) => ({
5605
+ id: (m.name ?? "").replace(/^models\//, ""),
5606
+ label: m.displayName ?? (m.name ?? "").replace(/^models\//, "")
5607
+ })).filter(
5608
+ (m) => m.id && !/tts|image|embedding|robotics|computer-use|veo|lyria|imagen|aqa/i.test(m.id)
5609
+ );
5610
+ }
5611
+ async function listOpenAiCompatibleModels(baseUrl, apiKey) {
5612
+ if (!baseUrl) throw new Error("This provider needs a base URL");
5613
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
5614
+ headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {}
5615
+ });
5616
+ const data = await res.json();
5617
+ if (!res.ok)
5618
+ throw new Error(data.error?.message ?? `Could not fetch the model list (${res.status})`);
5619
+ return (data.data ?? []).map((m) => ({ id: m.id ?? "", label: m.id ?? "" })).filter((m) => m.id);
5620
+ }
5621
+ async function callOpenAiCompatible(baseUrl, apiKey, model, system, prompt) {
5622
+ if (!baseUrl) throw new Error("This provider needs a base URL");
5623
+ const messages = [
5624
+ ...system ? [{ role: "system", content: system }] : [],
5625
+ { role: "user", content: prompt }
5626
+ ];
5627
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
5628
+ method: "POST",
5629
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
5630
+ body: JSON.stringify({ model, messages })
5631
+ });
5632
+ const data = await res.json();
5633
+ if (!res.ok) throw new Error(data.error?.message ?? `Request failed (${res.status})`);
5634
+ const text = data.choices?.[0]?.message?.content ?? "";
5635
+ if (!text) throw new Error("The provider returned an empty response");
5636
+ return text;
5637
+ }
4788
5638
  var MIME = {
4789
5639
  ".html": "text/html; charset=utf-8",
4790
5640
  ".js": "text/javascript",
@@ -4803,15 +5653,16 @@ async function createServer(root2) {
4803
5653
  let store = new ContentStore(adapter, schema, contentDir);
4804
5654
  const app = new Hono();
4805
5655
  app.onError((err, c) => {
4806
- const msg = err.message;
4807
- if (msg.startsWith("Bilinmeyen")) return c.json({ error: msg }, 404);
4808
- if (msg.startsWith("G\xFCvensiz") || msg.startsWith("Yol")) return c.json({ error: msg }, 400);
4809
- return c.json({ error: "sunucu hatas\u0131" }, 500);
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);
4810
5661
  });
4811
5662
  app.get(
4812
5663
  "/api/_project",
4813
5664
  (c) => c.json({
4814
- name: basename(root2) || "proje",
5665
+ name: basename2(root2) || "project",
4815
5666
  path: root2,
4816
5667
  contentDir,
4817
5668
  collections: schema.collections.length,
@@ -4822,9 +5673,9 @@ async function createServer(root2) {
4822
5673
  app.post("/api/_init", async (c) => {
4823
5674
  const { template: id } = await c.req.json();
4824
5675
  const t = id ? getTemplate(id) : void 0;
4825
- if (!t) return c.json({ error: "Bilinmeyen template" }, 404);
5676
+ if (!t) return c.json({ error: "Unknown template" }, 404);
4826
5677
  if (schema.collections.length > 0 || schema.singletons.length > 0) {
4827
- return c.json({ error: "Bu klas\xF6rde zaten bir \u015Fema var." }, 400);
5678
+ return c.json({ error: "This folder already has a schema." }, 400);
4828
5679
  }
4829
5680
  schema = await applyTemplate(adapter, contentDir, t);
4830
5681
  store = new ContentStore(adapter, schema, contentDir);
@@ -4832,7 +5683,7 @@ async function createServer(root2) {
4832
5683
  });
4833
5684
  app.post("/api/_import", async (c) => {
4834
5685
  if (schema.collections.length > 0 || schema.singletons.length > 0) {
4835
- return c.json({ error: "Bu klas\xF6rde zaten bir \u015Fema var." }, 400);
5686
+ return c.json({ error: "This folder already has a schema." }, 400);
4836
5687
  }
4837
5688
  const { raw } = await c.req.json();
4838
5689
  let next;
@@ -4847,7 +5698,7 @@ async function createServer(root2) {
4847
5698
  entries = inferred.entries;
4848
5699
  singletonData = inferred.singletons;
4849
5700
  } catch (e) {
4850
- return c.json({ error: `\u0130\xE7e aktar\u0131lamad\u0131: ${e.message}` }, 400);
5701
+ return c.json({ error: `Import failed: ${e.message}` }, 400);
4851
5702
  }
4852
5703
  }
4853
5704
  await saveSchema(adapter, next, contentDir);
@@ -4855,7 +5706,7 @@ async function createServer(root2) {
4855
5706
  store = new ContentStore(adapter, schema, contentDir);
4856
5707
  for (const [collection, rows] of Object.entries(entries)) {
4857
5708
  for (const row of rows) {
4858
- const slug = slugify(String(row.slug ?? row.title ?? "icerik")) || "icerik";
5709
+ const slug = slugify(String(row.slug ?? row.title ?? "content")) || "content";
4859
5710
  await store.writeEntry(collection, slug, row);
4860
5711
  }
4861
5712
  }
@@ -4897,25 +5748,58 @@ async function createServer(root2) {
4897
5748
  });
4898
5749
  app.post("/api/_media", async (c) => {
4899
5750
  const body = await c.req.json();
4900
- if (!body.dataBase64) return c.json({ error: "veri yok" }, 400);
5751
+ if (!body.dataBase64) return c.json({ error: "no data" }, 400);
4901
5752
  const base = slugify((body.filename ?? "gorsel").replace(/\.[^.]+$/, "")) || "gorsel";
4902
5753
  const name = `${base}-${Date.now().toString(36)}.webp`;
4903
5754
  const rel = `${contentDir}/media/${name}`;
4904
- const abs = join3(root2, rel);
5755
+ const abs = join4(root2, rel);
4905
5756
  await mkdir2(dirname2(abs), { recursive: true });
4906
5757
  await writeFile3(abs, Buffer.from(body.dataBase64, "base64"));
4907
5758
  return c.json({ path: rel });
4908
5759
  });
5760
+ app.post("/api/_ai/models", async (c) => {
5761
+ const body = await c.req.json();
5762
+ const { provider, apiKey } = body;
5763
+ if (!provider) return c.json({ error: "provider is required" }, 400);
5764
+ try {
5765
+ const models = provider === "gemini" ? await listGeminiModels(apiKey ?? "") : await listOpenAiCompatibleModels(
5766
+ body.baseUrl || PROVIDER_BASE_URLS[provider] || "",
5767
+ apiKey ?? ""
5768
+ );
5769
+ return c.json({ models });
5770
+ } catch (e) {
5771
+ return c.json({ error: e.message }, 502);
5772
+ }
5773
+ });
5774
+ app.post("/api/_ai/generate", async (c) => {
5775
+ const body = await c.req.json();
5776
+ const { provider, apiKey, model, system, prompt } = body;
5777
+ if (!provider || !apiKey || !model || !prompt) {
5778
+ return c.json({ error: "provider, model, apiKey and prompt are required" }, 400);
5779
+ }
5780
+ try {
5781
+ const text = provider === "gemini" ? await callGemini(apiKey, model, system, prompt) : await callOpenAiCompatible(
5782
+ body.baseUrl || PROVIDER_BASE_URLS[provider] || "",
5783
+ apiKey,
5784
+ model,
5785
+ system,
5786
+ prompt
5787
+ );
5788
+ return c.json({ text });
5789
+ } catch (e) {
5790
+ return c.json({ error: e.message }, 502);
5791
+ }
5792
+ });
4909
5793
  app.get("/media/:file", async (c) => {
4910
5794
  const file = c.req.param("file");
4911
5795
  if (file.includes("/") || file.includes("\\") || file.includes("..")) {
4912
5796
  return c.text("forbidden", 400);
4913
5797
  }
4914
5798
  try {
4915
- const bytes = await readFile2(join3(root2, contentDir, "media", file));
5799
+ const bytes = await readFile2(join4(root2, contentDir, "media", file));
4916
5800
  return new Response(bytes, { headers: { "content-type": "image/webp" } });
4917
5801
  } catch {
4918
- return c.text("yok", 404);
5802
+ return c.text("not found", 404);
4919
5803
  }
4920
5804
  });
4921
5805
  app.get("/api/:collection", async (c) => {
@@ -4924,7 +5808,7 @@ async function createServer(root2) {
4924
5808
  });
4925
5809
  app.get("/api/:collection/:slug", async (c) => {
4926
5810
  const data = await store.readEntry(c.req.param("collection"), c.req.param("slug"));
4927
- return data ? c.json(data) : c.json({ error: "bulunamad\u0131" }, 404);
5811
+ return data ? c.json(data) : c.json({ error: "not found" }, 404);
4928
5812
  });
4929
5813
  app.put("/api/:collection/:slug", async (c) => {
4930
5814
  const slug = slugify(c.req.param("slug"));
@@ -4939,17 +5823,17 @@ async function createServer(root2) {
4939
5823
  app.get("/*", async (c) => {
4940
5824
  const urlPath = c.req.path === "/" ? "/index.html" : c.req.path;
4941
5825
  const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
4942
- const file = join3(editorDir, rel);
5826
+ const file = join4(editorDir, rel);
4943
5827
  if (!file.startsWith(editorDir)) return c.text("forbidden", 403);
4944
5828
  const type = MIME[extname(file)] ?? "application/octet-stream";
4945
5829
  try {
4946
5830
  return new Response(await readFile2(file), { headers: { "content-type": type } });
4947
5831
  } catch {
4948
5832
  try {
4949
- const html = await readFile2(join3(editorDir, "index.html"));
5833
+ const html = await readFile2(join4(editorDir, "index.html"));
4950
5834
  return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
4951
5835
  } catch {
4952
- return c.text("Edit\xF6r aray\xFCz\xFC bulunamad\u0131 (justjson build gerekli).", 404);
5836
+ return c.text("Editor UI not found (run the justjson build first).", 404);
4953
5837
  }
4954
5838
  }
4955
5839
  });
@@ -4964,27 +5848,44 @@ async function startServer(root2, port) {
4964
5848
  const app = await createServer(root2);
4965
5849
  serve({ fetch: app.fetch, port });
4966
5850
  const url = `http://localhost:${port}`;
4967
- console.log(`JustJSON \xE7al\u0131\u015F\u0131yor: ${url}`);
5851
+ console.log(`JustJSON is running at ${url}`);
4968
5852
  openBrowser(url);
4969
5853
  }
4970
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
+
4971
5862
  // src/cli.ts
4972
5863
  var program = new Command();
4973
5864
  var root = process.cwd();
4974
- program.name("justjson").description("Lokalde \xE7al\u0131\u015Fan, JSON \xFCreten mini CMS").version("0.0.0");
4975
- program.command("init").description("Bir template ile projeyi ba\u015Flat\u0131r").argument("[template]", `template ad\u0131 (${listTemplates().join(", ")})`, "blog").action(async (template) => {
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) => {
4976
5867
  await initProject(root, template);
4977
- console.log(`'${template}' template'i ile ba\u015Flat\u0131ld\u0131.`);
5868
+ console.log(`Scaffolded from the '${template}' template.`);
4978
5869
  });
4979
- program.command("types").description("\u015Eemadan types.ts \xFCretir").action(async () => {
5870
+ program.command("types").description("Generate types.ts and a typed content.ts loader from your schema").action(async () => {
4980
5871
  const out = await generateTypesFile(root);
4981
- console.log(`Yaz\u0131ld\u0131: ${out}`);
5872
+ console.log(`Wrote: ${out}`);
4982
5873
  });
4983
- program.command("export").description("\u0130\xE7eri\u011Fi ZIP olarak d\u0131\u015Fa aktar\u0131r").action(async () => {
5874
+ program.command("export").description("Export schema, content and types as a ZIP").action(async () => {
4984
5875
  const out = await exportZip(root);
4985
- console.log(`Yaz\u0131ld\u0131: ${out}`);
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;
4986
5887
  });
4987
- program.command("serve", { isDefault: true }).description("Lokal edit\xF6r sunucusunu ba\u015Flat\u0131r").option("-p, --port <port>", "port", "5180").action(async (opts) => {
5888
+ program.command("serve", { isDefault: true }).description("Start the local editor server").option("-p, --port <port>", "port", "5180").action(async (opts) => {
4988
5889
  await startServer(root, Number(opts.port));
4989
5890
  });
4990
5891
  program.parseAsync();