@silverbulletmd/silverbullet 2.5.3 → 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.
Files changed (96) hide show
  1. package/README.md +4 -6
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +4 -2
  5. package/client/plugos/hooks/code_widget.ts +3 -5
  6. package/client/plugos/hooks/command.ts +54 -24
  7. package/client/plugos/hooks/document_editor.ts +10 -12
  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 -5
  11. package/client/plugos/hooks/slash_command.ts +15 -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 +7 -7
  15. package/client/plugos/plug_compile.ts +119 -67
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/worker_sandbox.ts +26 -15
  19. package/client/plugos/syscalls/asset.ts +1 -3
  20. package/client/plugos/syscalls/code_widget.ts +1 -3
  21. package/client/plugos/syscalls/config.ts +10 -7
  22. package/client/plugos/syscalls/datastore.ts +1 -1
  23. package/client/plugos/syscalls/editor.ts +199 -60
  24. package/client/plugos/syscalls/event.ts +9 -12
  25. package/client/plugos/syscalls/fetch.ts +30 -22
  26. package/client/plugos/syscalls/index.ts +11 -2
  27. package/client/plugos/syscalls/jsonschema.ts +54 -32
  28. package/client/plugos/syscalls/language.ts +9 -5
  29. package/client/plugos/syscalls/markdown.ts +29 -7
  30. package/client/plugos/syscalls/mq.ts +3 -11
  31. package/client/plugos/syscalls/service_registry.ts +1 -4
  32. package/client/plugos/syscalls/shell.ts +2 -5
  33. package/client/plugos/syscalls/sync.ts +69 -60
  34. package/client/plugos/syscalls/system.ts +15 -3
  35. package/client/plugos/system.ts +18 -20
  36. package/client/plugos/types.ts +25 -0
  37. package/client/plugos/util.ts +20 -0
  38. package/client/plugos/worker_runtime.ts +4 -3
  39. package/client/space_lua/aggregates.ts +633 -60
  40. package/client/space_lua/ast.ts +21 -9
  41. package/client/space_lua/ast_narrow.ts +4 -2
  42. package/client/space_lua/eval.ts +842 -536
  43. package/client/space_lua/labels.ts +6 -11
  44. package/client/space_lua/numeric.ts +5 -8
  45. package/client/space_lua/parse.ts +290 -169
  46. package/client/space_lua/query_collection.ts +213 -149
  47. package/client/space_lua/render_lua_markdown.ts +369 -0
  48. package/client/space_lua/rp.ts +5 -4
  49. package/client/space_lua/runtime.ts +245 -142
  50. package/client/space_lua/sliq_null.ts +6 -0
  51. package/client/space_lua/stdlib/format.ts +34 -20
  52. package/client/space_lua/stdlib/js.ts +3 -7
  53. package/client/space_lua/stdlib/load.ts +1 -3
  54. package/client/space_lua/stdlib/math.ts +15 -14
  55. package/client/space_lua/stdlib/net.ts +25 -15
  56. package/client/space_lua/stdlib/os.ts +76 -85
  57. package/client/space_lua/stdlib/pattern.ts +28 -35
  58. package/client/space_lua/stdlib/prng.ts +15 -12
  59. package/client/space_lua/stdlib/space_lua.ts +16 -17
  60. package/client/space_lua/stdlib/string.ts +7 -17
  61. package/client/space_lua/stdlib/string_pack.ts +23 -19
  62. package/client/space_lua/stdlib/table.ts +5 -9
  63. package/client/space_lua/stdlib.ts +20 -30
  64. package/client/space_lua/tonumber.ts +79 -40
  65. package/client/space_lua/util.ts +14 -10
  66. package/dist/plug-compile.js +87 -42
  67. package/package.json +24 -22
  68. package/plug-api/constants.ts +20 -0
  69. package/plug-api/lib/async.ts +19 -6
  70. package/plug-api/lib/crypto.ts +5 -6
  71. package/plug-api/lib/dates.ts +15 -7
  72. package/plug-api/lib/json.ts +10 -4
  73. package/plug-api/lib/ref.ts +27 -20
  74. package/plug-api/lib/resolve.ts +7 -11
  75. package/plug-api/lib/shortcut.ts +16 -0
  76. package/plug-api/lib/tags.ts +13 -4
  77. package/plug-api/lib/transclusion.ts +6 -17
  78. package/plug-api/lib/tree.ts +146 -43
  79. package/plug-api/lib/yaml.ts +25 -15
  80. package/plug-api/syscalls/asset.ts +1 -1
  81. package/plug-api/syscalls/config.ts +38 -4
  82. package/plug-api/syscalls/editor.ts +169 -14
  83. package/plug-api/syscalls/index.ts +18 -0
  84. package/plug-api/syscalls/jsonschema.ts +1 -3
  85. package/plug-api/syscalls/lua.ts +3 -9
  86. package/plug-api/syscalls/mq.ts +1 -4
  87. package/plug-api/syscalls/shell.ts +4 -1
  88. package/plug-api/syscalls/space.ts +3 -10
  89. package/plug-api/syscalls/system.ts +16 -4
  90. package/plug-api/syscalls/yaml.ts +2 -6
  91. package/plug-api/types/client.ts +16 -1
  92. package/plug-api/types/event.ts +6 -4
  93. package/plug-api/types/manifest.ts +71 -9
  94. package/plugs/builtin_plugs.ts +3 -3
  95. package/client/plugos/sandboxes/web_worker_sandbox.ts +0 -17
  96. package/dist/worker_runtime_bundle.js +0 -233
