@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 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
- | **Ship it** | Setup snippet for your framework, one-click content commit, push to GitHub using the `git`/`gh` already on your machine. |
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.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>`;
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
- <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="/">&larr; 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="/">&larr; 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
- <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>
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 readFile3, writeFile as writeFile4 } from "fs/promises";
5779
- import { basename as basename2, dirname as dirname3, extname, join as join6, normalize } from "path";
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 = join6(root2, rel);
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 readFile3(join6(root2, contentDir, "media", file));
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 = join6(editorDir, rel);
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 readFile3(file), { headers: { "content-type": type } });
6410
+ return new Response(await readFile4(file), { headers: { "content-type": type } });
6193
6411
  } catch {
6194
6412
  try {
6195
- const html = await readFile3(join6(editorDir, "index.html"));
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);