@kdrgny/justjson 1.4.0 → 1.6.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 +4 -1
- package/dist/cli.js +394 -25
- package/dist/editor/assets/index-DFUrgJEm.js +646 -0
- package/dist/editor/assets/index-Dy62f11G.css +1 -0
- package/dist/editor/index.html +2 -2
- package/package.json +1 -1
- package/dist/editor/assets/index-B7tRIQcy.js +0 -632
- package/dist/editor/assets/index-GwsCiKhf.css +0 -1
package/README.md
CHANGED
|
@@ -42,6 +42,9 @@ Design your schema, enter content, upload images. Everything is written to `cont
|
|
|
42
42
|
| **Type-safe output** | Generates `types.ts` **and** a zero-dependency `content.ts` loader — `loadPosts()`, `loadSettings()` — so your content is typed in your build. |
|
|
43
43
|
| **Validate in CI** | `justjson validate` checks content against the schema — broken relations, duplicate slugs, missing media, type errors. Exits non-zero on failure. |
|
|
44
44
|
| **Draft / published** | Toggle an entry's status; the generated loader returns published entries by default. |
|
|
45
|
+
| **One-click site** | No site yet? `init --astro` (or a button in the editor) generates a working Astro site around your content. |
|
|
46
|
+
| **Ship it** | Setup snippet for your framework, one-click content commit, push to GitHub — using the `git`/`gh` already on your machine. |
|
|
47
|
+
| **Astro collections** | [`@kdrgny/justjson-astro`](https://www.npmjs.com/package/@kdrgny/justjson-astro) turns your content into typed Astro content collections in one line. |
|
|
45
48
|
| **English or Turkish** | The editor ships in English; switch to Turkish from the project menu any time. |
|
|
46
49
|
| **Export any time** | One click downloads schema + content + types as a ZIP. Nothing is ever locked in. |
|
|
47
50
|
| **The endpoint is yours** | Wherever you put the JSON becomes your API — repo raw, jsDelivr, your build. |
|
|
@@ -53,7 +56,7 @@ Field types: `text` · `richtext` · `number` · `boolean` · `date` · `select`
|
|
|
53
56
|
| Command | What it does |
|
|
54
57
|
|---|---|
|
|
55
58
|
| `npx @kdrgny/justjson` (or `serve`) | Starts the local editor and opens it in your browser |
|
|
56
|
-
| `npx @kdrgny/justjson init [template]` | Scaffolds a schema from a template
|
|
59
|
+
| `npx @kdrgny/justjson init [template]` | Scaffolds a schema from a template. `--astro` also generates a working site |
|
|
57
60
|
| `npx @kdrgny/justjson types` | Generates `types.ts` + a typed `content.ts` loader from your schema |
|
|
58
61
|
| `npx @kdrgny/justjson validate` | Checks content against the schema (`--json`, `--strict`) — great for CI |
|
|
59
62
|
| `npx @kdrgny/justjson export` | Exports a ZIP snapshot (schema + content + types) |
|
package/dist/cli.js
CHANGED
|
@@ -6,11 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
// src/commands/export.ts
|
|
12
|
-
import { writeFile as writeFile2 } from "fs/promises";
|
|
13
|
-
import { join } from "path";
|
|
9
|
+
import { basename as basename3 } from "path";
|
|
14
10
|
|
|
15
11
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
16
12
|
var external_exports = {};
|
|
@@ -4712,7 +4708,12 @@ function inferProject(data) {
|
|
|
4712
4708
|
return { schema: { version: 1, collections, singletons }, entries, singletons: singletonData };
|
|
4713
4709
|
}
|
|
4714
4710
|
|
|
4711
|
+
// src/cli.ts
|
|
4712
|
+
import { Command } from "commander";
|
|
4713
|
+
|
|
4715
4714
|
// src/commands/export.ts
|
|
4715
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
4716
|
+
import { join } from "path";
|
|
4716
4717
|
import { zipSync } from "fflate";
|
|
4717
4718
|
|
|
4718
4719
|
// src/fs-adapter.ts
|
|
@@ -4879,7 +4880,10 @@ var blog_default = {
|
|
|
4879
4880
|
slug: "first-post",
|
|
4880
4881
|
body: "Hello."
|
|
4881
4882
|
}
|
|
4882
|
-
]
|
|
4883
|
+
],
|
|
4884
|
+
settings: {
|
|
4885
|
+
title: "My blog"
|
|
4886
|
+
}
|
|
4883
4887
|
}
|
|
4884
4888
|
};
|
|
4885
4889
|
|
|
@@ -4960,7 +4964,11 @@ var catalog_default = {
|
|
|
4960
4964
|
category: "Home",
|
|
4961
4965
|
description: "Hand-made ceramic mug."
|
|
4962
4966
|
}
|
|
4963
|
-
]
|
|
4967
|
+
],
|
|
4968
|
+
store: {
|
|
4969
|
+
title: "My store",
|
|
4970
|
+
tagline: "Small batch, made well"
|
|
4971
|
+
}
|
|
4964
4972
|
}
|
|
4965
4973
|
};
|
|
4966
4974
|
|
|
@@ -5081,7 +5089,11 @@ var cv_default = {
|
|
|
5081
5089
|
company: "Acme Inc.",
|
|
5082
5090
|
summary: "..."
|
|
5083
5091
|
}
|
|
5084
|
-
]
|
|
5092
|
+
],
|
|
5093
|
+
profile: {
|
|
5094
|
+
name: "Ada Lovelace",
|
|
5095
|
+
headline: "Software engineer"
|
|
5096
|
+
}
|
|
5085
5097
|
}
|
|
5086
5098
|
};
|
|
5087
5099
|
|
|
@@ -5150,7 +5162,11 @@ var docs_default = {
|
|
|
5150
5162
|
order: 1,
|
|
5151
5163
|
body: "Welcome aboard."
|
|
5152
5164
|
}
|
|
5153
|
-
]
|
|
5165
|
+
],
|
|
5166
|
+
settings: {
|
|
5167
|
+
title: "Docs",
|
|
5168
|
+
tagline: "Everything you need to know"
|
|
5169
|
+
}
|
|
5154
5170
|
}
|
|
5155
5171
|
};
|
|
5156
5172
|
|
|
@@ -5236,7 +5252,11 @@ var event_default = {
|
|
|
5236
5252
|
room: "Main hall",
|
|
5237
5253
|
description: "Welcome to the event."
|
|
5238
5254
|
}
|
|
5239
|
-
]
|
|
5255
|
+
],
|
|
5256
|
+
event: {
|
|
5257
|
+
title: "My event",
|
|
5258
|
+
tagline: "One day, one track"
|
|
5259
|
+
}
|
|
5240
5260
|
}
|
|
5241
5261
|
};
|
|
5242
5262
|
|
|
@@ -5333,7 +5353,11 @@ var portfolio_default = {
|
|
|
5333
5353
|
kind: "Web",
|
|
5334
5354
|
body: "Project details go here."
|
|
5335
5355
|
}
|
|
5336
|
-
]
|
|
5356
|
+
],
|
|
5357
|
+
about: {
|
|
5358
|
+
name: "Ada Lovelace",
|
|
5359
|
+
bio: "I build things for the web."
|
|
5360
|
+
}
|
|
5337
5361
|
}
|
|
5338
5362
|
};
|
|
5339
5363
|
|
|
@@ -5414,7 +5438,10 @@ var recipe_default = {
|
|
|
5414
5438
|
ingredients: "- 200g pasta\n- 2 cloves garlic\n- Olive oil",
|
|
5415
5439
|
steps: "Boil the pasta. Fry the garlic in olive oil. Toss together."
|
|
5416
5440
|
}
|
|
5417
|
-
]
|
|
5441
|
+
],
|
|
5442
|
+
kitchen: {
|
|
5443
|
+
title: "My recipe box"
|
|
5444
|
+
}
|
|
5418
5445
|
}
|
|
5419
5446
|
};
|
|
5420
5447
|
|
|
@@ -5454,10 +5481,15 @@ async function applyTemplate(adapter, contentDir, template) {
|
|
|
5454
5481
|
const schema = parseSchema(template.schema);
|
|
5455
5482
|
await saveSchema(adapter, schema, contentDir);
|
|
5456
5483
|
const store = new ContentStore(adapter, schema, contentDir);
|
|
5457
|
-
|
|
5458
|
-
|
|
5484
|
+
const singletonNames = new Set(schema.singletons.map((s) => s.name));
|
|
5485
|
+
for (const [name, sample] of Object.entries(template.samples)) {
|
|
5486
|
+
if (!Array.isArray(sample)) {
|
|
5487
|
+
if (singletonNames.has(name)) await store.writeSingleton(name, sample);
|
|
5488
|
+
continue;
|
|
5489
|
+
}
|
|
5490
|
+
for (const row of sample) {
|
|
5459
5491
|
const slug = slugify(typeof row.slug === "string" ? row.slug : String(row.title ?? "content"));
|
|
5460
|
-
await store.writeEntry(
|
|
5492
|
+
await store.writeEntry(name, slug, row);
|
|
5461
5493
|
}
|
|
5462
5494
|
}
|
|
5463
5495
|
return schema;
|
|
@@ -5566,13 +5598,304 @@ function formatJson(issues) {
|
|
|
5566
5598
|
return JSON.stringify({ ok: errors === 0, errors, warnings, issues }, null, 2);
|
|
5567
5599
|
}
|
|
5568
5600
|
|
|
5601
|
+
// src/scaffold.ts
|
|
5602
|
+
import { access, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
5603
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
5604
|
+
var ASTRO_VERSION = "^5.18.0";
|
|
5605
|
+
var LOADER_VERSION = "^1.6.0";
|
|
5606
|
+
var STYLE = ` <style is:global>
|
|
5607
|
+
:root { color-scheme: light dark; }
|
|
5608
|
+
body {
|
|
5609
|
+
margin: 0 auto;
|
|
5610
|
+
max-width: 44rem;
|
|
5611
|
+
padding: 3rem 1.25rem 6rem;
|
|
5612
|
+
font: 16px/1.65 ui-sans-serif, system-ui, sans-serif;
|
|
5613
|
+
}
|
|
5614
|
+
h1 { font-size: 1.9rem; line-height: 1.2; margin: 0 0 0.35rem; }
|
|
5615
|
+
h2 { font-size: 1.05rem; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.6; margin: 2.75rem 0 0.75rem; }
|
|
5616
|
+
ul { list-style: none; margin: 0; padding: 0; }
|
|
5617
|
+
li + li { margin-top: 0.6rem; }
|
|
5618
|
+
a { color: inherit; }
|
|
5619
|
+
time { opacity: 0.6; font-size: 0.9rem; }
|
|
5620
|
+
img { max-width: 100%; height: auto; border-radius: 8px; }
|
|
5621
|
+
.body { white-space: pre-wrap; }
|
|
5622
|
+
</style>`;
|
|
5623
|
+
function titleFieldOf(fields) {
|
|
5624
|
+
const preferred = fields.find(
|
|
5625
|
+
(f) => ["title", "name", "label", "role", "version"].includes(f.key)
|
|
5626
|
+
);
|
|
5627
|
+
return (preferred ?? fields.find((f) => f.type === "text"))?.key ?? "title";
|
|
5628
|
+
}
|
|
5629
|
+
function safePackageName(name) {
|
|
5630
|
+
return slugify(name) || "my-site";
|
|
5631
|
+
}
|
|
5632
|
+
function detailPage(collection) {
|
|
5633
|
+
const titleKey = titleFieldOf(collection.fields);
|
|
5634
|
+
const image = collection.fields.find((f) => f.type === "image");
|
|
5635
|
+
const rich = collection.fields.find((f) => f.type === "richtext");
|
|
5636
|
+
const date = collection.fields.find((f) => f.type === "date");
|
|
5637
|
+
const parts = [
|
|
5638
|
+
`<h1>{entry.data.${titleKey} ?? entry.id}</h1>`,
|
|
5639
|
+
date && `{entry.data.${date.key} && <time>{entry.data.${date.key}}</time>}`,
|
|
5640
|
+
image && `{entry.data.${image.key} && <img src={\`/media/\${entry.data.${image.key}.split('/').pop()}\`} alt="" />}`,
|
|
5641
|
+
rich && `{entry.data.${rich.key} && <div class="body">{entry.data.${rich.key}}</div>}`
|
|
5642
|
+
].filter(Boolean);
|
|
5643
|
+
return `---
|
|
5644
|
+
import { getCollection } from 'astro:content'
|
|
5645
|
+
|
|
5646
|
+
export async function getStaticPaths() {
|
|
5647
|
+
const entries = await getCollection('${collection.name}')
|
|
5648
|
+
return entries.map((entry) => ({ params: { slug: entry.id }, props: { entry } }))
|
|
5649
|
+
}
|
|
5650
|
+
|
|
5651
|
+
const { entry } = Astro.props
|
|
5652
|
+
---
|
|
5653
|
+
<html lang="en">
|
|
5654
|
+
<head>
|
|
5655
|
+
<meta charset="utf-8" />
|
|
5656
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
5657
|
+
<title>{entry.data.${titleKey} ?? entry.id}</title>
|
|
5658
|
+
${STYLE}
|
|
5659
|
+
</head>
|
|
5660
|
+
<body>
|
|
5661
|
+
<p><a href="/">← Back</a></p>
|
|
5662
|
+
${parts.map((p) => ` ${p}`).join("\n")}
|
|
5663
|
+
</body>
|
|
5664
|
+
</html>
|
|
5665
|
+
`;
|
|
5666
|
+
}
|
|
5667
|
+
function indexPage(schema, projectName) {
|
|
5668
|
+
const singleton = schema.singletons[0];
|
|
5669
|
+
const imports = ["getCollection", singleton && "getEntry"].filter(Boolean).join(", ");
|
|
5670
|
+
const loads = schema.collections.map(
|
|
5671
|
+
(c) => `const ${varFor(c.name)} = await getCollection('${c.name}')`
|
|
5672
|
+
);
|
|
5673
|
+
if (singleton) {
|
|
5674
|
+
loads.unshift(`const site = await getEntry('${singleton.name}', '${singleton.name}')`);
|
|
5675
|
+
}
|
|
5676
|
+
const heading = singleton ? `{site?.data.${titleFieldOf(singleton.fields)} ?? '${projectName}'}` : `${projectName}`;
|
|
5677
|
+
const sections = schema.collections.map((c) => {
|
|
5678
|
+
const titleKey = titleFieldOf(c.fields);
|
|
5679
|
+
return ` <h2>${c.label ?? c.name}</h2>
|
|
5680
|
+
<ul>
|
|
5681
|
+
{${varFor(c.name)}.map((entry) => (
|
|
5682
|
+
<li>
|
|
5683
|
+
<a href={\`/${c.name}/\${entry.id}\`}>{entry.data.${titleKey} ?? entry.id}</a>
|
|
5684
|
+
</li>
|
|
5685
|
+
))}
|
|
5686
|
+
</ul>`;
|
|
5687
|
+
});
|
|
5688
|
+
const empty = ` <p>No content yet \u2014 run <code>npx @kdrgny/justjson</code> and add some.</p>`;
|
|
5689
|
+
return `---
|
|
5690
|
+
import { ${imports} } from 'astro:content'
|
|
5691
|
+
|
|
5692
|
+
${loads.join("\n")}
|
|
5693
|
+
---
|
|
5694
|
+
<html lang="en">
|
|
5695
|
+
<head>
|
|
5696
|
+
<meta charset="utf-8" />
|
|
5697
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
5698
|
+
<title>${singleton ? `{site?.data.${titleFieldOf(singleton.fields)} ?? '${projectName}'}` : projectName}</title>
|
|
5699
|
+
${STYLE}
|
|
5700
|
+
</head>
|
|
5701
|
+
<body>
|
|
5702
|
+
<h1>${heading}</h1>
|
|
5703
|
+
${sections.length ? sections.join("\n") : empty}
|
|
5704
|
+
</body>
|
|
5705
|
+
</html>
|
|
5706
|
+
`;
|
|
5707
|
+
}
|
|
5708
|
+
function varFor(name) {
|
|
5709
|
+
const camel = name.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase());
|
|
5710
|
+
return /^[a-zA-Z_$]/.test(camel) ? camel : `c${camel}`;
|
|
5711
|
+
}
|
|
5712
|
+
function astroSiteFiles(schema, projectName) {
|
|
5713
|
+
const files = {
|
|
5714
|
+
"package.json": `${JSON.stringify(
|
|
5715
|
+
{
|
|
5716
|
+
name: safePackageName(projectName),
|
|
5717
|
+
type: "module",
|
|
5718
|
+
version: "0.0.1",
|
|
5719
|
+
private: true,
|
|
5720
|
+
scripts: { dev: "astro dev", build: "astro build", preview: "astro preview" },
|
|
5721
|
+
dependencies: {
|
|
5722
|
+
"@kdrgny/justjson-astro": LOADER_VERSION,
|
|
5723
|
+
astro: ASTRO_VERSION
|
|
5724
|
+
}
|
|
5725
|
+
},
|
|
5726
|
+
null,
|
|
5727
|
+
2
|
|
5728
|
+
)}
|
|
5729
|
+
`,
|
|
5730
|
+
"astro.config.mjs": `import { defineConfig } from 'astro/config'
|
|
5731
|
+
|
|
5732
|
+
export default defineConfig({})
|
|
5733
|
+
`,
|
|
5734
|
+
"tsconfig.json": `${JSON.stringify({ extends: "astro/tsconfigs/strict" }, null, 2)}
|
|
5735
|
+
`,
|
|
5736
|
+
".gitignore": `node_modules
|
|
5737
|
+
dist
|
|
5738
|
+
.astro
|
|
5739
|
+
`,
|
|
5740
|
+
"src/content.config.ts": `import { justjsonCollections } from '@kdrgny/justjson-astro'
|
|
5741
|
+
|
|
5742
|
+
// Every collection and singleton in content/_schema.json, typed from your fields.
|
|
5743
|
+
export const collections = await justjsonCollections()
|
|
5744
|
+
`,
|
|
5745
|
+
"src/pages/index.astro": indexPage(schema, projectName)
|
|
5746
|
+
};
|
|
5747
|
+
for (const collection of schema.collections) {
|
|
5748
|
+
files[`src/pages/${collection.name}/[slug].astro`] = detailPage(collection);
|
|
5749
|
+
}
|
|
5750
|
+
return files;
|
|
5751
|
+
}
|
|
5752
|
+
async function exists(path) {
|
|
5753
|
+
try {
|
|
5754
|
+
await access(path);
|
|
5755
|
+
return true;
|
|
5756
|
+
} catch {
|
|
5757
|
+
return false;
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
async function writeAstroSite(root2, schema, projectName) {
|
|
5761
|
+
const files = astroSiteFiles(schema, projectName);
|
|
5762
|
+
const result = { written: [], skipped: [] };
|
|
5763
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
5764
|
+
const target = join4(root2, relative);
|
|
5765
|
+
if (await exists(target)) {
|
|
5766
|
+
result.skipped.push(relative);
|
|
5767
|
+
continue;
|
|
5768
|
+
}
|
|
5769
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
5770
|
+
await writeFile3(target, contents, "utf8");
|
|
5771
|
+
result.written.push(relative);
|
|
5772
|
+
}
|
|
5773
|
+
return result;
|
|
5774
|
+
}
|
|
5775
|
+
|
|
5569
5776
|
// src/server.ts
|
|
5570
5777
|
import { exec } from "child_process";
|
|
5571
|
-
import { mkdir as
|
|
5572
|
-
import { basename as basename2, dirname as
|
|
5778
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
5779
|
+
import { basename as basename2, dirname as dirname3, extname, join as join6, normalize } from "path";
|
|
5573
5780
|
import { fileURLToPath } from "url";
|
|
5574
5781
|
import { serve } from "@hono/node-server";
|
|
5575
5782
|
import { Hono } from "hono";
|
|
5783
|
+
|
|
5784
|
+
// src/detect.ts
|
|
5785
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
5786
|
+
import { join as join5 } from "path";
|
|
5787
|
+
var SIGNATURES = [
|
|
5788
|
+
["astro", "astro"],
|
|
5789
|
+
["next", "next"],
|
|
5790
|
+
["nuxt", "nuxt"],
|
|
5791
|
+
["sveltekit", "@sveltejs/kit"],
|
|
5792
|
+
["vite", "vite"]
|
|
5793
|
+
];
|
|
5794
|
+
async function detectFramework(root2) {
|
|
5795
|
+
let pkg;
|
|
5796
|
+
try {
|
|
5797
|
+
pkg = JSON.parse(await readFile2(join5(root2, "package.json"), "utf8"));
|
|
5798
|
+
} catch {
|
|
5799
|
+
return "unknown";
|
|
5800
|
+
}
|
|
5801
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5802
|
+
for (const [framework, dependency] of SIGNATURES) {
|
|
5803
|
+
if (deps[dependency]) return framework;
|
|
5804
|
+
}
|
|
5805
|
+
return "node";
|
|
5806
|
+
}
|
|
5807
|
+
|
|
5808
|
+
// src/git.ts
|
|
5809
|
+
import { execFile } from "child_process";
|
|
5810
|
+
import { promisify } from "util";
|
|
5811
|
+
var run = promisify(execFile);
|
|
5812
|
+
async function git(root2, args) {
|
|
5813
|
+
const { stdout } = await run("git", args, { cwd: root2 });
|
|
5814
|
+
return stdout.trim();
|
|
5815
|
+
}
|
|
5816
|
+
async function tryGit(root2, args) {
|
|
5817
|
+
try {
|
|
5818
|
+
return await git(root2, args);
|
|
5819
|
+
} catch {
|
|
5820
|
+
return null;
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5823
|
+
async function hasGh() {
|
|
5824
|
+
try {
|
|
5825
|
+
await run("gh", ["--version"]);
|
|
5826
|
+
return true;
|
|
5827
|
+
} catch {
|
|
5828
|
+
return false;
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5831
|
+
async function gitStatus(root2, contentDir) {
|
|
5832
|
+
const inside = await tryGit(root2, ["rev-parse", "--is-inside-work-tree"]);
|
|
5833
|
+
if (inside !== "true") {
|
|
5834
|
+
return {
|
|
5835
|
+
isRepo: false,
|
|
5836
|
+
branch: null,
|
|
5837
|
+
hasRemote: false,
|
|
5838
|
+
remoteUrl: null,
|
|
5839
|
+
pendingFiles: 0,
|
|
5840
|
+
hasGh: await hasGh()
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5843
|
+
const branch = await tryGit(root2, ["branch", "--show-current"]);
|
|
5844
|
+
const remoteUrl = await tryGit(root2, ["remote", "get-url", "origin"]);
|
|
5845
|
+
const porcelain = await tryGit(root2, ["status", "--porcelain", "-uall", "--", contentDir]) ?? "";
|
|
5846
|
+
return {
|
|
5847
|
+
isRepo: true,
|
|
5848
|
+
branch: branch || null,
|
|
5849
|
+
hasRemote: remoteUrl !== null,
|
|
5850
|
+
remoteUrl,
|
|
5851
|
+
pendingFiles: porcelain ? porcelain.split("\n").filter(Boolean).length : 0,
|
|
5852
|
+
hasGh: await hasGh()
|
|
5853
|
+
};
|
|
5854
|
+
}
|
|
5855
|
+
async function commitContent(root2, contentDir, message) {
|
|
5856
|
+
const inside = await tryGit(root2, ["rev-parse", "--is-inside-work-tree"]);
|
|
5857
|
+
if (inside !== "true") throw new Error("Not a git repository \u2014 run `git init` first.");
|
|
5858
|
+
await git(root2, ["add", "--", contentDir]);
|
|
5859
|
+
const staged = await tryGit(root2, ["diff", "--cached", "--name-only", "--", contentDir]);
|
|
5860
|
+
if (!staged) return { committed: false, count: 0 };
|
|
5861
|
+
await git(root2, ["commit", "-m", message, "--", contentDir]);
|
|
5862
|
+
return { committed: true, count: staged.split("\n").filter(Boolean).length };
|
|
5863
|
+
}
|
|
5864
|
+
async function pushContent(root2) {
|
|
5865
|
+
const branch = await tryGit(root2, ["branch", "--show-current"]);
|
|
5866
|
+
if (!branch || branch === "HEAD") throw new Error("No branch to push \u2014 commit something first.");
|
|
5867
|
+
const hasUpstream = await tryGit(root2, ["rev-parse", "--abbrev-ref", `${branch}@{upstream}`]);
|
|
5868
|
+
const args = hasUpstream ? ["push"] : ["push", "-u", "origin", branch];
|
|
5869
|
+
await git(root2, args);
|
|
5870
|
+
return { branch };
|
|
5871
|
+
}
|
|
5872
|
+
async function createGitHubRepo(root2, options) {
|
|
5873
|
+
if (!await hasGh()) {
|
|
5874
|
+
throw new Error("GitHub CLI (gh) not found \u2014 install it, or add a remote yourself.");
|
|
5875
|
+
}
|
|
5876
|
+
const inside = await tryGit(root2, ["rev-parse", "--is-inside-work-tree"]);
|
|
5877
|
+
if (inside !== "true") {
|
|
5878
|
+
await git(root2, ["init", "-b", "main"]);
|
|
5879
|
+
}
|
|
5880
|
+
await run(
|
|
5881
|
+
"gh",
|
|
5882
|
+
[
|
|
5883
|
+
"repo",
|
|
5884
|
+
"create",
|
|
5885
|
+
options.name,
|
|
5886
|
+
options.private ? "--private" : "--public",
|
|
5887
|
+
"--source",
|
|
5888
|
+
".",
|
|
5889
|
+
"--push"
|
|
5890
|
+
],
|
|
5891
|
+
{
|
|
5892
|
+
cwd: root2
|
|
5893
|
+
}
|
|
5894
|
+
);
|
|
5895
|
+
return { name: options.name };
|
|
5896
|
+
}
|
|
5897
|
+
|
|
5898
|
+
// src/server.ts
|
|
5576
5899
|
var editorDir = fileURLToPath(new URL("./editor", import.meta.url));
|
|
5577
5900
|
var PROVIDER_BASE_URLS = {
|
|
5578
5901
|
groq: "https://api.groq.com/openai/v1",
|
|
@@ -5669,6 +5992,45 @@ async function createServer(root2) {
|
|
|
5669
5992
|
singletons: schema.singletons.length
|
|
5670
5993
|
})
|
|
5671
5994
|
);
|
|
5995
|
+
app.post("/api/_ship/scaffold", async (c) => {
|
|
5996
|
+
if (schema.collections.length === 0 && schema.singletons.length === 0) {
|
|
5997
|
+
return c.json({ error: "Pick a template or build a schema first." }, 400);
|
|
5998
|
+
}
|
|
5999
|
+
return c.json(await writeAstroSite(root2, schema, basename2(root2) || "my-site"));
|
|
6000
|
+
});
|
|
6001
|
+
app.get(
|
|
6002
|
+
"/api/_ship",
|
|
6003
|
+
async (c) => c.json({
|
|
6004
|
+
framework: await detectFramework(root2),
|
|
6005
|
+
git: await gitStatus(root2, contentDir)
|
|
6006
|
+
})
|
|
6007
|
+
);
|
|
6008
|
+
app.post("/api/_ship/commit", async (c) => {
|
|
6009
|
+
const { message } = await c.req.json();
|
|
6010
|
+
try {
|
|
6011
|
+
return c.json(await commitContent(root2, contentDir, message?.trim() || "content: update"));
|
|
6012
|
+
} catch (e) {
|
|
6013
|
+
return c.json({ error: e.message }, 400);
|
|
6014
|
+
}
|
|
6015
|
+
});
|
|
6016
|
+
app.post("/api/_ship/push", async (c) => {
|
|
6017
|
+
try {
|
|
6018
|
+
return c.json(await pushContent(root2));
|
|
6019
|
+
} catch (e) {
|
|
6020
|
+
return c.json({ error: e.message }, 400);
|
|
6021
|
+
}
|
|
6022
|
+
});
|
|
6023
|
+
app.post("/api/_ship/repo", async (c) => {
|
|
6024
|
+
const { name, private: isPrivate } = await c.req.json();
|
|
6025
|
+
if (!name?.trim()) return c.json({ error: "A repository name is required." }, 400);
|
|
6026
|
+
try {
|
|
6027
|
+
return c.json(
|
|
6028
|
+
await createGitHubRepo(root2, { name: name.trim(), private: isPrivate !== false })
|
|
6029
|
+
);
|
|
6030
|
+
} catch (e) {
|
|
6031
|
+
return c.json({ error: e.message }, 400);
|
|
6032
|
+
}
|
|
6033
|
+
});
|
|
5672
6034
|
app.get("/api/_templates", (c) => c.json({ items: templateList() }));
|
|
5673
6035
|
app.post("/api/_init", async (c) => {
|
|
5674
6036
|
const { template: id } = await c.req.json();
|
|
@@ -5752,9 +6114,9 @@ async function createServer(root2) {
|
|
|
5752
6114
|
const base = slugify((body.filename ?? "gorsel").replace(/\.[^.]+$/, "")) || "gorsel";
|
|
5753
6115
|
const name = `${base}-${Date.now().toString(36)}.webp`;
|
|
5754
6116
|
const rel = `${contentDir}/media/${name}`;
|
|
5755
|
-
const abs =
|
|
5756
|
-
await
|
|
5757
|
-
await
|
|
6117
|
+
const abs = join6(root2, rel);
|
|
6118
|
+
await mkdir3(dirname3(abs), { recursive: true });
|
|
6119
|
+
await writeFile4(abs, Buffer.from(body.dataBase64, "base64"));
|
|
5758
6120
|
return c.json({ path: rel });
|
|
5759
6121
|
});
|
|
5760
6122
|
app.post("/api/_ai/models", async (c) => {
|
|
@@ -5796,7 +6158,7 @@ async function createServer(root2) {
|
|
|
5796
6158
|
return c.text("forbidden", 400);
|
|
5797
6159
|
}
|
|
5798
6160
|
try {
|
|
5799
|
-
const bytes = await
|
|
6161
|
+
const bytes = await readFile3(join6(root2, contentDir, "media", file));
|
|
5800
6162
|
return new Response(bytes, { headers: { "content-type": "image/webp" } });
|
|
5801
6163
|
} catch {
|
|
5802
6164
|
return c.text("not found", 404);
|
|
@@ -5823,14 +6185,14 @@ async function createServer(root2) {
|
|
|
5823
6185
|
app.get("/*", async (c) => {
|
|
5824
6186
|
const urlPath = c.req.path === "/" ? "/index.html" : c.req.path;
|
|
5825
6187
|
const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
|
|
5826
|
-
const file =
|
|
6188
|
+
const file = join6(editorDir, rel);
|
|
5827
6189
|
if (!file.startsWith(editorDir)) return c.text("forbidden", 403);
|
|
5828
6190
|
const type = MIME[extname(file)] ?? "application/octet-stream";
|
|
5829
6191
|
try {
|
|
5830
|
-
return new Response(await
|
|
6192
|
+
return new Response(await readFile3(file), { headers: { "content-type": type } });
|
|
5831
6193
|
} catch {
|
|
5832
6194
|
try {
|
|
5833
|
-
const html = await
|
|
6195
|
+
const html = await readFile3(join6(editorDir, "index.html"));
|
|
5834
6196
|
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
5835
6197
|
} catch {
|
|
5836
6198
|
return c.text("Editor UI not found (run the justjson build first).", 404);
|
|
@@ -5863,9 +6225,16 @@ function readVersion() {
|
|
|
5863
6225
|
var program = new Command();
|
|
5864
6226
|
var root = process.cwd();
|
|
5865
6227
|
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) => {
|
|
6228
|
+
program.command("init").description("Scaffold a project from a template").argument("[template]", `template name (${listTemplates().join(", ")})`, "blog").option("--astro", "also generate a working Astro site around the content").action(async (template, opts) => {
|
|
5867
6229
|
await initProject(root, template);
|
|
5868
6230
|
console.log(`Scaffolded from the '${template}' template.`);
|
|
6231
|
+
if (!opts.astro) return;
|
|
6232
|
+
const schema = await loadSchema(new FsAdapter(root), await resolveContentDir(root));
|
|
6233
|
+
if (!schema) return;
|
|
6234
|
+
const { written, skipped } = await writeAstroSite(root, schema, basename3(root) || "my-site");
|
|
6235
|
+
console.log(`Wrote ${written.length} site file(s).`);
|
|
6236
|
+
if (skipped.length > 0) console.log(`Kept your existing: ${skipped.join(", ")}`);
|
|
6237
|
+
console.log("\nNext:\n npm install\n npm run dev");
|
|
5869
6238
|
});
|
|
5870
6239
|
program.command("types").description("Generate types.ts and a typed content.ts loader from your schema").action(async () => {
|
|
5871
6240
|
const out = await generateTypesFile(root);
|