@@ -1,40 +1,44 @@
1
1
  import type { SysCallMapping } from "../system.ts";
2
- import { Ajv, type ValidateFunction } from "ajv";
2
+ import { type OutputUnit, Validator, format } from "@cfworker/json-schema";
3
+ import { stripFunctions } from "../util.ts";
3
4
 
4
- const ajv = new Ajv();
5
+ // Register custom formats
6
+ format.email = (data: string) => data.includes("@");
7
+ format["page-ref"] = (data: string) =>
8
+ data.startsWith("[[") && data.endsWith("]]");
5
9
 
6
- ajv.addFormat("email", {
7
- validate: (data: string) => {
8
- // TODO: Implement email validation
9
- return data.includes("@");
10
- },
11
- async: false,
12
- });
10
+ const schemaCache = new Map<string, Validator>();
13
11
 
14
- ajv.addFormat("page-ref", {
15
- validate: (data: string) => {
16
- return data.startsWith("[[") && data.endsWith("]]");
17
- },
18
- async: false,
19
- });
12
+ function formatErrors(errors: OutputUnit[]): string {
13
+ // Filter out "properties" wrapper errors, keep only the specific leaf errors
14
+ const leafErrors = errors.filter((e) => e.keyword !== "properties");
15
+ const errorsToUse = leafErrors.length > 0 ? leafErrors : errors;
20
16
 
21
- const schemaCache = new Map<string, ValidateFunction>();
17
+ return errorsToUse
18
+ .map((e) => {
19
+ // Convert instanceLocation from "#/foo/bar" to "foo.bar"
20
+ const path =
21
+ e.instanceLocation === "#"
22
+ ? ""
23
+ : e.instanceLocation.slice(2).replaceAll("/", ".");
24
+ return path ? `${path}: ${e.error}` : e.error;
25
+ })
26
+ .join(", ");
27
+ }
22
28
 
