@silverbulletmd/silverbullet 2.4.2 → 2.6.1

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.
Files changed (97) hide show
  1. package/README.md +19 -4
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +5 -4
  5. package/client/plugos/hooks/code_widget.ts +3 -8
  6. package/client/plugos/hooks/command.ts +8 -8
  7. package/client/plugos/hooks/document_editor.ts +10 -15
  8. package/client/plugos/hooks/event.ts +33 -36
  9. package/client/plugos/hooks/mq.ts +17 -17
  10. package/client/plugos/hooks/plug_namespace.ts +3 -8
  11. package/client/plugos/hooks/slash_command.ts +13 -28
  12. package/client/plugos/hooks/syscall.ts +3 -3
  13. package/client/plugos/manifest_cache.ts +22 -15
  14. package/client/plugos/plug.ts +2 -6
  15. package/client/plugos/plug_compile.ts +79 -78
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/web_worker_sandbox.ts +1 -1
  19. package/client/plugos/sandboxes/worker_sandbox.ts +18 -18
  20. package/client/plugos/syscalls/asset.ts +1 -3
  21. package/client/plugos/syscalls/code_widget.ts +1 -3
  22. package/client/plugos/syscalls/config.ts +1 -5
  23. package/client/plugos/syscalls/datastore.ts +1 -1
  24. package/client/plugos/syscalls/editor.ts +72 -69
  25. package/client/plugos/syscalls/event.ts +9 -12
  26. package/client/plugos/syscalls/fetch.ts +31 -23
  27. package/client/plugos/syscalls/index.ts +10 -1
  28. package/client/plugos/syscalls/jsonschema.ts +72 -32
  29. package/client/plugos/syscalls/language.ts +9 -5
  30. package/client/plugos/syscalls/markdown.ts +29 -7
  31. package/client/plugos/syscalls/mq.ts +4 -12
  32. package/client/plugos/syscalls/service_registry.ts +1 -4
  33. package/client/plugos/syscalls/shell.ts +2 -5
  34. package/client/plugos/syscalls/space.ts +1 -1
  35. package/client/plugos/syscalls/sync.ts +69 -60
  36. package/client/plugos/syscalls/system.ts +2 -3
  37. package/client/plugos/system.ts +6 -12
  38. package/client/plugos/worker_runtime.ts +12 -33
  39. package/client/space_lua/aggregates.ts +782 -0
  40. package/client/space_lua/ast.ts +42 -8
  41. package/client/space_lua/ast_narrow.ts +4 -2
  42. package/client/space_lua/eval.ts +886 -575
  43. package/client/space_lua/labels.ts +7 -12
  44. package/client/space_lua/liq_null.ts +6 -0
  45. package/client/space_lua/numeric.ts +5 -8
  46. package/client/space_lua/parse.ts +346 -120
  47. package/client/space_lua/query_collection.ts +926 -82
  48. package/client/space_lua/query_env.ts +26 -0
  49. package/client/space_lua/render_lua_markdown.ts +369 -0
  50. package/client/space_lua/rp.ts +5 -4
  51. package/client/space_lua/runtime.ts +288 -155
  52. package/client/space_lua/stdlib/format.ts +53 -39
  53. package/client/space_lua/stdlib/js.ts +3 -7
  54. package/client/space_lua/stdlib/load.ts +1 -3
  55. package/client/space_lua/stdlib/math.ts +84 -58
  56. package/client/space_lua/stdlib/net.ts +27 -17
  57. package/client/space_lua/stdlib/os.ts +81 -85
  58. package/client/space_lua/stdlib/pattern.ts +695 -0
  59. package/client/space_lua/stdlib/prng.ts +148 -0
  60. package/client/space_lua/stdlib/space_lua.ts +17 -23
  61. package/client/space_lua/stdlib/string.ts +102 -190
  62. package/client/space_lua/stdlib/string_pack.ts +490 -0
  63. package/client/space_lua/stdlib/table.ts +76 -16
  64. package/client/space_lua/stdlib.ts +53 -39
  65. package/client/space_lua/tonumber.ts +82 -42
  66. package/client/space_lua/util.ts +53 -15
  67. package/dist/plug-compile.js +55 -98
  68. package/package.json +27 -20
  69. package/plug-api/constants.ts +0 -32
  70. package/plug-api/lib/async.ts +20 -7
  71. package/plug-api/lib/crypto.ts +16 -17
  72. package/plug-api/lib/dates.ts +15 -7
  73. package/plug-api/lib/json.ts +11 -5
  74. package/plug-api/lib/limited_map.ts +1 -1
  75. package/plug-api/lib/native_fetch.ts +2 -0
  76. package/plug-api/lib/ref.ts +23 -23
  77. package/plug-api/lib/resolve.ts +7 -11
  78. package/plug-api/lib/tags.ts +13 -4
  79. package/plug-api/lib/transclusion.ts +10 -21
  80. package/plug-api/lib/tree.ts +165 -45
  81. package/plug-api/lib/yaml.ts +35 -25
  82. package/plug-api/syscalls/asset.ts +1 -1
  83. package/plug-api/syscalls/config.ts +1 -4
  84. package/plug-api/syscalls/editor.ts +15 -15
  85. package/plug-api/syscalls/jsonschema.ts +1 -3
  86. package/plug-api/syscalls/lua.ts +3 -9
  87. package/plug-api/syscalls/mq.ts +1 -4
  88. package/plug-api/syscalls/shell.ts +4 -1
  89. package/plug-api/syscalls/space.ts +3 -10
  90. package/plug-api/syscalls/system.ts +1 -4
  91. package/plug-api/syscalls/yaml.ts +2 -6
  92. package/plug-api/system_mock.ts +0 -1
  93. package/plug-api/types/client.ts +16 -1
  94. package/plug-api/types/event.ts +6 -4
  95. package/plug-api/types/manifest.ts +8 -9
  96. package/plugs/builtin_plugs.ts +2 -2
  97. package/client/plugos/sandboxes/deno_worker_sandbox.ts +0 -6
