@open-slide/core 0.0.9 → 0.0.11
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/dist/{build-pqF4W1Yi.js → build-DHiRlpjn.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-CtwxMYv9.js → config-LZM903FE.js} +377 -0
- package/dist/{dev-CJX97uiy.js → dev-B3JzCYn7.js} +1 -1
- package/dist/{preview-IuLPcL5y.js → preview-UikovHEt.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/app/components/AssetView.tsx +846 -0
- package/src/app/components/ClickNavZones.tsx +2 -2
- package/src/app/components/ThumbnailRail.tsx +2 -2
- package/src/app/components/inspector/InspectorPanel.tsx +143 -0
- package/src/app/components/inspector/InspectorProvider.tsx +33 -3
- package/src/app/lib/assets.ts +166 -0
- package/src/app/lib/export-pdf.ts +1 -4
- package/src/app/lib/inspector/useEditor.ts +2 -1
- package/src/app/routes/Slide.tsx +96 -51
package/dist/cli/bin.js
CHANGED
|
@@ -22,15 +22,15 @@ async function run(argv) {
|
|
|
22
22
|
const program = new Command();
|
|
23
23
|
program.name("open-slide").description("Author slides — we handle the Vite/React stack.").version(version, "-v, --version", "print version").helpOption("-h, --help", "show help").showHelpAfterError(chalk.dim("(run `open-slide --help` for usage)"));
|
|
24
24
|
program.command("dev").description("Start the dev server").addOption(new Option("-p, --port <port>", "port to listen on").argParser(parsePort)).addOption(new Option("--host [host]", "expose on the network (optional host)")).option("--open", "open the browser on start").action(async (flags) => {
|
|
25
|
-
const { dev } = await import("../dev-
|
|
25
|
+
const { dev } = await import("../dev-B3JzCYn7.js");
|
|
26
26
|
await dev(flags);
|
|
27
27
|
});
|
|
28
28
|
program.command("build").description("Build a static site").option("--out-dir <dir>", "output directory (defaults to `dist`)").action(async (flags) => {
|
|
29
|
-
const { build } = await import("../build-
|
|
29
|
+
const { build } = await import("../build-DHiRlpjn.js");
|
|
30
30
|
await build(flags);
|
|
31
31
|
});
|
|
32
32
|
program.command("preview").description("Preview the production build").addOption(new Option("-p, --port <port>", "port to listen on").argParser(parsePort)).addOption(new Option("--host [host]", "expose on the network (optional host)")).option("--open", "open the browser on start").action(async (flags) => {
|
|
33
|
-
const { preview } = await import("../preview-
|
|
33
|
+
const { preview } = await import("../preview-UikovHEt.js");
|
|
34
34
|
await preview(flags);
|
|
35
35
|
});
|
|
36
36
|
await program.parseAsync(argv, { from: "user" });
|
|
@@ -312,6 +312,116 @@ function buildTextSplice(element, value) {
|
|
|
312
312
|
}
|
|
313
313
|
return { error: "element has complex children" };
|
|
314
314
|
}
|
|
315
|
+
function findImports(ast) {
|
|
316
|
+
const body = ast.program?.body ?? [];
|
|
317
|
+
const out = [];
|
|
318
|
+
for (const node of body) {
|
|
319
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
320
|
+
const src = node.source?.value;
|
|
321
|
+
if (typeof src !== "string") continue;
|
|
322
|
+
const specs = node.specifiers ?? [];
|
|
323
|
+
let def = null;
|
|
324
|
+
for (const spec of specs) if (spec.type === "ImportDefaultSpecifier") {
|
|
325
|
+
const local = spec.local?.name;
|
|
326
|
+
if (typeof local === "string") {
|
|
327
|
+
def = local;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
out.push({
|
|
332
|
+
node,
|
|
333
|
+
source: src,
|
|
334
|
+
defaultIdent: def
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
function collectTopLevelIdentifiers(ast) {
|
|
340
|
+
const names = new Set();
|
|
341
|
+
for (const imp of findImports(ast)) {
|
|
342
|
+
if (imp.defaultIdent) names.add(imp.defaultIdent);
|
|
343
|
+
const specs = imp.node.specifiers ?? [];
|
|
344
|
+
for (const spec of specs) if (spec.type !== "ImportDefaultSpecifier") {
|
|
345
|
+
const local = spec.local?.name;
|
|
346
|
+
if (typeof local === "string") names.add(local);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return names;
|
|
350
|
+
}
|
|
351
|
+
function safeAssetIdentifier(filename, taken) {
|
|
352
|
+
const stem = filename.replace(/\.[^.]+$/, "");
|
|
353
|
+
let camel = "";
|
|
354
|
+
let upper = false;
|
|
355
|
+
for (const ch of stem) if (/[A-Za-z0-9]/.test(ch)) {
|
|
356
|
+
camel += upper ? ch.toUpperCase() : ch;
|
|
357
|
+
upper = false;
|
|
358
|
+
} else upper = camel.length > 0;
|
|
359
|
+
let base = camel;
|
|
360
|
+
if (!base || !/^[A-Za-z_$]/.test(base)) base = `asset${base.charAt(0).toUpperCase()}${base.slice(1)}` || "asset";
|
|
361
|
+
base = base.charAt(0).toLowerCase() + base.slice(1);
|
|
362
|
+
let candidate = base;
|
|
363
|
+
let i = 2;
|
|
364
|
+
while (taken.has(candidate)) {
|
|
365
|
+
candidate = `${base}${i}`;
|
|
366
|
+
i += 1;
|
|
367
|
+
}
|
|
368
|
+
return candidate;
|
|
369
|
+
}
|
|
370
|
+
function findJsxAttr(opening, name) {
|
|
371
|
+
const attrs = opening.attributes ?? [];
|
|
372
|
+
for (const attr of attrs) {
|
|
373
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
374
|
+
const n = attr.name;
|
|
375
|
+
if (n?.type === "JSXIdentifier" && n.name === name) return attr;
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
function planAssetAttr(ast, element, attr, assetPath) {
|
|
380
|
+
const opening = element.openingElement;
|
|
381
|
+
if (!opening) return { error: "no opening element" };
|
|
382
|
+
if (!attr || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) return { error: "invalid attribute name" };
|
|
383
|
+
if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
|
|
384
|
+
const imports = findImports(ast);
|
|
385
|
+
let identifier = null;
|
|
386
|
+
for (const imp of imports) if (imp.source === assetPath && imp.defaultIdent) {
|
|
387
|
+
identifier = imp.defaultIdent;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
let importSplice = null;
|
|
391
|
+
if (!identifier) {
|
|
392
|
+
const filename = assetPath.slice(assetPath.lastIndexOf("/") + 1);
|
|
393
|
+
const taken = collectTopLevelIdentifiers(ast);
|
|
394
|
+
identifier = safeAssetIdentifier(filename, taken);
|
|
395
|
+
const importStmt = `import ${identifier} from '${assetPath.replace(/'/g, "\\'")}';\n`;
|
|
396
|
+
const insertAt = imports.length > 0 ? imports[imports.length - 1].node.end : 0;
|
|
397
|
+
const prefix = imports.length > 0 ? "\n" : "";
|
|
398
|
+
importSplice = {
|
|
399
|
+
from: insertAt,
|
|
400
|
+
to: insertAt,
|
|
401
|
+
text: prefix + importStmt
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const newAttr = `${attr}={${identifier}}`;
|
|
405
|
+
const existing = findJsxAttr(opening, attr);
|
|
406
|
+
let attrSplice;
|
|
407
|
+
if (existing) attrSplice = {
|
|
408
|
+
from: existing.start,
|
|
409
|
+
to: existing.end,
|
|
410
|
+
text: newAttr
|
|
411
|
+
};
|
|
412
|
+
else {
|
|
413
|
+
const name = opening.name;
|
|
414
|
+
attrSplice = {
|
|
415
|
+
from: name.end,
|
|
416
|
+
to: name.end,
|
|
417
|
+
text: ` ${newAttr}`
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
importSplice,
|
|
422
|
+
attrSplice
|
|
423
|
+
};
|
|
424
|
+
}
|
|
315
425
|
function applyEdit(source, line, column, ops) {
|
|
316
426
|
if (ops.length === 0) return {
|
|
317
427
|
ok: true,
|
|
@@ -347,6 +457,36 @@ function applyEdit(source, line, column, ops) {
|
|
|
347
457
|
};
|
|
348
458
|
splices.push(result);
|
|
349
459
|
}
|
|
460
|
+
const assetOps = ops.flatMap((op) => op.kind === "set-attr-asset" ? [op] : []);
|
|
461
|
+
if (assetOps.length > 0) {
|
|
462
|
+
const ast = parseSource(source);
|
|
463
|
+
if (!ast) return {
|
|
464
|
+
ok: false,
|
|
465
|
+
status: 422,
|
|
466
|
+
error: "could not parse source"
|
|
467
|
+
};
|
|
468
|
+
const importSplices = [];
|
|
469
|
+
for (const op of assetOps) {
|
|
470
|
+
const plan = planAssetAttr(ast, element, op.attr, op.assetPath);
|
|
471
|
+
if ("error" in plan) return {
|
|
472
|
+
ok: false,
|
|
473
|
+
status: 422,
|
|
474
|
+
error: plan.error
|
|
475
|
+
};
|
|
476
|
+
splices.push(plan.attrSplice);
|
|
477
|
+
if (plan.importSplice) importSplices.push(plan.importSplice);
|
|
478
|
+
}
|
|
479
|
+
if (importSplices.length > 0) {
|
|
480
|
+
const from = importSplices[0].from;
|
|
481
|
+
const to = importSplices[0].to;
|
|
482
|
+
const text = importSplices.map((s) => s.text).join("");
|
|
483
|
+
splices.push({
|
|
484
|
+
from,
|
|
485
|
+
to,
|
|
486
|
+
text
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
350
490
|
if (splices.length === 0) return {
|
|
351
491
|
ok: true,
|
|
352
492
|
source
|
|
@@ -517,6 +657,45 @@ function commentsPlugin(opts) {
|
|
|
517
657
|
const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
|
|
518
658
|
const SLIDE_ID_RE = /^[a-z0-9_-]+$/i;
|
|
519
659
|
const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
660
|
+
const ASSET_FORBIDDEN_RE = /[\x00-\x1F\x7F/\\:*?"<>|]/;
|
|
661
|
+
const ASSET_MAX_BYTES = 25 * 1024 * 1024;
|
|
662
|
+
const MIME_BY_EXT = {
|
|
663
|
+
png: "image/png",
|
|
664
|
+
jpg: "image/jpeg",
|
|
665
|
+
jpeg: "image/jpeg",
|
|
666
|
+
gif: "image/gif",
|
|
667
|
+
svg: "image/svg+xml",
|
|
668
|
+
webp: "image/webp",
|
|
669
|
+
avif: "image/avif",
|
|
670
|
+
ico: "image/x-icon",
|
|
671
|
+
mp4: "video/mp4",
|
|
672
|
+
webm: "video/webm",
|
|
673
|
+
mov: "video/quicktime",
|
|
674
|
+
woff: "font/woff",
|
|
675
|
+
woff2: "font/woff2",
|
|
676
|
+
ttf: "font/ttf",
|
|
677
|
+
otf: "font/otf",
|
|
678
|
+
json: "application/json",
|
|
679
|
+
txt: "text/plain; charset=utf-8",
|
|
680
|
+
md: "text/markdown; charset=utf-8"
|
|
681
|
+
};
|
|
682
|
+
function mimeForFilename(name) {
|
|
683
|
+
const dot = name.lastIndexOf(".");
|
|
684
|
+
if (dot < 0) return "application/octet-stream";
|
|
685
|
+
const ext = name.slice(dot + 1).toLowerCase();
|
|
686
|
+
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
687
|
+
}
|
|
688
|
+
function validateAssetName(v) {
|
|
689
|
+
if (typeof v !== "string") return null;
|
|
690
|
+
const trimmed = v.trim();
|
|
691
|
+
if (trimmed.length < 1 || trimmed.length > 120) return null;
|
|
692
|
+
if (ASSET_FORBIDDEN_RE.test(trimmed)) return null;
|
|
693
|
+
if (trimmed.startsWith(".") || trimmed.startsWith("~")) return null;
|
|
694
|
+
if (trimmed === ".." || trimmed.split(/[/\\]/).includes("..")) return null;
|
|
695
|
+
const dot = trimmed.lastIndexOf(".");
|
|
696
|
+
if (dot <= 0 || dot === trimmed.length - 1) return null;
|
|
697
|
+
return trimmed;
|
|
698
|
+
}
|
|
520
699
|
async function readBody(req) {
|
|
521
700
|
return await new Promise((resolve, reject) => {
|
|
522
701
|
const chunks = [];
|
|
@@ -590,6 +769,22 @@ async function rmSlideDir(slidesRoot, slideId) {
|
|
|
590
769
|
return false;
|
|
591
770
|
}
|
|
592
771
|
}
|
|
772
|
+
function resolveAssetsDir(slidesRoot, slideId) {
|
|
773
|
+
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
774
|
+
const slideDir = path.resolve(slidesRoot, slideId);
|
|
775
|
+
if (!slideDir.startsWith(slidesRoot + path.sep)) return null;
|
|
776
|
+
const assetsDir = path.resolve(slideDir, "assets");
|
|
777
|
+
if (assetsDir !== path.join(slideDir, "assets")) return null;
|
|
778
|
+
return assetsDir;
|
|
779
|
+
}
|
|
780
|
+
function resolveAssetFile(slidesRoot, slideId, filename) {
|
|
781
|
+
const assetsDir = resolveAssetsDir(slidesRoot, slideId);
|
|
782
|
+
if (!assetsDir) return null;
|
|
783
|
+
if (!validateAssetName(filename)) return null;
|
|
784
|
+
const file = path.resolve(assetsDir, filename);
|
|
785
|
+
if (!file.startsWith(assetsDir + path.sep)) return null;
|
|
786
|
+
return file;
|
|
787
|
+
}
|
|
593
788
|
function resolveSlideEntry(slidesRoot, slideId) {
|
|
594
789
|
if (!SLIDE_ID_RE.test(slideId)) return null;
|
|
595
790
|
const dir = path.resolve(slidesRoot, slideId);
|
|
@@ -690,6 +885,22 @@ function filesPlugin(opts) {
|
|
|
690
885
|
event: "open-slide:files-changed"
|
|
691
886
|
});
|
|
692
887
|
});
|
|
888
|
+
const onAssetChange = (p) => {
|
|
889
|
+
if (!p.startsWith(slidesRoot + path.sep)) return;
|
|
890
|
+
const rel = p.slice(slidesRoot.length + 1);
|
|
891
|
+
const parts = rel.split(path.sep);
|
|
892
|
+
if (parts.length < 3 || parts[1] !== "assets") return;
|
|
893
|
+
const slideId = parts[0];
|
|
894
|
+
if (!SLIDE_ID_RE.test(slideId)) return;
|
|
895
|
+
server.ws.send({
|
|
896
|
+
type: "custom",
|
|
897
|
+
event: "open-slide:assets-changed",
|
|
898
|
+
data: { slideId }
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
server.watcher.on("add", onAssetChange);
|
|
902
|
+
server.watcher.on("change", onAssetChange);
|
|
903
|
+
server.watcher.on("unlink", onAssetChange);
|
|
693
904
|
server.middlewares.use("/__slides", async (req, res, next) => {
|
|
694
905
|
const url = new URL(req.url ?? "/", "http://local");
|
|
695
906
|
const method = req.method ?? "GET";
|
|
@@ -733,6 +944,172 @@ function filesPlugin(opts) {
|
|
|
733
944
|
json(res, 500, { error: String(err.message ?? err) });
|
|
734
945
|
}
|
|
735
946
|
});
|
|
947
|
+
server.middlewares.use("/__assets", async (req, res, next) => {
|
|
948
|
+
const url = new URL(req.url ?? "/", "http://local");
|
|
949
|
+
const method = req.method ?? "GET";
|
|
950
|
+
try {
|
|
951
|
+
const listMatch = url.pathname.match(/^\/([^/]+)\/?$/);
|
|
952
|
+
const fileMatch = url.pathname.match(/^\/([^/]+)\/([^/]+)$/);
|
|
953
|
+
if (listMatch && method === "GET") {
|
|
954
|
+
const slideId = listMatch[1];
|
|
955
|
+
const assetsDir = resolveAssetsDir(slidesRoot, slideId);
|
|
956
|
+
if (!assetsDir) return json(res, 400, { error: "invalid slideId" });
|
|
957
|
+
let entries;
|
|
958
|
+
try {
|
|
959
|
+
entries = await fs.readdir(assetsDir);
|
|
960
|
+
} catch (err) {
|
|
961
|
+
if (err.code === "ENOENT") return json(res, 200, { assets: [] });
|
|
962
|
+
throw err;
|
|
963
|
+
}
|
|
964
|
+
const assets = [];
|
|
965
|
+
for (const name of entries) {
|
|
966
|
+
if (!validateAssetName(name)) continue;
|
|
967
|
+
const stat = await fs.stat(path.join(assetsDir, name));
|
|
968
|
+
if (!stat.isFile()) continue;
|
|
969
|
+
assets.push({
|
|
970
|
+
name,
|
|
971
|
+
size: stat.size,
|
|
972
|
+
mtime: stat.mtimeMs,
|
|
973
|
+
mime: mimeForFilename(name),
|
|
974
|
+
url: `/__assets/${slideId}/${encodeURIComponent(name)}`
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
assets.sort((a, b) => a.name.localeCompare(b.name));
|
|
978
|
+
return json(res, 200, { assets });
|
|
979
|
+
}
|
|
980
|
+
if (fileMatch) {
|
|
981
|
+
const slideId = fileMatch[1];
|
|
982
|
+
const filename = decodeURIComponent(fileMatch[2]);
|
|
983
|
+
const file = resolveAssetFile(slidesRoot, slideId, filename);
|
|
984
|
+
if (!file) return json(res, 400, { error: "invalid path" });
|
|
985
|
+
if (method === "GET") try {
|
|
986
|
+
const buf = await fs.readFile(file);
|
|
987
|
+
res.statusCode = 200;
|
|
988
|
+
res.setHeader("content-type", mimeForFilename(filename));
|
|
989
|
+
res.setHeader("cache-control", "no-store");
|
|
990
|
+
res.end(buf);
|
|
991
|
+
return;
|
|
992
|
+
} catch (err) {
|
|
993
|
+
if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
|
|
994
|
+
throw err;
|
|
995
|
+
}
|
|
996
|
+
if (method === "POST") {
|
|
997
|
+
const overwrite = url.searchParams.get("overwrite") === "1";
|
|
998
|
+
const lenHeader = req.headers["content-length"];
|
|
999
|
+
const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
|
|
1000
|
+
if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json(res, 413, { error: "file too large" });
|
|
1001
|
+
if (!overwrite) try {
|
|
1002
|
+
await fs.access(file);
|
|
1003
|
+
return json(res, 409, { error: "asset exists" });
|
|
1004
|
+
} catch {}
|
|
1005
|
+
const assetsDir = resolveAssetsDir(slidesRoot, slideId);
|
|
1006
|
+
if (!assetsDir) return json(res, 400, { error: "invalid slideId" });
|
|
1007
|
+
await fs.mkdir(assetsDir, { recursive: true });
|
|
1008
|
+
const chunks = [];
|
|
1009
|
+
let total = 0;
|
|
1010
|
+
let oversized = false;
|
|
1011
|
+
await new Promise((resolve, reject) => {
|
|
1012
|
+
req.on("data", (c) => {
|
|
1013
|
+
total += c.length;
|
|
1014
|
+
if (total > ASSET_MAX_BYTES) {
|
|
1015
|
+
oversized = true;
|
|
1016
|
+
req.destroy();
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
chunks.push(c);
|
|
1020
|
+
});
|
|
1021
|
+
req.on("end", () => resolve());
|
|
1022
|
+
req.on("error", reject);
|
|
1023
|
+
});
|
|
1024
|
+
if (oversized) return json(res, 413, { error: "file too large" });
|
|
1025
|
+
await fs.writeFile(file, Buffer.concat(chunks));
|
|
1026
|
+
return json(res, 200, {
|
|
1027
|
+
ok: true,
|
|
1028
|
+
name: filename,
|
|
1029
|
+
size: total,
|
|
1030
|
+
mime: mimeForFilename(filename),
|
|
1031
|
+
url: `/__assets/${slideId}/${encodeURIComponent(filename)}`
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
if (method === "PATCH") {
|
|
1035
|
+
const body = await readBody(req);
|
|
1036
|
+
const target = validateAssetName(body.name);
|
|
1037
|
+
if (!target) return json(res, 400, { error: "invalid name" });
|
|
1038
|
+
if (target === filename) return json(res, 200, {
|
|
1039
|
+
ok: true,
|
|
1040
|
+
name: filename
|
|
1041
|
+
});
|
|
1042
|
+
const dest = resolveAssetFile(slidesRoot, slideId, target);
|
|
1043
|
+
if (!dest) return json(res, 400, { error: "invalid name" });
|
|
1044
|
+
try {
|
|
1045
|
+
await fs.access(dest);
|
|
1046
|
+
return json(res, 409, { error: "target exists" });
|
|
1047
|
+
} catch {}
|
|
1048
|
+
try {
|
|
1049
|
+
await fs.rename(file, dest);
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
|
|
1052
|
+
throw err;
|
|
1053
|
+
}
|
|
1054
|
+
return json(res, 200, {
|
|
1055
|
+
ok: true,
|
|
1056
|
+
name: target
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
if (method === "DELETE") {
|
|
1060
|
+
try {
|
|
1061
|
+
await fs.unlink(file);
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
|
|
1064
|
+
throw err;
|
|
1065
|
+
}
|
|
1066
|
+
return json(res, 200, { ok: true });
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return next();
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
json(res, 500, { error: String(err.message ?? err) });
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
server.middlewares.use("/__svgl", async (req, res, next) => {
|
|
1075
|
+
const reqUrl = new URL(req.url ?? "/", "http://local");
|
|
1076
|
+
const method = req.method ?? "GET";
|
|
1077
|
+
if (method !== "GET") return next();
|
|
1078
|
+
try {
|
|
1079
|
+
let target = null;
|
|
1080
|
+
if (reqUrl.pathname === "/search") {
|
|
1081
|
+
const params = new URLSearchParams();
|
|
1082
|
+
const q = reqUrl.searchParams.get("q");
|
|
1083
|
+
const limit = reqUrl.searchParams.get("limit");
|
|
1084
|
+
if (q) params.set("search", q);
|
|
1085
|
+
if (limit) params.set("limit", limit);
|
|
1086
|
+
const qs = params.toString();
|
|
1087
|
+
target = `https://api.svgl.app/${qs ? `?${qs}` : ""}`;
|
|
1088
|
+
} else if (reqUrl.pathname === "/svg") {
|
|
1089
|
+
const u = reqUrl.searchParams.get("u");
|
|
1090
|
+
if (!u) return json(res, 400, { error: "missing u" });
|
|
1091
|
+
let parsed;
|
|
1092
|
+
try {
|
|
1093
|
+
parsed = new URL(u);
|
|
1094
|
+
} catch {
|
|
1095
|
+
return json(res, 400, { error: "invalid u" });
|
|
1096
|
+
}
|
|
1097
|
+
if (parsed.protocol !== "https:") return json(res, 400, { error: "https only" });
|
|
1098
|
+
const host = parsed.hostname.toLowerCase();
|
|
1099
|
+
if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json(res, 400, { error: "host not allowed" });
|
|
1100
|
+
target = parsed.toString();
|
|
1101
|
+
} else return next();
|
|
1102
|
+
const upstream = await fetch(target);
|
|
1103
|
+
const ct = upstream.headers.get("content-type") ?? "application/octet-stream";
|
|
1104
|
+
res.statusCode = upstream.status;
|
|
1105
|
+
res.setHeader("content-type", ct);
|
|
1106
|
+
res.setHeader("cache-control", "no-store");
|
|
1107
|
+
const buf = Buffer.from(await upstream.arrayBuffer());
|
|
1108
|
+
res.end(buf);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
json(res, 502, { error: String(err.message ?? err) });
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
736
1113
|
server.middlewares.use("/__folders", async (req, res, next) => {
|
|
737
1114
|
const url = new URL(req.url ?? "/", "http://local");
|
|
738
1115
|
const method = req.method ?? "GET";
|
package/dist/vite/index.js
CHANGED