@silverbulletmd/silverbullet 2.6.1 → 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 (35) hide show
  1. package/README.md +0 -1
  2. package/client/markdown_parser/constants.ts +1 -0
  3. package/client/plugos/hooks/command.ts +46 -16
  4. package/client/plugos/hooks/slash_command.ts +3 -1
  5. package/client/plugos/plug.ts +5 -2
  6. package/client/plugos/plug_compile.ts +52 -2
  7. package/client/plugos/sandboxes/worker_sandbox.ts +11 -1
  8. package/client/plugos/syscalls/config.ts +9 -2
  9. package/client/plugos/syscalls/editor.ts +136 -0
  10. package/client/plugos/syscalls/index.ts +1 -1
  11. package/client/plugos/syscalls/jsonschema.ts +1 -19
  12. package/client/plugos/syscalls/system.ts +13 -0
  13. package/client/plugos/system.ts +14 -10
  14. package/client/plugos/types.ts +25 -0
  15. package/client/plugos/util.ts +20 -0
  16. package/client/space_lua/aggregates.ts +2 -2
  17. package/client/space_lua/query_collection.ts +4 -4
  18. package/client/space_lua/render_lua_markdown.ts +1 -1
  19. package/client/space_lua/runtime.ts +1 -1
  20. package/client/space_lua/{liq_null.ts → sliq_null.ts} +2 -2
  21. package/client/space_lua/stdlib/space_lua.ts +1 -1
  22. package/client/space_lua/stdlib.ts +1 -1
  23. package/dist/plug-compile.js +44 -2
  24. package/package.json +1 -1
  25. package/plug-api/constants.ts +20 -0
  26. package/plug-api/lib/ref.ts +9 -2
  27. package/plug-api/lib/shortcut.ts +16 -0
  28. package/plug-api/lib/tree.ts +35 -4
  29. package/plug-api/syscalls/config.ts +37 -0
  30. package/plug-api/syscalls/editor.ts +155 -0
  31. package/plug-api/syscalls/index.ts +18 -0
  32. package/plug-api/syscalls/system.ts +15 -0
  33. package/plug-api/types/manifest.ts +63 -0
  34. package/plugs/builtin_plugs.ts +1 -1
  35. package/client/plugos/sandboxes/web_worker_sandbox.ts +0 -17
package/README.md CHANGED
@@ -107,7 +107,6 @@ docker build -t silverbullet .
107
107
  ```
108
108
 
109
109
  To run:
110
-
111
110
  ```shell
112
111
  docker run -p 3000:3000 -v <PATH-TO-YOUR-SPACE>:/space silverbullet
