dsh-claude-design 2.6.0 → 2.7.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 +3 -1
- package/lib/client.js +625 -40
- package/lib/index.js +273 -51
- package/package.json +8 -2
- package/skills/claude-design/SKILL.md +21 -1
- package/skills/claude-design/artifact-type/reference/print.md +13 -7
- package/skills/claude-design/scripts/artifact.mjs +234 -45
- package/skills/claude-design/scripts/editor/assets/anthropicons-fizzy-lake-CfC2JIbf.woff2 +0 -0
- package/skills/claude-design/scripts/editor/assets/{index-BZKwYnL9.js → index-DyKwtQBI.js} +1011 -373
- package/skills/claude-design/scripts/editor/assets/index-tSD8GYWj.css +1 -0
- package/skills/claude-design/scripts/editor/index.html +2 -2
- package/skills/claude-design/scripts/editor/share-ui.js +37 -7
- package/skills/claude-design/scripts/server.mjs +859 -105
- package/skills/claude-design/scripts/editor/assets/anthropicons-fizzy-lake-c1fxtHDl.woff2 +0 -0
- package/skills/claude-design/scripts/editor/assets/index-oDSQuawR.css +0 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// claude-design: local editor server (prebuilt; do not edit).
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs11 from "node:fs";
|
|
5
|
+
import path11 from "node:path";
|
|
6
6
|
|
|
7
7
|
import { createHash as createHash3, randomBytes as randomBytes2, randomUUID } from "node:crypto";
|
|
8
|
-
import
|
|
9
|
-
import
|
|
8
|
+
import fs10 from "node:fs";
|
|
9
|
+
import fsp4 from "node:fs/promises";
|
|
10
10
|
import http from "node:http";
|
|
11
|
-
import
|
|
11
|
+
import path10 from "node:path";
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
13
|
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
@@ -773,6 +773,8 @@ import net from "node:net";
|
|
|
773
773
|
import os2 from "node:os";
|
|
774
774
|
import path2 from "node:path";
|
|
775
775
|
var STATE_FILE = ".artifact-server.json";
|
|
776
|
+
var LOG_FILE = ".artifact-server.log";
|
|
777
|
+
var EXPORTS_DIR = "exports";
|
|
776
778
|
function readState(root) {
|
|
777
779
|
try {
|
|
778
780
|
return JSON.parse(fs3.readFileSync(path2.join(root, STATE_FILE), "utf8"));
|
|
@@ -802,6 +804,7 @@ function recordServer(state, close, signals = ["SIGINT", "SIGTERM"]) {
|
|
|
802
804
|
const file = path2.join(state.root, STATE_FILE);
|
|
803
805
|
fs3.writeFileSync(file, `${JSON.stringify(state, null, 2)}
|
|
804
806
|
`);
|
|
807
|
+
writeGitignore(state.root, state.kind);
|
|
805
808
|
const shutdown = () => {
|
|
806
809
|
if (readState(state.root)?.pid === process.pid) fs3.rmSync(file, { force: true });
|
|
807
810
|
close();
|
|
@@ -809,6 +812,15 @@ function recordServer(state, close, signals = ["SIGINT", "SIGTERM"]) {
|
|
|
809
812
|
};
|
|
810
813
|
for (const signal of signals) process.on(signal, shutdown);
|
|
811
814
|
}
|
|
815
|
+
var GITIGNORE_FILE = ".gitignore";
|
|
816
|
+
var gitignoreLines = (kind) => [STATE_FILE, LOG_FILE, ".sharing.json", ...kind === "docs" ? [] : ["history/"], ".*.tmp", ".DS_Store"];
|
|
817
|
+
function writeGitignore(root, kind) {
|
|
818
|
+
const file = path2.join(root, GITIGNORE_FILE);
|
|
819
|
+
try {
|
|
820
|
+
fs3.writeFileSync(file, ["# Written by baoyu-design when the editor first opened this folder: what only this computer's editor server needs.", ...gitignoreLines(kind), ""].join("\n"), { flag: "wx" });
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
}
|
|
812
824
|
async function listen(server2, port, host) {
|
|
813
825
|
await new Promise((resolve, reject) => {
|
|
814
826
|
server2.once("error", reject);
|
|
@@ -830,6 +842,10 @@ var PortsTaken = class extends Error {
|
|
|
830
842
|
}
|
|
831
843
|
};
|
|
832
844
|
var isBenchmarkRange = (address) => /^198\.1[89]\./.test(address);
|
|
845
|
+
function isTailnet(address) {
|
|
846
|
+
const m = /^100\.(\d{1,3})\./.exec(address);
|
|
847
|
+
return !!m && Number(m[1]) >= 64 && Number(m[1]) <= 127;
|
|
848
|
+
}
|
|
833
849
|
function lanAddresses() {
|
|
834
850
|
const out = [];
|
|
835
851
|
for (const list of Object.values(os2.networkInterfaces())) {
|
|
@@ -838,7 +854,7 @@ function lanAddresses() {
|
|
|
838
854
|
out.push(entry.address);
|
|
839
855
|
}
|
|
840
856
|
}
|
|
841
|
-
return [...new Set(out)];
|
|
857
|
+
return [...new Set(out)].sort((a, b) => Number(isTailnet(a)) - Number(isTailnet(b)));
|
|
842
858
|
}
|
|
843
859
|
function answers(host, port) {
|
|
844
860
|
return new Promise((resolve) => {
|
|
@@ -890,8 +906,8 @@ async function listenFree(server2, from, host, first) {
|
|
|
890
906
|
throw new PortsTaken(from, to);
|
|
891
907
|
}
|
|
892
908
|
function serverOrigin(host, port) {
|
|
893
|
-
const
|
|
894
|
-
return `http://${
|
|
909
|
+
const shown2 = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host.includes(":") ? `[${host}]` : host;
|
|
910
|
+
return `http://${shown2}:${port}`;
|
|
895
911
|
}
|
|
896
912
|
function sendJson2(response, status, body, headers = {}) {
|
|
897
913
|
const text = JSON.stringify(body);
|
|
@@ -1088,9 +1104,401 @@ function mergeTextBytes(base, mine, current) {
|
|
|
1088
1104
|
}
|
|
1089
1105
|
}
|
|
1090
1106
|
|
|
1091
|
-
import
|
|
1107
|
+
import path4 from "node:path";
|
|
1108
|
+
|
|
1092
1109
|
import fs4 from "node:fs";
|
|
1093
1110
|
import path3 from "node:path";
|
|
1111
|
+
var FOLDER = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
1112
|
+
var COMPONENT = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
1113
|
+
var NOT_COMPONENTS = /* @__PURE__ */ new Set(["lib", "src", "api", "archived"]);
|
|
1114
|
+
var MAX_COMPONENTS = 400;
|
|
1115
|
+
var MAX_TOKENS = 2e3;
|
|
1116
|
+
var MAX_README = 24e3;
|
|
1117
|
+
var MAX_COMPONENT_README = 8e3;
|
|
1118
|
+
var MAX_TYPES = 4e3;
|
|
1119
|
+
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
1120
|
+
var str = (v) => typeof v === "string" ? v : typeof v === "number" ? String(v) : void 0;
|
|
1121
|
+
function noLink(p) {
|
|
1122
|
+
try {
|
|
1123
|
+
return fs4.realpathSync(p) === path3.resolve(p);
|
|
1124
|
+
} catch {
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function readText(file, max) {
|
|
1129
|
+
try {
|
|
1130
|
+
if (!noLink(file)) return void 0;
|
|
1131
|
+
const stat = fs4.lstatSync(file);
|
|
1132
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return void 0;
|
|
1133
|
+
const text = fs4.readFileSync(file, "utf8");
|
|
1134
|
+
return text.length > max ? `${text.slice(0, max)}
|
|
1135
|
+
\u2026` : text;
|
|
1136
|
+
} catch {
|
|
1137
|
+
return void 0;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function readable(file) {
|
|
1141
|
+
return noLink(file) && fs4.lstatSync(file).isFile();
|
|
1142
|
+
}
|
|
1143
|
+
function readJson2(file) {
|
|
1144
|
+
const text = readText(file, MAX_FILE_BYTES);
|
|
1145
|
+
if (text === void 0) return void 0;
|
|
1146
|
+
try {
|
|
1147
|
+
return JSON.parse(text);
|
|
1148
|
+
} catch {
|
|
1149
|
+
return void 0;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function tokensOf(tokens) {
|
|
1153
|
+
const themes = [];
|
|
1154
|
+
const families = [];
|
|
1155
|
+
let count = 0;
|
|
1156
|
+
for (const [name, family] of Object.entries(tokens)) {
|
|
1157
|
+
if (name === "type" || !family || typeof family !== "object" || Array.isArray(family)) continue;
|
|
1158
|
+
const list = family.tokens;
|
|
1159
|
+
if (!Array.isArray(list)) continue;
|
|
1160
|
+
if (name === "color") {
|
|
1161
|
+
for (const theme of family.themes ?? []) {
|
|
1162
|
+
const t = theme;
|
|
1163
|
+
const id = str(t?.id);
|
|
1164
|
+
if (id) themes.push({ id, name: str(t.name) ?? id });
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const out = [];
|
|
1168
|
+
for (const raw of list) {
|
|
1169
|
+
if (count >= MAX_TOKENS) break;
|
|
1170
|
+
const t = raw;
|
|
1171
|
+
const tokenName = str(t?.name);
|
|
1172
|
+
if (!tokenName) continue;
|
|
1173
|
+
let value = str(t.value);
|
|
1174
|
+
if (value === void 0 && t.value && typeof t.value === "object" && !Array.isArray(t.value)) {
|
|
1175
|
+
const byTheme = {};
|
|
1176
|
+
for (const [k, v] of Object.entries(t.value)) if (str(v) !== void 0) byTheme[k] = str(v);
|
|
1177
|
+
value = byTheme;
|
|
1178
|
+
}
|
|
1179
|
+
if (value === void 0) continue;
|
|
1180
|
+
out.push({ name: tokenName, value, ...str(t.usage) ? { usage: str(t.usage) } : {} });
|
|
1181
|
+
count += 1;
|
|
1182
|
+
}
|
|
1183
|
+
if (out.length) families.push({ name, tokens: out });
|
|
1184
|
+
}
|
|
1185
|
+
families.sort((a, b) => Number(b.name === "color") - Number(a.name === "color"));
|
|
1186
|
+
const type = tokens.type ?? {};
|
|
1187
|
+
const fontFamilies = {};
|
|
1188
|
+
if (type.families && typeof type.families === "object") {
|
|
1189
|
+
for (const [k, v] of Object.entries(type.families)) if (str(v)) fontFamilies[k] = str(v);
|
|
1190
|
+
}
|
|
1191
|
+
const textStyles = [];
|
|
1192
|
+
for (const g of Array.isArray(type.groups) ? type.groups : []) {
|
|
1193
|
+
const group = g;
|
|
1194
|
+
for (const st of Array.isArray(group.styles) ? group.styles : []) {
|
|
1195
|
+
const s = st;
|
|
1196
|
+
const name = str(s?.name);
|
|
1197
|
+
if (!name || textStyles.length >= MAX_TOKENS) continue;
|
|
1198
|
+
textStyles.push({
|
|
1199
|
+
group: str(group.name) ?? "",
|
|
1200
|
+
name,
|
|
1201
|
+
...str(s.family) ?? str(group.family) ? { family: str(s.family) ?? str(group.family) } : {},
|
|
1202
|
+
...str(s.fontSize) ? { fontSize: str(s.fontSize) } : {},
|
|
1203
|
+
...str(s.lineHeight) ? { lineHeight: str(s.lineHeight) } : {},
|
|
1204
|
+
...str(s.fontWeight) ? { fontWeight: str(s.fontWeight) } : {}
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return { themes, families, fontFamilies, textStyles };
|
|
1209
|
+
}
|
|
1210
|
+
function summaryOf(readme) {
|
|
1211
|
+
const body = readme.split("\n").filter((line) => !/^\s*#/.test(line)).join(" ").replace(/\s+/g, " ").trim();
|
|
1212
|
+
if (!body) return void 0;
|
|
1213
|
+
const sentence = /^(.+?[.!?。!?])(\s|$)/.exec(body)?.[1] ?? body;
|
|
1214
|
+
return sentence.length > 200 ? `${sentence.slice(0, 200)}\u2026` : sentence;
|
|
1215
|
+
}
|
|
1216
|
+
function typesOf(dts, name) {
|
|
1217
|
+
const lines = dts.split("\n");
|
|
1218
|
+
const named = new RegExp(`^\\s*(?:export\\s+)?(?:declare\\s+)?(?:function|const|let|class|interface|type|namespace)\\s+${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:Props)?\\b`);
|
|
1219
|
+
const out = [];
|
|
1220
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1221
|
+
if (!named.test(lines[i])) continue;
|
|
1222
|
+
let start = i;
|
|
1223
|
+
while (start > 0 && /^\s*(\*|\/\*\*|\*\/)/.test(lines[start - 1])) start -= 1;
|
|
1224
|
+
let end = i;
|
|
1225
|
+
let depth = 0;
|
|
1226
|
+
for (; end < lines.length; end += 1) {
|
|
1227
|
+
for (const ch of lines[end]) depth += ch === "{" ? 1 : ch === "}" ? -1 : 0;
|
|
1228
|
+
if (depth <= 0 && (/[;}]\s*$/.test(lines[end]) || end > i)) break;
|
|
1229
|
+
}
|
|
1230
|
+
out.push(lines.slice(start, end + 1).join("\n"));
|
|
1231
|
+
i = end;
|
|
1232
|
+
if (out.join("\n\n").length > MAX_TYPES) break;
|
|
1233
|
+
}
|
|
1234
|
+
const text = out.join("\n\n");
|
|
1235
|
+
return text ? text.length > MAX_TYPES ? `${text.slice(0, MAX_TYPES)}
|
|
1236
|
+
\u2026` : text : void 0;
|
|
1237
|
+
}
|
|
1238
|
+
function componentsOf(dir) {
|
|
1239
|
+
const components = path3.join(dir, "components");
|
|
1240
|
+
if (!noLink(components)) return [];
|
|
1241
|
+
let entries;
|
|
1242
|
+
try {
|
|
1243
|
+
entries = fs4.readdirSync(components, { withFileTypes: true });
|
|
1244
|
+
} catch {
|
|
1245
|
+
return [];
|
|
1246
|
+
}
|
|
1247
|
+
const dts = readText(path3.join(components, "index.d.ts"), MAX_FILE_BYTES) ?? "";
|
|
1248
|
+
const out = [];
|
|
1249
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1250
|
+
if (out.length >= MAX_COMPONENTS) break;
|
|
1251
|
+
if (!entry.isDirectory() || !COMPONENT.test(entry.name) || NOT_COMPONENTS.has(entry.name)) continue;
|
|
1252
|
+
const own = path3.join(components, entry.name);
|
|
1253
|
+
const readme = readText(path3.join(own, "README.md"), MAX_COMPONENT_README);
|
|
1254
|
+
const types = dts ? typesOf(dts, entry.name) : void 0;
|
|
1255
|
+
out.push({
|
|
1256
|
+
name: entry.name,
|
|
1257
|
+
...readme ? { readme } : {},
|
|
1258
|
+
...readme && summaryOf(readme) ? { summary: summaryOf(readme) } : {},
|
|
1259
|
+
...types ? { types } : {},
|
|
1260
|
+
preview: readable(path3.join(own, "preview.html"))
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
return out;
|
|
1264
|
+
}
|
|
1265
|
+
function insideDs(projectDir, dir) {
|
|
1266
|
+
try {
|
|
1267
|
+
if (!noLink(dir)) return false;
|
|
1268
|
+
const real = fs4.realpathSync(dir);
|
|
1269
|
+
const ds = fs4.realpathSync(path3.join(projectDir, "ds"));
|
|
1270
|
+
return real === ds || real.startsWith(ds + path3.sep);
|
|
1271
|
+
} catch {
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function designSystemOf(projectDir, indexFile, folder) {
|
|
1276
|
+
if (!FOLDER.test(folder)) return null;
|
|
1277
|
+
const dir = path3.join(projectDir, "ds", folder);
|
|
1278
|
+
if (!fs4.existsSync(dir) || !insideDs(projectDir, dir) || !fs4.lstatSync(dir).isDirectory()) return null;
|
|
1279
|
+
const index = readJson2(path3.join(projectDir, indexFile));
|
|
1280
|
+
const records = Array.isArray(index?.designSystems) ? index.designSystems : [];
|
|
1281
|
+
const record = records.find((r) => str(r?.namespace) === folder);
|
|
1282
|
+
const tokens = readJson2(path3.join(dir, "tokens.json"));
|
|
1283
|
+
const usable = tokens && typeof tokens === "object" && !Array.isArray(tokens) ? tokens : void 0;
|
|
1284
|
+
const readme = readText(path3.join(dir, "README.md"), MAX_README);
|
|
1285
|
+
return {
|
|
1286
|
+
folder,
|
|
1287
|
+
title: (str(record?.title) ?? str(usable?.name) ?? folder).replace(/\s+[-\u2013\u2014]\s+design system$/i, ""),
|
|
1288
|
+
...str(record?.version) ? { version: str(record.version) } : {},
|
|
1289
|
+
...str(record?.copiedAt) ? { copiedAt: str(record.copiedAt) } : {},
|
|
1290
|
+
...readme ? { readme } : {},
|
|
1291
|
+
...usable ? tokensOf(usable) : { themes: [], families: [], fontFamilies: {}, textStyles: [] },
|
|
1292
|
+
components: componentsOf(dir)
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
var DS_PAGE = /^__ds\/([^/]+)\/$/;
|
|
1297
|
+
var esc = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1298
|
+
var copyHref = (folder, rel) => `../../project/ds/${folder}/${rel}`;
|
|
1299
|
+
function linkHref(href, folder, dir) {
|
|
1300
|
+
if (/^(https?:|mailto:)/i.test(href)) return href;
|
|
1301
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("/") || href.startsWith("#")) return null;
|
|
1302
|
+
const rel = path4.posix.normalize(path4.posix.join(dir, href));
|
|
1303
|
+
return rel.startsWith("..") ? null : copyHref(folder, rel);
|
|
1304
|
+
}
|
|
1305
|
+
var INLINE = /(`+)(.+?)\1|!?\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|\*\*(.+?)\*\*|\*([^*\s][^*]*?)\*/g;
|
|
1306
|
+
function inline(text, folder, dir) {
|
|
1307
|
+
let out = "";
|
|
1308
|
+
let at = 0;
|
|
1309
|
+
for (const m of text.matchAll(INLINE)) {
|
|
1310
|
+
out += esc(text.slice(at, m.index));
|
|
1311
|
+
at = m.index + m[0].length;
|
|
1312
|
+
if (m[2] !== void 0) out += `<code>${esc(m[2])}</code>`;
|
|
1313
|
+
else if (m[4] !== void 0) {
|
|
1314
|
+
const href = linkHref(m[4], folder, dir);
|
|
1315
|
+
const label = inline(m[3] || m[4], folder, dir);
|
|
1316
|
+
out += href === null ? label : `<a href="${esc(href)}" rel="noreferrer">${label}</a>`;
|
|
1317
|
+
} else if (m[5] !== void 0) out += `<strong>${inline(m[5], folder, dir)}</strong>`;
|
|
1318
|
+
else out += `<em>${inline(m[6], folder, dir)}</em>`;
|
|
1319
|
+
}
|
|
1320
|
+
return out + esc(text.slice(at));
|
|
1321
|
+
}
|
|
1322
|
+
var tableCells = (line) => line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
1323
|
+
function markdown(source, folder, dir) {
|
|
1324
|
+
const lines = source.replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n/, "").split("\n");
|
|
1325
|
+
const out = [];
|
|
1326
|
+
let i = 0;
|
|
1327
|
+
const startsBlock = (line) => /^\s*(#{1,6}\s|```|>|[-*+]\s|\d+[.)]\s)/.test(line) || /^\s*([-*_])\1{2,}\s*$/.test(line);
|
|
1328
|
+
while (i < lines.length) {
|
|
1329
|
+
const line = lines[i];
|
|
1330
|
+
const fence = /^\s*(```+)/.exec(line);
|
|
1331
|
+
if (fence) {
|
|
1332
|
+
const code = [];
|
|
1333
|
+
for (i += 1; i < lines.length && !lines[i].trim().startsWith(fence[1]); i += 1) code.push(lines[i]);
|
|
1334
|
+
i += 1;
|
|
1335
|
+
out.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);
|
|
1336
|
+
} else if (!line.trim()) {
|
|
1337
|
+
i += 1;
|
|
1338
|
+
} else if (/^#{1,6}\s/.test(line)) {
|
|
1339
|
+
const hashes = /^#+/.exec(line)[0].length;
|
|
1340
|
+
const level = Math.min(hashes + 3, 6);
|
|
1341
|
+
out.push(`<h${level}>${inline(line.slice(hashes).trim().replace(/\s#+$/, ""), folder, dir)}</h${level}>`);
|
|
1342
|
+
i += 1;
|
|
1343
|
+
} else if (/^\s*([-*_])\1{2,}\s*$/.test(line)) {
|
|
1344
|
+
out.push("<hr>");
|
|
1345
|
+
i += 1;
|
|
1346
|
+
} else if (line.includes("|") && /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(lines[i + 1] ?? "")) {
|
|
1347
|
+
const rows = [`<tr>${tableCells(line).map((c) => `<th>${inline(c, folder, dir)}</th>`).join("")}</tr>`];
|
|
1348
|
+
for (i += 2; i < lines.length && lines[i].includes("|"); i += 1) {
|
|
1349
|
+
rows.push(`<tr>${tableCells(lines[i]).map((c) => `<td>${inline(c, folder, dir)}</td>`).join("")}</tr>`);
|
|
1350
|
+
}
|
|
1351
|
+
out.push(`<div class="scroll"><table>${rows.join("")}</table></div>`);
|
|
1352
|
+
} else if (/^\s*>/.test(line)) {
|
|
1353
|
+
const quote = [];
|
|
1354
|
+
for (; i < lines.length && /^\s*>/.test(lines[i]); i += 1) quote.push(lines[i].replace(/^\s*>\s?/, ""));
|
|
1355
|
+
out.push(`<blockquote>${markdown(quote.join("\n"), folder, dir)}</blockquote>`);
|
|
1356
|
+
} else if (/^\s*([-*+]|\d+[.)])\s/.test(line)) {
|
|
1357
|
+
const ordered = /^\s*\d/.test(line);
|
|
1358
|
+
const items = [];
|
|
1359
|
+
for (; i < lines.length && /^\s*([-*+]|\d+[.)])\s/.test(lines[i]); i += 1) {
|
|
1360
|
+
const item = /^(\s*)(?:[-*+]|\d+[.)])\s+(.*)$/.exec(lines[i]);
|
|
1361
|
+
const depth = Math.floor(item[1].length / 2);
|
|
1362
|
+
items.push(`<li${depth ? ` style="margin-left:${Math.min(depth, 4)}em"` : ""}>${inline(item[2], folder, dir)}</li>`);
|
|
1363
|
+
}
|
|
1364
|
+
out.push(`<${ordered ? "ol" : "ul"}>${items.join("")}</${ordered ? "ol" : "ul"}>`);
|
|
1365
|
+
} else {
|
|
1366
|
+
const text = [];
|
|
1367
|
+
for (; i < lines.length && lines[i].trim() && !startsBlock(lines[i]); i += 1) text.push(lines[i].trim());
|
|
1368
|
+
if (!text.length) text.push(lines[i++].trim());
|
|
1369
|
+
out.push(`<p>${inline(text.join(" "), folder, dir)}</p>`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return out.join("\n");
|
|
1373
|
+
}
|
|
1374
|
+
function colorOf(token2, theme, all, depth = 0) {
|
|
1375
|
+
const value = typeof token2.value === "string" ? token2.value : token2.value[theme ?? ""] ?? Object.values(token2.value)[0] ?? "";
|
|
1376
|
+
const ref = /^\{([^{}]+)\}$/.exec(value.trim());
|
|
1377
|
+
const target = ref ? all.get(ref[1]) : void 0;
|
|
1378
|
+
return target && depth < 8 ? colorOf(target, theme, all, depth + 1) : value;
|
|
1379
|
+
}
|
|
1380
|
+
function colorsSection(family, system) {
|
|
1381
|
+
const all = new Map(family.tokens.map((token2) => [token2.name, token2]));
|
|
1382
|
+
const columns = system.themes.length ? system.themes : [{ id: "", name: "" }];
|
|
1383
|
+
const head = system.themes.length ? `<tr><th></th>${columns.map((t) => `<th>${esc(t.name)}</th>`).join("")}<th></th></tr>` : "";
|
|
1384
|
+
const rows = family.tokens.map((token2) => {
|
|
1385
|
+
const cells = columns.map((theme) => {
|
|
1386
|
+
const value = colorOf(token2, theme.id || void 0, all);
|
|
1387
|
+
return `<td><span class="swatch" style="background:${esc(value)}"></span><code>${esc(value)}</code></td>`;
|
|
1388
|
+
});
|
|
1389
|
+
return `<tr><th scope="row">${esc(token2.name)}</th>${cells.join("")}<td class="muted">${esc(token2.usage ?? "")}</td></tr>`;
|
|
1390
|
+
});
|
|
1391
|
+
return `<h3>Colours</h3><div class="scroll"><table class="tokens">${head}${rows.join("")}</table></div>`;
|
|
1392
|
+
}
|
|
1393
|
+
var tokenValue = (token2) => typeof token2.value === "string" ? token2.value : Object.entries(token2.value).map(([theme, value]) => `${theme}: ${value}`).join(", ");
|
|
1394
|
+
function familySection(family) {
|
|
1395
|
+
const rows = family.tokens.map(
|
|
1396
|
+
(token2) => `<tr><th scope="row">${esc(token2.name)}</th><td><code>${esc(tokenValue(token2))}</code></td><td class="muted">${esc(token2.usage ?? "")}</td></tr>`
|
|
1397
|
+
);
|
|
1398
|
+
const title = family.name.charAt(0).toUpperCase() + family.name.slice(1);
|
|
1399
|
+
return `<h3>${esc(title)}</h3><div class="scroll"><table class="tokens">${rows.join("")}</table></div>`;
|
|
1400
|
+
}
|
|
1401
|
+
function textSection(system) {
|
|
1402
|
+
if (!system.textStyles.length) return "";
|
|
1403
|
+
const rows = system.textStyles.map((style) => {
|
|
1404
|
+
const stack = style.family ? system.fontFamilies[style.family] ?? style.family : void 0;
|
|
1405
|
+
const css = [
|
|
1406
|
+
stack ? `font-family:${stack}` : "",
|
|
1407
|
+
style.fontSize ? `font-size:${style.fontSize}` : "",
|
|
1408
|
+
style.lineHeight ? `line-height:${style.lineHeight}` : "",
|
|
1409
|
+
style.fontWeight ? `font-weight:${style.fontWeight}` : ""
|
|
1410
|
+
].filter(Boolean).join(";");
|
|
1411
|
+
const facts = [style.fontSize, style.lineHeight, style.fontWeight, style.family].filter(Boolean).join(" \xB7 ");
|
|
1412
|
+
return `<tr><td class="sample" style="${esc(css)}">${esc(style.name)}</td><td class="muted">${esc(style.group)}</td><td><code>${esc(facts)}</code></td></tr>`;
|
|
1413
|
+
});
|
|
1414
|
+
return `<h3>Text styles</h3><div class="scroll"><table class="tokens">${rows.join("")}</table></div>`;
|
|
1415
|
+
}
|
|
1416
|
+
function componentBlock(component, folder) {
|
|
1417
|
+
const dir = `components/${component.name}`;
|
|
1418
|
+
const preview = component.preview ? `<p><a href="${esc(copyHref(folder, `${dir}/preview.html`))}">Open its preview</a></p>` : "";
|
|
1419
|
+
const body = component.readme?.replace(/^\s*#\s+.*\n?/, "");
|
|
1420
|
+
const readme = body?.trim() ? `<div class="md">${markdown(body, folder, dir)}</div>` : "";
|
|
1421
|
+
const types = component.types ? `<h4>Types</h4><pre><code>${esc(component.types)}</code></pre>` : "";
|
|
1422
|
+
const empty = preview || readme || types ? "" : '<p class="muted">This component has no guidelines, types or preview.</p>';
|
|
1423
|
+
return `<details><summary><strong>${esc(component.name)}</strong>${component.summary ? `<span class="muted">${inline(component.summary, folder, dir)}</span>` : ""}</summary><div class="body">${preview}${readme}${types}${empty}</div></details>`;
|
|
1424
|
+
}
|
|
1425
|
+
var STYLE = `
|
|
1426
|
+
:root{color-scheme:light dark;--ink:#1f1e1d;--muted:#6f6e69;--line:#e4e2dc;--ground:#faf9f7;--panel:#fff;--code:#f1efea}
|
|
1427
|
+
@media (prefers-color-scheme:dark){:root{--ink:#ecebe7;--muted:#a3a29c;--line:#3a3936;--ground:#1f1e1d;--panel:#262624;--code:#32312e}}
|
|
1428
|
+
*{box-sizing:border-box}
|
|
1429
|
+
body{margin:0;background:var(--ground);color:var(--ink);font:14px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
1430
|
+
main{max-width:960px;margin:0 auto;padding:40px 24px 80px}
|
|
1431
|
+
h1{font-size:28px;line-height:1.2;margin:0 0 6px}
|
|
1432
|
+
h2{font-size:20px;margin:44px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--line)}
|
|
1433
|
+
h3{font-size:15px;margin:24px 0 8px}
|
|
1434
|
+
h4,h5,h6{font-size:14px;margin:18px 0 6px}
|
|
1435
|
+
a{color:inherit;text-underline-offset:2px}
|
|
1436
|
+
code{font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
1437
|
+
p code,li code,td code{background:var(--code);padding:1px 4px;border-radius:4px}
|
|
1438
|
+
pre{background:var(--code);padding:12px;border-radius:8px;overflow-x:auto;margin:10px 0}
|
|
1439
|
+
nav{display:flex;flex-wrap:wrap;gap:16px;margin-top:14px}
|
|
1440
|
+
hr{border:0;border-top:1px solid var(--line);margin:18px 0}
|
|
1441
|
+
blockquote{margin:10px 0;padding-left:12px;border-left:3px solid var(--line);color:var(--muted)}
|
|
1442
|
+
table{border-collapse:collapse;width:100%}
|
|
1443
|
+
th,td{padding:7px 12px 7px 0;border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;font-weight:400}
|
|
1444
|
+
tr:first-child th:not([scope]){color:var(--muted);font-size:12px}
|
|
1445
|
+
.md th{font-weight:600}
|
|
1446
|
+
th[scope]{font-weight:500;white-space:nowrap}
|
|
1447
|
+
.scroll{overflow-x:auto}
|
|
1448
|
+
.muted{color:var(--muted)}
|
|
1449
|
+
.swatch{display:inline-block;width:18px;height:18px;border-radius:5px;vertical-align:middle;margin-right:7px;box-shadow:inset 0 0 0 1px rgba(127,127,127,.35)}
|
|
1450
|
+
.tokens td code{background:none;padding:0;white-space:nowrap}
|
|
1451
|
+
.sample{white-space:nowrap}
|
|
1452
|
+
details{background:var(--panel);border:1px solid var(--line);border-radius:8px;margin:8px 0}
|
|
1453
|
+
summary{cursor:pointer;padding:10px 14px}
|
|
1454
|
+
summary .muted{margin-left:10px}
|
|
1455
|
+
.body{padding:2px 14px 12px;border-top:1px solid var(--line)}
|
|
1456
|
+
@media (max-width:600px){main{padding:24px 16px 60px}summary .muted{display:block;margin:2px 0 0}}
|
|
1457
|
+
`;
|
|
1458
|
+
function designSystemPage(projectDir, indexFile, folder, noun) {
|
|
1459
|
+
const system = designSystemOf(projectDir, indexFile, folder);
|
|
1460
|
+
if (system === null) return null;
|
|
1461
|
+
const colors = system.families.find((family) => family.name === "color");
|
|
1462
|
+
const others = system.families.filter((family) => family !== colors);
|
|
1463
|
+
const hasTokens = system.families.length > 0 || system.textStyles.length > 0;
|
|
1464
|
+
const facts = [
|
|
1465
|
+
`Read-only copy this ${noun} carries, in <code>project/ds/${esc(folder)}/</code>`,
|
|
1466
|
+
...system.version ? [`version ${esc(system.version)}`] : [],
|
|
1467
|
+
...system.copiedAt ? [`copied ${esc(system.copiedAt.slice(0, 10))}`] : []
|
|
1468
|
+
].join(" \xB7 ");
|
|
1469
|
+
const nav = [
|
|
1470
|
+
hasTokens ? '<a href="#tokens">Tokens</a>' : "",
|
|
1471
|
+
system.components.length ? `<a href="#components">Components (${system.components.length})</a>` : "",
|
|
1472
|
+
system.readme ? '<a href="#guide">Guide</a>' : ""
|
|
1473
|
+
].filter(Boolean).join("");
|
|
1474
|
+
const sections = [
|
|
1475
|
+
hasTokens ? `<section id="tokens"><h2>Tokens</h2>${colors ? colorsSection(colors, system) : ""}${textSection(system)}${others.map(familySection).join("")}</section>` : "",
|
|
1476
|
+
system.components.length ? `<section id="components"><h2>Components</h2>${system.components.map((component) => componentBlock(component, folder)).join("")}</section>` : "",
|
|
1477
|
+
system.readme ? `<section id="guide"><h2>Guide</h2><div class="md">${markdown(system.readme, folder, "")}</div></section>` : ""
|
|
1478
|
+
].filter(Boolean).join("");
|
|
1479
|
+
return `<!doctype html>
|
|
1480
|
+
<html lang="en">
|
|
1481
|
+
<head>
|
|
1482
|
+
<meta charset="utf-8">
|
|
1483
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1484
|
+
<title>${esc(system.title)}</title>
|
|
1485
|
+
<style>${STYLE}</style>
|
|
1486
|
+
</head>
|
|
1487
|
+
<body>
|
|
1488
|
+
<main>
|
|
1489
|
+
<h1>${esc(system.title)}</h1>
|
|
1490
|
+
<p class="muted">${facts}</p>
|
|
1491
|
+
${nav ? `<nav>${nav}</nav>` : ""}
|
|
1492
|
+
${sections || '<p class="muted">This copy has no tokens, components or guide.</p>'}
|
|
1493
|
+
</main>
|
|
1494
|
+
</body>
|
|
1495
|
+
</html>
|
|
1496
|
+
`;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
1500
|
+
import fs5 from "node:fs";
|
|
1501
|
+
import path5 from "node:path";
|
|
1094
1502
|
var HISTORY_DIR = "history";
|
|
1095
1503
|
var INDEX = "index.json";
|
|
1096
1504
|
var OBJECTS = "objects";
|
|
@@ -1105,13 +1513,13 @@ var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
|
1105
1513
|
var SHA = /^[0-9a-f]{64}$/;
|
|
1106
1514
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1107
1515
|
function writeAtomic2(file, data) {
|
|
1108
|
-
|
|
1109
|
-
const tmp =
|
|
1516
|
+
fs5.mkdirSync(path5.dirname(file), { recursive: true });
|
|
1517
|
+
const tmp = path5.join(path5.dirname(file), `.${path5.basename(file)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
1110
1518
|
try {
|
|
1111
|
-
|
|
1112
|
-
|
|
1519
|
+
fs5.writeFileSync(tmp, data);
|
|
1520
|
+
fs5.renameSync(tmp, file);
|
|
1113
1521
|
} catch (error) {
|
|
1114
|
-
|
|
1522
|
+
fs5.rmSync(tmp, { force: true });
|
|
1115
1523
|
throw error;
|
|
1116
1524
|
}
|
|
1117
1525
|
}
|
|
@@ -1192,10 +1600,10 @@ var History = class {
|
|
|
1192
1600
|
/** path → sha: what the files hold after the newest version. */
|
|
1193
1601
|
headState;
|
|
1194
1602
|
constructor(root) {
|
|
1195
|
-
this.dir =
|
|
1603
|
+
this.dir = path5.join(root, HISTORY_DIR);
|
|
1196
1604
|
let raw = null;
|
|
1197
1605
|
try {
|
|
1198
|
-
raw = JSON.parse(
|
|
1606
|
+
raw = JSON.parse(fs5.readFileSync(path5.join(this.dir, INDEX), "utf8"));
|
|
1199
1607
|
} catch {
|
|
1200
1608
|
}
|
|
1201
1609
|
this.index = parseIndex(raw);
|
|
@@ -1239,7 +1647,7 @@ var History = class {
|
|
|
1239
1647
|
read(sha) {
|
|
1240
1648
|
if (!SHA.test(sha)) return null;
|
|
1241
1649
|
try {
|
|
1242
|
-
return
|
|
1650
|
+
return fs5.readFileSync(path5.join(this.dir, OBJECTS, sha));
|
|
1243
1651
|
} catch {
|
|
1244
1652
|
return null;
|
|
1245
1653
|
}
|
|
@@ -1253,7 +1661,7 @@ var History = class {
|
|
|
1253
1661
|
size(sha) {
|
|
1254
1662
|
if (!SHA.test(sha)) return null;
|
|
1255
1663
|
try {
|
|
1256
|
-
return
|
|
1664
|
+
return fs5.statSync(path5.join(this.dir, OBJECTS, sha)).size;
|
|
1257
1665
|
} catch {
|
|
1258
1666
|
return null;
|
|
1259
1667
|
}
|
|
@@ -1387,8 +1795,8 @@ var History = class {
|
|
|
1387
1795
|
return out;
|
|
1388
1796
|
}
|
|
1389
1797
|
putObject(sha, bytes) {
|
|
1390
|
-
const file =
|
|
1391
|
-
if (
|
|
1798
|
+
const file = path5.join(this.dir, OBJECTS, sha);
|
|
1799
|
+
if (fs5.existsSync(file)) return;
|
|
1392
1800
|
writeAtomic2(file, bytes);
|
|
1393
1801
|
}
|
|
1394
1802
|
/** Past MAX_VERSIONS the oldest fold into the base; old notices go; contents no one holds are removed. */
|
|
@@ -1425,14 +1833,14 @@ var History = class {
|
|
|
1425
1833
|
}
|
|
1426
1834
|
let names;
|
|
1427
1835
|
try {
|
|
1428
|
-
names =
|
|
1836
|
+
names = fs5.readdirSync(path5.join(this.dir, OBJECTS));
|
|
1429
1837
|
} catch {
|
|
1430
1838
|
return;
|
|
1431
1839
|
}
|
|
1432
|
-
for (const name of names) if (SHA.test(name) && !keep.has(name))
|
|
1840
|
+
for (const name of names) if (SHA.test(name) && !keep.has(name)) fs5.rmSync(path5.join(this.dir, OBJECTS, name), { force: true });
|
|
1433
1841
|
}
|
|
1434
1842
|
save() {
|
|
1435
|
-
writeAtomic2(
|
|
1843
|
+
writeAtomic2(path5.join(this.dir, INDEX), `${JSON.stringify(this.index)}
|
|
1436
1844
|
`);
|
|
1437
1845
|
}
|
|
1438
1846
|
};
|
|
@@ -1502,13 +1910,19 @@ function diffContents(before, after) {
|
|
|
1502
1910
|
}
|
|
1503
1911
|
|
|
1504
1912
|
import { createHash as createHash2 } from "node:crypto";
|
|
1505
|
-
import
|
|
1506
|
-
import
|
|
1507
|
-
import
|
|
1913
|
+
import fs9 from "node:fs";
|
|
1914
|
+
import os4 from "node:os";
|
|
1915
|
+
import path9 from "node:path";
|
|
1916
|
+
|
|
1917
|
+
import fs7 from "node:fs";
|
|
1918
|
+
import fsp2 from "node:fs/promises";
|
|
1919
|
+
import path7 from "node:path";
|
|
1920
|
+
import { promisify } from "node:util";
|
|
1921
|
+
import zlib from "node:zlib";
|
|
1508
1922
|
|
|
1509
1923
|
import crypto from "node:crypto";
|
|
1510
|
-
import
|
|
1511
|
-
import
|
|
1924
|
+
import fs6 from "node:fs";
|
|
1925
|
+
import path6 from "node:path";
|
|
1512
1926
|
var GUEST_ROLES = ["editor", "commenter", "viewer"];
|
|
1513
1927
|
var isGuestRole = (value) => typeof value === "string" && GUEST_ROLES.includes(value);
|
|
1514
1928
|
var SHARING_FILE = ".sharing.json";
|
|
@@ -1535,13 +1949,13 @@ var Sharing = class _Sharing {
|
|
|
1535
1949
|
file;
|
|
1536
1950
|
state;
|
|
1537
1951
|
constructor(root) {
|
|
1538
|
-
this.file =
|
|
1952
|
+
this.file = path6.join(root, SHARING_FILE);
|
|
1539
1953
|
this.state = _Sharing.read(this.file);
|
|
1540
1954
|
}
|
|
1541
1955
|
static read(file) {
|
|
1542
1956
|
let raw;
|
|
1543
1957
|
try {
|
|
1544
|
-
raw = JSON.parse(
|
|
1958
|
+
raw = JSON.parse(fs6.readFileSync(file, "utf8"));
|
|
1545
1959
|
} catch {
|
|
1546
1960
|
return fresh();
|
|
1547
1961
|
}
|
|
@@ -1573,9 +1987,9 @@ var Sharing = class _Sharing {
|
|
|
1573
1987
|
}
|
|
1574
1988
|
save() {
|
|
1575
1989
|
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
1576
|
-
|
|
1990
|
+
fs6.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
|
|
1577
1991
|
`, { mode: 384 });
|
|
1578
|
-
|
|
1992
|
+
fs6.renameSync(tmp, this.file);
|
|
1579
1993
|
}
|
|
1580
1994
|
/** The role whose link has this token, while that link is on. */
|
|
1581
1995
|
linkRole(tokenGiven) {
|
|
@@ -1646,6 +2060,293 @@ var Sharing = class _Sharing {
|
|
|
1646
2060
|
}
|
|
1647
2061
|
};
|
|
1648
2062
|
|
|
2063
|
+
var LOCAL_FILES = /* @__PURE__ */ new Set([STATE_FILE, LOG_FILE, SHARING_FILE]);
|
|
2064
|
+
var LOCAL_DIRS = /* @__PURE__ */ new Set([EXPORTS_DIR]);
|
|
2065
|
+
var JUNK_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
|
|
2066
|
+
var isTemp = (name) => name.endsWith(".tmp") || name.startsWith(".tmp-");
|
|
2067
|
+
var STORED = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".heic", ".mp4", ".m4v", ".mov", ".webm", ".mp3", ".m4a", ".aac", ".ogg", ".opus", ".flac", ".woff", ".woff2", ".zip", ".gz", ".br", ".zst", ".7z"]);
|
|
2068
|
+
var MAX_ENTRIES = 65535;
|
|
2069
|
+
var MAX_BYTES = 4294967295;
|
|
2070
|
+
var ArchiveTooLarge = class extends Error {
|
|
2071
|
+
code = "TOO_LARGE";
|
|
2072
|
+
};
|
|
2073
|
+
async function archiveEntries(root, skip = []) {
|
|
2074
|
+
const folder = path7.basename(path7.resolve(root)) || "artifact";
|
|
2075
|
+
const skipped = new Set(skip.map((file) => path7.resolve(file)));
|
|
2076
|
+
const entries = [];
|
|
2077
|
+
const walk = async (dir, rel) => {
|
|
2078
|
+
let names;
|
|
2079
|
+
try {
|
|
2080
|
+
names = await fsp2.readdir(dir, { withFileTypes: true });
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
if (rel && error.code === "ENOENT") return;
|
|
2083
|
+
throw error;
|
|
2084
|
+
}
|
|
2085
|
+
for (const dirent of names.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
2086
|
+
const name = dirent.name;
|
|
2087
|
+
if (JUNK_FILES.has(name) || isTemp(name) || !rel && (LOCAL_FILES.has(name) || LOCAL_DIRS.has(name) && dirent.isDirectory())) continue;
|
|
2088
|
+
const file = path7.join(dir, name);
|
|
2089
|
+
if (skipped.has(file)) continue;
|
|
2090
|
+
const inner = rel ? `${rel}/${name}` : name;
|
|
2091
|
+
if (dirent.isDirectory()) await walk(file, inner);
|
|
2092
|
+
else if (dirent.isFile()) {
|
|
2093
|
+
const stat = await fsp2.stat(file).catch(() => null);
|
|
2094
|
+
if (stat?.isFile()) entries.push({ name: `${folder}/${inner}`, file, size: stat.size, mode: stat.mode & 511, mtime: stat.mtime });
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
await walk(path7.resolve(root), "");
|
|
2099
|
+
if (entries.length > MAX_ENTRIES) throw new ArchiveTooLarge(`The folder holds ${entries.length} files, more than a zip holds (${MAX_ENTRIES}); copy the folder itself instead.`);
|
|
2100
|
+
const bound = entries.reduce((sum, entry) => sum + entry.size + 2 * (46 + Buffer.byteLength(entry.name)), 22);
|
|
2101
|
+
if (bound > MAX_BYTES) throw new ArchiveTooLarge(`The folder holds ${(bound / 2 ** 30).toFixed(1)} GiB, more than a zip holds (4 GiB); copy the folder itself instead.`);
|
|
2102
|
+
return entries;
|
|
2103
|
+
}
|
|
2104
|
+
var CRC_TABLE = (() => {
|
|
2105
|
+
const table = new Uint32Array(256);
|
|
2106
|
+
for (let n = 0; n < 256; n++) {
|
|
2107
|
+
let c = n;
|
|
2108
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
2109
|
+
table[n] = c >>> 0;
|
|
2110
|
+
}
|
|
2111
|
+
return table;
|
|
2112
|
+
})();
|
|
2113
|
+
function crc32(bytes) {
|
|
2114
|
+
let crc = 4294967295;
|
|
2115
|
+
for (let i = 0; i < bytes.length; i++) crc = CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
|
|
2116
|
+
return (crc ^ 4294967295) >>> 0;
|
|
2117
|
+
}
|
|
2118
|
+
function dosTime(date) {
|
|
2119
|
+
const year = date.getFullYear();
|
|
2120
|
+
if (year < 1980) return { time: 0, date: 1 << 5 | 1 };
|
|
2121
|
+
if (year > 2107) return { time: 23 << 11 | 59 << 5 | 29, date: 127 << 9 | 12 << 5 | 31 };
|
|
2122
|
+
return {
|
|
2123
|
+
time: date.getHours() << 11 | date.getMinutes() << 5 | date.getSeconds() >> 1,
|
|
2124
|
+
date: year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate()
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
var deflateRaw = promisify(zlib.deflateRaw);
|
|
2128
|
+
var UTF8 = 1 << 11;
|
|
2129
|
+
async function writeZip(entries, write) {
|
|
2130
|
+
const central = [];
|
|
2131
|
+
let offset = 0;
|
|
2132
|
+
const put = async (chunk) => {
|
|
2133
|
+
await write(chunk);
|
|
2134
|
+
offset += chunk.length;
|
|
2135
|
+
};
|
|
2136
|
+
for (const entry of entries) {
|
|
2137
|
+
let data;
|
|
2138
|
+
try {
|
|
2139
|
+
data = await fsp2.readFile(entry.file);
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
if (error.code === "ENOENT") continue;
|
|
2142
|
+
throw error;
|
|
2143
|
+
}
|
|
2144
|
+
const crc = crc32(data);
|
|
2145
|
+
let method = 0;
|
|
2146
|
+
let body = data;
|
|
2147
|
+
if (data.length > 0 && !STORED.has(path7.extname(entry.name).toLowerCase())) {
|
|
2148
|
+
const deflated = await deflateRaw(data);
|
|
2149
|
+
if (deflated.length < data.length) {
|
|
2150
|
+
method = 8;
|
|
2151
|
+
body = deflated;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (offset + body.length > MAX_BYTES) throw new ArchiveTooLarge("The folder grew past what a zip holds (4 GiB) while it was being zipped.");
|
|
2155
|
+
const name = Buffer.from(entry.name, "utf8");
|
|
2156
|
+
const { time, date } = dosTime(entry.mtime);
|
|
2157
|
+
const local = Buffer.alloc(30);
|
|
2158
|
+
local.writeUInt32LE(67324752, 0);
|
|
2159
|
+
local.writeUInt16LE(20, 4);
|
|
2160
|
+
local.writeUInt16LE(UTF8, 6);
|
|
2161
|
+
local.writeUInt16LE(method, 8);
|
|
2162
|
+
local.writeUInt16LE(time, 10);
|
|
2163
|
+
local.writeUInt16LE(date, 12);
|
|
2164
|
+
local.writeUInt32LE(crc, 14);
|
|
2165
|
+
local.writeUInt32LE(body.length, 18);
|
|
2166
|
+
local.writeUInt32LE(data.length, 22);
|
|
2167
|
+
local.writeUInt16LE(name.length, 26);
|
|
2168
|
+
local.writeUInt16LE(0, 28);
|
|
2169
|
+
const header = Buffer.alloc(46);
|
|
2170
|
+
header.writeUInt32LE(33639248, 0);
|
|
2171
|
+
header.writeUInt16LE(3 << 8 | 20, 4);
|
|
2172
|
+
header.writeUInt16LE(20, 6);
|
|
2173
|
+
header.writeUInt16LE(UTF8, 8);
|
|
2174
|
+
header.writeUInt16LE(method, 10);
|
|
2175
|
+
header.writeUInt16LE(time, 12);
|
|
2176
|
+
header.writeUInt16LE(date, 14);
|
|
2177
|
+
header.writeUInt32LE(crc, 16);
|
|
2178
|
+
header.writeUInt32LE(body.length, 20);
|
|
2179
|
+
header.writeUInt32LE(data.length, 24);
|
|
2180
|
+
header.writeUInt16LE(name.length, 28);
|
|
2181
|
+
header.writeUInt32LE((32768 | (entry.mode || 420) | 384) << 16 >>> 0, 38);
|
|
2182
|
+
header.writeUInt32LE(offset, 42);
|
|
2183
|
+
central.push(header, name);
|
|
2184
|
+
await put(Buffer.concat([local, name]));
|
|
2185
|
+
await put(body);
|
|
2186
|
+
}
|
|
2187
|
+
const start = offset;
|
|
2188
|
+
const directory = Buffer.concat(central);
|
|
2189
|
+
await put(directory);
|
|
2190
|
+
const end = Buffer.alloc(22);
|
|
2191
|
+
end.writeUInt32LE(101010256, 0);
|
|
2192
|
+
end.writeUInt16LE(central.length / 2, 8);
|
|
2193
|
+
end.writeUInt16LE(central.length / 2, 10);
|
|
2194
|
+
end.writeUInt32LE(directory.length, 12);
|
|
2195
|
+
end.writeUInt32LE(start, 16);
|
|
2196
|
+
await put(end);
|
|
2197
|
+
return offset;
|
|
2198
|
+
}
|
|
2199
|
+
function archiveFileName(root, now = /* @__PURE__ */ new Date()) {
|
|
2200
|
+
const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2201
|
+
return `${path7.basename(path7.resolve(root)) || "artifact"}-${day}.zip`;
|
|
2202
|
+
}
|
|
2203
|
+
async function writeArchive(root, out) {
|
|
2204
|
+
const file = path7.resolve(out);
|
|
2205
|
+
const entries = await archiveEntries(root, [file]);
|
|
2206
|
+
const tmp = path7.join(path7.dirname(file), `.${path7.basename(file)}.${process.pid}.tmp`);
|
|
2207
|
+
const handle = await fsp2.open(tmp, "w");
|
|
2208
|
+
let bytes;
|
|
2209
|
+
try {
|
|
2210
|
+
bytes = await writeZip(entries, async (chunk) => {
|
|
2211
|
+
await handle.write(chunk);
|
|
2212
|
+
});
|
|
2213
|
+
await handle.close();
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
await handle.close().catch(() => {
|
|
2216
|
+
});
|
|
2217
|
+
await fsp2.rm(tmp, { force: true });
|
|
2218
|
+
throw error;
|
|
2219
|
+
}
|
|
2220
|
+
await fsp2.rename(tmp, file);
|
|
2221
|
+
return { file, files: entries.length, bytes };
|
|
2222
|
+
}
|
|
2223
|
+
async function archiveCommand(root, out) {
|
|
2224
|
+
const where = out ? path7.resolve(out) : path7.dirname(path7.resolve(root));
|
|
2225
|
+
const target = fs7.statSync(where, { throwIfNoEntry: false })?.isDirectory() ? path7.join(where, archiveFileName(root)) : where;
|
|
2226
|
+
const { file, files, bytes } = await writeArchive(root, target);
|
|
2227
|
+
return { file, folder: path7.basename(path7.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
|
|
2228
|
+
}
|
|
2229
|
+
function attachment(name) {
|
|
2230
|
+
const ascii = name.replace(/[^\x20-\x7e]|["\\]/g, "_");
|
|
2231
|
+
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(name)}`;
|
|
2232
|
+
}
|
|
2233
|
+
async function sendArchive(request, response, root) {
|
|
2234
|
+
request.resume();
|
|
2235
|
+
let entries;
|
|
2236
|
+
try {
|
|
2237
|
+
entries = await archiveEntries(root);
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
const text = JSON.stringify({ code: error instanceof ArchiveTooLarge ? "too_large" : "upstream_error", message: error instanceof Error ? error.message : String(error) });
|
|
2240
|
+
response.writeHead(error instanceof ArchiveTooLarge ? 413 : 500, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }).end(text);
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
response.writeHead(200, {
|
|
2244
|
+
"Content-Type": "application/zip",
|
|
2245
|
+
"Content-Disposition": attachment(archiveFileName(root)),
|
|
2246
|
+
"Cache-Control": "no-store",
|
|
2247
|
+
"X-Content-Type-Options": "nosniff"
|
|
2248
|
+
});
|
|
2249
|
+
if (request.method === "HEAD") {
|
|
2250
|
+
response.end();
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
const write = (chunk) => new Promise((resolve, reject) => {
|
|
2254
|
+
if (response.destroyed) return reject(new Error("The download was cancelled."));
|
|
2255
|
+
if (response.write(chunk)) return resolve();
|
|
2256
|
+
const drained = () => {
|
|
2257
|
+
response.off("close", closed);
|
|
2258
|
+
resolve();
|
|
2259
|
+
};
|
|
2260
|
+
const closed = () => {
|
|
2261
|
+
response.off("drain", drained);
|
|
2262
|
+
reject(new Error("The download was cancelled."));
|
|
2263
|
+
};
|
|
2264
|
+
response.once("drain", drained);
|
|
2265
|
+
response.once("close", closed);
|
|
2266
|
+
});
|
|
2267
|
+
try {
|
|
2268
|
+
await writeZip(entries, write);
|
|
2269
|
+
response.end();
|
|
2270
|
+
} catch {
|
|
2271
|
+
response.destroy();
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
import fs8 from "node:fs";
|
|
2276
|
+
import fsp3 from "node:fs/promises";
|
|
2277
|
+
import os3 from "node:os";
|
|
2278
|
+
import path8 from "node:path";
|
|
2279
|
+
var MAX_BYTES2 = 4294967295;
|
|
2280
|
+
function shown(file) {
|
|
2281
|
+
const home = os3.homedir();
|
|
2282
|
+
return home && (file === home || file.startsWith(home + path8.sep)) ? `~${file.slice(home.length)}` : file;
|
|
2283
|
+
}
|
|
2284
|
+
function saved(file, bytes) {
|
|
2285
|
+
return { path: file, name: path8.basename(file), folder: path8.dirname(file), shown: shown(file), bytes };
|
|
2286
|
+
}
|
|
2287
|
+
function safeFileName(raw) {
|
|
2288
|
+
let name = (raw.replace(/\\/g, "/").split("/").pop() ?? "").replace(/[\u0000-\u001f\u007f<>:"|?*]/g, "_").replace(/^[.\s]+|[.\s]+$/g, "");
|
|
2289
|
+
if (name.length > 200) {
|
|
2290
|
+
const ext = path8.extname(name).slice(0, 16);
|
|
2291
|
+
name = name.slice(0, 200 - ext.length) + ext;
|
|
2292
|
+
}
|
|
2293
|
+
return name || "download";
|
|
2294
|
+
}
|
|
2295
|
+
async function openFree(dir, name) {
|
|
2296
|
+
const ext = path8.extname(name);
|
|
2297
|
+
const stem = name.slice(0, name.length - ext.length);
|
|
2298
|
+
for (let n = 0; ; n++) {
|
|
2299
|
+
const file = path8.join(dir, n === 0 ? name : `${stem} (${n})${ext}`);
|
|
2300
|
+
try {
|
|
2301
|
+
return { file, handle: await fsp3.open(file, "wx") };
|
|
2302
|
+
} catch (error) {
|
|
2303
|
+
if (error.code !== "EEXIST" || n >= 999) throw error;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
async function saveDownload(request, response, root) {
|
|
2308
|
+
const declared = Number(request.headers["content-length"]);
|
|
2309
|
+
if (Number.isFinite(declared) && declared > MAX_BYTES2) {
|
|
2310
|
+
request.resume();
|
|
2311
|
+
return sendJson2(response, 413, { code: "too_large", message: "The file is over 4 GiB." });
|
|
2312
|
+
}
|
|
2313
|
+
const name = safeFileName(new URL(request.url ?? "/", "http://local").searchParams.get("name") ?? "");
|
|
2314
|
+
let file = "";
|
|
2315
|
+
let handle = null;
|
|
2316
|
+
let bytes = 0;
|
|
2317
|
+
try {
|
|
2318
|
+
const dir = path8.join(root, EXPORTS_DIR);
|
|
2319
|
+
await fsp3.mkdir(dir, { recursive: true });
|
|
2320
|
+
({ file, handle } = await openFree(dir, name));
|
|
2321
|
+
for await (const chunk of request) {
|
|
2322
|
+
bytes += chunk.length;
|
|
2323
|
+
if (bytes > MAX_BYTES2) throw Object.assign(new Error("The file is over 4 GiB."), { status: 413 });
|
|
2324
|
+
await handle.write(chunk);
|
|
2325
|
+
}
|
|
2326
|
+
await handle.close();
|
|
2327
|
+
handle = null;
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
await handle?.close().catch(() => {
|
|
2330
|
+
});
|
|
2331
|
+
if (file) await fsp3.rm(file, { force: true });
|
|
2332
|
+
request.resume();
|
|
2333
|
+
const status = error.status ?? 500;
|
|
2334
|
+
if (!response.headersSent) sendJson2(response, status, { code: status === 413 ? "too_large" : "upstream_error", message: error instanceof Error ? error.message : String(error) });
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
sendJson2(response, 200, saved(file, bytes));
|
|
2338
|
+
}
|
|
2339
|
+
async function saveArchive(request, response, root) {
|
|
2340
|
+
request.resume();
|
|
2341
|
+
try {
|
|
2342
|
+
const { file } = await archiveCommand(root, void 0);
|
|
2343
|
+
sendJson2(response, 200, saved(file, fs8.statSync(file).size));
|
|
2344
|
+
} catch (error) {
|
|
2345
|
+
const tooLarge = error instanceof ArchiveTooLarge;
|
|
2346
|
+
sendJson2(response, tooLarge ? 413 : 500, { code: tooLarge ? "too_large" : "upstream_error", message: error instanceof Error ? error.message : String(error) });
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
1649
2350
|
var NOUNS = {
|
|
1650
2351
|
docs: { en: "doc", zh: "\u6587\u6863", zhThis: "\u8FD9\u7BC7\u6587\u6863" },
|
|
1651
2352
|
slides: { en: "deck", zh: "\u6F14\u793A\u6587\u7A3F", zhThis: "\u8FD9\u4EFD\u6F14\u793A\u6587\u7A3F" },
|
|
@@ -1666,7 +2367,7 @@ function commentsViewer(viewer) {
|
|
|
1666
2367
|
};
|
|
1667
2368
|
}
|
|
1668
2369
|
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1669
|
-
var
|
|
2370
|
+
var esc2 = (text) => text.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1670
2371
|
var script = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
1671
2372
|
function ownHosts(host, port) {
|
|
1672
2373
|
const named = host === "0.0.0.0" || host === "::" ? [] : [host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`];
|
|
@@ -1675,12 +2376,12 @@ function ownHosts(host, port) {
|
|
|
1675
2376
|
function localOwner() {
|
|
1676
2377
|
let name = "You";
|
|
1677
2378
|
try {
|
|
1678
|
-
name =
|
|
2379
|
+
name = os4.userInfo().username.trim() || name;
|
|
1679
2380
|
} catch {
|
|
1680
2381
|
}
|
|
1681
2382
|
return { id: "local", name, email: `${name}@localhost` };
|
|
1682
2383
|
}
|
|
1683
|
-
var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash2("sha256").update(
|
|
2384
|
+
var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash2("sha256").update(path9.resolve(root)).digest("hex").slice(0, 12)}`;
|
|
1684
2385
|
var ShareHost = class {
|
|
1685
2386
|
sharing;
|
|
1686
2387
|
options;
|
|
@@ -1731,7 +2432,7 @@ var ShareHost = class {
|
|
|
1731
2432
|
const suffix = `:${this.port}`;
|
|
1732
2433
|
if (!host.endsWith(suffix)) return false;
|
|
1733
2434
|
const name = host.slice(0, -suffix.length).toLowerCase();
|
|
1734
|
-
const machine =
|
|
2435
|
+
const machine = os4.hostname().toLowerCase();
|
|
1735
2436
|
return this.lanServers.has(name) || name === machine || name === `${machine.replace(/\.local$/, "")}.local`;
|
|
1736
2437
|
}
|
|
1737
2438
|
/**
|
|
@@ -1845,13 +2546,16 @@ var ShareHost = class {
|
|
|
1845
2546
|
}
|
|
1846
2547
|
if (method === "GET" && p === "/__share/invite") return sendJson2(response, 200, this.invites(viewer, request.headers.host ?? "")), true;
|
|
1847
2548
|
if (method === "GET" && p === "/__share/ui.js") {
|
|
1848
|
-
|
|
2549
|
+
fs9.stat(this.options.uiFile, (error, stat) => {
|
|
1849
2550
|
if (error || !stat.isFile()) return sendJson2(response, 404, { code: "not_found", message: "The Share panel is missing." });
|
|
1850
2551
|
sendFile(request, response, this.options.uiFile, { "Content-Type": "text/javascript; charset=utf-8", "Content-Length": stat.size, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
1851
2552
|
});
|
|
1852
2553
|
return true;
|
|
1853
2554
|
}
|
|
1854
2555
|
if (viewer.role !== "owner") return sendJson2(response, 403, { code: "not_granted", message: "Only the owner can do this." }), true;
|
|
2556
|
+
if ((method === "GET" || method === "HEAD") && p === "/__share/archive") return await sendArchive(request, response, this.options.root), true;
|
|
2557
|
+
if (method === "POST" && p === "/__share/archive") return await saveArchive(request, response, this.options.root), true;
|
|
2558
|
+
if (method === "POST" && p === "/__share/save") return await saveDownload(request, response, this.options.root), true;
|
|
1855
2559
|
if (p === "/__share") return await this.shareApi(request, response, method), true;
|
|
1856
2560
|
sendJson2(response, 404, { code: "not_found", message: "Not found" });
|
|
1857
2561
|
return true;
|
|
@@ -1862,7 +2566,7 @@ var ShareHost = class {
|
|
|
1862
2566
|
*/
|
|
1863
2567
|
headTags(_viewer, query = "") {
|
|
1864
2568
|
let tags = `<script src="/__share/boot.js${query.replace(/[<>"']/g, "").replace(/&/g, "&")}"></script>`;
|
|
1865
|
-
if (
|
|
2569
|
+
if (fs9.existsSync(this.options.uiFile)) tags += '<script type="module" src="/__share/ui.js"></script>';
|
|
1866
2570
|
return tags;
|
|
1867
2571
|
}
|
|
1868
2572
|
/** `html` with the head tags first in its <head>; null when it has none. */
|
|
@@ -1886,11 +2590,11 @@ var ShareHost = class {
|
|
|
1886
2590
|
const navigation = (request.method === "GET" || request.method === "HEAD") && /\btext\/html\b/.test(request.headers.accept ?? "");
|
|
1887
2591
|
if (!navigation) return sendJson2(response, 403, { code: "not_granted", message: `This ${this.options.noun.en} is not shared with you.` });
|
|
1888
2592
|
const t = this.words(request);
|
|
1889
|
-
this.sendPage(response, 403, t.noAccessTitle, `<h1>${
|
|
2593
|
+
this.sendPage(response, 403, t.noAccessTitle, `<h1>${esc2(t.noAccessTitle)}</h1><p>${esc2(t.noAccess)}</p>${this.ownerHint(request, t)}`);
|
|
1890
2594
|
}
|
|
1891
2595
|
/** A small page of our own (the join form, the refusal), in the reader's language. */
|
|
1892
2596
|
sendPage(response, status, title, body, headers = {}) {
|
|
1893
|
-
const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${
|
|
2597
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc2(title)}</title><style>${PAGE_CSS}</style></head><body><main>${body}</main></body></html>`;
|
|
1894
2598
|
response.writeHead(status, {
|
|
1895
2599
|
"Content-Type": "text/html; charset=utf-8",
|
|
1896
2600
|
"Cache-Control": "no-store",
|
|
@@ -1904,7 +2608,7 @@ var ShareHost = class {
|
|
|
1904
2608
|
const from = (request.socket.remoteAddress ?? "").replace(/^::ffff:/, "");
|
|
1905
2609
|
if (!this.lanServers.has(from)) return "";
|
|
1906
2610
|
const url = `http://127.0.0.1:${this.port}/`;
|
|
1907
|
-
return `<p class="note">${
|
|
2611
|
+
return `<p class="note">${esc2(t.ownerHere)} <a href="${esc2(url)}">${esc2(url)}</a></p>`;
|
|
1908
2612
|
}
|
|
1909
2613
|
/** GET /s/<token>: the name form (or straight in, for a guest already in through this link); POST: join. */
|
|
1910
2614
|
async join(request, response, method, token2) {
|
|
@@ -1913,12 +2617,12 @@ var ShareHost = class {
|
|
|
1913
2617
|
const local = isLoopback(request.socket.remoteAddress);
|
|
1914
2618
|
const form = method === "POST" ? await readBody(request, 8 << 10).then((body) => body.toString("utf8"), () => "") : "";
|
|
1915
2619
|
const title = this.options.title();
|
|
1916
|
-
if (!role || !local && !this.lanServers.size) return this.sendPage(response, 404, t.linkGoneTitle, `<h1>${
|
|
2620
|
+
if (!role || !local && !this.lanServers.size) return this.sendPage(response, 404, t.linkGoneTitle, `<h1>${esc2(t.linkGoneTitle)}</h1><p>${esc2(t.linkGone)}</p>`);
|
|
1917
2621
|
if (local) {
|
|
1918
2622
|
const lan = [...this.lanServers.keys()][0];
|
|
1919
2623
|
const there = lan ? `http://${lan}:${this.port}/s/${token2}` : "";
|
|
1920
|
-
const tryIt = there ? `<p>${
|
|
1921
|
-
return this.sendPage(response, 200, title, `<h1>${
|
|
2624
|
+
const tryIt = there ? `<p>${esc2(t.tryRole(t.roles[role]))} <a href="${esc2(there)}">${esc2(there)}</a></p>` : "";
|
|
2625
|
+
return this.sendPage(response, 200, title, `<h1>${esc2(title)}</h1><p>${esc2(t.youOwn)} <a href="/">${esc2(t.openAsOwner)}</a></p>${tryIt}`);
|
|
1922
2626
|
}
|
|
1923
2627
|
const current = this.sharing.guest(this.cookie(request));
|
|
1924
2628
|
if (method === "GET" && current && current.link === this.sharing.state.links[role].token) {
|
|
@@ -1930,7 +2634,7 @@ var ShareHost = class {
|
|
|
1930
2634
|
response,
|
|
1931
2635
|
200,
|
|
1932
2636
|
title,
|
|
1933
|
-
`<h1>${
|
|
2637
|
+
`<h1>${esc2(title)}</h1><p>${esc2(t.invited(t.roles[role]))}</p><form method="post"><label for="n">${esc2(t.yourName)}</label><input id="n" name="name" maxlength="${MAX_NAME}" value="${esc2(current?.name ?? "")}" required autofocus autocomplete="name"><button type="submit">${esc2(t.open)}</button></form><p class="note">${esc2(t.nameNote)}</p>`
|
|
1934
2638
|
);
|
|
1935
2639
|
}
|
|
1936
2640
|
const name = new URLSearchParams(form).get("name") ?? "";
|
|
@@ -1951,18 +2655,23 @@ var ShareHost = class {
|
|
|
1951
2655
|
}
|
|
1952
2656
|
/** Links, addresses and people, for the share panel and `artifact.mjs share`. */
|
|
1953
2657
|
shareView() {
|
|
1954
|
-
const addresses = [...this.lanServers.keys()];
|
|
1955
|
-
const
|
|
2658
|
+
const addresses = [...this.lanServers.keys()].sort((a, b) => Number(isTailnet(a)) - Number(isTailnet(b)));
|
|
2659
|
+
const local = addresses.filter((a) => !isTailnet(a));
|
|
2660
|
+
const base = local.length ? `http://${local[0]}:${this.port}` : addresses.length ? `http://${addresses[0]}:${this.port}` : `http://127.0.0.1:${this.port}`;
|
|
2661
|
+
const tailnet = addresses.find(isTailnet);
|
|
1956
2662
|
const online = /* @__PURE__ */ new Set();
|
|
1957
2663
|
for (const live of this.live) if (live.viewer.guest) online.add(live.viewer.guest.id);
|
|
1958
2664
|
const { links, guests } = this.sharing.state;
|
|
2665
|
+
const lan = addresses.length ? addresses : [...lanAddresses()].sort((a, b) => Number(isTailnet(a)) - Number(isTailnet(b)));
|
|
2666
|
+
const network = lan.length ? `http://${lan[0]}:${this.port}` : base;
|
|
1959
2667
|
return {
|
|
1960
2668
|
title: this.options.title(),
|
|
1961
2669
|
lan: this.lanServers.size > 0,
|
|
1962
2670
|
port: this.port,
|
|
1963
2671
|
addresses,
|
|
1964
2672
|
local: `http://127.0.0.1:${this.port}/`,
|
|
1965
|
-
links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `${base}/s/${links[role].token}` }])),
|
|
2673
|
+
links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `${base}/s/${links[role].token}`, networkUrl: `${network}/s/${links[role].token}` }])),
|
|
2674
|
+
...tailnet ? { tailnet: { address: tailnet, links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `http://${tailnet}:${this.port}/s/${links[role].token}` }])) } } : {},
|
|
1966
2675
|
guests: guests.filter((g) => this.sharing.valid(g) || links[g.role].token === g.link).map((g) => ({ id: g.id, name: g.name, role: g.role, joined: g.joined, ...g.seen ? { seen: g.seen } : {}, online: online.has(g.id), active: this.sharing.valid(g) }))
|
|
1967
2676
|
};
|
|
1968
2677
|
}
|
|
@@ -2403,6 +3112,7 @@ data: ${JSON.stringify({ peers: this.list() })}
|
|
|
2403
3112
|
var KIND = "";
|
|
2404
3113
|
var NOUN = "";
|
|
2405
3114
|
var INDEX_FILE = "";
|
|
3115
|
+
var DESIGN_SYSTEMS = false;
|
|
2406
3116
|
var LAYOUT;
|
|
2407
3117
|
var pageScriptSrc;
|
|
2408
3118
|
var sessionExtras;
|
|
@@ -2548,10 +3258,10 @@ async function collectFiles(root, limit) {
|
|
|
2548
3258
|
const seen = /* @__PURE__ */ new Set();
|
|
2549
3259
|
let truncated = false;
|
|
2550
3260
|
const visit = async (relDir, descend) => {
|
|
2551
|
-
const dir = relDir === "" ? root :
|
|
3261
|
+
const dir = relDir === "" ? root : path10.join(root, ...relDir.split("/"));
|
|
2552
3262
|
let entries;
|
|
2553
3263
|
try {
|
|
2554
|
-
entries = await
|
|
3264
|
+
entries = await fsp4.readdir(dir, { withFileTypes: true });
|
|
2555
3265
|
} catch {
|
|
2556
3266
|
return;
|
|
2557
3267
|
}
|
|
@@ -2579,13 +3289,13 @@ async function collectFiles(root, limit) {
|
|
|
2579
3289
|
return { rels, truncated };
|
|
2580
3290
|
}
|
|
2581
3291
|
async function writeFileAtomic(file, bytes) {
|
|
2582
|
-
await
|
|
2583
|
-
const tmp =
|
|
3292
|
+
await fsp4.mkdir(path10.dirname(file), { recursive: true });
|
|
3293
|
+
const tmp = path10.join(path10.dirname(file), `.${path10.basename(file)}.${randomUUID()}.tmp`);
|
|
2584
3294
|
try {
|
|
2585
|
-
await
|
|
2586
|
-
await
|
|
3295
|
+
await fsp4.writeFile(tmp, bytes);
|
|
3296
|
+
await fsp4.rename(tmp, file);
|
|
2587
3297
|
} catch (error) {
|
|
2588
|
-
await
|
|
3298
|
+
await fsp4.rm(tmp, { force: true }).catch(() => {
|
|
2589
3299
|
});
|
|
2590
3300
|
throw error;
|
|
2591
3301
|
}
|
|
@@ -2598,6 +3308,11 @@ var Store = class {
|
|
|
2598
3308
|
ownWrites;
|
|
2599
3309
|
/** "<user id>\0project/<path>" → when the server last refused that person's save of the file as a conflict. */
|
|
2600
3310
|
refused;
|
|
3311
|
+
/**
|
|
3312
|
+
* "<page>\0project/<path>" → the sha256 of that page's last save of the file ('' for a delete). A page sends no
|
|
3313
|
+
* ifMatch for a file whose version it believes is still its own last save; the server checks that belief for it.
|
|
3314
|
+
*/
|
|
3315
|
+
lastSaved;
|
|
2601
3316
|
queue;
|
|
2602
3317
|
seq;
|
|
2603
3318
|
truncationReported;
|
|
@@ -2609,6 +3324,7 @@ var Store = class {
|
|
|
2609
3324
|
this.log = log;
|
|
2610
3325
|
this.ownWrites = /* @__PURE__ */ new Map();
|
|
2611
3326
|
this.refused = /* @__PURE__ */ new Map();
|
|
3327
|
+
this.lastSaved = /* @__PURE__ */ new Map();
|
|
2612
3328
|
this.queue = Promise.resolve();
|
|
2613
3329
|
this.seq = 0;
|
|
2614
3330
|
this.truncationReported = false;
|
|
@@ -2628,19 +3344,19 @@ var Store = class {
|
|
|
2628
3344
|
}
|
|
2629
3345
|
/** The file on disk (it may not exist yet); null when a directory on the way is a symlink or the name is not a file. */
|
|
2630
3346
|
async diskPath(rel) {
|
|
2631
|
-
const realRoot = await
|
|
3347
|
+
const realRoot = await fsp4.realpath(this.root).catch(() => null);
|
|
2632
3348
|
if (realRoot === null) return null;
|
|
2633
|
-
const file =
|
|
2634
|
-
for (let dir =
|
|
2635
|
-
const real = await
|
|
3349
|
+
const file = path10.join(realRoot, ...rel.split("/"));
|
|
3350
|
+
for (let dir = path10.dirname(file); ; dir = path10.dirname(dir)) {
|
|
3351
|
+
const real = await fsp4.realpath(dir).catch(() => null);
|
|
2636
3352
|
if (real === null) {
|
|
2637
|
-
if (
|
|
3353
|
+
if (path10.dirname(dir) === dir) return null;
|
|
2638
3354
|
continue;
|
|
2639
3355
|
}
|
|
2640
3356
|
if (real !== dir) return null;
|
|
2641
3357
|
break;
|
|
2642
3358
|
}
|
|
2643
|
-
const info = await
|
|
3359
|
+
const info = await fsp4.lstat(file).catch(() => null);
|
|
2644
3360
|
return info && !info.isFile() ? null : file;
|
|
2645
3361
|
}
|
|
2646
3362
|
async listing() {
|
|
@@ -2655,7 +3371,7 @@ var Store = class {
|
|
|
2655
3371
|
for (const rel of rels.sort()) {
|
|
2656
3372
|
const file = await this.diskPath(rel);
|
|
2657
3373
|
if (file === null) continue;
|
|
2658
|
-
const bytes = await
|
|
3374
|
+
const bytes = await fsp4.readFile(file).catch(() => null);
|
|
2659
3375
|
if (bytes === null) continue;
|
|
2660
3376
|
files[`project/${rel}`] = { sha256: sha2562(bytes), size: bytes.length, contentType: contentTypeOf(rel) };
|
|
2661
3377
|
}
|
|
@@ -2665,16 +3381,18 @@ var Store = class {
|
|
|
2665
3381
|
const rel = this.fileOf(p);
|
|
2666
3382
|
const file = rel === null ? null : await this.diskPath(rel);
|
|
2667
3383
|
if (file === null) return null;
|
|
2668
|
-
return
|
|
3384
|
+
return fsp4.readFile(file).catch(() => null);
|
|
2669
3385
|
}
|
|
2670
3386
|
/**
|
|
2671
3387
|
* A save from the page by `who`: all entries or none (ifMatch is checked for all before anything is written). It is
|
|
2672
3388
|
* a version in the history under their name; what changed on disk before it is one of its own, Claude's.
|
|
2673
3389
|
*/
|
|
2674
|
-
publish(batch, who, over = [], merged = []) {
|
|
2675
|
-
return this.exclusive(() => this.publishNow(batch, who, over, merged));
|
|
3390
|
+
publish(batch, who, over = [], merged = [], page = "") {
|
|
3391
|
+
return this.exclusive(() => this.publishNow(batch, who, over, merged, page));
|
|
2676
3392
|
}
|
|
2677
|
-
async publishNow(batch, who, over, merged) {
|
|
3393
|
+
async publishNow(batch, who, over, merged, page) {
|
|
3394
|
+
const saver = page ? `page:${page}` : `user:${who.user.id}`;
|
|
3395
|
+
const named = new Set([...over, ...merged].map((o) => o.path));
|
|
2678
3396
|
const files = /* @__PURE__ */ new Map();
|
|
2679
3397
|
let total = 0;
|
|
2680
3398
|
for (const [p, entry] of Object.entries(batch)) {
|
|
@@ -2697,8 +3415,12 @@ var Store = class {
|
|
|
2697
3415
|
const conflicts = [];
|
|
2698
3416
|
for (const [p, entry] of Object.entries(batch)) {
|
|
2699
3417
|
const expected = entry?.ifMatch;
|
|
2700
|
-
if (expected === void 0) continue;
|
|
2701
3418
|
const actual = before.files[p]?.sha256 ?? null;
|
|
3419
|
+
if (expected === void 0) {
|
|
3420
|
+
const own = this.lastSaved.get(`${saver}\0${p}`);
|
|
3421
|
+
if (own !== void 0 && !named.has(p) && (actual ?? "") !== own) conflicts.push({ path: p, actual, expected: own || null });
|
|
3422
|
+
continue;
|
|
3423
|
+
}
|
|
2702
3424
|
if (actual !== expected) conflicts.push({ path: p, actual, expected });
|
|
2703
3425
|
}
|
|
2704
3426
|
if (conflicts.length > 0) {
|
|
@@ -2722,13 +3444,15 @@ var Store = class {
|
|
|
2722
3444
|
const file = files.get(p);
|
|
2723
3445
|
if (!isWriteEntry(entry)) {
|
|
2724
3446
|
this.ownWrites.set(p, "");
|
|
2725
|
-
|
|
3447
|
+
this.rememberSave(saver, p, "");
|
|
3448
|
+
await fsp4.rm(file, { force: true });
|
|
2726
3449
|
written.set(p, null);
|
|
2727
3450
|
continue;
|
|
2728
3451
|
}
|
|
2729
3452
|
const bytes = Buffer.from(entry.base64, "base64");
|
|
2730
3453
|
const sha = sha2562(bytes);
|
|
2731
3454
|
this.ownWrites.set(p, sha);
|
|
3455
|
+
this.rememberSave(saver, p, sha);
|
|
2732
3456
|
await writeFileAtomic(file, bytes);
|
|
2733
3457
|
shas[p] = sha;
|
|
2734
3458
|
written.set(p, bytes);
|
|
@@ -2738,6 +3462,13 @@ var Store = class {
|
|
|
2738
3462
|
this.seq += 1;
|
|
2739
3463
|
return { status: 200, body: { version: after.ver, seq: this.seq, shas }, version };
|
|
2740
3464
|
}
|
|
3465
|
+
/** A page's last save of a file (lastSaved), the oldest forgotten past a few thousand. */
|
|
3466
|
+
rememberSave(saver, p, sha) {
|
|
3467
|
+
const key = `${saver}\0${p}`;
|
|
3468
|
+
this.lastSaved.delete(key);
|
|
3469
|
+
this.lastSaved.set(key, sha);
|
|
3470
|
+
if (this.lastSaved.size > 4e3) this.lastSaved.delete(this.lastSaved.keys().next().value);
|
|
3471
|
+
}
|
|
2741
3472
|
/**
|
|
2742
3473
|
* The files this save replaces without its page having seen them: the server refused this person's save of the file
|
|
2743
3474
|
* as a conflict moments ago (someone else had saved it since their page loaded it), and now the page saves on top of
|
|
@@ -2807,7 +3538,7 @@ var Store = class {
|
|
|
2807
3538
|
if (file === null) continue;
|
|
2808
3539
|
if (bytes === null) {
|
|
2809
3540
|
this.ownWrites.set(p, "");
|
|
2810
|
-
await
|
|
3541
|
+
await fsp4.rm(file, { force: true });
|
|
2811
3542
|
} else {
|
|
2812
3543
|
this.ownWrites.set(p, sha2562(bytes));
|
|
2813
3544
|
await writeFileAtomic(file, bytes);
|
|
@@ -2821,7 +3552,7 @@ var Store = class {
|
|
|
2821
3552
|
try {
|
|
2822
3553
|
const file = await this.diskPath(`${LAYOUT.blobDir}/${ASSET_INDEX}`);
|
|
2823
3554
|
if (file === null) return {};
|
|
2824
|
-
const parsed = JSON.parse(await
|
|
3555
|
+
const parsed = JSON.parse(await fsp4.readFile(file, "utf8"));
|
|
2825
3556
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
2826
3557
|
} catch {
|
|
2827
3558
|
return {};
|
|
@@ -2849,11 +3580,11 @@ var Store = class {
|
|
|
2849
3580
|
}
|
|
2850
3581
|
async findBlobFile(id) {
|
|
2851
3582
|
try {
|
|
2852
|
-
const names = await
|
|
3583
|
+
const names = await fsp4.readdir(path10.join(this.root, LAYOUT.blobDir));
|
|
2853
3584
|
const name = names.find((n) => n.startsWith(`${id}.`) && !n.endsWith(".tmp"));
|
|
2854
3585
|
if (name === void 0) return null;
|
|
2855
3586
|
const file = await this.diskPath(`${LAYOUT.blobDir}/${name}`);
|
|
2856
|
-
return file !== null && (await
|
|
3587
|
+
return file !== null && (await fsp4.lstat(file).catch(() => null))?.isFile() ? file : null;
|
|
2857
3588
|
} catch {
|
|
2858
3589
|
return null;
|
|
2859
3590
|
}
|
|
@@ -2876,7 +3607,7 @@ var Store = class {
|
|
|
2876
3607
|
let sha = "";
|
|
2877
3608
|
try {
|
|
2878
3609
|
const file = await this.diskPath(rel);
|
|
2879
|
-
if (file !== null && (await
|
|
3610
|
+
if (file !== null && (await fsp4.lstat(file)).isFile()) sha = sha2562(await fsp4.readFile(file));
|
|
2880
3611
|
} catch {
|
|
2881
3612
|
sha = "";
|
|
2882
3613
|
}
|
|
@@ -2904,13 +3635,13 @@ function watchArtifact(store, onChange, onError) {
|
|
|
2904
3635
|
const directories = /* @__PURE__ */ new Map();
|
|
2905
3636
|
const visit = (rel, descend) => {
|
|
2906
3637
|
if (directories.size >= maxDirectories) return;
|
|
2907
|
-
const dir = rel === "" ? store.root :
|
|
2908
|
-
if (directories.has(dir) || !
|
|
2909
|
-
const info =
|
|
3638
|
+
const dir = rel === "" ? store.root : path10.join(store.root, ...rel.split("/"));
|
|
3639
|
+
if (directories.has(dir) || !fs10.existsSync(dir)) return;
|
|
3640
|
+
const info = fs10.lstatSync(dir);
|
|
2910
3641
|
if (!info.isDirectory()) return;
|
|
2911
3642
|
directories.set(dir, `${info.dev}:${info.ino}`);
|
|
2912
3643
|
if (!descend) return;
|
|
2913
|
-
for (const entry of
|
|
3644
|
+
for (const entry of fs10.readdirSync(dir, { withFileTypes: true })) {
|
|
2914
3645
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
2915
3646
|
const child = rel === "" ? entry.name : `${rel}/${entry.name}`;
|
|
2916
3647
|
if (LAYOUT.descendInto(child)) visit(child, true);
|
|
@@ -2927,19 +3658,19 @@ function watchArtifact(store, onChange, onError) {
|
|
|
2927
3658
|
if (watchers.has(dir)) continue;
|
|
2928
3659
|
let watcher;
|
|
2929
3660
|
try {
|
|
2930
|
-
watcher =
|
|
3661
|
+
watcher = fs10.watch(dir, (event, name) => {
|
|
2931
3662
|
if (stopped) return;
|
|
2932
3663
|
if (name) {
|
|
2933
|
-
const changed =
|
|
3664
|
+
const changed = path10.join(dir, String(name));
|
|
2934
3665
|
if (event === "rename" && watchers.has(changed)) {
|
|
2935
3666
|
for (const [watchedDir, item] of watchers) {
|
|
2936
|
-
if (watchedDir === changed || watchedDir.startsWith(changed +
|
|
3667
|
+
if (watchedDir === changed || watchedDir.startsWith(changed + path10.sep)) {
|
|
2937
3668
|
item.watcher.close();
|
|
2938
3669
|
watchers.delete(watchedDir);
|
|
2939
3670
|
}
|
|
2940
3671
|
}
|
|
2941
3672
|
}
|
|
2942
|
-
const rel =
|
|
3673
|
+
const rel = path10.relative(store.root, changed).split(path10.sep).join("/");
|
|
2943
3674
|
if (isIgnoredPath(rel) || !LAYOUT.watches(rel)) return;
|
|
2944
3675
|
}
|
|
2945
3676
|
schedule();
|
|
@@ -2975,7 +3706,7 @@ function watchArtifact(store, onChange, onError) {
|
|
|
2975
3706
|
const { rels } = await collectFiles(store.root, LAYOUT.maxFiles);
|
|
2976
3707
|
const parts = [];
|
|
2977
3708
|
for (const rel of rels) {
|
|
2978
|
-
const info = await
|
|
3709
|
+
const info = await fsp4.lstat(path10.join(store.root, ...rel.split("/"))).catch(() => null);
|
|
2979
3710
|
parts.push(info ? `${rel}\0${info.size}\0${info.mtimeMs}\0${info.ino}` : `${rel}\0-`);
|
|
2980
3711
|
}
|
|
2981
3712
|
return parts.join("\n");
|
|
@@ -3006,9 +3737,9 @@ function watchArtifact(store, onChange, onError) {
|
|
|
3006
3737
|
var publishError = (response, status, code, message) => sendJson2(response, status, { code, message });
|
|
3007
3738
|
async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1", log = (line) => process.stderr.write(`${line}
|
|
3008
3739
|
`), agentHook, lan = false, portGiven = false }) {
|
|
3009
|
-
const project =
|
|
3740
|
+
const project = path10.join(root, "project");
|
|
3010
3741
|
const stamp = (line) => log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}`);
|
|
3011
|
-
const projectDir = await
|
|
3742
|
+
const projectDir = await fsp4.realpath(project);
|
|
3012
3743
|
const history = new History(root);
|
|
3013
3744
|
const store = new Store(projectDir, history, stamp);
|
|
3014
3745
|
const owner = localOwner();
|
|
@@ -3020,7 +3751,7 @@ async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1
|
|
|
3020
3751
|
title: () => artifactTitle(root, projectDir),
|
|
3021
3752
|
log: stamp,
|
|
3022
3753
|
createServer: () => http.createServer(handle),
|
|
3023
|
-
uiFile:
|
|
3754
|
+
uiFile: path10.join(EDITOR_DIR, "share-ui.js"),
|
|
3024
3755
|
// The History panel's address and what this person may do there; on a version's page (?at=n), which version.
|
|
3025
3756
|
globals: (viewer, query) => {
|
|
3026
3757
|
const at = Number(query.get("at") ?? "");
|
|
@@ -3153,7 +3884,7 @@ data: ${JSON.stringify(event)}
|
|
|
3153
3884
|
return;
|
|
3154
3885
|
}
|
|
3155
3886
|
const found = await store.blobFile(id);
|
|
3156
|
-
const info = found === null ? null : await
|
|
3887
|
+
const info = found === null ? null : await fsp4.stat(found.file).catch(() => null);
|
|
3157
3888
|
if (found === null || !info?.isFile()) {
|
|
3158
3889
|
response.writeHead(404, { "Cache-Control": "no-store" }).end();
|
|
3159
3890
|
return;
|
|
@@ -3170,10 +3901,10 @@ data: ${JSON.stringify(event)}
|
|
|
3170
3901
|
const serveEditor = async (request, response, rest, viewer, query = "") => {
|
|
3171
3902
|
const parts = rest === "" ? ["index.html"] : rest.split("/");
|
|
3172
3903
|
const invalid = parts.some((p) => p === "" || p === "." || p === ".." || p.startsWith(".") || p.includes("\\")) || parts[0] === "node";
|
|
3173
|
-
const realDist = invalid ? null : await
|
|
3174
|
-
const file = realDist === null ? null :
|
|
3175
|
-
const real = file === null ? null : await
|
|
3176
|
-
const info = real === null ? null : await
|
|
3904
|
+
const realDist = invalid ? null : await fsp4.realpath(EDITOR_DIR).catch(() => null);
|
|
3905
|
+
const file = realDist === null ? null : path10.join(realDist, ...parts);
|
|
3906
|
+
const real = file === null ? null : await fsp4.realpath(file).catch(() => null);
|
|
3907
|
+
const info = real === null ? null : await fsp4.lstat(real).catch(() => null);
|
|
3177
3908
|
if (file === null || real !== file || !info?.isFile()) {
|
|
3178
3909
|
if (realDist === null && !invalid && parts[0] === "index.html") {
|
|
3179
3910
|
response.writeHead(503, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("The editor page is missing; reinstall the skill.");
|
|
@@ -3182,13 +3913,13 @@ data: ${JSON.stringify(event)}
|
|
|
3182
3913
|
response.writeHead(404, { "Cache-Control": "no-store" }).end();
|
|
3183
3914
|
return;
|
|
3184
3915
|
}
|
|
3185
|
-
const ext =
|
|
3916
|
+
const ext = path10.extname(file).toLowerCase();
|
|
3186
3917
|
const type = STATIC_TYPES[ext] ?? "application/octet-stream";
|
|
3187
3918
|
if (ext === ".html") {
|
|
3188
3919
|
const key = parts.join("/");
|
|
3189
3920
|
let page = pageCache.get(key);
|
|
3190
3921
|
if (!page || page.mtimeMs !== info.mtimeMs) {
|
|
3191
|
-
const html = await
|
|
3922
|
+
const html = await fsp4.readFile(file, "utf8");
|
|
3192
3923
|
page = { mtimeMs: info.mtimeMs, html, csp: pageCsp(pageScriptSrc(html)) };
|
|
3193
3924
|
pageCache.set(key, page);
|
|
3194
3925
|
}
|
|
@@ -3233,7 +3964,8 @@ data: ${JSON.stringify(event)}
|
|
|
3233
3964
|
publishError(response, 400, "invalid_content", "malformed publish request");
|
|
3234
3965
|
return;
|
|
3235
3966
|
}
|
|
3236
|
-
const
|
|
3967
|
+
const page = String(request.headers["x-deck-client"] ?? "");
|
|
3968
|
+
const outcome = await store.publish(files, whoOf(viewer), over, merged, CLIENT_ID.test(page) ? page : "");
|
|
3237
3969
|
const names = Object.keys(files).map((p) => p.replace(/^project\//, ""));
|
|
3238
3970
|
const made = outcome.version ? ` as version ${outcome.version.n}` : "";
|
|
3239
3971
|
stamp(`saved from the page by ${viewer.user.name} (${outcome.status}${outcome.body.code ? ` ${outcome.body.code}` : ""})${made}: ${names.join(", ")}`);
|
|
@@ -3377,6 +4109,26 @@ data: ${JSON.stringify(event)}
|
|
|
3377
4109
|
if (rest === ROUTES.listing) return sendJson2(response, 200, await store.listing());
|
|
3378
4110
|
if (rest === ROUTES.session) return sendJson2(response, 200, session(viewer));
|
|
3379
4111
|
if (rest === ROUTES.events) return events(request, response, viewer);
|
|
4112
|
+
const dsPage = DESIGN_SYSTEMS ? DS_PAGE.exec(rest) : null;
|
|
4113
|
+
if (dsPage) {
|
|
4114
|
+
const html = designSystemPage(projectDir, INDEX_FILE, dsPage[1], NOUN);
|
|
4115
|
+
if (html === null) {
|
|
4116
|
+
response.writeHead(404, { "Cache-Control": "no-store" }).end();
|
|
4117
|
+
return;
|
|
4118
|
+
}
|
|
4119
|
+
const body = Buffer.from(html);
|
|
4120
|
+
response.writeHead(200, {
|
|
4121
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
4122
|
+
"Content-Length": body.length,
|
|
4123
|
+
"Cache-Control": "no-store",
|
|
4124
|
+
"X-Content-Type-Options": "nosniff",
|
|
4125
|
+
// The address holds the grant, and a README may link anywhere.
|
|
4126
|
+
"Referrer-Policy": "no-referrer",
|
|
4127
|
+
"Content-Security-Policy": DATA_CSP
|
|
4128
|
+
});
|
|
4129
|
+
response.end(method === "HEAD" ? void 0 : body);
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
3380
4132
|
if (rest.startsWith("project/")) {
|
|
3381
4133
|
const bytes = await store.readProjectFile(rest);
|
|
3382
4134
|
if (bytes === null) {
|
|
@@ -3474,11 +4226,11 @@ data: ${JSON.stringify(event)}
|
|
|
3474
4226
|
}
|
|
3475
4227
|
function artifactTitle(root, projectDir) {
|
|
3476
4228
|
try {
|
|
3477
|
-
const index = JSON.parse(
|
|
4229
|
+
const index = JSON.parse(fs10.readFileSync(path10.join(projectDir, INDEX_FILE), "utf8"));
|
|
3478
4230
|
for (const value of [index.title, index.name]) if (typeof value === "string" && value.trim() && value.trim() !== "project") return value.trim();
|
|
3479
4231
|
} catch {
|
|
3480
4232
|
}
|
|
3481
|
-
return
|
|
4233
|
+
return path10.basename(root);
|
|
3482
4234
|
}
|
|
3483
4235
|
var runningServer2 = (root) => runningServer(root, KIND, ROUTES.session);
|
|
3484
4236
|
async function serveForeground({ root, port, host, agentHook, lan, portGiven }) {
|
|
@@ -3499,18 +4251,18 @@ async function main(argv) {
|
|
|
3499
4251
|
}
|
|
3500
4252
|
if (positional.length !== 1 || flags.has("help")) {
|
|
3501
4253
|
process.stderr.write(
|
|
3502
|
-
`Usage: node ${
|
|
4254
|
+
`Usage: node ${path10.basename(SCRIPT)} <root> [--port n] [--host h] [--lan] [--agent-hook "<command>"]
|
|
3503
4255
|
<root> holds project/${INDEX_FILE}.
|
|
3504
4256
|
`
|
|
3505
4257
|
);
|
|
3506
4258
|
return 2;
|
|
3507
4259
|
}
|
|
3508
|
-
let root =
|
|
3509
|
-
if (
|
|
3510
|
-
root =
|
|
4260
|
+
let root = path10.resolve(positional[0]);
|
|
4261
|
+
if (path10.basename(root) === "project" && fs10.existsSync(path10.join(root, INDEX_FILE)) && !fs10.existsSync(path10.join(root, "project"))) {
|
|
4262
|
+
root = path10.dirname(root);
|
|
3511
4263
|
}
|
|
3512
|
-
if (!
|
|
3513
|
-
process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${
|
|
4264
|
+
if (!fs10.statSync(path10.join(root, "project", INDEX_FILE), { throwIfNoEntry: false })?.isFile()) {
|
|
4265
|
+
process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${path10.join(root, "project", INDEX_FILE)} does not exist.` })}
|
|
3514
4266
|
`);
|
|
3515
4267
|
return 2;
|
|
3516
4268
|
}
|
|
@@ -3534,7 +4286,7 @@ async function main(argv) {
|
|
|
3534
4286
|
}
|
|
3535
4287
|
function isMain(moduleUrl) {
|
|
3536
4288
|
try {
|
|
3537
|
-
return Boolean(process.argv[1]) && pathToFileURL(
|
|
4289
|
+
return Boolean(process.argv[1]) && pathToFileURL(fs10.realpathSync(process.argv[1])).href === moduleUrl;
|
|
3538
4290
|
} catch {
|
|
3539
4291
|
return false;
|
|
3540
4292
|
}
|
|
@@ -3543,6 +4295,7 @@ function createArtifactServer(kind, moduleUrl) {
|
|
|
3543
4295
|
KIND = kind.kind;
|
|
3544
4296
|
NOUN = kind.noun;
|
|
3545
4297
|
INDEX_FILE = kind.indexFile;
|
|
4298
|
+
DESIGN_SYSTEMS = kind.designSystems === true;
|
|
3546
4299
|
LAYOUT = kind.layout;
|
|
3547
4300
|
pageScriptSrc = kind.pageScriptSrc;
|
|
3548
4301
|
sessionExtras = kind.sessionExtras;
|
|
@@ -3550,7 +4303,7 @@ function createArtifactServer(kind, moduleUrl) {
|
|
|
3550
4303
|
kindLabels = kind.labels;
|
|
3551
4304
|
kindAgentContext = kind.agentContext;
|
|
3552
4305
|
SCRIPT = fileURLToPath(moduleUrl);
|
|
3553
|
-
EDITOR_DIR =
|
|
4306
|
+
EDITOR_DIR = path10.join(path10.dirname(SCRIPT), "editor");
|
|
3554
4307
|
const runIfMain = () => {
|
|
3555
4308
|
if (!isMain(moduleUrl)) return;
|
|
3556
4309
|
main(process.argv.slice(2)).then(
|
|
@@ -3600,7 +4353,7 @@ function commentLocation2(input, projectDir) {
|
|
|
3600
4353
|
});
|
|
3601
4354
|
let boards = [];
|
|
3602
4355
|
try {
|
|
3603
|
-
const listed = JSON.parse(
|
|
4356
|
+
const listed = JSON.parse(fs11.readFileSync(path11.join(projectDir, INDEX_FILE2), "utf8")).boards;
|
|
3604
4357
|
if (listed && typeof listed === "object") boards = Object.keys(listed);
|
|
3605
4358
|
} catch {
|
|
3606
4359
|
}
|
|
@@ -3611,7 +4364,7 @@ function commentLocation2(input, projectDir) {
|
|
|
3611
4364
|
function labels(projectDir) {
|
|
3612
4365
|
const titles = /* @__PURE__ */ new Map();
|
|
3613
4366
|
try {
|
|
3614
|
-
const listed = JSON.parse(
|
|
4367
|
+
const listed = JSON.parse(fs11.readFileSync(path11.join(projectDir, INDEX_FILE2), "utf8")).boards;
|
|
3615
4368
|
if (listed && typeof listed === "object") {
|
|
3616
4369
|
for (const [file, board] of Object.entries(listed)) {
|
|
3617
4370
|
const title = board?.title;
|
|
@@ -3635,6 +4388,7 @@ var server = createArtifactServer(
|
|
|
3635
4388
|
kind: KIND2,
|
|
3636
4389
|
noun: NOUN2,
|
|
3637
4390
|
indexFile: INDEX_FILE2,
|
|
4391
|
+
designSystems: true,
|
|
3638
4392
|
layout: LAYOUT2,
|
|
3639
4393
|
/** Boards run inline scripts, compile JSX with Babel (eval) and load from blob:. */
|
|
3640
4394
|
pageScriptSrc: () => "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:",
|