@kdrgny/justjson 1.6.0 → 1.8.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 +2 -1
- package/dist/cli.js +267 -49
- package/dist/editor/assets/index-C538Afvx.js +720 -0
- package/dist/editor/assets/index-EIuWwPh_.css +1 -0
- package/dist/editor/index.html +2 -2
- package/package.json +1 -1
- package/dist/editor/assets/index-DFUrgJEm.js +0 -646
- package/dist/editor/assets/index-Dy62f11G.css +0 -1
package/README.md
CHANGED
|
@@ -43,7 +43,8 @@ Design your schema, enter content, upload images. Everything is written to `cont
|
|
|
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
45
|
| **One-click site** | No site yet? `init --astro` (or a button in the editor) generates a working Astro site around your content. |
|
|
46
|
-
| **
|
|
46
|
+
| **Design & Preview** | Theme your generated site (palette, accent, font, corners) with a live sample, and run it inside the editor to watch changes land. |
|
|
47
|
+
| **Ship it** | Setup snippet for your framework, one-click content commit, push to GitHub, then deploy links for Vercel and Netlify — using the `git`/`gh` already on your machine. No tokens stored. |
|
|
47
48
|
| **Astro collections** | [`@kdrgny/justjson-astro`](https://www.npmjs.com/package/@kdrgny/justjson-astro) turns your content into typed Astro content collections in one line. |
|
|
48
49
|
| **English or Turkish** | The editor ships in English; switch to Turkish from the project menu any time. |
|
|
49
50
|
| **Export any time** | One click downloads schema + content + types as a ZIP. Nothing is ever locked in. |
|
package/dist/cli.js
CHANGED
|
@@ -4707,6 +4707,86 @@ function inferProject(data) {
|
|
|
4707
4707
|
}
|
|
4708
4708
|
return { schema: { version: 1, collections, singletons }, entries, singletons: singletonData };
|
|
4709
4709
|
}
|
|
4710
|
+
var PALETTES = [
|
|
4711
|
+
{
|
|
4712
|
+
id: "paper",
|
|
4713
|
+
label: "Paper",
|
|
4714
|
+
bg: "#faf9f5",
|
|
4715
|
+
text: "#1c1a17",
|
|
4716
|
+
muted: "#6b6558",
|
|
4717
|
+
border: "#e4e0d5"
|
|
4718
|
+
},
|
|
4719
|
+
{
|
|
4720
|
+
id: "plain",
|
|
4721
|
+
label: "Plain",
|
|
4722
|
+
bg: "#ffffff",
|
|
4723
|
+
text: "#18181b",
|
|
4724
|
+
muted: "#71717a",
|
|
4725
|
+
border: "#e4e4e7"
|
|
4726
|
+
},
|
|
4727
|
+
{ id: "ink", label: "Ink", bg: "#141414", text: "#f4f4f5", muted: "#a1a1aa", border: "#2a2a2a" },
|
|
4728
|
+
{
|
|
4729
|
+
id: "sand",
|
|
4730
|
+
label: "Sand",
|
|
4731
|
+
bg: "#f5f1e8",
|
|
4732
|
+
text: "#2b2620",
|
|
4733
|
+
muted: "#7a7060",
|
|
4734
|
+
border: "#ddd5c4"
|
|
4735
|
+
},
|
|
4736
|
+
{ id: "sky", label: "Sky", bg: "#f7fafc", text: "#111b26", muted: "#5d7183", border: "#dbe6ef" }
|
|
4737
|
+
];
|
|
4738
|
+
var THEME_FONTS = [
|
|
4739
|
+
{
|
|
4740
|
+
id: "sans",
|
|
4741
|
+
label: "Sans",
|
|
4742
|
+
stack: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'
|
|
4743
|
+
},
|
|
4744
|
+
{ id: "serif", label: "Serif", stack: 'ui-serif, Georgia, "Times New Roman", serif' },
|
|
4745
|
+
{ id: "mono", label: "Mono", stack: "ui-monospace, SFMono-Regular, Menlo, monospace" },
|
|
4746
|
+
{
|
|
4747
|
+
id: "humanist",
|
|
4748
|
+
label: "Humanist",
|
|
4749
|
+
stack: '"Optima", "Gill Sans", "Trebuchet MS", ui-sans-serif, sans-serif'
|
|
4750
|
+
}
|
|
4751
|
+
];
|
|
4752
|
+
var HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
4753
|
+
var MAX_RADIUS = 24;
|
|
4754
|
+
function defaultTheme() {
|
|
4755
|
+
return { palette: "paper", accent: "#b8860b", font: "sans", radius: 8, density: "normal" };
|
|
4756
|
+
}
|
|
4757
|
+
function pick(list, id, fallback) {
|
|
4758
|
+
return typeof id === "string" && list.some((x) => x.id === id) ? id : fallback;
|
|
4759
|
+
}
|
|
4760
|
+
function parseTheme(input) {
|
|
4761
|
+
const base = defaultTheme();
|
|
4762
|
+
if (typeof input !== "object" || input === null) return base;
|
|
4763
|
+
const raw = input;
|
|
4764
|
+
const radius = typeof raw.radius === "number" ? raw.radius : base.radius;
|
|
4765
|
+
const density = raw.density;
|
|
4766
|
+
return {
|
|
4767
|
+
palette: pick(PALETTES, raw.palette, base.palette),
|
|
4768
|
+
accent: typeof raw.accent === "string" && HEX.test(raw.accent) ? raw.accent : base.accent,
|
|
4769
|
+
font: pick(THEME_FONTS, raw.font, base.font),
|
|
4770
|
+
radius: Math.min(MAX_RADIUS, Math.max(0, Math.round(radius))),
|
|
4771
|
+
density: density === "tight" || density === "normal" || density === "roomy" ? density : base.density
|
|
4772
|
+
};
|
|
4773
|
+
}
|
|
4774
|
+
function themePath(contentDir) {
|
|
4775
|
+
return `${contentDir}/_theme.json`;
|
|
4776
|
+
}
|
|
4777
|
+
async function loadTheme(adapter, contentDir = "content") {
|
|
4778
|
+
const raw = await adapter.read(themePath(contentDir));
|
|
4779
|
+
if (raw === null) return parseTheme(null);
|
|
4780
|
+
try {
|
|
4781
|
+
return parseTheme(JSON.parse(raw));
|
|
4782
|
+
} catch {
|
|
4783
|
+
return parseTheme(null);
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
async function saveTheme(adapter, theme, contentDir = "content") {
|
|
4787
|
+
await adapter.write(themePath(contentDir), `${JSON.stringify(parseTheme(theme), null, 2)}
|
|
4788
|
+
`);
|
|
4789
|
+
}
|
|
4710
4790
|
|
|
4711
4791
|
// src/cli.ts
|
|
4712
4792
|
import { Command } from "commander";
|
|
@@ -5602,24 +5682,56 @@ function formatJson(issues) {
|
|
|
5602
5682
|
import { access, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
5603
5683
|
import { dirname as dirname2, join as join4 } from "path";
|
|
5604
5684
|
var ASTRO_VERSION = "^5.18.0";
|
|
5605
|
-
var LOADER_VERSION = "^1.
|
|
5606
|
-
var
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5685
|
+
var LOADER_VERSION = "^1.8.0";
|
|
5686
|
+
var LAYOUT = `---
|
|
5687
|
+
import { parseTheme, themeCss } from '@kdrgny/justjson-astro/theme'
|
|
5688
|
+
import theme from '../../content/_theme.json'
|
|
5689
|
+
|
|
5690
|
+
// G\xF6r\xFCn\xFCm content/_theme.json'dan gelir \u2014 JustJSON'da de\u011Fi\u015Ftirince buras\u0131 da de\u011Fi\u015Fir.
|
|
5691
|
+
const css = themeCss(parseTheme(theme))
|
|
5692
|
+
const { title } = Astro.props
|
|
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>{title}</title>
|
|
5699
|
+
<style is:global set:html={css} />
|
|
5700
|
+
<style is:global>
|
|
5701
|
+
body {
|
|
5702
|
+
margin: 0 auto;
|
|
5703
|
+
max-width: 44rem;
|
|
5704
|
+
padding: var(--jj-page) 1.25rem calc(var(--jj-page) * 2);
|
|
5705
|
+
background: var(--jj-bg);
|
|
5706
|
+
color: var(--jj-text);
|
|
5707
|
+
font-family: var(--jj-font);
|
|
5708
|
+
font-size: 16px;
|
|
5709
|
+
line-height: var(--jj-lead);
|
|
5710
|
+
}
|
|
5711
|
+
h1 { font-size: 1.9rem; line-height: 1.2; margin: 0 0 0.35rem; }
|
|
5712
|
+
h2 {
|
|
5713
|
+
font-size: 0.8rem;
|
|
5714
|
+
text-transform: uppercase;
|
|
5715
|
+
letter-spacing: 0.12em;
|
|
5716
|
+
color: var(--jj-muted);
|
|
5717
|
+
margin: calc(var(--jj-page) * 0.8) 0 var(--jj-gap);
|
|
5718
|
+
}
|
|
5719
|
+
ul { list-style: none; margin: 0; padding: 0; }
|
|
5720
|
+
li + li { margin-top: var(--jj-gap); }
|
|
5721
|
+
a { color: var(--jj-accent); text-decoration: none; }
|
|
5722
|
+
a:hover { text-decoration: underline; }
|
|
5723
|
+
time { color: var(--jj-muted); font-size: 0.9rem; }
|
|
5724
|
+
img { max-width: 100%; height: auto; border-radius: var(--jj-radius); }
|
|
5725
|
+
code { background: color-mix(in oklab, var(--jj-text) 8%, transparent); padding: 0.1em 0.35em; border-radius: calc(var(--jj-radius) / 2); }
|
|
5726
|
+
.body { white-space: pre-wrap; }
|
|
5727
|
+
.back { color: var(--jj-muted); font-size: 0.9rem; }
|
|
5728
|
+
</style>
|
|
5729
|
+
</head>
|
|
5730
|
+
<body>
|
|
5731
|
+
<slot />
|
|
5732
|
+
</body>
|
|
5733
|
+
</html>
|
|
5734
|
+
`;
|
|
5623
5735
|
function titleFieldOf(fields) {
|
|
5624
5736
|
const preferred = fields.find(
|
|
5625
5737
|
(f) => ["title", "name", "label", "role", "version"].includes(f.key)
|
|
@@ -5642,6 +5754,7 @@ function detailPage(collection) {
|
|
|
5642
5754
|
].filter(Boolean);
|
|
5643
5755
|
return `---
|
|
5644
5756
|
import { getCollection } from 'astro:content'
|
|
5757
|
+
import Layout from '../../layouts/Base.astro'
|
|
5645
5758
|
|
|
5646
5759
|
export async function getStaticPaths() {
|
|
5647
5760
|
const entries = await getCollection('${collection.name}')
|
|
@@ -5650,18 +5763,10 @@ export async function getStaticPaths() {
|
|
|
5650
5763
|
|
|
5651
5764
|
const { entry } = Astro.props
|
|
5652
5765
|
---
|
|
5653
|
-
<
|
|
5654
|
-
<
|
|
5655
|
-
|
|
5656
|
-
|
|
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>
|
|
5766
|
+
<Layout title={entry.data.${titleKey} ?? entry.id}>
|
|
5767
|
+
<p class="back"><a href="/">← Back</a></p>
|
|
5768
|
+
${parts.map((p) => ` ${p}`).join("\n")}
|
|
5769
|
+
</Layout>
|
|
5665
5770
|
`;
|
|
5666
5771
|
}
|
|
5667
5772
|
function indexPage(schema, projectName) {
|
|
@@ -5686,23 +5791,17 @@ function indexPage(schema, projectName) {
|
|
|
5686
5791
|
</ul>`;
|
|
5687
5792
|
});
|
|
5688
5793
|
const empty = ` <p>No content yet \u2014 run <code>npx @kdrgny/justjson</code> and add some.</p>`;
|
|
5794
|
+
const pageTitle = singleton ? `{site?.data.${titleFieldOf(singleton.fields)} ?? '${projectName}'}` : `"${projectName}"`;
|
|
5689
5795
|
return `---
|
|
5690
5796
|
import { ${imports} } from 'astro:content'
|
|
5797
|
+
import Layout from '../layouts/Base.astro'
|
|
5691
5798
|
|
|
5692
5799
|
${loads.join("\n")}
|
|
5693
5800
|
---
|
|
5694
|
-
<
|
|
5695
|
-
<
|
|
5696
|
-
|
|
5697
|
-
|
|
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>
|
|
5801
|
+
<Layout title={${pageTitle.startsWith("{") ? pageTitle.slice(1, -1) : pageTitle}}>
|
|
5802
|
+
<h1>${heading}</h1>
|
|
5803
|
+
${(sections.length ? sections.join("\n") : empty).replace(/^ {4}/gm, " ")}
|
|
5804
|
+
</Layout>
|
|
5706
5805
|
`;
|
|
5707
5806
|
}
|
|
5708
5807
|
function varFor(name) {
|
|
@@ -5742,6 +5841,9 @@ dist
|
|
|
5742
5841
|
// Every collection and singleton in content/_schema.json, typed from your fields.
|
|
5743
5842
|
export const collections = await justjsonCollections()
|
|
5744
5843
|
`,
|
|
5844
|
+
"content/_theme.json": `${JSON.stringify(defaultTheme(), null, 2)}
|
|
5845
|
+
`,
|
|
5846
|
+
"src/layouts/Base.astro": LAYOUT,
|
|
5745
5847
|
"src/pages/index.astro": indexPage(schema, projectName)
|
|
5746
5848
|
};
|
|
5747
5849
|
for (const collection of schema.collections) {
|
|
@@ -5775,8 +5877,8 @@ async function writeAstroSite(root2, schema, projectName) {
|
|
|
5775
5877
|
|
|
5776
5878
|
// src/server.ts
|
|
5777
5879
|
import { exec } from "child_process";
|
|
5778
|
-
import { mkdir as mkdir3, readFile as
|
|
5779
|
-
import { basename as basename2, dirname as dirname3, extname, join as
|
|
5880
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
5881
|
+
import { basename as basename2, dirname as dirname3, extname, join as join7, normalize } from "path";
|
|
5780
5882
|
import { fileURLToPath } from "url";
|
|
5781
5883
|
import { serve } from "@hono/node-server";
|
|
5782
5884
|
import { Hono } from "hono";
|
|
@@ -5809,6 +5911,14 @@ async function detectFramework(root2) {
|
|
|
5809
5911
|
import { execFile } from "child_process";
|
|
5810
5912
|
import { promisify } from "util";
|
|
5811
5913
|
var run = promisify(execFile);
|
|
5914
|
+
function remoteWebUrl(remote) {
|
|
5915
|
+
if (!remote) return null;
|
|
5916
|
+
const ssh = remote.match(/^(?:ssh:\/\/)?git@([^:/]+)[:/](.+?)(?:\.git)?$/);
|
|
5917
|
+
if (ssh) return `https://${ssh[1]}/${ssh[2]}`;
|
|
5918
|
+
const https = remote.match(/^https?:\/\/(?:[^@]+@)?(.+?)(?:\.git)?$/);
|
|
5919
|
+
if (https) return `https://${https[1]}`;
|
|
5920
|
+
return null;
|
|
5921
|
+
}
|
|
5812
5922
|
async function git(root2, args) {
|
|
5813
5923
|
const { stdout } = await run("git", args, { cwd: root2 });
|
|
5814
5924
|
return stdout.trim();
|
|
@@ -5836,6 +5946,7 @@ async function gitStatus(root2, contentDir) {
|
|
|
5836
5946
|
branch: null,
|
|
5837
5947
|
hasRemote: false,
|
|
5838
5948
|
remoteUrl: null,
|
|
5949
|
+
remoteWebUrl: null,
|
|
5839
5950
|
pendingFiles: 0,
|
|
5840
5951
|
hasGh: await hasGh()
|
|
5841
5952
|
};
|
|
@@ -5848,6 +5959,7 @@ async function gitStatus(root2, contentDir) {
|
|
|
5848
5959
|
branch: branch || null,
|
|
5849
5960
|
hasRemote: remoteUrl !== null,
|
|
5850
5961
|
remoteUrl,
|
|
5962
|
+
remoteWebUrl: remoteWebUrl(remoteUrl),
|
|
5851
5963
|
pendingFiles: porcelain ? porcelain.split("\n").filter(Boolean).length : 0,
|
|
5852
5964
|
hasGh: await hasGh()
|
|
5853
5965
|
};
|
|
@@ -5895,6 +6007,90 @@ async function createGitHubRepo(root2, options) {
|
|
|
5895
6007
|
return { name: options.name };
|
|
5896
6008
|
}
|
|
5897
6009
|
|
|
6010
|
+
// src/preview.ts
|
|
6011
|
+
import { spawn } from "child_process";
|
|
6012
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
6013
|
+
import { join as join6 } from "path";
|
|
6014
|
+
function detectDevScript(pkg) {
|
|
6015
|
+
if (typeof pkg !== "object" || pkg === null) return null;
|
|
6016
|
+
const scripts = pkg.scripts;
|
|
6017
|
+
if (!scripts?.dev) return null;
|
|
6018
|
+
return { command: "npm", args: ["run", "dev"] };
|
|
6019
|
+
}
|
|
6020
|
+
var ANSI = /\u001b\[[0-9;]*m/g;
|
|
6021
|
+
var URL_RE2 = /(https?:\/\/localhost:\d+[^\s\u001b]*)/i;
|
|
6022
|
+
function cleanUrl(line) {
|
|
6023
|
+
const match = line.replace(ANSI, "").match(URL_RE2);
|
|
6024
|
+
if (!match?.[1]) return null;
|
|
6025
|
+
return match[1].replace(/\/$/, "");
|
|
6026
|
+
}
|
|
6027
|
+
var PreviewProcess = class {
|
|
6028
|
+
constructor(root2) {
|
|
6029
|
+
this.root = root2;
|
|
6030
|
+
}
|
|
6031
|
+
root;
|
|
6032
|
+
child = null;
|
|
6033
|
+
state = { status: "idle" };
|
|
6034
|
+
log = [];
|
|
6035
|
+
getState() {
|
|
6036
|
+
return this.state;
|
|
6037
|
+
}
|
|
6038
|
+
recentLog() {
|
|
6039
|
+
return this.log.slice(-40).join("\n");
|
|
6040
|
+
}
|
|
6041
|
+
async start() {
|
|
6042
|
+
if (this.state.status === "running" || this.state.status === "starting") return this.state;
|
|
6043
|
+
let pkg;
|
|
6044
|
+
try {
|
|
6045
|
+
pkg = JSON.parse(await readFile3(join6(this.root, "package.json"), "utf8"));
|
|
6046
|
+
} catch {
|
|
6047
|
+
this.state = {
|
|
6048
|
+
status: "error",
|
|
6049
|
+
message: "No package.json \u2014 this folder has no site to preview."
|
|
6050
|
+
};
|
|
6051
|
+
return this.state;
|
|
6052
|
+
}
|
|
6053
|
+
const dev = detectDevScript(pkg);
|
|
6054
|
+
if (!dev) {
|
|
6055
|
+
this.state = {
|
|
6056
|
+
status: "error",
|
|
6057
|
+
message: 'No "dev" script \u2014 generate a site first (Ship it).'
|
|
6058
|
+
};
|
|
6059
|
+
return this.state;
|
|
6060
|
+
}
|
|
6061
|
+
this.log = [];
|
|
6062
|
+
this.state = { status: "starting" };
|
|
6063
|
+
const child = spawn(dev.command, dev.args, {
|
|
6064
|
+
cwd: this.root,
|
|
6065
|
+
env: { ...process.env, FORCE_COLOR: "0", BROWSER: "none" },
|
|
6066
|
+
shell: process.platform === "win32"
|
|
6067
|
+
});
|
|
6068
|
+
this.child = child;
|
|
6069
|
+
const capture = (buf) => {
|
|
6070
|
+
const text = buf.toString();
|
|
6071
|
+
this.log.push(text);
|
|
6072
|
+
const url = cleanUrl(text);
|
|
6073
|
+
if (url && this.state.status !== "running") {
|
|
6074
|
+
this.state = { status: "running", url };
|
|
6075
|
+
}
|
|
6076
|
+
};
|
|
6077
|
+
child.stdout?.on("data", capture);
|
|
6078
|
+
child.stderr?.on("data", capture);
|
|
6079
|
+
child.on("exit", (code) => {
|
|
6080
|
+
this.child = null;
|
|
6081
|
+
if (this.state.status !== "error") {
|
|
6082
|
+
this.state = code === 0 || code === null ? { status: "idle" } : { status: "error", message: `Dev server stopped (exit ${code}).` };
|
|
6083
|
+
}
|
|
6084
|
+
});
|
|
6085
|
+
return this.state;
|
|
6086
|
+
}
|
|
6087
|
+
stop() {
|
|
6088
|
+
this.child?.kill();
|
|
6089
|
+
this.child = null;
|
|
6090
|
+
this.state = { status: "idle" };
|
|
6091
|
+
}
|
|
6092
|
+
};
|
|
6093
|
+
|
|
5898
6094
|
// src/server.ts
|
|
5899
6095
|
var editorDir = fileURLToPath(new URL("./editor", import.meta.url));
|
|
5900
6096
|
var PROVIDER_BASE_URLS = {
|
|
@@ -5974,6 +6170,7 @@ async function createServer(root2) {
|
|
|
5974
6170
|
const empty = { version: 1, collections: [], singletons: [] };
|
|
5975
6171
|
let schema = await loadSchema(adapter, contentDir) ?? empty;
|
|
5976
6172
|
let store = new ContentStore(adapter, schema, contentDir);
|
|
6173
|
+
const preview = new PreviewProcess(root2);
|
|
5977
6174
|
const app = new Hono();
|
|
5978
6175
|
app.onError((err, c) => {
|
|
5979
6176
|
if (err instanceof NotFoundError) return c.json({ error: err.message }, 404);
|
|
@@ -5998,6 +6195,12 @@ async function createServer(root2) {
|
|
|
5998
6195
|
}
|
|
5999
6196
|
return c.json(await writeAstroSite(root2, schema, basename2(root2) || "my-site"));
|
|
6000
6197
|
});
|
|
6198
|
+
app.get("/api/_preview", (c) => c.json(preview.getState()));
|
|
6199
|
+
app.post("/api/_preview/start", async (c) => c.json(await preview.start()));
|
|
6200
|
+
app.post("/api/_preview/stop", (c) => {
|
|
6201
|
+
preview.stop();
|
|
6202
|
+
return c.json(preview.getState());
|
|
6203
|
+
});
|
|
6001
6204
|
app.get(
|
|
6002
6205
|
"/api/_ship",
|
|
6003
6206
|
async (c) => c.json({
|
|
@@ -6013,6 +6216,16 @@ async function createServer(root2) {
|
|
|
6013
6216
|
return c.json({ error: e.message }, 400);
|
|
6014
6217
|
}
|
|
6015
6218
|
});
|
|
6219
|
+
app.post("/api/_ship/publish", async (c) => {
|
|
6220
|
+
const { message } = await c.req.json().catch(() => ({}));
|
|
6221
|
+
try {
|
|
6222
|
+
const commit = await commitContent(root2, contentDir, message?.trim() || "content: update");
|
|
6223
|
+
const { branch } = await pushContent(root2);
|
|
6224
|
+
return c.json({ committed: commit.committed, count: commit.count, branch });
|
|
6225
|
+
} catch (e) {
|
|
6226
|
+
return c.json({ error: e.message }, 400);
|
|
6227
|
+
}
|
|
6228
|
+
});
|
|
6016
6229
|
app.post("/api/_ship/push", async (c) => {
|
|
6017
6230
|
try {
|
|
6018
6231
|
return c.json(await pushContent(root2));
|
|
@@ -6031,6 +6244,11 @@ async function createServer(root2) {
|
|
|
6031
6244
|
return c.json({ error: e.message }, 400);
|
|
6032
6245
|
}
|
|
6033
6246
|
});
|
|
6247
|
+
app.get("/api/_theme", async (c) => c.json(await loadTheme(adapter, contentDir)));
|
|
6248
|
+
app.put("/api/_theme", async (c) => {
|
|
6249
|
+
await saveTheme(adapter, parseTheme(await c.req.json()), contentDir);
|
|
6250
|
+
return c.json(await loadTheme(adapter, contentDir));
|
|
6251
|
+
});
|
|
6034
6252
|
app.get("/api/_templates", (c) => c.json({ items: templateList() }));
|
|
6035
6253
|
app.post("/api/_init", async (c) => {
|
|
6036
6254
|
const { template: id } = await c.req.json();
|
|
@@ -6114,7 +6332,7 @@ async function createServer(root2) {
|
|
|
6114
6332
|
const base = slugify((body.filename ?? "gorsel").replace(/\.[^.]+$/, "")) || "gorsel";
|
|
6115
6333
|
const name = `${base}-${Date.now().toString(36)}.webp`;
|
|
6116
6334
|
const rel = `${contentDir}/media/${name}`;
|
|
6117
|
-
const abs =
|
|
6335
|
+
const abs = join7(root2, rel);
|
|
6118
6336
|
await mkdir3(dirname3(abs), { recursive: true });
|
|
6119
6337
|
await writeFile4(abs, Buffer.from(body.dataBase64, "base64"));
|
|
6120
6338
|
return c.json({ path: rel });
|
|
@@ -6158,7 +6376,7 @@ async function createServer(root2) {
|
|
|
6158
6376
|
return c.text("forbidden", 400);
|
|
6159
6377
|
}
|
|
6160
6378
|
try {
|
|
6161
|
-
const bytes = await
|
|
6379
|
+
const bytes = await readFile4(join7(root2, contentDir, "media", file));
|
|
6162
6380
|
return new Response(bytes, { headers: { "content-type": "image/webp" } });
|
|
6163
6381
|
} catch {
|
|
6164
6382
|
return c.text("not found", 404);
|
|
@@ -6185,14 +6403,14 @@ async function createServer(root2) {
|
|
|
6185
6403
|
app.get("/*", async (c) => {
|
|
6186
6404
|
const urlPath = c.req.path === "/" ? "/index.html" : c.req.path;
|
|
6187
6405
|
const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
|
|
6188
|
-
const file =
|
|
6406
|
+
const file = join7(editorDir, rel);
|
|
6189
6407
|
if (!file.startsWith(editorDir)) return c.text("forbidden", 403);
|
|
6190
6408
|
const type = MIME[extname(file)] ?? "application/octet-stream";
|
|
6191
6409
|
try {
|
|
6192
|
-
return new Response(await
|
|
6410
|
+
return new Response(await readFile4(file), { headers: { "content-type": type } });
|
|
6193
6411
|
} catch {
|
|
6194
6412
|
try {
|
|
6195
|
-
const html = await
|
|
6413
|
+
const html = await readFile4(join7(editorDir, "index.html"));
|
|
6196
6414
|
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
6197
6415
|
} catch {
|
|
6198
6416
|
return c.text("Editor UI not found (run the justjson build first).", 404);
|