113
112
  ```
@@ -12,3 +12,4 @@ export const frontmatterWikiLinkRegex =
12
12
  /(?<leadingTrivia>!?\[\[)(?<stringRef>.*?)(?:\|(?<alias>.*?))?(?<trailingTrivia>\]\])/g;
13
13
  export const frontmatterMailtoRegex = /(mailto:[^@\s]+@[^@\s"']+)/gi;
14
14
  export const pWikiLinkRegex = new RegExp(`^${wikiLinkRegex.source}`); // Modified regex used only in parser
15
+ export const anchorRegex = /\$[A-Za-z_][A-Za-z0-9_/:-]*/;
@@ -14,6 +14,8 @@ export class CommandHook
14
14
  this.buildAllCommandsAndEmit();
15
15
  }, 200);
16
16
 
17
+ private registeredCommands = new Map<string, Command>();
18
+
17
19
  constructor(
18
20
  private readOnly: boolean,
19
21
  private additionalCommands: Map<string, Command>,
@@ -21,11 +23,51 @@ export class CommandHook
21
23
  super();
22
24
  }
23
25
 
26
+ /**
27
+ * Register a command directly with the hook. Intended for client-side code
28
+ * that wants to define commands without going through a plug manifest.
29
+ * Overwrites any previously-registered command with the same name.
30
+ */
31
+ registerCommand(command: Command): void {
32
+ this.registeredCommands.set(command.name, command);
33
+ this.throttledBuildAllCommandsAndEmit();
34
+ }
35
+
36
+ private mergeCommand(
37
+ commands: Map<string, Command>,
38
+ name: string,
39
+ cmd: Command,
40
+ ) {
41
+ if (commands.has(name)) {
42
+ // Existing command, let's do some inline patching
43
+ const existingCommand = commands.get(name)!;
44
+ const command: Command = {
45
+ ...existingCommand,
46
+ ...cmd,
47
+ };
48
+ if (cmd.run) {
49
+ command.run = cmd.run;
50
+ }
51
+ commands.set(name, command);
52
+ } else {
53
+ commands.set(name, cmd);
54
+ }
55
+ }
56
+
24
57
  /**
25
58
  * Build the command map
26
59
  */
27
60
  buildAllCommands(): Map<string, Command> {
28
61
  const commands = new Map<string, Command>();
62
+ // Start with directly-registered commands (built-in client commands).
63
+ // These form the base layer and can be overridden by plug commands or
64
+ // script commands further down.
65
+ for (const [name, cmd] of this.registeredCommands) {
66
+ if (cmd.requireMode === "rw" && this.readOnly) {
67
+ continue;
68
+ }
69
+ commands.set(name, cmd);
70
+ }
29
71
  // Add commands from plugs
30
72
  if (!this.system) {
31
73
  // Not initialized yet
@@ -43,7 +85,7 @@ export class CommandHook
43
85
  // Bit hacky, but don't expose commands that require write mode in read-only mode
44
86
  continue;
45
87
  }
46
- commands.set(cmd.name, {
88
+ this.mergeCommand(commands, cmd.name, {
47
89
  ...cmd,
48
90
  run: (args?: string[]) => {
49
91
  return plug.invoke(name, [cmd, ...(args ?? [])]);
@@ -51,22 +93,10 @@ export class CommandHook
51
93
  });
52
94
  }
53
95
  }
96
+ // Script commands come last — they always win over built-ins and plugs,
97
+ // so users can rebind keys / replace run bodies from Lua.
54
98
  for (const [name, cmd] of this.additionalCommands) {
55
- if (commands.has(name)) {
56
- // Existing command, let's do some inline patching
57
- const existingCommand = commands.get(name)!;
58
- const command: Command = {
59
- ...existingCommand,
60
- ...cmd,
61
- };
62
- if (cmd.run) {
63
- command.run = cmd.run;
64
- }
65
- commands.set(name, command);
66
- } else {
67
- // New command, let's just set
68
- commands.set(name, cmd);
69
- }
99
+ this.mergeCommand(commands, name, cmd);
70
100
  }
71
101
  return commands;
72
102
  }
@@ -66,7 +66,9 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
66
66
  const options: Completion[] = [];
67
67
 
68
68
  // No slash commands in comment blocks (queries and such) or links
69
- const currentNode = syntaxTree(ctx.state).resolveInner(ctx.pos);
69
+ // Pass -1 so we resolve into the node ending at the cursor: when typing,
70
+ // the user is referring to preceding text, not what comes after.
71
+ const currentNode = syntaxTree(ctx.state).resolveInner(ctx.pos, -1);
70
72
  if (
71
73
  currentNode.type.name === "CommentBlock" ||
72
74
  currentNode.type.name === "Link"
@@ -11,6 +11,8 @@ export class Plug<HookT> {
11
11
 
12
12
  public manifest!: Manifest<HookT>;
13
13
  public assets?: AssetBundle;
14
+ // File path the plug was loaded from; also used as the manifest cache key.
15
+ public path!: string;
14
16
 
15
17
  constructor(
16
18
  readonly system: System<HookT>,
@@ -22,16 +24,17 @@ export class Plug<HookT> {
22
24
 
23
25
  static async createLazily<HookT>(
24
26
  system: System<HookT>,
25
- cacheKey: string,
27
+ path: string,
26
28
  cacheHash: number,
27
29
  sandboxFactory: SandboxFactory<HookT>,
28
30
  ): Promise<Plug<HookT>> {
29
31
  const plug = new Plug(system, sandboxFactory);
32
+ plug.path = path;
30
33
 
31
34
  // Retrieve the manifest, which may either come from a cache or be loaded from the worker
32
35
  plug.manifest = await system.options.manifestCache!.getManifest(
33
36
  plug,
34
- cacheKey,
37
+ path,
35
38
  cacheHash,
36
39
  );
37
40
 
@@ -1,11 +1,19 @@
1
1
  import * as path from "node:path";
2
- import { readFile, writeFile, mkdtemp, rm, mkdir } from "node:fs/promises";
2
+ import {
3
+ copyFile,
4
+ mkdir,
5
+ mkdtemp,
6
+ readFile,
7
+ rm,
8
+ writeFile,
9
+ } from "node:fs/promises";
3
10
  import { tmpdir } from "node:os";
4
11
  import * as YAML from "js-yaml";
5
12
 
6
13
  import * as esbuild from "esbuild";
14
+ import * as sass from "sass";
7
15
  import { bundleAssets } from "../asset_bundle/builder.ts";
8
- import type { Manifest } from "./types.ts";
16
+ import type { BuildStep, Manifest } from "./types.ts";
9
17
 
10
18
  // When bundled by build_plug_compile.ts, this is replaced with the pre-bundled
11
19
  // worker runtime JS. When running from source, it's undefined and esbuild
@@ -53,6 +61,15 @@ export async function compileManifest(
53
61
  throw new Error(`Missing 'name' in ${manifestPath}`);
54
62
  }
55
63
 
64
+ // Build steps (run before asset bundling so produced files can be picked
65
+ // up by the `assets` glob).
66
+ if (manifest.build) {
67
+ await Promise.all(
68
+ manifest.build.map((step) => runBuildStep(step, rootPath, options)),
69
+ );
70
+ delete manifest.build;
71
+ }
72
+
56
73
  // Assets
57
74
  const assetsBundle = await bundleAssets(
58
75
  path.resolve(rootPath),
@@ -177,6 +194,39 @@ export async function compileManifests(
177
194
  await buildAll();
178
195
  }
179
196
 
197
+ async function runBuildStep(
198
+ step: BuildStep,
199
+ rootPath: string,
200
+ options: CompileOptions,
201
+ ) {
202
+ const type = step.type ?? "esbuild";
203
+ const inFile = path.resolve(rootPath, step.in);
204
+ const outFile = path.resolve(rootPath, step.out);
205
+ await mkdir(path.dirname(outFile), { recursive: true });
206
+ if (type === "esbuild") {
207
+ await esbuild.build({
208
+ entryPoints: [inFile],
209
+ bundle: true,
210
+ format: "iife",
211
+ platform: "browser",
212
+ minify: !options.debug,
213
+ outfile: outFile,
214
+ treeShaking: true,
215
+ });
216
+ } else if (type === "sass") {
217
+ const scss = await readFile(inFile, "utf-8");
218
+ const result = sass.compileString(scss, {
219
+ loadPaths: [path.dirname(inFile)],
220
+ style: options.debug ? "expanded" : "compressed",
221
+ });
222
+ await writeFile(outFile, result.css, "utf-8");
223
+ } else if (type === "copy") {
224
+ await copyFile(inFile, outFile);
225
+ } else {
226
+ throw new Error(`Unsupported build step type: ${type}`);
227
+ }
228
+ }
229
+
180
230
  export function patchBundledJS(code: string): string {
181
231
  // One bundled dependency has a lookbehind regex that WebKit can't parse; replace it with a no-op
182
232
  return code.replaceAll("/(?<=\\n)/", "/()/");
@@ -2,8 +2,9 @@ import type { Manifest } from "../types.ts";
2
2
  import type { ControllerMessage, WorkerMessage } from "../protocol.ts";
3
3
  import type { Plug } from "../plug.ts";
4
4
  import { AssetBundle, type AssetJson } from "../../asset_bundle/bundle.ts";
5
- import type { Sandbox } from "./sandbox.ts";
5
+ import type { Sandbox, SandboxFactory } from "./sandbox.ts";
6
6
  import { race, timeout } from "@silverbulletmd/silverbullet/lib/async";
7
+ import { fsEndpoint } from "../../spaces/constants.ts";
7
8
 
8
9
  /**
9
10
  * Represents a "safe" execution environment for plug code
@@ -24,6 +25,15 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
24
25
  private workerOptions = {},
25
26
  ) {}
26
27
 
28
+ /** Factory producing a WorkerSandbox for a plug file at a local space path. */
29
+ static forPath<HookT>(path: string): SandboxFactory<HookT> {
30
+ return (plug) =>
31
+ new WorkerSandbox(
32
+ plug,
33
+ new URL(path, `${document.baseURI.slice(0, -1) + fsEndpoint}/`),
34
+ );
35
+ }
36
+
27
37
  /**
28
38
  * Should only invoked lazily (either by invoke, or by a ManifestCache to load the manifest)
29
39
  */
@@ -1,6 +1,7 @@
1
1
  import type { SysCallMapping } from "../system.ts";
2
2
  import type { Config } from "../../config.ts";
3
3
  import { LuaStackFrame, luaValueToJS } from "../../space_lua/runtime.ts";
4
+ import { stripFunctions } from "../util.ts";
4
5
 
5
6
  export function configSyscalls(config: Config): SysCallMapping {
6
7
  return {
@@ -32,11 +33,17 @@ export function configSyscalls(config: Config): SysCallMapping {
32
33
  "config.define": (_ctx, key: string, schema: any) => {
33
34
  config.define(key, schema);
34
35
  },
36
+ "config.defineCategory": (_ctx, definition: any) => {
37
+ config.defineCategory(definition);
38
+ },
35
39
  "config.getValues": () => {
36
- return config.values;
40
+ return stripFunctions(config.values);
37
41
  },
38
42
  "config.getSchemas": () => {
39
- return config.schemas;
43
+ return stripFunctions(config.schemas);
44
+ },
45
+ "config.getCategories": () => {
46
+ return { ...config.categories };
40
47
  },
41
48
  };
42
49
  }
@@ -7,15 +7,59 @@ import {
7
7
  unfoldCode,
8
8
  } from "@codemirror/language";
9
9
  import {
10
+ cursorCharLeft,
11
+ cursorCharRight,
12
+ cursorDocEnd,
13
+ cursorDocStart,
14
+ cursorGroupLeft,
15
+ cursorGroupRight,
16
+ cursorLineBoundaryLeft,
17
+ cursorLineBoundaryRight,
18
+ cursorLineDown,
19
+ cursorLineEnd,
20
+ cursorLineStart,
21
+ cursorLineUp,
22
+ cursorPageDown,
23
+ cursorPageUp,
24
+ deleteCharBackward,
25
+ deleteCharForward,
26
+ deleteGroupBackward,
27
+ deleteGroupForward,
10
28
  deleteLine,
29
+ deleteLineBoundaryBackward,
30
+ deleteLineBoundaryForward,
31
+ indentLess,
32
+ indentMore,
11
33
  insertNewline,
12
34
  insertNewlineAndIndent,
13
35
  moveLineDown,
14
36
  moveLineUp,
15
37
  redo,
38
+ selectAll,
39
+ selectCharLeft,
40
+ selectCharRight,
41
+ selectDocEnd,
42
+ selectDocStart,
43
+ selectGroupLeft,
44
+ selectGroupRight,
45
+ selectLineBoundaryLeft,
46
+ selectLineBoundaryRight,
47
+ selectLineDown,
48
+ selectLineEnd,
49
+ selectLineStart,
50
+ selectLineUp,
51
+ selectPageDown,
52
+ selectPageUp,
16
53
  toggleComment,
54
+ transposeChars,
17
55
  undo,
18
56
  } from "@codemirror/commands";
57
+ import {
58
+ acceptCompletion,
59
+ closeCompletion,
60
+ moveCompletionSelection,
61
+ startCompletion,
62
+ } from "@codemirror/autocomplete";
19
63
  import type { Transaction } from "@codemirror/state";
20
64
  import { EditorView } from "@codemirror/view";
21
65
  import { getVimModule } from "../../vim_loader.ts";
@@ -324,6 +368,9 @@ export function editorSyscalls(client: Client): SysCallMapping {
324
368
  client.editorView.dispatch({});
325
369
  });
326
370
  },
371
+ "editor.focus": () => {
372
+ client.focus();
373
+ },
327
374
  "editor.hidePanel": (_ctx, id: string) => {
328
375
  client.ui.viewDispatch({
329
376
  type: "hide-panel",
@@ -600,6 +647,24 @@ export function editorSyscalls(client: Client): SysCallMapping {
600
647
  "editor.deleteLine": () => {
601
648
  deleteLine(client.editorView);
602
649
  },
650
+ "editor.selectAll": () => {
651
+ return selectAll({
652
+ state: client.editorView.state,
653
+ dispatch: client.editorView.dispatch,
654
+ });
655
+ },
656
+ "editor.indentMore": () => {
657
+ return indentMore({
658
+ state: client.editorView.state,
659
+ dispatch: client.editorView.dispatch,
660
+ });
661
+ },
662
+ "editor.indentLess": () => {
663
+ return indentLess({
664
+ state: client.editorView.state,
665
+ dispatch: client.editorView.dispatch,
666
+ });
667
+ },
603
668
  "editor.toggleComment": () => {
604
669
  return toggleComment({
605
670
  state: client.editorView.state,
@@ -640,6 +705,77 @@ export function editorSyscalls(client: Client): SysCallMapping {
640
705
  "editor.redo": () => {
641
706
  return redo(client.editorView);
642
707
  },
708
+ // Cursor motion syscalls
709
+ "editor.cursorCharLeft": () => cursorCharLeft(client.editorView),
710
+ "editor.cursorCharRight": () => cursorCharRight(client.editorView),
711
+ "editor.cursorGroupLeft": () => cursorGroupLeft(client.editorView),
712
+ "editor.cursorGroupRight": () => cursorGroupRight(client.editorView),
713
+ "editor.cursorLineBoundaryLeft": () =>
714
+ cursorLineBoundaryLeft(client.editorView),
715
+ "editor.cursorLineBoundaryRight": () =>
716
+ cursorLineBoundaryRight(client.editorView),
717
+ "editor.cursorLineStart": () => cursorLineStart(client.editorView),
718
+ "editor.cursorLineEnd": () => cursorLineEnd(client.editorView),
719
+ "editor.cursorDocStart": () => cursorDocStart(client.editorView),
720
+ "editor.cursorDocEnd": () => cursorDocEnd(client.editorView),
721
+ // Cursor motions that also navigate the completion popup if it's open
722
+ "editor.cursorLineUp": () => {
723
+ const view = client.editorView;
724
+ if (moveCompletionSelection(false)(view)) return true;
725
+ return cursorLineUp(view);
726
+ },
727
+ "editor.cursorLineDown": () => {
728
+ const view = client.editorView;
729
+ if (moveCompletionSelection(true)(view)) return true;
730
+ return cursorLineDown(view);
731
+ },
732
+ "editor.cursorPageUp": () => {
733
+ const view = client.editorView;
734
+ if (moveCompletionSelection(false, "page")(view)) return true;
735
+ return cursorPageUp(view);
736
+ },
737
+ "editor.cursorPageDown": () => {
738
+ const view = client.editorView;
739
+ if (moveCompletionSelection(true, "page")(view)) return true;
740
+ return cursorPageDown(view);
741
+ },
742
+ // Selection-extending motions
743
+ "editor.selectCharLeft": () => selectCharLeft(client.editorView),
744
+ "editor.selectCharRight": () => selectCharRight(client.editorView),
745
+ "editor.selectGroupLeft": () => selectGroupLeft(client.editorView),
746
+ "editor.selectGroupRight": () => selectGroupRight(client.editorView),
747
+ "editor.selectLineBoundaryLeft": () =>
748
+ selectLineBoundaryLeft(client.editorView),
749
+ "editor.selectLineBoundaryRight": () =>
750
+ selectLineBoundaryRight(client.editorView),
751
+ "editor.selectLineStart": () => selectLineStart(client.editorView),
752
+ "editor.selectLineEnd": () => selectLineEnd(client.editorView),
753
+ "editor.selectDocStart": () => selectDocStart(client.editorView),
754
+ "editor.selectDocEnd": () => selectDocEnd(client.editorView),
755
+ "editor.selectLineUp": () => selectLineUp(client.editorView),
756
+ "editor.selectLineDown": () => selectLineDown(client.editorView),
757
+ "editor.selectPageUp": () => selectPageUp(client.editorView),
758
+ "editor.selectPageDown": () => selectPageDown(client.editorView),
759
+ // Delete
760
+ "editor.deleteCharBackward": () => deleteCharBackward(client.editorView),
761
+ "editor.deleteCharForward": () => deleteCharForward(client.editorView),
762
+ "editor.deleteGroupBackward": () => deleteGroupBackward(client.editorView),
763
+ "editor.deleteGroupForward": () => deleteGroupForward(client.editorView),
764
+ "editor.deleteLineBoundaryBackward": () =>
765
+ deleteLineBoundaryBackward(client.editorView),
766
+ "editor.deleteLineBoundaryForward": () =>
767
+ deleteLineBoundaryForward(client.editorView),
768
+ "editor.transposeChars": () => transposeChars(client.editorView),
769
+ // Enter: accept completion if popup is open, else newline-and-indent
770
+ "editor.insertNewline": () => {
771
+ const view = client.editorView;
772
+ if (acceptCompletion(view)) return true;
773
+ return insertNewlineAndIndent(view);
774
+ },
775
+ // Completion popup control
776
+ "editor.acceptCompletion": () => acceptCompletion(client.editorView),
777
+ "editor.startCompletion": () => startCompletion(client.editorView),
778
+ "editor.closeCompletion": () => closeCompletion(client.editorView),
643
779
  "editor.openSearchPanel": () => {
644
780
  openSearchPanel(client.editorView);
645
781
  },
@@ -67,7 +67,7 @@ export function indexSyscalls(
67
67
  tag: string,
68
68
  ref: string,
69
69
  ): Promise<ObjectValue | undefined> => {
70
- return objectIndex.getObjectByRef(ref, page, tag);
70
+ return objectIndex.getObjectByRef(page, tag, ref);
71
71
  },
72
72
  "index.queryLuaObjects": (
73
73
  _ctx,
@@ -1,5 +1,6 @@
1
1
  import type { SysCallMapping } from "../system.ts";
2
2
  import { type OutputUnit, Validator, format } from "@cfworker/json-schema";
3
+ import { stripFunctions } from "../util.ts";
3
4
 
4
5
  // Register custom formats
5
6
  format.email = (data: string) => data.includes("@");
@@ -8,25 +9,6 @@ format["page-ref"] = (data: string) =>
8
9
 
9
10
  const schemaCache = new Map<string, Validator>();
10
11
 
11
- /**
12
- * Deep-clone a value, replacing any functions with null.
13
- * JSON schema can't validate functions, so we strip them before validation.
14
- */
15
- function stripFunctions(value: any): any {
16
- if (typeof value === "function") return null;
17
- if (value === null || value === undefined || typeof value !== "object") {
18
- return value;
19
- }
20
- if (Array.isArray(value)) {
21
- return value.map(stripFunctions);
22
- }
23
- const result: Record<string, any> = {};
24
- for (const key of Object.keys(value)) {
25
- result[key] = stripFunctions(value[key]);
26
- }
27
- return result;
28
- }
29
-
30
12
  function formatErrors(errors: OutputUnit[]): string {
31
13
  // Filter out "properties" wrapper errors, keep only the specific leaf errors
32
14
  const leafErrors = errors.filter((e) => e.keyword !== "properties");
@@ -60,6 +60,11 @@ 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;
@@ -82,6 +87,14 @@ export function systemSyscalls(
82
87
  }
83
88
  return client.loadPlugs();
84
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
+ },
85
98
  "system.reloadConfig": (): Record<string, any> => {
86
99
  console.warn("system.reloadConfig is deprecated, it's now a no-op");
87
100
  return client.config.values;
@@ -133,26 +133,20 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
133
133
  }
134
134
 
135
135
  /**
136
- * @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.
137
137
  * @param cacheHash Used to determine if the manifest is up to date. Should be `lastModified`
138
138
  */
139
139
  async loadPlug(
140
140
  sandboxFactory: SandboxFactory<HookT>,
141
- cacheKey: string,
141
+ path: string,
142
142
  cacheHash: number = -1,
143
143
  ): Promise<Plug<HookT>> {
144
- const plug = await Plug.createLazily(
145
- this,
146
- cacheKey,
147
- cacheHash,
148
- sandboxFactory,
149
- );
144
+ const plug = await Plug.createLazily(this, path, cacheHash, sandboxFactory);
150
145
 
151
146
  const manifest = plug.manifest;
152
147
 
153
- // This depends on cacheKey being the file path
154
148
  if (
155
- !builtinPlugPaths.includes(cacheKey) &&
149
+ !builtinPlugPaths.includes(path) &&
156
150
  builtinPlugNames.includes(manifest.name)
157
151
  ) {
158
152
  plug.stop();
@@ -177,6 +171,16 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
177
171
  return plug;
178
172
  }
179
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
+
180
184
  unload(name: string) {
181
185
  const plug = this.plugs.get(name);
182
186
  if (!plug) {
@@ -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
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Aggregate function definitions and execution for LIQ.
2
+ * Aggregate function definitions and execution for SLIQ.
3
3
  *
4
4
  * Builtins implement ILuaFunction via plain objects rather than
5
5
  * LuaBuiltinFunction instances. This avoids ES module TDZ issues:
@@ -16,7 +16,7 @@ import {
16
16
  luaValueToJS,
17
17
  type LuaValue,
18
18
  } from "./runtime.ts";
19
- import { isSqlNull } from "./liq_null.ts";
19
+ import { isSqlNull } from "./sliq_null.ts";
20
20
  import type { LuaExpression, LuaOrderBy } from "./ast.ts";
21
21
  import { buildItemEnv } from "./query_env.ts";
22
22
  import { asyncMergeSort } from "./util.ts";