@@ -4,69 +4,18 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // version.ts
7
- var version = "2.4.1";
7
+ var version = "2.6.1";
8
8
 
9
9
  // client/plugos/plug_compile.ts
10
10
  import * as path from "node:path";
11
11
  import { readFile as readFile2, writeFile, mkdtemp, rm, mkdir } from "node:fs/promises";
12
12
  import { tmpdir } from "node:os";
13
13
  import * as YAML from "js-yaml";
14
-
15
- // build_deps.ts
16
14
  import * as esbuild from "esbuild";
17
- import { readFileSync } from "node:fs";
18
- import { resolve, dirname } from "node:path";
19
- import { fileURLToPath } from "node:url";
20
- var __filename = fileURLToPath(import.meta.url);
21
- var __dirname = dirname(__filename);
22
- function denoPlugin(options) {
23
- const configPath = options?.configPath || resolve(__dirname, "deno.json");
24
- const denoConfig = JSON.parse(readFileSync(configPath, "utf-8"));
25
- const imports = denoConfig.imports || {};
26
- return {
27
- name: "deno-resolver",
28
- setup(build) {
29
- build.onResolve({ filter: /^file:\/\// }, (args) => {
30
- const filePath = args.path.replace(/^file:\/\//, "");
31
- return { path: filePath };
32
- });
33
- build.onResolve({ filter: /^@silverbulletmd\/silverbullet\// }, (args) => {
34
- const importPath = args.path;
35
- if (imports[importPath]) {
36
- const resolvedPath = resolve(__dirname, imports[importPath]);
37
- return { path: resolvedPath };
38
- }
39
- return null;
40
- });
41
- build.onResolve({ filter: /.*/ }, (args) => {
42
- if (args.path.startsWith(".") || args.path.startsWith("/")) {
43
- return null;
44
- }
45
- const importPath = args.path;
46
- if (imports[importPath]) {
47
- let mapped = imports[importPath];
48
- if (mapped.startsWith("npm:")) {
49
- return { path: mapped.replace(/^npm:/, ""), external: true };
50
- }
51
- if (mapped.startsWith("jsr:")) {
52
- return { path: args.path, external: true };
53
- }
54
- if (mapped.startsWith("http://") || mapped.startsWith("https://")) {
55
- return { path: args.path, external: true };
56
- }
57
- const resolvedPath = resolve(__dirname, mapped);
58
- return { path: resolvedPath };
59
- }
60
- return null;
61
- });
62
- }
63
- };
64
- }
65
15
 
66
16
  // client/asset_bundle/builder.ts
67
- import picomatch from "picomatch";
68
17
  import { readFile, readdir } from "node:fs/promises";
69
- import { join } from "node:path";
18
+ import { join, matchesGlob } from "node:path";
70
19
 
71
20
  // node_modules/mime/dist/types/other.js
72
21
  var types = {
@@ -1350,10 +1299,9 @@ async function bundleAssets(rootPath, patterns) {
1350
1299
  if (patterns.length === 0) {
1351
1300
  return bundle;
1352
1301
  }
1353
- const isMatch = picomatch(patterns);
1354
1302
  for await (const file of walk(rootPath)) {
1355
1303
  const cleanPath = file.path.substring(rootPath.length + 1);
1356
- if (isMatch(cleanPath)) {
1304
+ if (patterns.some((p) => matchesGlob(cleanPath, p))) {
1357
1305
  bundle.writeFileSync(
1358
1306
  cleanPath,
1359
1307
  src_default.getType(cleanPath) || "application/octet-stream",
@@ -1365,7 +1313,25 @@ async function bundleAssets(rootPath, patterns) {
1365
1313
  }
1366
1314
 
1367
1315
  // client/plugos/plug_compile.ts
1368
- var workerRuntimeUrl = `https://deno.land/x/silverbullet@${version}/client/plugos/worker_runtime.ts`;
1316
+ var workerRuntimePlugin = {
1317
+ name: "worker-runtime",
1318
+ setup(build2) {
1319
+ if (true) {
1320
+ build2.onResolve({ filter: /^worker-runtime$/ }, () => ({
1321
+ path: "worker-runtime",
1322
+ namespace: "worker-runtime"
1323
+ }));
1324
+ build2.onLoad({ filter: /.*/, namespace: "worker-runtime" }, () => ({
1325
+ contents: '// plug-api/lib/crypto.ts\nfunction base64Decode(s) {\n const binString = atob(s);\n const len = binString.length;\n const bytes = new Uint8Array(len);\n for (let i = 0; i < len; i++) {\n bytes[i] = binString.charCodeAt(i);\n }\n return bytes;\n}\nfunction base64Encode(buffer) {\n if (typeof buffer === "string") {\n buffer = new TextEncoder().encode(buffer);\n }\n let binary = "";\n const len = buffer.byteLength;\n for (let i = 0; i < len; i++) {\n binary += String.fromCharCode(buffer[i]);\n }\n return btoa(binary);\n}\nvar fixedCounter = new Uint8Array(16);\n\n// client/lib/logger.ts\nvar Logger = class {\n constructor(prefix = "", maxCaptureSize = 1e3) {\n this.prefix = prefix;\n this.maxCaptureSize = maxCaptureSize;\n this.prefix = prefix;\n this.originalConsole = {\n log: console.log.bind(console),\n info: console.info.bind(console),\n warn: console.warn.bind(console),\n error: console.error.bind(console),\n debug: console.debug.bind(console)\n };\n this.patchConsole();\n }\n originalConsole;\n logBuffer = [];\n patchConsole() {\n const createPatchedMethod = (level) => {\n return (...args) => {\n const prefixedArgs = this.prefix ? [this.prefix, ...args] : args;\n this.originalConsole[level](...prefixedArgs);\n this.captureLog(level, args);\n };\n };\n console.log = createPatchedMethod("log");\n console.info = createPatchedMethod("info");\n console.warn = createPatchedMethod("warn");\n console.error = createPatchedMethod("error");\n console.debug = createPatchedMethod("debug");\n }\n captureLog(level, args) {\n const entry = {\n level,\n timestamp: Date.now(),\n message: args.map((arg) => {\n if (typeof arg === "string") {\n return arg;\n }\n try {\n return JSON.stringify(arg);\n } catch {\n return String(arg);\n }\n }).join(" ")\n };\n this.logBuffer.push(entry);\n if (this.logBuffer.length > this.maxCaptureSize) {\n this.logBuffer.shift();\n }\n }\n /**\n * Posts all buffered logs to a server endpoint\n */\n async postToServer(logEndpoint, source) {\n const logs = this.logBuffer;\n if (logs.length > 0) {\n const logCopy = [...this.logBuffer];\n this.logBuffer = [];\n try {\n const resp = await fetch(logEndpoint, {\n method: "POST",\n headers: {\n "Content-Type": "application/json"\n },\n body: JSON.stringify(logCopy.map((entry) => ({ ...entry, source })))\n });\n if (!resp.ok) {\n throw new Error("Failed to post logs to server");\n }\n } catch (e) {\n console.warn("Could not post logs to server", e.message);\n this.logBuffer.unshift(...logCopy);\n }\n }\n }\n};\nvar globalLogger;\nfunction initLogger(prefix = "") {\n globalLogger = new Logger(prefix);\n return globalLogger;\n}\n\n// client/plugos/worker_runtime.ts\nvar workerPostMessage = (_msg) => {\n throw new Error("Not initialized yet");\n};\nvar runningAsWebWorker = typeof window === "undefined" && // @ts-expect-error: globalThis\ntypeof globalThis.WebSocketPair === "undefined";\nvar pendingRequests = /* @__PURE__ */ new Map();\nvar syscallReqId = 0;\nif (runningAsWebWorker) {\n globalThis.syscall = async (name, ...args) => {\n return await new Promise((resolve, reject) => {\n syscallReqId++;\n pendingRequests.set(syscallReqId, { resolve, reject });\n workerPostMessage({\n type: "sys",\n id: syscallReqId,\n name,\n args\n });\n });\n };\n}\nfunction setupMessageListener(functionMapping, manifest, postMessageFn) {\n if (!runningAsWebWorker) {\n return;\n }\n workerPostMessage = postMessageFn;\n self.addEventListener("message", (event) => {\n (async () => {\n const data = event.data;\n switch (data.type) {\n case "inv":\n {\n const fn = functionMapping[data.name];\n if (!fn) {\n throw new Error(`Function not loaded: ${data.name}`);\n }\n try {\n const result = await Promise.resolve(fn(...data.args || []));\n workerPostMessage({\n type: "invr",\n id: data.id,\n result\n });\n } catch (e) {\n console.error(\n "An exception was thrown as a result of invoking function",\n data.name,\n "error:",\n e.message\n );\n workerPostMessage({\n type: "invr",\n id: data.id,\n error: e.message\n });\n }\n }\n break;\n case "sysr":\n {\n const syscallId = data.id;\n const lookup = pendingRequests.get(syscallId);\n if (!lookup) {\n throw Error("Invalid request id");\n }\n pendingRequests.delete(syscallId);\n if (data.error) {\n lookup.reject(new Error(data.error));\n } else {\n lookup.resolve(data.result);\n }\n }\n break;\n }\n })().catch(console.error);\n });\n workerPostMessage({\n type: "manifest",\n manifest\n });\n initLogger(`[${manifest.name} plug]`);\n}\nasync function sandboxFetch(reqInfo, options) {\n if (typeof reqInfo !== "string") {\n const body = new Uint8Array(await reqInfo.arrayBuffer());\n const encodedBody = body.length > 0 ? base64Encode(body) : void 0;\n options = {\n method: reqInfo.method,\n headers: Object.fromEntries(reqInfo.headers.entries()),\n base64Body: encodedBody\n };\n reqInfo = reqInfo.url;\n }\n return syscall("sandboxFetch.fetch", reqInfo, options);\n}\nglobalThis.nativeFetch = globalThis.fetch;\nfunction monkeyPatchFetch() {\n globalThis.fetch = async (reqInfo, init) => {\n const encodedBody = init?.body ? base64Encode(\n new Uint8Array(await new Response(init.body).arrayBuffer())\n ) : void 0;\n const r = await sandboxFetch(\n reqInfo,\n init && {\n method: init.method,\n headers: init.headers,\n base64Body: encodedBody\n }\n );\n return new Response(\n r.base64Body ? base64Decode(r.base64Body) : null,\n {\n status: r.status,\n headers: r.headers\n }\n );\n };\n}\nif (runningAsWebWorker) {\n monkeyPatchFetch();\n}\nexport {\n monkeyPatchFetch,\n sandboxFetch,\n setupMessageListener\n};\n',
1326
+ loader: "js"
1327
+ }));
1328
+ } else {
1329
+ build2.onResolve({ filter: /^worker-runtime$/ }, () => ({
1330
+ path: path.join(import.meta.dirname, "worker_runtime.ts")
1331
+ }));
1332
+ }
1333
+ }
1334
+ };
1369
1335
  async function compileManifest(manifestPath, destPath, options = {}) {
1370
1336
  const rootPath = path.dirname(manifestPath);
1371
1337
  const manifestContent = await readFile2(manifestPath, "utf-8");
@@ -1382,7 +1348,7 @@ async function compileManifest(manifestPath, destPath, options = {}) {
1382
1348
  manifest.functions = {};
1383
1349
  }
1384
1350
  const jsFile = `
1385
- import { setupMessageListener } from "${options.runtimeUrl || workerRuntimeUrl}";
1351
+ import { setupMessageListener } from "worker-runtime";
1386
1352
 
1387
1353
  // Imports
1388
1354
  ${Object.entries(manifest.functions).map(([funcName, def]) => {
@@ -1391,11 +1357,8 @@ ${Object.entries(manifest.functions).map(([funcName, def]) => {
1391
1357
  }
1392
1358
  let [filePath, jsFunctionName] = def.path.split(":");
1393
1359
  filePath = path.join(rootPath, filePath);
1394
- return `import {${jsFunctionName} as ${funcName}} from "file://${// Replacing \ with / for Windows
1395
- path.resolve(filePath).replaceAll(
1396
- "\\",
1397
- "\\\\"
1398
- )}";
1360
+ return `import {${jsFunctionName} as ${funcName}} from "${// Replacing \ with / for Windows
1361
+ path.resolve(filePath).replaceAll("\\", "\\\\")}";
1399
1362
  `;
1400
1363
  }).join("")}
1401
1364
 
@@ -1432,18 +1395,14 @@ setupMessageListener(functionMapping, manifest, self.postMessage);
1432
1395
  outfile: outFile,
1433
1396
  metafile: options.info,
1434
1397
  treeShaking: true,
1435
- plugins: [
1436
- denoPlugin({
1437
- configPath: options.configPath && path.resolve(process.cwd(), options.configPath)
1438
- })
1439
- ]
1398
+ plugins: [workerRuntimePlugin]
1440
1399
  });
1441
1400
  if (options.info) {
1442
1401
  const text = await esbuild.analyzeMetafile(result.metafile);
1443
1402
  console.log("Bundle info for", manifestPath, text);
1444
1403
  }
1445
1404
  let jsCode = await readFile2(outFile, "utf-8");
1446
- jsCode = patchDenoLibJS(jsCode);
1405
+ jsCode = patchBundledJS(jsCode);
1447
1406
  await writeFile(outFile, jsCode, "utf-8");
1448
1407
  await rm(tempDir, { recursive: true, force: true });
1449
1408
  console.log(`Plug ${manifest.name} written to ${outFile}.`);
@@ -1460,52 +1419,50 @@ async function compileManifests(manifestFiles, dist, options = {}) {
1460
1419
  building = true;
1461
1420
  await mkdir(dist, { recursive: true });
1462
1421
  const startTime = Date.now();
1463
- await Promise.all(manifestFiles.map(async (plugManifestPath) => {
1464
- const manifestPath = plugManifestPath;
1465
- try {
1466
- await compileManifest(
1467
- manifestPath,
1468
- dist,
1469
- options
1470
- );
1471
- } catch (e) {
1472
- console.error(`Error building ${manifestPath}:`, e.message);
1473
- throw e;
1474
- }
1475
- }));
1422
+ await Promise.all(
1423
+ manifestFiles.map(async (plugManifestPath) => {
1424
+ const manifestPath = plugManifestPath;
1425
+ try {
1426
+ await compileManifest(manifestPath, dist, options);
1427
+ } catch (e) {
1428
+ console.error(`Error building ${manifestPath}:`, e.message);
1429
+ throw e;
1430
+ }
1431
+ })
1432
+ );
1476
1433
  console.log(`Done building plugs in ${Date.now() - startTime}ms`);
1477
1434
  building = false;
1478
1435
  }
1479
1436
  await buildAll();
1480
1437
  }
1481
- function patchDenoLibJS(code) {
1438
+ function patchBundledJS(code) {
1482
1439
  return code.replaceAll("/(?<=\\n)/", "/()/");
1483
1440
  }
1484
- async function plugCompileCommand({ dist, debug, info, config, runtimeUrl }, ...manifestPaths) {
1485
- await compileManifests(
1486
- manifestPaths,
1487
- dist,
1488
- {
1489
- debug,
1490
- info,
1491
- runtimeUrl,
1492
- configPath: config
1493
- }
1494
- );
1495
- esbuild.stop();
1441
+ async function plugCompileCommand({
1442
+ dist,
1443
+ debug,
1444
+ info
1445
+ }, ...manifestPaths) {
1446
+ await compileManifests(manifestPaths, dist, {
1447
+ debug,
1448
+ info
1449
+ });
1450
+ await esbuild.stop();
1496
1451
  process.exit(0);
1497
1452
  }
1498
1453
 
1499
1454
  // bin/plug-compile.ts
1500
1455
  var program = new Command();
1501
- program.name("plug-compile").description("Bundle (compile) one or more plug manifests").version(version).usage("<options> <manifest paths>").argument("<manifestPaths...>", "One or more .plug.yaml manifest files").option("--debug", "Do not minify code", false).option("--info", "Print out size info per function", false).option("-w, --watch", "Watch for changes and rebuild", false).option("--dist <path>", "Folder to put the resulting .plug.json file into", ".").option("-c, --config <path>", "Path to deno.json file to use").option("--runtimeUrl <url>", "URL to worker_runtime.ts to use").action(async (manifestPaths, options) => {
1456
+ program.name("plug-compile").description("Bundle (compile) one or more plug manifests").version(version).usage("<options> <manifest paths>").argument("<manifestPaths...>", "One or more .plug.yaml manifest files").option("--debug", "Do not minify code", false).option("--info", "Print out size info per function", false).option("-w, --watch", "Watch for changes and rebuild", false).option(
1457
+ "--dist <path>",
1458
+ "Folder to put the resulting .plug.json file into",
1459
+ "."
1460
+ ).action(async (manifestPaths, options) => {
1502
1461
  await plugCompileCommand(
1503
1462
  {
1504
1463
  dist: options.dist,
1505
1464
  debug: options.debug,
1506
- info: options.info,
1507
- config: options.config,
1508
- runtimeUrl: options.runtimeUrl
1465
+ info: options.info
1509
1466
  },
1510
1467
  ...manifestPaths
1511
1468
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silverbulletmd/silverbullet",
3
- "version": "2.4.2",
3
+ "version": "2.6.1",
4
4
  "type": "module",
5
5
  "description": "A self-hosted, web-based note taking app",
6
6
  "publishConfig": {
@@ -21,6 +21,7 @@
21
21
  "./lib/tags": "./plug-api/lib/tags.ts",
22
22
  "./lib/transclusion": "./plug-api/lib/transclusion.ts",
23
23
  "./lib/native_fetch": "./plug-api/lib/native_fetch.ts",
24
+ "./lib/query_expression": "./plug-api/lib/query_expression.ts",
24
25
  "./type/client": "./plug-api/types/client.ts",
25
26
  "./type/config": "./plug-api/types/config.ts",
26
27
  "./type/manifest": "./plug-api/types/manifest.ts",
@@ -50,39 +51,44 @@
50
51
  ],
51
52
  "scripts": {
52
53
  "build": "npm run build:plugs && npm run build:client",
53
- "build:plugs": "tsx build_plugs_libraries.ts",
54
- "build:client": "tsx build_client.ts",
55
- "build:plug-compile": "tsx build_plug_compile.ts",
54
+ "build:plugs": "tsx build/build_plugs.ts",
55
+ "build:client": "tsx build/build_client.ts",
56
+ "build:plug-compile": "tsx build/build_plug_compile.ts",
56
57
  "prepublishOnly": "npm run build:plug-compile",
57
58
  "test": "vitest run",
58
59
  "check": "tsc --noEmit",
59
- "bench": "vitest bench"
60
+ "lint": "biome lint .",
61
+ "fmt": "biome format --write .",
62
+ "fmt:check": "biome format .",
63
+ "bench": "vitest bench",
64
+ "test:e2e": "npx playwright test",
65
+ "test:e2e:headed": "npx playwright test --headed"
60
66
  },
61
67
  "dependencies": {
62
- "@codemirror/autocomplete": "6.20.0",
63
- "@codemirror/commands": "6.10.1",
68
+ "@cfworker/json-schema": "^4.1.1",
69
+ "@codemirror/autocomplete": "6.20.1",
70
+ "@codemirror/commands": "6.10.3",
64
71
  "@codemirror/lang-css": "6.3.1",
65
72
  "@codemirror/lang-html": "6.4.11",
66
- "@codemirror/lang-javascript": "6.2.4",
73
+ "@codemirror/lang-javascript": "6.2.5",
67
74
  "@codemirror/lang-markdown": "6.5.0",
68
- "@codemirror/language": "6.12.1",
75
+ "@codemirror/language": "6.12.3",
69
76
  "@codemirror/legacy-modes": "6.5.2",
70
- "@codemirror/lint": "6.9.2",
77
+ "@codemirror/lint": "6.9.5",
71
78
  "@codemirror/search": "6.6.0",
72
- "@codemirror/state": "6.5.4",
73
- "@codemirror/view": "6.39.11",
79
+ "@codemirror/state": "6.6.0",
80
+ "@codemirror/view": "6.41.0",
74
81
  "@joplin/turndown-plugin-gfm": "1.0.64",
75
- "@lezer/common": "1.5.0",
76
- "@lezer/css": "1.3.0",
82
+ "@lezer/common": "1.5.1",
83
+ "@lezer/css": "1.3.3",
77
84
  "@lezer/highlight": "1.2.3",
78
85
  "@lezer/html": "1.3.13",
79
86
  "@lezer/javascript": "1.5.4",
80
- "@lezer/lr": "1.4.7",
87
+ "@lezer/lr": "1.4.8",
81
88
  "@lezer/markdown": "1.6.3",
82
89
  "@msgpack/msgpack": "3.1.3",
83
90
  "@replit/codemirror-lang-nix": "6.0.1",
84
91
  "@replit/codemirror-vim": "6.3.0",
85
- "ajv": "8.17.1",
86
92
  "commander": "^14.0.3",
87
93
  "crelt": "1.0.6",
88
94
  "esbuild": "^0.27.3",
@@ -93,20 +99,20 @@
93
99
  "idb": "8.0.3",
94
100
  "js-yaml": "^4.1.1",
95
101
  "mime": "4.1.0",
96
- "picomatch": "^4.0.3",
97
102
  "preact": "10.28.2",
98
103
  "preact-feather": "4.2.1",
99
104
  "react-icons": "5.5.0",
100
105
  "sass": "^1.97.3",
101
- "style-mod": "4.1.2",
106
+ "style-mod": "4.1.3",
102
107
  "turndown": "7.2.2"
103
108
  },
104
109
  "devDependencies": {
110
+ "@biomejs/biome": "2.4.6",
111
+ "@playwright/test": "^1.58.2",
105
112
  "@preact/preset-vite": "^2.10.3",
106
113
  "@types/gitignore-parser": "^0.0.3",
107
114
  "@types/js-yaml": "^4.0.9",
108
115
  "@types/node": "^22.0.0",
109
- "@types/picomatch": "^4.0.2",
110
116
  "@types/turndown": "^5.0.6",
111
117
  "fake-indexeddb": "6.0.1",
112
118
  "tsx": "^4.19.0",
@@ -115,6 +121,7 @@
115
121
  "vitest": "^4.0.18"
116
122
  },
117
123
  "engines": {
118
- "node": ">=20.0.0"
124
+ "node": ">=24.13.0",
125
+ "npm": ">=10.0.0"
119
126
  }
120
127
  }
@@ -8,35 +8,3 @@ export const wrongSpacePathError: Error = new Error(
8
8
  );
9
9
  export const pingTimeout: number = 2000;
10
10
  export const pingInterval: number = 5000;
11
-
12
- /**
13
- * HTTP status codes that should be treated as "offline" conditions.
14
- *
15
- * This is particularly useful for cases where a proxy (such as Cloudflare or other reverse proxies)
16
- * indicates that the backend server is down, but there is still network connectivity between
17
- * the user and the proxy. In these scenarios, we want to allow the user to continue working
18
- * with their cached data rather than showing an error, even though technically there is network
19
- * connectivity to the proxy.
20
- *
21
- * This enables SilverBullet to work in a true "offline-first" manner, falling back to cached
22
- * content when the backend is unavailable through no fault of the user's network connection.
23
- *
24
- * All 5xx server errors are included to prevent the client from caching error HTML pages
25
- * (e.g., Nginx 500 error pages) which would prevent the client from booting in offline mode.
26
- */
27
- export const offlineStatusCodes = {
28
- 500: "Internal Server Error", // Server encountered an unexpected condition
29
- 501: "Not Implemented", // Server does not support the functionality required
30
- 502: "Bad Gateway", // Proxy server received invalid response from upstream server
31
- 503: "Service Unavailable", // Server is temporarily unable to handle the request
32
- 504: "Gateway Timeout", // Proxy server did not receive a timely response from upstream server
33
- 505: "HTTP Version Not Supported", // Server does not support the HTTP version
34
- 506: "Variant Also Negotiates", // Server has an internal configuration error
35
- 507: "Insufficient Storage", // Server is unable to store the representation
36
- 508: "Loop Detected", // Server detected an infinite loop while processing
37
- 509: "Bandwidth Limit Exceeded", // Server bandwidth limit has been exceeded
38
- 510: "Not Extended", // Further extensions to the request are required
39
- 511: "Network Authentication Required", // Client needs to authenticate to gain network access
40
-
41
- 530: "Unable to resolve origin hostname", // Served when cloudflared is down on the host
42
- } as const;
@@ -1,6 +1,9 @@
1
- export function throttle(func: () => void, limit: number): () => void {
1
+ export function throttle(
2
+ func: () => void,
3
+ limit: number,
4
+ ): (() => void) & { flush(): void } {
2
5
  let timer: any = null;
3
- return function () {
6
+ const throttled = () => {
4
7
  if (!timer) {
5
8
  timer = setTimeout(() => {
6
9
  func();
@@ -8,6 +11,15 @@ export function throttle(func: () => void, limit: number): () => void {
8
11
  }, limit);
9
12
  }
10
13
  };
14
+ // Immediately execute any pending call and cancel the timer
15
+ throttled.flush = () => {
16
+ if (timer) {
17
+ clearTimeout(timer);
18
+ timer = null;
19
+ func();
20
+ }
21
+ };
22
+ return throttled;
11
23
  }
12
24
 
13
25
  export function throttleImmediately(
@@ -15,7 +27,7 @@ export function throttleImmediately(
15
27
  limit: number,
16
28
  ): () => void {
17
29
  let timer: any = null;
18
- return function () {
30
+ return () => {
19
31
  if (!timer) {
20
32
  func();
21
33
  timer = setTimeout(() => {
@@ -38,7 +50,7 @@ export function timeout(ms: number): Promise<never> {
38
50
  return new Promise((_resolve, reject) =>
39
51
  setTimeout(() => {
40
52
  reject(new Error("timeout"));
41
- }, ms)
53
+ }, ms),
42
54
  );
43
55
  }
44
56
 
@@ -58,7 +70,7 @@ export class PromiseQueue {
58
70
  return new Promise((resolve, reject) => {
59
71
  this.queue.push({ fn, resolve, reject });
60
72
  if (!this.processing) {
61
- this.process();
73
+ void this.process();
62
74
  }
63
75
  });
64
76
  }
@@ -79,7 +91,7 @@ export class PromiseQueue {
79
91
  reject(error);
80
92
  }
81
93
 
82
- this.process(); // Continue processing the next promise in the queue
94
+ void this.process(); // Continue processing the next promise in the queue
83
95
  }
84
96
  }
85
97
 
@@ -105,7 +117,8 @@ export async function batchRequests<I, O>(
105
117
  const batchResults = await Promise.all(batches.map(fn));
106
118
  // Flatten the results
107
119
  for (const batchResult of batchResults) {
108
- if (Array.isArray(batchResult)) { // If fn returns an array, collect them
120
+ if (Array.isArray(batchResult)) {
121
+ // If fn returns an array, collect them
109
122
  results.push(...batchResult);
110
123
  }
111
124
  }
@@ -42,9 +42,8 @@ export async function hashSHA256(
42
42
  ): Promise<string> {
43
43
  // Transform the string into an ArrayBuffer
44
44
  const encoder = new TextEncoder();
45
- const data: Uint8Array = typeof message === "string"
46
- ? encoder.encode(message)
47
- : message;
45
+ const data: Uint8Array =
46
+ typeof message === "string" ? encoder.encode(message) : message;
48
47
 
49
48
  // Generate the hash
50
49
  const hashBuffer = await globalThis.crypto.subtle.digest(
@@ -53,9 +52,9 @@ export async function hashSHA256(
53
52
  );
54
53
 
55
54
  // Transform the hash into a hex string
56
- return Array.from(new Uint8Array(hashBuffer)).map((b) =>
57
- b.toString(16).padStart(2, "0")
58
- ).join("");
55
+ return Array.from(new Uint8Array(hashBuffer))
56
+ .map((b) => b.toString(16).padStart(2, "0"))
57
+ .join("");
59
58
  }
60
59
 
61
60
  /**
@@ -85,7 +84,7 @@ export async function encryptStringDeterministic(
85
84
  key: CryptoKey,
86
85
  clearText: string,
87
86
  ): Promise<string> {
88
- const encrypted = await crypto.subtle.encrypt(
87
+ const encrypted = await globalThis.crypto.subtle.encrypt(
89
88
  { name: "AES-CTR", counter: fixedCounter, length: fixedCounter.length * 8 },
90
89
  key,
91
90
  new TextEncoder().encode(clearText),
@@ -97,7 +96,7 @@ export async function decryptStringDeterministic(
97
96
  key: CryptoKey,
98
97
  cipherText: string,
99
98
  ): Promise<string> {
100
- const decrypted = await crypto.subtle.decrypt(
99
+ const decrypted = await globalThis.crypto.subtle.decrypt(
101
100
  { name: "AES-CTR", counter: fixedCounter, length: fixedCounter.length * 8 },
102
101
  key,
103
102
  base64Decode(cipherText) as BufferSource,
@@ -110,8 +109,8 @@ export async function encryptAesGcm(
110
109
  key: CryptoKey,
111
110
  data: Uint8Array,
112
111
  ): Promise<Uint8Array> {
113
- const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit IV recommended for GCM
114
- const encryptedBuffer = await crypto.subtle.encrypt(
112
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); // 96-bit IV recommended for GCM
113
+ const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
115
114
  { name: "AES-GCM", iv },
116
115
  key,
117
116
  data as BufferSource,
@@ -132,7 +131,7 @@ export async function decryptAesGcm(
132
131
  ): Promise<Uint8Array> {
133
132
  const iv = encryptedData.slice(0, 12); // extract IV (first 12 bytes)
134
133
  const ciphertext = encryptedData.slice(12);
135
- const decryptedBuffer = await crypto.subtle.decrypt(
134
+ const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
136
135
  { name: "AES-GCM", iv },
137
136
  key,
138
137
  ciphertext,
@@ -148,7 +147,7 @@ export async function deriveCTRKeyFromPassword(
148
147
  const passwordBytes = new TextEncoder().encode(password);
149
148
 
150
149
  // Import password as a CryptoKey
151
- const baseKey = await crypto.subtle.importKey(
150
+ const baseKey = await globalThis.crypto.subtle.importKey(
152
151
  "raw",
153
152
  passwordBytes,
154
153
  { name: "PBKDF2" },
@@ -156,7 +155,7 @@ export async function deriveCTRKeyFromPassword(
156
155
  ["deriveBits", "deriveKey"],
157
156
  );
158
157
 
159
- return crypto.subtle.deriveKey(
158
+ return globalThis.crypto.subtle.deriveKey(
160
159
  {
161
160
  name: "PBKDF2",
162
161
  salt: salt as BufferSource,
@@ -174,7 +173,7 @@ export async function deriveCTRKeyFromPassword(
174
173
  }
175
174
 
176
175
  export function importKey(b64EncodedKey: string): Promise<CryptoKey> {
177
- return crypto.subtle.importKey(
176
+ return globalThis.crypto.subtle.importKey(
178
177
  "raw",
179
178
  base64Decode(b64EncodedKey) as BufferSource,
180
179
  { name: "AES-CTR" },
@@ -184,15 +183,15 @@ export function importKey(b64EncodedKey: string): Promise<CryptoKey> {
184
183
  }
185
184
 
186
185
  export async function exportKey(ctrKey: CryptoKey): Promise<string> {
187
- const key = await crypto.subtle.exportKey("raw", ctrKey);
186
+ const key = await globalThis.crypto.subtle.exportKey("raw", ctrKey);
188
187
  return base64Encode(new Uint8Array(key));
189
188
  }
190
189
 
191
190
  export async function deriveGCMKeyFromCTR(
192
191
  ctrKey: CryptoKey,
193
192
  ): Promise<CryptoKey> {
194
- const rawKey = await crypto.subtle.exportKey("raw", ctrKey);
195
- return crypto.subtle.importKey(
193
+ const rawKey = await globalThis.crypto.subtle.exportKey("raw", ctrKey);
194
+ return globalThis.crypto.subtle.importKey(
196
195
  "raw",
197
196
  rawKey,
198
197
  { name: "AES-GCM" },
@@ -3,11 +3,19 @@ export function niceDate(d: Date): string {
3
3
  }
4
4
 
5
5
  export function localDateString(d: Date): string {
6
- return d.getFullYear() +
7
- "-" + String(d.getMonth() + 1).padStart(2, "0") +
8
- "-" + String(d.getDate()).padStart(2, "0") +
9
- "T" + String(d.getHours()).padStart(2, "0") +
10
- ":" + String(d.getMinutes()).padStart(2, "0") +
11
- ":" + String(d.getSeconds()).padStart(2, "0") +
12
- "." + String(d.getMilliseconds()).padStart(3, "0");
6
+ return (
7
+ d.getFullYear() +
8
+ "-" +
9
+ String(d.getMonth() + 1).padStart(2, "0") +
10
+ "-" +
11
+ String(d.getDate()).padStart(2, "0") +
12
+ "T" +
13
+ String(d.getHours()).padStart(2, "0") +
14
+ ":" +
15
+ String(d.getMinutes()).padStart(2, "0") +
16
+ ":" +
17
+ String(d.getSeconds()).padStart(2, "0") +
18
+ "." +
19
+ String(d.getMilliseconds()).padStart(3, "0")
20
+ );
13
21
  }
@@ -52,11 +52,17 @@ export function deepEqual(a: any, b: any): boolean {
52
52
  export function cleanStringDate(d: Date): string {
53
53
  // If no significant time, return a date string only
54
54
  if (
55
- d.getUTCHours() === 0 && d.getUTCMinutes() === 0 && d.getUTCSeconds() === 0
55
+ d.getUTCHours() === 0 &&
56
+ d.getUTCMinutes() === 0 &&
57
+ d.getUTCSeconds() === 0
56
58
  ) {
57
- return d.getUTCFullYear() + "-" +
58
- String(d.getUTCMonth() + 1).padStart(2, "0") + "-" +
59
- String(d.getUTCDate()).padStart(2, "0");
59
+ return (
60
+ d.getUTCFullYear() +
61
+ "-" +
62
+ String(d.getUTCMonth() + 1).padStart(2, "0") +
63
+ "-" +
64
+ String(d.getUTCDate()).padStart(2, "0")
65
+ );
60
66
  } else {
61
67
  return d.toISOString();
62
68
  }
@@ -125,7 +131,7 @@ export function deepClone<T>(obj: T, ignoreKeys: string[] = []): T {
125
131
  for (const key in obj) {
126
132
  if (ignoreKeys.includes(key)) {
127
133
  objClone[key] = obj[key];
128
- } else if (Object.prototype.hasOwnProperty.call(obj, key)) {
134
+ } else if (Object.hasOwn(obj, key)) {
129
135
  objClone[key] = deepClone(obj[key], ignoreKeys);
130
136
  }
131
137
  }
@@ -1,7 +1,7 @@
1
1
  type LimitedMapRecord<V> = {
2
2
  value: V;
3
3
  la: number;
4
- expTimer?: number;
4
+ expTimer?: ReturnType<typeof setTimeout>;
5
5
  };
6
6
 
7
7
  export class LimitedMap<V> {
@@ -1,3 +1,5 @@
1
+ export {};
2
+
1
3
  declare global {
2
4
  function nativeFetch(
3
5
  input: RequestInfo | URL,