23
29
  export function validateObject(schema: any, object: any): undefined | string {
24
30
  try {
25
31
  const schemaKey = JSON.stringify(schema);
26
32
  if (!schemaCache.has(schemaKey)) {
27
- const validate = ajv.compile(schema);
28
- schemaCache.set(schemaKey, validate);
33
+ const validator = new Validator(schema, "7");
34
+ schemaCache.set(schemaKey, validator);
29
35
  }
30
- const validate = schemaCache.get(schemaKey)!;
31
- if (validate(object)) {
36
+ const validator = schemaCache.get(schemaKey)!;
37
+ const result = validator.validate(stripFunctions(object));
38
+ if (result.valid) {
32
39
  return;
33
40
  } else {
34
- let text = ajv.errorsText(validate.errors);
35
- text = text.replaceAll("/", ".");
36
- text = text.replace(/^data[.\s]/, "");
37
- return text;
41
+ return formatErrors(result.errors);
38
42
  }
39
43
  } catch (e: any) {
40
44
  return e.message;
@@ -42,12 +46,33 @@ export function validateObject(schema: any, object: any): undefined | string {
42
46
  }
43
47
 
44
48
  export function validateSchema(schema: any): undefined | string {
45
- const valid = ajv.validateSchema(schema);
46
- if (valid) {
49
+ if (schema === null || schema === undefined) {
50
+ return "schema must not be null or undefined";
51
+ }
52
+ if (typeof schema === "boolean") {
47
53
  return;
48
- } else {
49
- return ajv.errorsText(ajv.errors);
50
54
  }
55
+ if (typeof schema !== "object" || Array.isArray(schema)) {
56
+ return "schema must be an object or boolean";
57
+ }
58
+ if (schema.type !== undefined) {
59
+ const validTypes = [
60
+ "string",
61
+ "number",
62
+ "integer",
63
+ "boolean",
64
+ "object",
65
+ "array",
66
+ "null",
67
+ ];
68
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
69
+ for (const t of types) {
70
+ if (!validTypes.includes(t)) {
71
+ return `schema.type must be one of ${validTypes.join(", ")}`;
72
+ }
73
+ }
74
+ }
75
+ return;
51
76
  }
52
77
 
53
78
  export function jsonschemaSyscalls(): SysCallMapping {
@@ -59,10 +84,7 @@ export function jsonschemaSyscalls(): SysCallMapping {
59
84
  ): undefined | string => {
60
85
  return validateObject(schema, object);
61
86
  },
62
- "jsonschema.validateSchema": (
63
- _ctx,
64
- schema: any,
65
- ): undefined | string => {
87
+ "jsonschema.validateSchema": (_ctx, schema: any): undefined | string => {
66
88
  return validateSchema(schema);
67
89
  },
68
90
  };
@@ -1,23 +1,27 @@
1
1
  import type { SysCallMapping } from "../system.ts";
2
2
  import { parse } from "../../markdown_parser/parse_tree.ts";
3
3
  import type { ParseTree } from "@silverbulletmd/silverbullet/lib/tree";
4
- import { builtinLanguages, languageFor } from "../../languages.ts";
4
+ import {
5
+ allLanguageNames,
6
+ languageFor,
7
+ loadLanguageFor,
8
+ } from "../../languages.ts";
5
9
 
6
10
  export function languageSyscalls(): SysCallMapping {
7
11
  return {
8
- "language.parseLanguage": (
12
+ "language.parseLanguage": async (
9
13
  _ctx,
10
14
  language: string,
11
15
  code: string,
12
- ): ParseTree => {
13
- const lang = languageFor(language);
16
+ ): Promise<ParseTree> => {
17
+ const lang = languageFor(language) ?? await loadLanguageFor(language);
14
18
  if (!lang) {
15
19
  throw new Error(`Unknown language ${language}`);
16
20
  }
17
21
  return parse(lang, code);
18
22
  },
19
23
  "language.listLanguages": (): string[] => {
20
- return Object.keys(builtinLanguages);
24
+ return allLanguageNames;
21
25
  },
22
26
  };
23
27
  }
@@ -4,7 +4,7 @@ import {
4
4
  type ParseTree,
5
5
  renderToText,
6
6
  } from "@silverbulletmd/silverbullet/lib/tree";
7
- import { extendedMarkdownLanguage } from "../../markdown_parser/parser.ts";
7
+ import { buildExtendedMarkdownLanguage } from "../../markdown_parser/parser.ts";
8
8
  import {
9
9
  expandMarkdown,
10
10
  type MarkdownExpandOptions,
@@ -18,11 +18,12 @@ import {
18
18
  jsonToMDTable,
19
19
  refCellTransformer,
20
20
  } from "../../markdown_renderer/result_render.ts";
21
+ import * as TagConstants from "../../../plugs/index/constants.ts";
21
22
 
22
23
  export function markdownSyscalls(client: Client): SysCallMapping {
23
24
  return {
24
25
  "markdown.parseMarkdown": (_ctx, text: string): ParseTree => {
25
- return parse(extendedMarkdownLanguage, text);
26
+ return parse(markdownLanguageWithUserExtensions(client), text);
26
27
  },
27
28
  "markdown.renderParseTree": (_ctx, tree: ParseTree): string => {
28
29
  return renderToText(tree);
@@ -34,7 +35,10 @@ export function markdownSyscalls(client: Client): SysCallMapping {
34
35
  ): Promise<ParseTree | string> => {
35
36
  const outputString = typeof treeOrText === "string";
36
37
  if (typeof treeOrText === "string") {
37
- treeOrText = parse(extendedMarkdownLanguage, treeOrText);
38
+ treeOrText = parse(
39
+ markdownLanguageWithUserExtensions(client),
40
+ treeOrText,
41
+ );
38
42
  }
39
43
  const result = await expandMarkdownWithClient(
40
44
  client,
@@ -52,23 +56,38 @@ export function markdownSyscalls(client: Client): SysCallMapping {
52
56
  text: string,
53
57
  options: MarkdownRenderOptions = {},
54
58
  ) => {
55
- let mdTree = parse(extendedMarkdownLanguage, text);
59
+ let mdTree = parse(markdownLanguageWithUserExtensions(client), text);
56
60
  if (options.expand) {
57
61
  mdTree = await expandMarkdownWithClient(client, mdTree);
58
62
  }
63
+ if (!options.resolveTagHref) {
64
+ options.resolveTagHref = (tagName: string) => {
65
+ return client.config.get<string | null>(
66
+ ["tags", tagName, "tagPage"],
67
+ null,
68
+ ) ?? TagConstants.tagPrefix + tagName;
69
+ };
70
+ }
59
71
  return renderMarkdownToHtml(mdTree, options);
60
72
  },
61
73
  "markdown.objectsToTable": (
62
74
  _ctx,
63
75
  data: any[],
64
- options: { renderCell?: (val: any, key: string) => Promise<any> | any } =
65
- {},
76
+ options: {
77
+ renderCell?: (val: any, key: string) => Promise<any> | any;
78
+ } = {},
66
79
  ) => {
67
80
  return jsonToMDTable(data, options.renderCell || refCellTransformer);
68
81
  },
69
82
  };
70
83
  }
71
84
 
85
+ function markdownLanguageWithUserExtensions(client: Client) {
86
+ return buildExtendedMarkdownLanguage(
87
+ client.config.get("syntaxExtensions", {}),
88
+ );
89
+ }
90
+
72
91
  function expandMarkdownWithClient(
73
92
  client: Client,
74
93
  tree: ParseTree,
@@ -79,6 +98,9 @@ function expandMarkdownWithClient(
79
98
  client.currentName(),
80
99
  tree,
81
100
  client.clientSystem.spaceLuaEnv,
82
- options,
101
+ {
102
+ ...options,
103
+ syntaxExtensions: client.config.get("syntaxExtensions", {}),
104
+ },
83
105
  );
84
106
  }
@@ -6,23 +6,15 @@ export type EventSubscription = MQListenerSpec & {
6
6
  run: (...args: any[]) => Promise<any>;
7
7
  };
8
8
 
9
- export function mqSyscalls(
10
- mq: DataStoreMQ,
11
- ): SysCallMapping {
9
+ export function mqSyscalls(mq: DataStoreMQ): SysCallMapping {
12
10
  return {
13
11
  /**
14
12
  * Define a Lua event listener
15
13
  */
16
- "mq.subscribe": (
17
- _ctx,
18
- def: MQListenerSpec,
19
- ) => {
14
+ "mq.subscribe": (_ctx, def: MQListenerSpec) => {
20
15
  def.autoAck = def.autoAck !== false;
21
16
  // console.log("Registering Lua event listener: ", def.name);
22
- client.config.insert([
23
- "mqSubscriptions",
24
- def.queue,
25
- ], def);
17
+ client.config.insert(["mqSubscriptions", def.queue], def);
26
18
  },
27
19
  "mq.send": (_ctx, queue: string, body: any) => {
28
20
  return mq.send(queue, body);
@@ -12,10 +12,7 @@ export function serviceRegistrySyscalls(
12
12
  /**
13
13
  * Define a Lua event listener
14
14
  */
15
- "service.define": (
16
- _ctx,
17
- def: ServiceSpec,
18
- ) => {
15
+ "service.define": (_ctx, def: ServiceSpec) => {
19
16
  return serviceRegistry.define(def);
20
17
  },
21
18
  "service.discover": (
@@ -2,9 +2,7 @@ import type { SysCallMapping } from "../system.ts";
2
2
  import type { Client } from "../../client.ts";
3
3
  import { fsEndpoint } from "../../spaces/constants.ts";
4
4
 
5
- export function shellSyscalls(
6
- client: Client,
7
- ): SysCallMapping {
5
+ export function shellSyscalls(client: Client): SysCallMapping {
8
6
  return {
9
7
  "shell.run": async (
10
8
  _ctx,
@@ -34,6 +32,5 @@ export function shellSyscalls(
34
32
 
35
33
  function buildShellUrl(client: Client) {
36
34
  // Strip off the /.fs and replace with /.shell
37
- return client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length) +
38
- "/.shell";
35
+ return `${client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length)}/.shell`;
39
36
  }
@@ -1,77 +1,86 @@
1
1
  import type { SysCallMapping } from "../system.ts";
2
2
  import type { Client } from "../../client.ts";
3
3
 
4
- // TODO: Reimplement this
5
4
  export function syncSyscalls(client: Client): SysCallMapping {
6
- return {
7
- "sync.hasInitialSyncCompleted": (): boolean => {
8
- return client.fullSyncCompleted;
9
- },
10
- "sync.performFileSync": (_ctx, path: string): Promise<void> => {
11
- client.postServiceWorkerMessage({ type: "perform-file-sync", path });
12
- return waitForServiceWorkerActivation(path);
13
- },
14
- "sync.performSpaceSync": (): Promise<number> => {
15
- client.postServiceWorkerMessage({ type: "perform-space-sync" });
16
- return waitForServiceWorkerActivation();
17
- },
18
- };
19
- }
5
+ const syncTimeoutMs = 30000;
20
6
 
21
- function waitForServiceWorkerActivation(path?: string): Promise<any> {
22
- return new Promise<any>((resolve, reject) => {
23
- client.eventHook.addLocalListener(
24
- "service-worker:file-sync-complete",
25
- eventHandler,
26
- );
27
- client.eventHook.addLocalListener(
28
- "service-worker:space-sync-complete",
29
- eventHandler,
30
- );
31
- client.eventHook.addLocalListener(
32
- "service-worker:sync-error",
33
- errorHandler,
34
- );
35
- function eventHandler(data: any) {
36
- // If data.path is set, we are notified about a specific file sync -> all good, even for an individual file sync
37
- // If data.path is not set, we are notified about a full space sync
38
- // If we were waiting for a specific path, ignore other paths
39
- if (data.path && path && data.path !== path) {
40
- // Event for other file sync
41
- return;
7
+ function waitForServiceWorkerActivation(path?: string): Promise<any> {
8
+ return new Promise<any>((resolve, reject) => {
9
+ const timeout = setTimeout(() => {
10
+ cleanup();
11
+ reject(new Error(`Sync timeout after ${syncTimeoutMs / 1000}s`));
12
+ }, syncTimeoutMs);
13
+
14
+ function cleanup() {
15
+ clearTimeout(timeout);
16
+ client.eventHook.removeLocalListener(
17
+ "service-worker:file-sync-complete",
18
+ eventHandler,
19
+ );
20
+ client.eventHook.removeLocalListener(
21
+ "service-worker:space-sync-complete",
22
+ eventHandler,
23
+ );
24
+ client.eventHook.removeLocalListener(
25
+ "service-worker:sync-error",
26
+ errorHandler,
27
+ );
42
28
  }
43
- // If we were waiting for a specific path, ignore other paths
44
- resolve(data);
45
29
 
46
- // Unsubscribe from all these events
47
- client.eventHook.removeLocalListener(
48
- "service-worker:file-sync-complete",
49
- eventHandler,
50
- );
51
- client.eventHook.removeLocalListener(
52
- "service-worker:space-sync-complete",
53
- eventHandler,
54
- );
55
- client.eventHook.removeLocalListener(
56
- "service-worker:sync-error",
57
- errorHandler,
58
- );
59
- }
60
- function errorHandler(e: any) {
61
- reject(e);
62
- // Unsubscribe from all these events
63
- client.eventHook.removeLocalListener(
30
+ client.eventHook.addLocalListener(
64
31
  "service-worker:file-sync-complete",
65
32
  eventHandler,
66
33
  );
67
- client.eventHook.removeLocalListener(
34
+ client.eventHook.addLocalListener(
68
35
  "service-worker:space-sync-complete",
69
36
  eventHandler,
70
37
  );
71
- client.eventHook.removeLocalListener(
38
+ client.eventHook.addLocalListener(
72
39
  "service-worker:sync-error",
73
40
  errorHandler,
74
41
  );
75
- }
76
- });
42
+
43
+ function eventHandler(data: any) {
44
+ if (data.path && path && data.path !== path) {
45
+ return;
46
+ }
47
+ cleanup();
48
+ resolve(data);
49
+ }
50
+
51
+ function errorHandler(e: any) {
52
+ // Only reject if the error is for our specific path, or if no path was specified
53
+ if (e.path && path && e.path !== path) {
54
+ return;
55
+ }
56
+ cleanup();
57
+ reject(e);
58
+ }
59
+ });
60
+ }
61
+
62
+ return {
63
+ "sync.hasInitialSyncCompleted": (): boolean => {
64
+ return client.fullSyncCompleted;
65
+ },
66
+ "sync.performFileSync": async (_ctx, path: string): Promise<void> => {
67
+ await client.postServiceWorkerMessage({
68
+ type: "perform-file-sync",
69
+ path,
70
+ });
71
+ // postServiceWorkerMessage returns silently if no SW, so only wait if SW is active
72
+ const registration = await navigator.serviceWorker.getRegistration();
73
+ if (registration?.active) {
74
+ return waitForServiceWorkerActivation(path);
75
+ }
76
+ },
77
+ "sync.performSpaceSync": async (): Promise<number> => {
78
+ await client.postServiceWorkerMessage({ type: "perform-space-sync" });
79
+ const registration = await navigator.serviceWorker.getRegistration();
80
+ if (registration?.active) {
81
+ return waitForServiceWorkerActivation();
82
+ }
83
+ return 0;
84
+ },
85
+ };
77
86
  }
@@ -60,15 +60,19 @@ export function systemSyscalls(
60
60
  mac: def.mac,
61
61
  hide: def.hide,
62
62
  requireMode: def.requireMode,
63
+ requireEditor: def.requireEditor,
64
+ menu: def.menu,
65
+ menuMac: def.menuMac,
66
+ menuWindows: def.menuWindows,
67
+ menuLinux: def.menuLinux,
63
68
  };
64
69
  }
65
70
  return allCommands;
66
71
  },
67
72
  "system.listSyscalls": (): SyscallMeta[] => {
68
73
  const syscalls: SyscallMeta[] = [];
69
- for (
70
- const [name, info] of client.clientSystem.system.registeredSyscalls
71
- ) {
74
+ for (const [name, info] of client.clientSystem.system
75
+ .registeredSyscalls) {
72
76
  syscalls.push({
73
77
  name,
74
78
  requiredPermissions: info.requiredPermissions,
@@ -83,6 +87,14 @@ export function systemSyscalls(
83
87
  }
84
88
  return client.loadPlugs();
85
89
  },
90
+ "system.loadPlug": async (_ctx, path: string) => {
91
+ const meta = await client.space.spacePrimitives.getFileMeta(path);
92
+ await client.clientSystem.loadPlugFromPath(path, meta.lastModified);
93
+ await client.dispatchAppEvent("plugs:loaded");
94
+ },
95
+ "system.unloadPlug": (_ctx, path: string) => {
96
+ return client.clientSystem.system.unloadByPath(path);
97
+ },
86
98
  "system.reloadConfig": (): Record<string, any> => {
87
99
  console.warn("system.reloadConfig is deprecated, it's now a no-op");
88
100
  return client.config.values;
@@ -24,9 +24,7 @@ export type SyscallContext = {
24
24
  plug?: string;
25
25
  };
26
26
 
27
- type SyscallSignature = (
28
- ...args: any[]
29
- ) => Promise<any> | any;
27
+ type SyscallSignature = (...args: any[]) => Promise<any> | any;
30
28
 
31
29
  type Syscall = {
32
30
  requiredPermissions: string[];
@@ -112,11 +110,7 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
112
110
  return this.syscall({}, name, args);
113
111
  }
114
112
 
115
- syscall(
116
- ctx: SyscallContext,
117
- name: string,
118
- args: any[],
119
- ): Promise<any> {
113
+ syscall(ctx: SyscallContext, name: string, args: any[]): Promise<any> {
120
114
  const syscall = this.registeredSyscalls.get(name);
121
115
  if (!syscall) {
122
116
  throw Error(`Unregistered syscall ${name}`);
@@ -139,26 +133,20 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
139
133
  }
140
134
 
141
135
  /**
142
- * @param cacheKey Used to cache the manifest of the worker. Should be equal to the filepath
136
+ * @param path The plug file's space path; also used as the manifest cache key.
143
137
  * @param cacheHash Used to determine if the manifest is up to date. Should be `lastModified`
144
138
  */
145
139
  async loadPlug(
146
140
  sandboxFactory: SandboxFactory<HookT>,
147
- cacheKey: string,
141
+ path: string,
148
142
  cacheHash: number = -1,
149
143
  ): Promise<Plug<HookT>> {
150
- const plug = await Plug.createLazily(
151
- this,
152
- cacheKey,
153
- cacheHash,
154
- sandboxFactory,
155
- );
144
+ const plug = await Plug.createLazily(this, path, cacheHash, sandboxFactory);
156
145
 
157
146
  const manifest = plug.manifest;
158
147
 
159
- // This depends on cacheKey being the file path
160
148
  if (
161
- !builtinPlugPaths.includes(cacheKey) &&
149
+ !builtinPlugPaths.includes(path) &&
162
150
  builtinPlugNames.includes(manifest.name)
163
151
  ) {
164
152
  plug.stop();
@@ -183,13 +171,23 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
183
171
  return plug;
184
172
  }
185
173
 
174
+ unloadByPath(path: string): boolean {
175
+ for (const [name, plug] of this.plugs) {
176
+ if (plug.path === path) {
177
+ this.unload(name);
178
+ return true;
179
+ }
180
+ }
181
+ return false;
182
+ }
183
+
186
184
  unload(name: string) {
187
185
  const plug = this.plugs.get(name);
188
186
  if (!plug) {
189
187
  return;
190
188
  }
191
- plug.stop();
192
- this.emit("plugUnloaded", name);
189
+ void plug.stop();
190
+ void this.emit("plugUnloaded", name);
193
191
  this.plugs.delete(name);
194
192
  }
195
193
 
@@ -34,6 +34,31 @@ export interface Manifest<HookT> {
34
34
  * A map of configuration options for the plug (to be merged with the system configuration).
35
35
  */
36
36
  config?: any;
37
+
38
+ /** Build steps run before asset bundling. Each step transforms an `in` file
39
+ * into an `out` file, which is then picked up by the `assets` glob.
40
+ *
41
+ * Currently only `type: "esbuild"` is supported (the default when omitted),
42
+ * which runs esbuild in bundle mode on `in` and writes the result to `out`.
43
+ */
44
+ build?: BuildStep[];
45
+ }
46
+
47
+ /** A pre-asset-bundling transformation, e.g. bundling a TypeScript entry point
48
+ * into a single JS file that is then loaded from plug assets.
49
+ *
50
+ * Supported types:
51
+ * - `esbuild` (default): bundle `in` into `out` as a minified browser IIFE.
52
+ * - `sass`: compile an SCSS/SASS file at `in` to CSS at `out`.
53
+ * - `copy`: copy `in` verbatim to `out`.
54
+ */
55
+ export interface BuildStep {
56
+ /** Transformation type. Defaults to "esbuild" when omitted. */
57
+ type?: "esbuild" | "sass" | "copy";
58
+ /** Input path relative to the manifest. */
59
+ in: string;
60
+ /** Output path relative to the manifest. */
61
+ out: string;
37
62
  }
38
63
 
39
64
  /** Associates hooks with a function. This is the generic base structure, that identifies the function. Hooks are defined by the type parameter. */
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Deep-clone a value, replacing any functions with null.
3
+ * Needed because values containing functions can't be serialized
4
+ * via postMessage or validated with JSON Schema.
5
+ */
6
+ export function stripFunctions(value: any): any {
7
+ if (typeof value === "function") return null;
8
+ if (value === null || value === undefined || typeof value !== "object") {
9
+ return value;
10
+ }
11
+ if (Array.isArray(value)) {
12
+ return value.map(stripFunctions);
13
+ }
14
+ const result: Record<string, any> = {};
15
+ for (const key of Object.keys(value)) {
16
+ result[key] = stripFunctions(value[key]);
17
+ }
18
+ return result;
19
+ }
20
+
1
21
  /**
2
22
  * Checks if an object is sendable across the plugos worker boundary.
3
23
  *
@@ -19,7 +19,8 @@ let workerPostMessage = (_msg: ControllerMessage): void => {
19
19
  // - in a browser's main thread, typeof window is "object"
20
20
  // - in a browser's worker threads, typeof window === "undefined"
21
21
  // - in Cloudflare workers typeof window === "undefined", but typeof globalThis.WebSocketPair is defined
22
- const runningAsWebWorker = typeof window === "undefined" &&
22
+ const runningAsWebWorker =
23
+ typeof window === "undefined" &&
23
24
  // @ts-expect-error: globalThis
24
25
  typeof globalThis.WebSocketPair === "undefined";
25
26
 
@@ -147,8 +148,8 @@ export function monkeyPatchFetch() {
147
148
  ): Promise<Response> => {
148
149
  const encodedBody = init?.body
149
150
  ? base64Encode(
150
- new Uint8Array(await (new Response(init.body)).arrayBuffer()),
151
- )
151
+ new Uint8Array(await new Response(init.body).arrayBuffer()),
152
+ )
152
153
  : undefined;
153
154
  const r = await sandboxFetch(
154
155
  reqInfo,