@open-slide/core 1.8.0 → 1.9.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.
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { randomUUID } from "node:crypto";
6
- import { existsSync } from "node:fs";
6
+ import { existsSync, readFileSync } from "node:fs";
7
7
  import tailwindcss from "@tailwindcss/vite";
8
8
  import react from "@vitejs/plugin-react";
9
9
  import * as t$4 from "@babel/types";
@@ -1244,7 +1244,7 @@ function applyRevertAsset(source, assetPath) {
1244
1244
 
1245
1245
  //#endregion
1246
1246
  //#region src/editing/slide-ops.ts
1247
- const SLIDE_ID_RE$3 = /^[a-z0-9_-]+$/i;
1247
+ const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
1248
1248
  function validateSlideName(v) {
1249
1249
  if (typeof v !== "string") return null;
1250
1250
  const trimmed = v.trim();
@@ -1308,7 +1308,7 @@ function readMetaTitleInSource(source) {
1308
1308
  return { kind: "missing" };
1309
1309
  }
1310
1310
  async function rmSlideDir(slidesRoot, slideId) {
1311
- if (!SLIDE_ID_RE$3.test(slideId)) return false;
1311
+ if (!SLIDE_ID_RE.test(slideId)) return false;
1312
1312
  const dir = path.resolve(slidesRoot, slideId);
1313
1313
  if (!dir.startsWith(slidesRoot + path.sep)) return false;
1314
1314
  try {
@@ -1322,7 +1322,7 @@ async function rmSlideDir(slidesRoot, slideId) {
1322
1322
  }
1323
1323
  }
1324
1324
  async function duplicateSlideDir(slidesRoot, slideId, desiredId) {
1325
- if (!SLIDE_ID_RE$3.test(slideId)) return {
1325
+ if (!SLIDE_ID_RE.test(slideId)) return {
1326
1326
  ok: false,
1327
1327
  status: 400,
1328
1328
  error: "invalid slideId"
@@ -1345,7 +1345,7 @@ async function duplicateSlideDir(slidesRoot, slideId, desiredId) {
1345
1345
  }
1346
1346
  let newId;
1347
1347
  if (desiredId !== void 0) {
1348
- if (!SLIDE_ID_RE$3.test(desiredId)) return {
1348
+ if (!SLIDE_ID_RE.test(desiredId)) return {
1349
1349
  ok: false,
1350
1350
  status: 400,
1351
1351
  error: "invalid newId"
@@ -1433,7 +1433,7 @@ async function duplicateSlideDir(slidesRoot, slideId, desiredId) {
1433
1433
  }
1434
1434
  }
1435
1435
  function resolveSlideEntry(slidesRoot, slideId) {
1436
- if (!SLIDE_ID_RE$3.test(slideId)) return null;
1436
+ if (!SLIDE_ID_RE.test(slideId)) return null;
1437
1437
  const dir = path.resolve(slidesRoot, slideId);
1438
1438
  if (!dir.startsWith(slidesRoot + path.sep)) return null;
1439
1439
  return path.join(dir, "index.tsx");
@@ -1770,7 +1770,7 @@ function validateAssetName(v) {
1770
1770
  return trimmed;
1771
1771
  }
1772
1772
  function resolveAssetsDir(slidesRoot, slideId) {
1773
- if (!SLIDE_ID_RE$3.test(slideId)) return null;
1773
+ if (!SLIDE_ID_RE.test(slideId)) return null;
1774
1774
  const slideDir = path.resolve(slidesRoot, slideId);
1775
1775
  if (!slideDir.startsWith(slidesRoot + path.sep)) return null;
1776
1776
  const assetsDir = path.resolve(slideDir, "assets");
@@ -1883,10 +1883,11 @@ function makeContext(opts) {
1883
1883
  slidesDir,
1884
1884
  slidesRoot,
1885
1885
  globalAssetsRoot,
1886
- manifestPath
1886
+ manifestPath,
1887
+ coreVersion: opts.coreVersion
1887
1888
  };
1888
1889
  }
1889
- async function readBody$2(req) {
1890
+ async function readBody(req) {
1890
1891
  return await new Promise((resolve, reject) => {
1891
1892
  const chunks = [];
1892
1893
  req.on("data", (c) => chunks.push(c));
@@ -1902,17 +1903,21 @@ async function readBody$2(req) {
1902
1903
  req.on("error", reject);
1903
1904
  });
1904
1905
  }
1905
- function json$2(res, status, body) {
1906
+ function json(res, status, body) {
1906
1907
  res.statusCode = status;
1907
1908
  res.setHeader("content-type", "application/json");
1908
1909
  res.end(JSON.stringify(body));
1909
1910
  }
1910
- function resolveSlideEntryPath(ctx, slideId) {
1911
- if (!SLIDE_ID_RE$3.test(slideId)) return null;
1912
- const full = path.resolve(ctx.slidesRoot, slideId, "index.tsx");
1913
- if (!full.startsWith(ctx.slidesRoot + path.sep)) return null;
1911
+ function resolveSlidePath(userCwd, slidesDir, slideId) {
1912
+ if (!SLIDE_ID_RE.test(slideId)) return null;
1913
+ const slidesRoot = path.resolve(userCwd, slidesDir);
1914
+ const full = path.resolve(slidesRoot, slideId, "index.tsx");
1915
+ if (!full.startsWith(slidesRoot + path.sep)) return null;
1914
1916
  return full;
1915
1917
  }
1918
+ function resolveSlideEntryPath(ctx, slideId) {
1919
+ return resolveSlidePath(ctx.userCwd, ctx.slidesDir, slideId);
1920
+ }
1916
1921
 
1917
1922
  //#endregion
1918
1923
  //#region src/vite/routes/assets.ts
@@ -1927,18 +1932,18 @@ function registerAssetRoutes(server, ctx) {
1927
1932
  if (usagesMatch && method === "GET") {
1928
1933
  const scope = usagesMatch[1];
1929
1934
  const filename = decodeURIComponent(usagesMatch[2]);
1930
- if (!validateAssetName(filename)) return json$2(res, 400, { error: "invalid path" });
1935
+ if (!validateAssetName(filename)) return json(res, 400, { error: "invalid path" });
1931
1936
  const isGlobal = scope === GLOBAL_SCOPE;
1932
1937
  const assetPath = isGlobal ? `@assets/${filename}` : `./assets/${filename}`;
1933
1938
  let slideIds;
1934
1939
  if (isGlobal) try {
1935
1940
  const entries = await fs.readdir(ctx.slidesRoot, { withFileTypes: true });
1936
- slideIds = entries.filter((e) => e.isDirectory() && SLIDE_ID_RE$3.test(e.name)).map((e) => e.name);
1941
+ slideIds = entries.filter((e) => e.isDirectory() && SLIDE_ID_RE.test(e.name)).map((e) => e.name);
1937
1942
  } catch {
1938
1943
  slideIds = [];
1939
1944
  }
1940
1945
  else {
1941
- if (!SLIDE_ID_RE$3.test(scope)) return json$2(res, 400, { error: "invalid slideId" });
1946
+ if (!SLIDE_ID_RE.test(scope)) return json(res, 400, { error: "invalid slideId" });
1942
1947
  slideIds = [scope];
1943
1948
  }
1944
1949
  const usages = [];
@@ -1961,7 +1966,7 @@ function registerAssetRoutes(server, ctx) {
1961
1966
  totalCount += count;
1962
1967
  }
1963
1968
  }
1964
- return json$2(res, 200, {
1969
+ return json(res, 200, {
1965
1970
  usages,
1966
1971
  totalCount
1967
1972
  });
@@ -1969,12 +1974,12 @@ function registerAssetRoutes(server, ctx) {
1969
1974
  if (listMatch && method === "GET") {
1970
1975
  const slideId = listMatch[1];
1971
1976
  const scopedDir = resolveScopedAssetsDir(ctx.slidesRoot, ctx.globalAssetsRoot, slideId);
1972
- if (!scopedDir) return json$2(res, 400, { error: "invalid slideId" });
1977
+ if (!scopedDir) return json(res, 400, { error: "invalid slideId" });
1973
1978
  let entries;
1974
1979
  try {
1975
1980
  entries = await fs.readdir(scopedDir);
1976
1981
  } catch (err) {
1977
- if (err.code === "ENOENT") return json$2(res, 200, { assets: [] });
1982
+ if (err.code === "ENOENT") return json(res, 200, { assets: [] });
1978
1983
  throw err;
1979
1984
  }
1980
1985
  const assets = [];
@@ -1997,11 +2002,11 @@ function registerAssetRoutes(server, ctx) {
1997
2002
  let scanIds;
1998
2003
  if (isGlobal) try {
1999
2004
  const dirs = await fs.readdir(ctx.slidesRoot, { withFileTypes: true });
2000
- scanIds = dirs.filter((e) => e.isDirectory() && SLIDE_ID_RE$3.test(e.name)).map((e) => e.name);
2005
+ scanIds = dirs.filter((e) => e.isDirectory() && SLIDE_ID_RE.test(e.name)).map((e) => e.name);
2001
2006
  } catch {
2002
2007
  scanIds = [];
2003
2008
  }
2004
- else scanIds = SLIDE_ID_RE$3.test(slideId) ? [slideId] : [];
2009
+ else scanIds = SLIDE_ID_RE.test(slideId) ? [slideId] : [];
2005
2010
  const paths = assets.map((a) => isGlobal ? `@assets/${a.name}` : `./assets/${a.name}`);
2006
2011
  const pathToAsset = new Map(paths.map((p, i) => [p, assets[i]]));
2007
2012
  for (const sid of scanIds) {
@@ -2019,13 +2024,13 @@ function registerAssetRoutes(server, ctx) {
2019
2024
  }
2020
2025
  }
2021
2026
  }
2022
- return json$2(res, 200, { assets });
2027
+ return json(res, 200, { assets });
2023
2028
  }
2024
2029
  if (fileMatch) {
2025
2030
  const slideId = fileMatch[1];
2026
2031
  const filename = decodeURIComponent(fileMatch[2]);
2027
2032
  const file = resolveScopedAssetFile(ctx.slidesRoot, ctx.globalAssetsRoot, slideId, filename);
2028
- if (!file) return json$2(res, 400, { error: "invalid path" });
2033
+ if (!file) return json(res, 400, { error: "invalid path" });
2029
2034
  if (method === "GET") try {
2030
2035
  const buf = await fs.readFile(file);
2031
2036
  res.statusCode = 200;
@@ -2034,22 +2039,22 @@ function registerAssetRoutes(server, ctx) {
2034
2039
  res.end(buf);
2035
2040
  return;
2036
2041
  } catch (err) {
2037
- if (err.code === "ENOENT") return json$2(res, 404, { error: "asset not found" });
2042
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
2038
2043
  throw err;
2039
2044
  }
2040
2045
  if (method === "POST") {
2041
2046
  const requestCheck = validateMutationRequest(req);
2042
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2047
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2043
2048
  const overwrite = url.searchParams.get("overwrite") === "1";
2044
2049
  const lenHeader = req.headers["content-length"];
2045
2050
  const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
2046
- if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json$2(res, 413, { error: "file too large" });
2051
+ if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json(res, 413, { error: "file too large" });
2047
2052
  if (!overwrite) try {
2048
2053
  await fs.access(file);
2049
- return json$2(res, 409, { error: "asset exists" });
2054
+ return json(res, 409, { error: "asset exists" });
2050
2055
  } catch {}
2051
2056
  const scopedDir = resolveScopedAssetsDir(ctx.slidesRoot, ctx.globalAssetsRoot, slideId);
2052
- if (!scopedDir) return json$2(res, 400, { error: "invalid slideId" });
2057
+ if (!scopedDir) return json(res, 400, { error: "invalid slideId" });
2053
2058
  await fs.mkdir(scopedDir, { recursive: true });
2054
2059
  const chunks = [];
2055
2060
  let total = 0;
@@ -2067,9 +2072,9 @@ function registerAssetRoutes(server, ctx) {
2067
2072
  req.on("end", () => resolve());
2068
2073
  req.on("error", reject);
2069
2074
  });
2070
- if (oversized) return json$2(res, 413, { error: "file too large" });
2075
+ if (oversized) return json(res, 413, { error: "file too large" });
2071
2076
  await fs.writeFile(file, Buffer.concat(chunks));
2072
- return json$2(res, 200, {
2077
+ return json(res, 200, {
2073
2078
  ok: true,
2074
2079
  name: filename,
2075
2080
  size: total,
@@ -2079,46 +2084,46 @@ function registerAssetRoutes(server, ctx) {
2079
2084
  }
2080
2085
  if (method === "PATCH") {
2081
2086
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2082
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2083
- const body = await readBody$2(req);
2087
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2088
+ const body = await readBody(req);
2084
2089
  const target = validateAssetName(body.name);
2085
- if (!target) return json$2(res, 400, { error: "invalid name" });
2086
- if (target === filename) return json$2(res, 200, {
2090
+ if (!target) return json(res, 400, { error: "invalid name" });
2091
+ if (target === filename) return json(res, 200, {
2087
2092
  ok: true,
2088
2093
  name: filename
2089
2094
  });
2090
2095
  const dest = resolveScopedAssetFile(ctx.slidesRoot, ctx.globalAssetsRoot, slideId, target);
2091
- if (!dest) return json$2(res, 400, { error: "invalid name" });
2096
+ if (!dest) return json(res, 400, { error: "invalid name" });
2092
2097
  try {
2093
2098
  await fs.access(dest);
2094
- return json$2(res, 409, { error: "target exists" });
2099
+ return json(res, 409, { error: "target exists" });
2095
2100
  } catch {}
2096
2101
  try {
2097
2102
  await fs.rename(file, dest);
2098
2103
  } catch (err) {
2099
- if (err.code === "ENOENT") return json$2(res, 404, { error: "asset not found" });
2104
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
2100
2105
  throw err;
2101
2106
  }
2102
- return json$2(res, 200, {
2107
+ return json(res, 200, {
2103
2108
  ok: true,
2104
2109
  name: target
2105
2110
  });
2106
2111
  }
2107
2112
  if (method === "DELETE") {
2108
2113
  const requestCheck = validateMutationRequest(req);
2109
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2114
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2110
2115
  try {
2111
2116
  await fs.unlink(file);
2112
2117
  } catch (err) {
2113
- if (err.code === "ENOENT") return json$2(res, 404, { error: "asset not found" });
2118
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
2114
2119
  throw err;
2115
2120
  }
2116
- return json$2(res, 200, { ok: true });
2121
+ return json(res, 200, { ok: true });
2117
2122
  }
2118
2123
  }
2119
2124
  return next();
2120
2125
  } catch (err) {
2121
- json$2(res, 500, { error: String(err.message ?? err) });
2126
+ json(res, 500, { error: String(err.message ?? err) });
2122
2127
  }
2123
2128
  });
2124
2129
  }
@@ -2238,32 +2243,32 @@ function registerCommentRoutes(server, ctx) {
2238
2243
  if (method === "GET" && url.pathname === "/") {
2239
2244
  const slideId = url.searchParams.get("slideId") ?? "";
2240
2245
  const file = resolveSlideEntryPath(ctx, slideId);
2241
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2246
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2242
2247
  let source;
2243
2248
  try {
2244
2249
  source = await fs.readFile(file, "utf8");
2245
2250
  } catch {
2246
- return json$2(res, 404, { error: "slide not found" });
2251
+ return json(res, 404, { error: "slide not found" });
2247
2252
  }
2248
- return json$2(res, 200, { comments: parseMarkers(source) });
2253
+ return json(res, 200, { comments: parseMarkers(source) });
2249
2254
  }
2250
2255
  if (method === "POST" && url.pathname === "/add") {
2251
2256
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2252
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2253
- const body = await readBody$2(req);
2257
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2258
+ const body = await readBody(req);
2254
2259
  const slideId = body.slideId ?? "";
2255
2260
  const file = resolveSlideEntryPath(ctx, slideId);
2256
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2257
- if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
2258
- if (!body.text || typeof body.text !== "string") return json$2(res, 400, { error: "missing text" });
2261
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2262
+ if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
2263
+ if (!body.text || typeof body.text !== "string") return json(res, 400, { error: "missing text" });
2259
2264
  let source;
2260
2265
  try {
2261
2266
  source = await fs.readFile(file, "utf8");
2262
2267
  } catch {
2263
- return json$2(res, 404, { error: "slide not found" });
2268
+ return json(res, 404, { error: "slide not found" });
2264
2269
  }
2265
2270
  const plan = findInsertion(source, body.line, body.column);
2266
- if (!plan) return json$2(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
2271
+ if (!plan) return json(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
2267
2272
  const id = newCommentId();
2268
2273
  const ts = new Date().toISOString();
2269
2274
  const payload = b64urlEncode(JSON.stringify({
@@ -2274,36 +2279,36 @@ function registerCommentRoutes(server, ctx) {
2274
2279
  const next$1 = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
2275
2280
  await fs.writeFile(file, next$1, "utf8");
2276
2281
  const markerLine = offsetToLine(next$1, plan.offset + 1);
2277
- return json$2(res, 200, {
2282
+ return json(res, 200, {
2278
2283
  id,
2279
2284
  line: markerLine
2280
2285
  });
2281
2286
  }
2282
2287
  if (method === "DELETE" && url.pathname.startsWith("/")) {
2283
2288
  const requestCheck = validateMutationRequest(req);
2284
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2289
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2285
2290
  const id = url.pathname.slice(1);
2286
- if (!/^c-[a-f0-9]+$/.test(id)) return json$2(res, 400, { error: "invalid id" });
2291
+ if (!/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
2287
2292
  const slideId = url.searchParams.get("slideId") ?? "";
2288
2293
  const file = resolveSlideEntryPath(ctx, slideId);
2289
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2294
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2290
2295
  let source;
2291
2296
  try {
2292
2297
  source = await fs.readFile(file, "utf8");
2293
2298
  } catch {
2294
- return json$2(res, 404, { error: "slide not found" });
2299
+ return json(res, 404, { error: "slide not found" });
2295
2300
  }
2296
2301
  const lines = source.split("\n");
2297
2302
  const idRe = markerDeleteRegex(id);
2298
2303
  const hit = lines.findIndex((l) => idRe.test(l));
2299
- if (hit === -1) return json$2(res, 404, { error: "marker not found" });
2304
+ if (hit === -1) return json(res, 404, { error: "marker not found" });
2300
2305
  lines.splice(hit, 1);
2301
2306
  await fs.writeFile(file, lines.join("\n"), "utf8");
2302
- return json$2(res, 200, { ok: true });
2307
+ return json(res, 200, { ok: true });
2303
2308
  }
2304
2309
  next();
2305
2310
  } catch (err) {
2306
- json$2(res, 500, { error: String(err.message ?? err) });
2311
+ json(res, 500, { error: String(err.message ?? err) });
2307
2312
  }
2308
2313
  });
2309
2314
  }
@@ -2316,64 +2321,64 @@ function registerEditRoutes(server, ctx) {
2316
2321
  const method = req.method ?? "GET";
2317
2322
  if (method !== "POST") return next();
2318
2323
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2319
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2324
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2320
2325
  try {
2321
2326
  if (url.pathname === "/") {
2322
- const body = await readBody$2(req);
2327
+ const body = await readBody(req);
2323
2328
  const slideId = body.slideId ?? "";
2324
2329
  const file = resolveSlideEntryPath(ctx, slideId);
2325
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2326
- if (!body.line || body.line < 1) return json$2(res, 400, { error: "invalid line" });
2327
- if (!Array.isArray(body.ops)) return json$2(res, 400, { error: "missing ops" });
2330
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2331
+ if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
2332
+ if (!Array.isArray(body.ops)) return json(res, 400, { error: "missing ops" });
2328
2333
  let source;
2329
2334
  try {
2330
2335
  source = await fs.readFile(file, "utf8");
2331
2336
  } catch {
2332
- return json$2(res, 404, { error: "slide not found" });
2337
+ return json(res, 404, { error: "slide not found" });
2333
2338
  }
2334
2339
  const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
2335
- if (!result.ok) return json$2(res, result.status, { error: result.error });
2340
+ if (!result.ok) return json(res, result.status, { error: result.error });
2336
2341
  const changed = result.source !== source;
2337
2342
  if (changed) await fs.writeFile(file, result.source, "utf8");
2338
- return json$2(res, 200, {
2343
+ return json(res, 200, {
2339
2344
  ok: true,
2340
2345
  changed
2341
2346
  });
2342
2347
  }
2343
2348
  if (url.pathname === "/revert-asset") {
2344
- const body = await readBody$2(req);
2349
+ const body = await readBody(req);
2345
2350
  const slideId = body.slideId ?? "";
2346
2351
  const assetPath = body.assetPath;
2347
2352
  const file = resolveSlideEntryPath(ctx, slideId);
2348
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2349
- if (typeof assetPath !== "string" || !assetPath) return json$2(res, 400, { error: "missing assetPath" });
2350
- if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return json$2(res, 400, { error: "asset path must start with ./assets/ or @assets/" });
2353
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2354
+ if (typeof assetPath !== "string" || !assetPath) return json(res, 400, { error: "missing assetPath" });
2355
+ if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return json(res, 400, { error: "asset path must start with ./assets/ or @assets/" });
2351
2356
  let source;
2352
2357
  try {
2353
2358
  source = await fs.readFile(file, "utf8");
2354
2359
  } catch {
2355
- return json$2(res, 404, { error: "slide not found" });
2360
+ return json(res, 404, { error: "slide not found" });
2356
2361
  }
2357
2362
  const result = applyRevertAsset(source, assetPath);
2358
- if (!result.ok) return json$2(res, result.status, { error: result.error });
2363
+ if (!result.ok) return json(res, result.status, { error: result.error });
2359
2364
  const changed = result.source !== source;
2360
2365
  if (changed) await fs.writeFile(file, result.source, "utf8");
2361
- return json$2(res, 200, {
2366
+ return json(res, 200, {
2362
2367
  ok: true,
2363
2368
  changed
2364
2369
  });
2365
2370
  }
2366
2371
  if (url.pathname === "/batch") {
2367
- const body = await readBody$2(req);
2372
+ const body = await readBody(req);
2368
2373
  const slideId = body.slideId ?? "";
2369
2374
  const file = resolveSlideEntryPath(ctx, slideId);
2370
- if (!file) return json$2(res, 400, { error: "invalid slideId" });
2371
- if (!Array.isArray(body.edits)) return json$2(res, 400, { error: "missing edits" });
2375
+ if (!file) return json(res, 400, { error: "invalid slideId" });
2376
+ if (!Array.isArray(body.edits)) return json(res, 400, { error: "missing edits" });
2372
2377
  let source;
2373
2378
  try {
2374
2379
  source = await fs.readFile(file, "utf8");
2375
2380
  } catch {
2376
- return json$2(res, 404, { error: "slide not found" });
2381
+ return json(res, 404, { error: "slide not found" });
2377
2382
  }
2378
2383
  const original = source;
2379
2384
  const results = [];
@@ -2396,7 +2401,7 @@ function registerEditRoutes(server, ctx) {
2396
2401
  }
2397
2402
  const changed = source !== original;
2398
2403
  if (changed) await fs.writeFile(file, source, "utf8");
2399
- return json$2(res, 200, {
2404
+ return json(res, 200, {
2400
2405
  ok: true,
2401
2406
  changed,
2402
2407
  results
@@ -2404,7 +2409,7 @@ function registerEditRoutes(server, ctx) {
2404
2409
  }
2405
2410
  return next();
2406
2411
  } catch (err) {
2407
- json$2(res, 500, { error: String(err.message ?? err) });
2412
+ json(res, 500, { error: String(err.message ?? err) });
2408
2413
  }
2409
2414
  });
2410
2415
  }
@@ -2488,16 +2493,16 @@ function registerFolderRoutes(server, ctx) {
2488
2493
  try {
2489
2494
  if (method === "GET" && url.pathname === "/") {
2490
2495
  const manifest = await readManifest(ctx.manifestPath);
2491
- return json$2(res, 200, manifest);
2496
+ return json(res, 200, manifest);
2492
2497
  }
2493
2498
  if (method === "POST" && url.pathname === "/") {
2494
2499
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2495
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2496
- const body = await readBody$2(req);
2500
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2501
+ const body = await readBody(req);
2497
2502
  const name = validateName(body.name);
2498
- if (!name) return json$2(res, 400, { error: "invalid name" });
2503
+ if (!name) return json(res, 400, { error: "invalid name" });
2499
2504
  const icon = validateIcon(body.icon);
2500
- if (!icon) return json$2(res, 400, { error: "invalid icon" });
2505
+ if (!icon) return json(res, 400, { error: "invalid icon" });
2501
2506
  const manifest = await readManifest(ctx.manifestPath);
2502
2507
  const folder = {
2503
2508
  id: newFolderId(),
@@ -2506,76 +2511,76 @@ function registerFolderRoutes(server, ctx) {
2506
2511
  };
2507
2512
  manifest.folders.push(folder);
2508
2513
  await writeManifest(ctx.manifestPath, manifest);
2509
- return json$2(res, 200, folder);
2514
+ return json(res, 200, folder);
2510
2515
  }
2511
2516
  if (method === "PUT" && url.pathname === "/assign") {
2512
2517
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2513
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2514
- const body = await readBody$2(req);
2515
- if (typeof body.slideId !== "string" || !SLIDE_ID_RE$3.test(body.slideId)) return json$2(res, 400, { error: "invalid slideId" });
2518
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2519
+ const body = await readBody(req);
2520
+ if (typeof body.slideId !== "string" || !SLIDE_ID_RE.test(body.slideId)) return json(res, 400, { error: "invalid slideId" });
2516
2521
  const slideId = body.slideId;
2517
2522
  let folderId;
2518
2523
  if (body.folderId === null) folderId = null;
2519
2524
  else if (typeof body.folderId === "string" && FOLDER_ID_RE.test(body.folderId)) folderId = body.folderId;
2520
- else return json$2(res, 400, { error: "invalid folderId" });
2525
+ else return json(res, 400, { error: "invalid folderId" });
2521
2526
  const manifest = await readManifest(ctx.manifestPath);
2522
- if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json$2(res, 404, { error: "folder not found" });
2527
+ if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json(res, 404, { error: "folder not found" });
2523
2528
  if (folderId === null) delete manifest.assignments[slideId];
2524
2529
  else manifest.assignments[slideId] = folderId;
2525
2530
  await writeManifest(ctx.manifestPath, manifest);
2526
- return json$2(res, 200, { ok: true });
2531
+ return json(res, 200, { ok: true });
2527
2532
  }
2528
2533
  if (method === "PUT" && url.pathname === "/reorder") {
2529
2534
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2530
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2531
- const body = await readBody$2(req);
2535
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2536
+ const body = await readBody(req);
2532
2537
  const manifest = await readManifest(ctx.manifestPath);
2533
2538
  const ids = validateReorder(body.ids, manifest.folders);
2534
- if (!ids) return json$2(res, 400, { error: "invalid ids" });
2539
+ if (!ids) return json(res, 400, { error: "invalid ids" });
2535
2540
  const byId = new Map(manifest.folders.map((f) => [f.id, f]));
2536
2541
  manifest.folders = ids.map((id) => byId.get(id));
2537
2542
  await writeManifest(ctx.manifestPath, manifest);
2538
- return json$2(res, 200, { ok: true });
2543
+ return json(res, 200, { ok: true });
2539
2544
  }
2540
2545
  const idMatch = url.pathname.match(/^\/([^/]+)$/);
2541
2546
  if (idMatch) {
2542
2547
  const id = idMatch[1];
2543
- if (!FOLDER_ID_RE.test(id)) return json$2(res, 400, { error: "invalid id" });
2548
+ if (!FOLDER_ID_RE.test(id)) return json(res, 400, { error: "invalid id" });
2544
2549
  if (method === "PATCH") {
2545
2550
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2546
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2547
- const body = await readBody$2(req);
2551
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2552
+ const body = await readBody(req);
2548
2553
  const manifest = await readManifest(ctx.manifestPath);
2549
2554
  const folder = manifest.folders.find((f) => f.id === id);
2550
- if (!folder) return json$2(res, 404, { error: "folder not found" });
2555
+ if (!folder) return json(res, 404, { error: "folder not found" });
2551
2556
  if (body.name !== void 0) {
2552
2557
  const name = validateName(body.name);
2553
- if (!name) return json$2(res, 400, { error: "invalid name" });
2558
+ if (!name) return json(res, 400, { error: "invalid name" });
2554
2559
  folder.name = name;
2555
2560
  }
2556
2561
  if (body.icon !== void 0) {
2557
2562
  const icon = validateIcon(body.icon);
2558
- if (!icon) return json$2(res, 400, { error: "invalid icon" });
2563
+ if (!icon) return json(res, 400, { error: "invalid icon" });
2559
2564
  folder.icon = icon;
2560
2565
  }
2561
2566
  await writeManifest(ctx.manifestPath, manifest);
2562
- return json$2(res, 200, folder);
2567
+ return json(res, 200, folder);
2563
2568
  }
2564
2569
  if (method === "DELETE") {
2565
2570
  const requestCheck = validateMutationRequest(req);
2566
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2571
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2567
2572
  const manifest = await readManifest(ctx.manifestPath);
2568
2573
  const before = manifest.folders.length;
2569
2574
  manifest.folders = manifest.folders.filter((f) => f.id !== id);
2570
- if (manifest.folders.length === before) return json$2(res, 404, { error: "folder not found" });
2575
+ if (manifest.folders.length === before) return json(res, 404, { error: "folder not found" });
2571
2576
  for (const [slideId, folderId] of Object.entries(manifest.assignments)) if (folderId === id) delete manifest.assignments[slideId];
2572
2577
  await writeManifest(ctx.manifestPath, manifest);
2573
- return json$2(res, 200, { ok: true });
2578
+ return json(res, 200, { ok: true });
2574
2579
  }
2575
2580
  }
2576
2581
  next();
2577
2582
  } catch (err) {
2578
- json$2(res, 500, { error: String(err.message ?? err) });
2583
+ json(res, 500, { error: String(err.message ?? err) });
2579
2584
  }
2580
2585
  });
2581
2586
  }
@@ -2590,30 +2595,30 @@ function registerSlideRoutes(server, ctx) {
2590
2595
  const reorderMatch = url.pathname.match(/^\/([^/]+)\/reorder$/);
2591
2596
  if (reorderMatch && method === "PUT") {
2592
2597
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2593
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2598
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2594
2599
  const slideId$1 = reorderMatch[1];
2595
- if (!SLIDE_ID_RE$3.test(slideId$1)) return json$2(res, 400, { error: "invalid slideId" });
2596
- const body = await readBody$2(req);
2597
- if (!Array.isArray(body.order)) return json$2(res, 400, { error: "invalid order" });
2600
+ if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
2601
+ const body = await readBody(req);
2602
+ if (!Array.isArray(body.order)) return json(res, 400, { error: "invalid order" });
2598
2603
  const order = [];
2599
2604
  for (const v of body.order) {
2600
- if (!Number.isInteger(v)) return json$2(res, 400, { error: "invalid order" });
2605
+ if (!Number.isInteger(v)) return json(res, 400, { error: "invalid order" });
2601
2606
  order.push(v);
2602
2607
  }
2603
2608
  const entry = resolveSlideEntry(ctx.slidesRoot, slideId$1);
2604
- if (!entry) return json$2(res, 400, { error: "invalid slideId" });
2609
+ if (!entry) return json(res, 400, { error: "invalid slideId" });
2605
2610
  let source;
2606
2611
  try {
2607
2612
  source = await fs.readFile(entry, "utf8");
2608
2613
  } catch {
2609
- return json$2(res, 404, { error: "slide not found" });
2614
+ return json(res, 404, { error: "slide not found" });
2610
2615
  }
2611
2616
  const reordered = reorderDefaultExportPagesInSource(source, order);
2612
- if (reordered === null) return json$2(res, 422, { error: "could not reorder pages — order must be a permutation of the existing array" });
2617
+ if (reordered === null) return json(res, 422, { error: "could not reorder pages — order must be a permutation of the existing array" });
2613
2618
  const withNotes = reorderNotesArrayInSource(reordered, order);
2614
- if (withNotes === null) return json$2(res, 422, { error: "could not reorder pages — `notes` export has an unexpected shape" });
2619
+ if (withNotes === null) return json(res, 422, { error: "could not reorder pages — `notes` export has an unexpected shape" });
2615
2620
  if (withNotes !== source) await fs.writeFile(entry, withNotes, "utf8");
2616
- return json$2(res, 200, {
2621
+ return json(res, 200, {
2617
2622
  ok: true,
2618
2623
  slideId: slideId$1,
2619
2624
  order
@@ -2624,25 +2629,25 @@ function registerSlideRoutes(server, ctx) {
2624
2629
  const slideId$1 = pageOpMatch[1];
2625
2630
  const pageIndex = Number.parseInt(pageOpMatch[2], 10);
2626
2631
  const op = pageOpMatch[3];
2627
- if (!SLIDE_ID_RE$3.test(slideId$1)) return json$2(res, 400, { error: "invalid slideId" });
2628
- if (!Number.isInteger(pageIndex) || pageIndex < 0) return json$2(res, 400, { error: "invalid page index" });
2632
+ if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
2633
+ if (!Number.isInteger(pageIndex) || pageIndex < 0) return json(res, 400, { error: "invalid page index" });
2629
2634
  const isDelete = method === "DELETE" && !op;
2630
2635
  const isDuplicate = method === "POST" && op === "duplicate";
2631
2636
  if (!isDelete && !isDuplicate) return next();
2632
2637
  const requestCheck = validateMutationRequest(req);
2633
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2638
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2634
2639
  const entry = resolveSlideEntry(ctx.slidesRoot, slideId$1);
2635
- if (!entry) return json$2(res, 400, { error: "invalid slideId" });
2640
+ if (!entry) return json(res, 400, { error: "invalid slideId" });
2636
2641
  let source;
2637
2642
  try {
2638
2643
  source = await fs.readFile(entry, "utf8");
2639
2644
  } catch {
2640
- return json$2(res, 404, { error: "slide not found" });
2645
+ return json(res, 404, { error: "slide not found" });
2641
2646
  }
2642
2647
  const updated = isDelete ? removePageFromDefaultExportInSource(source, pageIndex) : duplicatePageInDefaultExportInSource(source, pageIndex);
2643
- if (updated === null) return json$2(res, 422, { error: isDelete ? "could not delete page — index out of range or default export is not an array" : "could not duplicate page — index out of range or default export is not an array" });
2648
+ if (updated === null) return json(res, 422, { error: isDelete ? "could not delete page — index out of range or default export is not an array" : "could not duplicate page — index out of range or default export is not an array" });
2644
2649
  if (updated !== source) await fs.writeFile(entry, updated, "utf8");
2645
- return json$2(res, 200, {
2650
+ return json(res, 200, {
2646
2651
  ok: true,
2647
2652
  slideId: slideId$1,
2648
2653
  index: pageIndex
@@ -2651,20 +2656,20 @@ function registerSlideRoutes(server, ctx) {
2651
2656
  const duplicateMatch = url.pathname.match(/^\/([^/]+)\/duplicate$/);
2652
2657
  if (duplicateMatch && method === "POST") {
2653
2658
  const requestCheck = validateMutationRequest(req);
2654
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2659
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2655
2660
  const slideId$1 = duplicateMatch[1];
2656
- if (!SLIDE_ID_RE$3.test(slideId$1)) return json$2(res, 400, { error: "invalid slideId" });
2657
- const body = await readBody$2(req);
2658
- if (body.newId !== void 0 && typeof body.newId !== "string") return json$2(res, 400, { error: "invalid newId" });
2661
+ if (!SLIDE_ID_RE.test(slideId$1)) return json(res, 400, { error: "invalid slideId" });
2662
+ const body = await readBody(req);
2663
+ if (body.newId !== void 0 && typeof body.newId !== "string") return json(res, 400, { error: "invalid newId" });
2659
2664
  const duplicated = await duplicateSlideDir(ctx.slidesRoot, slideId$1, body.newId);
2660
- if (!duplicated.ok) return json$2(res, duplicated.status, { error: duplicated.error });
2665
+ if (!duplicated.ok) return json(res, duplicated.status, { error: duplicated.error });
2661
2666
  const manifest = await readManifest(ctx.manifestPath);
2662
2667
  const folderId = manifest.assignments[slideId$1];
2663
2668
  if (folderId) {
2664
2669
  manifest.assignments[duplicated.slideId] = folderId;
2665
2670
  await writeManifest(ctx.manifestPath, manifest);
2666
2671
  }
2667
- return json$2(res, 200, {
2672
+ return json(res, 200, {
2668
2673
  ok: true,
2669
2674
  slideId: duplicated.slideId
2670
2675
  });
@@ -2672,26 +2677,26 @@ function registerSlideRoutes(server, ctx) {
2672
2677
  const idMatch = url.pathname.match(/^\/([^/]+)$/);
2673
2678
  if (!idMatch) return next();
2674
2679
  const slideId = idMatch[1];
2675
- if (!SLIDE_ID_RE$3.test(slideId)) return json$2(res, 400, { error: "invalid slideId" });
2680
+ if (!SLIDE_ID_RE.test(slideId)) return json(res, 400, { error: "invalid slideId" });
2676
2681
  if (method === "PATCH") {
2677
2682
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2678
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2679
- const body = await readBody$2(req);
2683
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2684
+ const body = await readBody(req);
2680
2685
  const name = validateSlideName(body.name);
2681
- if (!name) return json$2(res, 400, { error: "invalid name" });
2686
+ if (!name) return json(res, 400, { error: "invalid name" });
2682
2687
  const entry = resolveSlideEntry(ctx.slidesRoot, slideId);
2683
- if (!entry) return json$2(res, 400, { error: "invalid slideId" });
2688
+ if (!entry) return json(res, 400, { error: "invalid slideId" });
2684
2689
  let source;
2685
2690
  try {
2686
2691
  source = await fs.readFile(entry, "utf8");
2687
2692
  } catch {
2688
- return json$2(res, 404, { error: "slide not found" });
2693
+ return json(res, 404, { error: "slide not found" });
2689
2694
  }
2690
2695
  const updated = updateMetaTitleInSource(source, name);
2691
- if (updated === null) return json$2(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
2696
+ if (updated === null) return json(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
2692
2697
  if (updated !== source) await fs.writeFile(entry, updated, "utf8");
2693
2698
  server.ws.send({ type: "full-reload" });
2694
- return json$2(res, 200, {
2699
+ return json(res, 200, {
2695
2700
  ok: true,
2696
2701
  slideId,
2697
2702
  name
@@ -2699,17 +2704,17 @@ function registerSlideRoutes(server, ctx) {
2699
2704
  }
2700
2705
  if (method === "DELETE") {
2701
2706
  const requestCheck = validateMutationRequest(req);
2702
- if (!requestCheck.ok) return json$2(res, requestCheck.status, { error: requestCheck.error });
2707
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2703
2708
  const removed = await rmSlideDir(ctx.slidesRoot, slideId);
2704
- if (!removed) return json$2(res, 404, { error: "slide not found" });
2709
+ if (!removed) return json(res, 404, { error: "slide not found" });
2705
2710
  const manifest = await readManifest(ctx.manifestPath);
2706
2711
  delete manifest.assignments[slideId];
2707
2712
  await writeManifest(ctx.manifestPath, manifest);
2708
- return json$2(res, 200, { ok: true });
2713
+ return json(res, 200, { ok: true });
2709
2714
  }
2710
2715
  return next();
2711
2716
  } catch (err) {
2712
- json$2(res, 500, { error: String(err.message ?? err) });
2717
+ json(res, 500, { error: String(err.message ?? err) });
2713
2718
  }
2714
2719
  });
2715
2720
  }
@@ -2733,16 +2738,16 @@ function registerSvglRoutes(server) {
2733
2738
  target = `https://api.svgl.app/${qs ? `?${qs}` : ""}`;
2734
2739
  } else if (reqUrl.pathname === "/svg") {
2735
2740
  const u = reqUrl.searchParams.get("u");
2736
- if (!u) return json$2(res, 400, { error: "missing u" });
2741
+ if (!u) return json(res, 400, { error: "missing u" });
2737
2742
  let parsed;
2738
2743
  try {
2739
2744
  parsed = new URL(u);
2740
2745
  } catch {
2741
- return json$2(res, 400, { error: "invalid u" });
2746
+ return json(res, 400, { error: "invalid u" });
2742
2747
  }
2743
- if (parsed.protocol !== "https:") return json$2(res, 400, { error: "https only" });
2748
+ if (parsed.protocol !== "https:") return json(res, 400, { error: "https only" });
2744
2749
  const host = parsed.hostname.toLowerCase();
2745
- if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json$2(res, 400, { error: "host not allowed" });
2750
+ if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json(res, 400, { error: "host not allowed" });
2746
2751
  target = parsed.toString();
2747
2752
  } else return next();
2748
2753
  const upstream = await fetch(target);
@@ -2753,11 +2758,68 @@ function registerSvglRoutes(server) {
2753
2758
  const buf = Buffer.from(await upstream.arrayBuffer());
2754
2759
  res.end(buf);
2755
2760
  } catch (err) {
2756
- json$2(res, 502, { error: String(err.message ?? err) });
2761
+ json(res, 502, { error: String(err.message ?? err) });
2757
2762
  }
2758
2763
  });
2759
2764
  }
2760
2765
 
2766
+ //#endregion
2767
+ //#region src/vite/routes/update.ts
2768
+ const PKG = "@open-slide/core";
2769
+ const CACHE_TTL_MS = 10 * 60 * 1e3;
2770
+ let cache = null;
2771
+ function parseSemver(v) {
2772
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
2773
+ if (!m) return null;
2774
+ return [
2775
+ Number(m[1]),
2776
+ Number(m[2]),
2777
+ Number(m[3])
2778
+ ];
2779
+ }
2780
+ function isOutdated(current, latest) {
2781
+ const a = parseSemver(current);
2782
+ const b = parseSemver(latest);
2783
+ if (!a || !b) return false;
2784
+ for (let i = 0; i < 3; i++) {
2785
+ if (b[i] > a[i]) return true;
2786
+ if (b[i] < a[i]) return false;
2787
+ }
2788
+ return false;
2789
+ }
2790
+ async function fetchLatest(now) {
2791
+ if (cache && now - cache.at < CACHE_TTL_MS) return cache.latest;
2792
+ try {
2793
+ const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
2794
+ signal: AbortSignal.timeout(3e3),
2795
+ headers: { accept: "application/json" }
2796
+ });
2797
+ if (!res.ok) throw new Error(`registry ${res.status}`);
2798
+ const body = await res.json();
2799
+ const latest = typeof body.version === "string" ? body.version : null;
2800
+ cache = {
2801
+ at: now,
2802
+ latest
2803
+ };
2804
+ return latest;
2805
+ } catch {
2806
+ return cache?.latest ?? null;
2807
+ }
2808
+ }
2809
+ function registerUpdateRoutes(server, current) {
2810
+ server.middlewares.use("/__update-check", async (req, res, next) => {
2811
+ if ((req.method ?? "GET") !== "GET") return next();
2812
+ const latest = await fetchLatest(Date.now());
2813
+ const result = {
2814
+ current,
2815
+ latest,
2816
+ outdated: latest ? isOutdated(current, latest) : false
2817
+ };
2818
+ res.setHeader("cache-control", "no-store");
2819
+ json(res, 200, result);
2820
+ });
2821
+ }
2822
+
2761
2823
  //#endregion
2762
2824
  //#region src/vite/routes/watchers.ts
2763
2825
  function registerWatchers(server, ctx) {
@@ -2783,7 +2845,7 @@ function registerWatchers(server, ctx) {
2783
2845
  const parts = rel.split(path.sep);
2784
2846
  if (parts.length < 3 || parts[1] !== "assets") return;
2785
2847
  const slideId = parts[0];
2786
- if (!SLIDE_ID_RE$3.test(slideId)) return;
2848
+ if (!SLIDE_ID_RE.test(slideId)) return;
2787
2849
  server.ws.send({
2788
2850
  type: "custom",
2789
2851
  event: "open-slide:assets-changed",
@@ -2810,13 +2872,13 @@ function apiPlugin(opts) {
2810
2872
  registerAssetRoutes(server, ctx);
2811
2873
  registerSvglRoutes(server);
2812
2874
  registerFolderRoutes(server, ctx);
2875
+ registerUpdateRoutes(server, ctx.coreVersion);
2813
2876
  }
2814
2877
  };
2815
2878
  }
2816
2879
 
2817
2880
  //#endregion
2818
2881
  //#region src/vite/current-plugin.ts
2819
- const SLIDE_ID_RE$2 = /^[a-z0-9_-]+$/i;
2820
2882
  const TEXT_SNIPPET_MAX = 120;
2821
2883
  function parseSelection(raw) {
2822
2884
  if (raw == null || typeof raw !== "object") return null;
@@ -2855,7 +2917,7 @@ function currentPlugin(opts) {
2855
2917
  selection: null
2856
2918
  };
2857
2919
  if (typeof raw?.slideId === "string") {
2858
- if (!SLIDE_ID_RE$2.test(raw.slideId)) return;
2920
+ if (!SLIDE_ID_RE.test(raw.slideId)) return;
2859
2921
  const totalPages = typeof raw.totalPages === "number" && Number.isFinite(raw.totalPages) && raw.totalPages > 0 ? Math.floor(raw.totalPages) : 1;
2860
2922
  const rawIndex = typeof raw.pageIndex === "number" && Number.isFinite(raw.pageIndex) ? Math.floor(raw.pageIndex) : 0;
2861
2923
  const pageIndex = Math.max(0, Math.min(totalPages - 1, rawIndex));
@@ -2890,35 +2952,6 @@ function currentPlugin(opts) {
2890
2952
 
2891
2953
  //#endregion
2892
2954
  //#region src/vite/design-plugin.ts
2893
- const SLIDE_ID_RE$1 = /^[a-z0-9_-]+$/i;
2894
- async function readBody$1(req) {
2895
- return await new Promise((resolve, reject) => {
2896
- const chunks = [];
2897
- req.on("data", (c) => chunks.push(c));
2898
- req.on("end", () => {
2899
- const raw = Buffer.concat(chunks).toString("utf8");
2900
- if (!raw) return resolve({});
2901
- try {
2902
- resolve(JSON.parse(raw));
2903
- } catch (e) {
2904
- reject(e);
2905
- }
2906
- });
2907
- req.on("error", reject);
2908
- });
2909
- }
2910
- function json$1(res, status, body) {
2911
- res.statusCode = status;
2912
- res.setHeader("content-type", "application/json");
2913
- res.end(JSON.stringify(body));
2914
- }
2915
- function resolveSlidePath$1(userCwd, slidesDir, slideId) {
2916
- if (!SLIDE_ID_RE$1.test(slideId)) return null;
2917
- const slidesRoot = path.resolve(userCwd, slidesDir);
2918
- const full = path.resolve(slidesRoot, slideId, "index.tsx");
2919
- if (!full.startsWith(`${slidesRoot}${path.sep}`)) return null;
2920
- return full;
2921
- }
2922
2955
  function parseSource$1(source) {
2923
2956
  try {
2924
2957
  return parse(source, {
@@ -3241,28 +3274,28 @@ function designPlugin(opts) {
3241
3274
  const url = new URL(req.url ?? "/", "http://local");
3242
3275
  const method = req.method ?? "GET";
3243
3276
  const slideId = url.searchParams.get("slideId") ?? "";
3244
- const file = resolveSlidePath$1(userCwd, slidesDir, slideId);
3245
- if (!file) return json$1(res, 400, { error: "invalid slideId" });
3277
+ const file = resolveSlidePath(userCwd, slidesDir, slideId);
3278
+ if (!file) return json(res, 400, { error: "invalid slideId" });
3246
3279
  try {
3247
3280
  if (method === "GET" && url.pathname === "/") {
3248
3281
  let source;
3249
3282
  try {
3250
3283
  source = await fs.readFile(file, "utf8");
3251
3284
  } catch {
3252
- return json$1(res, 404, { error: "slide not found" });
3285
+ return json(res, 404, { error: "slide not found" });
3253
3286
  }
3254
3287
  const parsed = parseSlideDesign(source);
3255
- if (parsed.ok) return json$1(res, 200, {
3288
+ if (parsed.ok) return json(res, 200, {
3256
3289
  design: parsed.design,
3257
3290
  exists: true,
3258
3291
  warning: null
3259
3292
  });
3260
- if (parsed.exists === false) return json$1(res, 200, {
3293
+ if (parsed.exists === false) return json(res, 200, {
3261
3294
  design: defaultDesign,
3262
3295
  exists: false,
3263
3296
  warning: null
3264
3297
  });
3265
- return json$1(res, 200, {
3298
+ return json(res, 200, {
3266
3299
  design: defaultDesign,
3267
3300
  exists: true,
3268
3301
  warning: parsed.error
@@ -3270,24 +3303,24 @@ function designPlugin(opts) {
3270
3303
  }
3271
3304
  if (method === "PUT" && url.pathname === "/") {
3272
3305
  const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
3273
- if (!requestCheck.ok) return json$1(res, requestCheck.status, { error: requestCheck.error });
3274
- const body = await readBody$1(req);
3306
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
3307
+ const body = await readBody(req);
3275
3308
  const patch = body.patch;
3276
- if (!patch || typeof patch !== "object") return json$1(res, 400, { error: "missing patch object" });
3309
+ if (!patch || typeof patch !== "object") return json(res, 400, { error: "missing patch object" });
3277
3310
  let source;
3278
3311
  try {
3279
3312
  source = await fs.readFile(file, "utf8");
3280
3313
  } catch {
3281
- return json$1(res, 404, { error: "slide not found" });
3314
+ return json(res, 404, { error: "slide not found" });
3282
3315
  }
3283
3316
  const parsed = parseSlideDesign(source);
3284
3317
  const baseDesign = parsed.ok ? parsed.design : defaultDesign;
3285
- if (!parsed.ok && parsed.exists) return json$1(res, 422, { error: parsed.error });
3318
+ if (!parsed.ok && parsed.exists) return json(res, 422, { error: parsed.error });
3286
3319
  const merged = mergeDesign(baseDesign, patch);
3287
3320
  const written = applyDesignWrite(source, merged);
3288
- if (!written.ok) return json$1(res, written.status, { error: written.error });
3321
+ if (!written.ok) return json(res, written.status, { error: written.error });
3289
3322
  if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
3290
- return json$1(res, 200, {
3323
+ return json(res, 200, {
3291
3324
  ok: true,
3292
3325
  design: merged,
3293
3326
  created: written.created
@@ -3295,17 +3328,17 @@ function designPlugin(opts) {
3295
3328
  }
3296
3329
  if (method === "POST" && url.pathname === "/reset") {
3297
3330
  const requestCheck = validateMutationRequest(req);
3298
- if (!requestCheck.ok) return json$1(res, requestCheck.status, { error: requestCheck.error });
3331
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
3299
3332
  let source;
3300
3333
  try {
3301
3334
  source = await fs.readFile(file, "utf8");
3302
3335
  } catch {
3303
- return json$1(res, 404, { error: "slide not found" });
3336
+ return json(res, 404, { error: "slide not found" });
3304
3337
  }
3305
3338
  const written = applyDesignWrite(source, defaultDesign);
3306
- if (!written.ok) return json$1(res, written.status, { error: written.error });
3339
+ if (!written.ok) return json(res, written.status, { error: written.error });
3307
3340
  if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
3308
- return json$1(res, 200, {
3341
+ return json(res, 200, {
3309
3342
  ok: true,
3310
3343
  design: defaultDesign,
3311
3344
  created: written.created
@@ -3313,7 +3346,7 @@ function designPlugin(opts) {
3313
3346
  }
3314
3347
  return next();
3315
3348
  } catch (err) {
3316
- json$1(res, 500, { error: String(err.message ?? err) });
3349
+ json(res, 500, { error: String(err.message ?? err) });
3317
3350
  }
3318
3351
  });
3319
3352
  }
@@ -3386,35 +3419,6 @@ function locTagsPlugin(opts) {
3386
3419
 
3387
3420
  //#endregion
3388
3421
  //#region src/vite/notes-plugin.ts
3389
- const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
3390
- async function readBody(req) {
3391
- return await new Promise((resolve, reject) => {
3392
- const chunks = [];
3393
- req.on("data", (c) => chunks.push(c));
3394
- req.on("end", () => {
3395
- const raw = Buffer.concat(chunks).toString("utf8");
3396
- if (!raw) return resolve({});
3397
- try {
3398
- resolve(JSON.parse(raw));
3399
- } catch (e) {
3400
- reject(e);
3401
- }
3402
- });
3403
- req.on("error", reject);
3404
- });
3405
- }
3406
- function json(res, status, body) {
3407
- res.statusCode = status;
3408
- res.setHeader("content-type", "application/json");
3409
- res.end(JSON.stringify(body));
3410
- }
3411
- function resolveSlidePath(userCwd, slidesDir, slideId) {
3412
- if (!SLIDE_ID_RE.test(slideId)) return null;
3413
- const slidesRoot = path.resolve(userCwd, slidesDir);
3414
- const full = path.resolve(slidesRoot, slideId, "index.tsx");
3415
- if (!full.startsWith(slidesRoot + path.sep)) return null;
3416
- return full;
3417
- }
3418
3422
  function parseSource(source) {
3419
3423
  try {
3420
3424
  return parse(source, {
@@ -3727,7 +3731,7 @@ ${cases}
3727
3731
  `;
3728
3732
  }
3729
3733
  function openSlidePlugin(opts) {
3730
- const { userCwd, config } = opts;
3734
+ const { userCwd, config, coreVersion } = opts;
3731
3735
  const slidesDir = config.slidesDir ?? "slides";
3732
3736
  const slidesRoot = path.resolve(userCwd, slidesDir);
3733
3737
  const foldersManifestPath = path.join(slidesRoot, ".folders.json");
@@ -3788,7 +3792,8 @@ function openSlidePlugin(opts) {
3788
3792
  };
3789
3793
  const resolvedConfig = {
3790
3794
  ...config,
3791
- build: buildResolved
3795
+ build: buildResolved,
3796
+ version: coreVersion
3792
3797
  };
3793
3798
  return `export default ${JSON.stringify(resolvedConfig)};\n`;
3794
3799
  }
@@ -4005,6 +4010,15 @@ function findPackageRoot(fromFile) {
4005
4010
  }
4006
4011
  const PKG_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
4007
4012
  const APP_ROOT = path.join(PKG_ROOT, "src", "app");
4013
+ function readCoreVersion() {
4014
+ try {
4015
+ const raw = readFileSync(path.join(PKG_ROOT, "package.json"), "utf8");
4016
+ return JSON.parse(raw).version ?? "0.0.0";
4017
+ } catch {
4018
+ return "0.0.0";
4019
+ }
4020
+ }
4021
+ const CORE_VERSION = readCoreVersion();
4008
4022
  async function createViteConfig(opts) {
4009
4023
  const userCwd = path.resolve(opts.userCwd);
4010
4024
  const config = opts.config ?? await loadUserConfig(userCwd);
@@ -4027,7 +4041,8 @@ async function createViteConfig(opts) {
4027
4041
  tailwindcss(),
4028
4042
  openSlidePlugin({
4029
4043
  userCwd,
4030
- config
4044
+ config,
4045
+ coreVersion: CORE_VERSION
4031
4046
  }),
4032
4047
  themesPlugin({
4033
4048
  userCwd,
@@ -4037,7 +4052,8 @@ async function createViteConfig(opts) {
4037
4052
  apiPlugin({
4038
4053
  userCwd,
4039
4054
  slidesDir,
4040
- assetsDir
4055
+ assetsDir,
4056
+ coreVersion: CORE_VERSION
4041
4057
  }),
4042
4058
  notesPlugin({
4043
4059
  userCwd,