@weborigami/language 0.6.17 → 0.7.0-beta.2

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 (90) hide show
  1. package/index.ts +1 -0
  2. package/main.js +7 -1
  3. package/package.json +7 -6
  4. package/src/compiler/compile.js +10 -3
  5. package/src/compiler/optimize.js +71 -40
  6. package/src/compiler/parse.js +1 -1
  7. package/src/compiler/parserHelpers.js +5 -3
  8. package/src/handlers/addExtensionKeyFn.js +18 -0
  9. package/src/handlers/epub_handler.js +54 -0
  10. package/src/handlers/getSource.js +11 -0
  11. package/src/handlers/handlers.js +2 -0
  12. package/src/handlers/htm_handler.js +1 -1
  13. package/src/handlers/js_handler.js +13 -4
  14. package/src/handlers/mediaTypeExtensions.json +15 -0
  15. package/src/handlers/ori_handler.js +8 -7
  16. package/src/handlers/oridocument_handler.js +19 -28
  17. package/src/handlers/processOriExport.js +17 -0
  18. package/src/handlers/tsv_handler.js +1 -1
  19. package/src/handlers/txt_handler.js +4 -2
  20. package/src/handlers/xhtml_handler.js +1 -1
  21. package/src/handlers/yaml_handler.js +6 -3
  22. package/src/handlers/zip_handler.js +112 -0
  23. package/src/project/activeProjectRoot.js +9 -0
  24. package/src/project/getGlobalsForTree.js +5 -0
  25. package/src/project/{projectGlobals.js → initializeGlobalsForTree.js} +8 -13
  26. package/src/project/jsGlobals.js +1 -0
  27. package/src/project/projectConfig.js +2 -2
  28. package/src/project/projectRootFromPath.js +2 -0
  29. package/src/protocols/constructHref.js +3 -3
  30. package/src/protocols/constructSiteTree.js +11 -2
  31. package/src/protocols/explore.js +1 -1
  32. package/src/protocols/explorehttp.js +1 -1
  33. package/src/protocols/fetchAndHandleExtension.js +23 -11
  34. package/src/protocols/files.js +1 -0
  35. package/src/protocols/http.js +4 -1
  36. package/src/protocols/https.js +4 -1
  37. package/src/protocols/httpstree.js +1 -1
  38. package/src/protocols/httptree.js +1 -1
  39. package/src/protocols/package.js +15 -3
  40. package/src/runtime/AsyncCacheTransform.d.ts +5 -0
  41. package/src/runtime/AsyncCacheTransform.js +134 -0
  42. package/src/runtime/HandleExtensionsTransform.d.ts +3 -1
  43. package/src/runtime/HandleExtensionsTransform.js +18 -2
  44. package/src/runtime/OrigamiFileMap.d.ts +5 -2
  45. package/src/runtime/OrigamiFileMap.js +27 -4
  46. package/src/runtime/ScopeMap.js +72 -0
  47. package/src/runtime/SyncCacheTransform.d.ts +8 -0
  48. package/src/runtime/SyncCacheTransform.js +133 -0
  49. package/src/runtime/SystemCacheMap.js +259 -0
  50. package/src/runtime/WatchFilesMixin.js +52 -19
  51. package/src/runtime/enableValueCaching.js +192 -0
  52. package/src/runtime/execute.js +2 -2
  53. package/src/runtime/executionContext.js +7 -0
  54. package/src/runtime/explainReferenceError.js +7 -2
  55. package/src/runtime/expressionObject.js +54 -46
  56. package/src/runtime/handleExtension.js +65 -34
  57. package/src/runtime/interop.js +2 -2
  58. package/src/runtime/mergeTrees.js +1 -1
  59. package/src/runtime/ops.js +28 -33
  60. package/src/runtime/symbols.js +3 -0
  61. package/src/runtime/systemCache.js +3 -0
  62. package/src/runtime/volatile.js +14 -0
  63. package/test/compiler/codeHelpers.js +2 -1
  64. package/test/compiler/optimize.test.js +62 -54
  65. package/test/handlers/epub_handler.test.js +27 -0
  66. package/test/handlers/fixtures/test.zip +0 -0
  67. package/test/handlers/ori_handler.test.js +22 -3
  68. package/test/handlers/oridocument_handler.test.js +1 -1
  69. package/test/handlers/zip_handler.test.js +45 -0
  70. package/test/protocols/https.test.js +19 -0
  71. package/test/protocols/package.test.js +7 -2
  72. package/test/runtime/AsyncCacheTransform.test.js +91 -0
  73. package/test/runtime/OrigamiFileMap.test.js +26 -23
  74. package/test/runtime/ScopeMap.test.js +49 -0
  75. package/test/runtime/SyncCacheTransform.test.js +93 -0
  76. package/test/runtime/SystemCacheMap.test.js +239 -0
  77. package/test/runtime/asyncCalcs.js +28 -0
  78. package/test/runtime/enableValueCaching.test.js +55 -0
  79. package/test/runtime/errors.test.js +53 -30
  80. package/test/runtime/evaluate.test.js +9 -4
  81. package/test/runtime/execute.test.js +6 -1
  82. package/test/runtime/expressionObject.test.js +55 -15
  83. package/test/runtime/fetchAndHandleExtension.test.js +24 -0
  84. package/test/runtime/fixtures/unpack/hello.json +1 -0
  85. package/test/runtime/handleExtension.test.js +12 -1
  86. package/test/runtime/ops.test.js +70 -65
  87. package/test/runtime/syncCalcs.js +27 -0
  88. package/test/runtime/systemCache.test.js +66 -0
  89. package/src/runtime/assignPropertyDescriptors.js +0 -23
  90. package/src/runtime/asyncStorage.js +0 -7
@@ -6,7 +6,8 @@ import {
6
6
  } from "@weborigami/async-tree";
7
7
  import * as YAMLModule from "yaml";
8
8
  import * as compile from "../compiler/compile.js";
9
- import projectGlobals from "../project/projectGlobals.js";
9
+ import coreGlobals from "../project/coreGlobals.js";
10
+ import getGlobalsForTree from "../project/getGlobalsForTree.js";
10
11
  import parseFrontMatter from "./parseFrontMatter.js";
11
12
 
12
13
  // The "yaml" package doesn't seem to provide a default export that the browser can
@@ -80,7 +81,8 @@ export default {
80
81
  const { body, frontText, isOrigami } = parsed;
81
82
  let frontData;
82
83
  if (isOrigami) {
83
- const globals = await projectGlobals(parent);
84
+ const globals =
85
+ options.globals ?? getGlobalsForTree(parent) ?? (await coreGlobals());
84
86
  const compiled = compile.expression(frontText.trim(), {
85
87
  globals,
86
88
  parent,
@@ -1,2 +1,2 @@
1
1
  // .xhtml is a synonynm for .html
2
- export { html_handler as default } from "./handlers.js";
2
+ export { default } from "./html_handler.js";
@@ -8,7 +8,8 @@ import {
8
8
  } from "@weborigami/async-tree";
9
9
  import * as YAMLModule from "yaml";
10
10
  import * as compile from "../compiler/compile.js";
11
- import projectGlobals from "../project/projectGlobals.js";
11
+ import coreGlobals from "../project/coreGlobals.js";
12
+ import getGlobalsForTree from "../project/getGlobalsForTree.js";
12
13
  import getSource from "./getSource.js";
13
14
 
14
15
  // The "yaml" package doesn't seem to provide a default export that the browser can
@@ -104,7 +105,8 @@ export default {
104
105
  };
105
106
 
106
107
  async function oriCallTagForParent(parent, options, yaml) {
107
- const globals = await projectGlobals(parent);
108
+ const globals =
109
+ options.globals ?? getGlobalsForTree(parent) ?? (await coreGlobals());
108
110
  return {
109
111
  collection: "seq",
110
112
 
@@ -168,7 +170,8 @@ async function oriCallTagForParent(parent, options, yaml) {
168
170
  // Define the !ori tag for YAML parsing. This will run in the context of the
169
171
  // supplied parent.
170
172
  async function oriTagForParent(parent, options, yaml) {
171
- const globals = await projectGlobals(parent);
173
+ const globals =
174
+ options.globals ?? getGlobalsForTree(parent) ?? (await coreGlobals());
172
175
  return {
173
176
  resolve(text) {
174
177
  hasOriTags = true;
@@ -0,0 +1,112 @@
1
+ import { isUnpackable, SyncMap, Tree } from "@weborigami/async-tree";
2
+ import {
3
+ getGlobalsForTree,
4
+ HandleExtensionsTransform,
5
+ } from "@weborigami/language";
6
+ import Zip from "adm-zip";
7
+ import addExtensionKeyFn from "./addExtensionKeyFn.js";
8
+
9
+ /**
10
+ * Handler for ZIP files
11
+ */
12
+ const zip_handler = {
13
+ mediaType: "application/zip",
14
+
15
+ /**
16
+ * Pack a tree of files as a ZIP file in Buffer form.
17
+ *
18
+ * @param {import("@weborigami/async-tree").Maplike} maplike
19
+ */
20
+ async pack(maplike) {
21
+ // The ZIP file should leave the files in tree order.
22
+ const zip = new Zip({ noSort: true });
23
+
24
+ if (isUnpackable(maplike)) {
25
+ maplike = await maplike.unpack();
26
+ }
27
+ const tree = Tree.from(maplike, { deep: true });
28
+ const deflated = await Tree.deflatePaths(tree);
29
+ for (let [path, value] of deflated) {
30
+ if (typeof value === "function") {
31
+ value = value();
32
+ }
33
+ if (value instanceof Promise) {
34
+ value = await value;
35
+ } else if (value instanceof String) {
36
+ value = value.toString(); // adm-zip wants simple strings
37
+ }
38
+ zip.addFile(path, value);
39
+
40
+ // Special case for EPUB files, where `mimetype` must be uncompressed.
41
+ if (path === "mimetype") {
42
+ const entry = zip.getEntry(path);
43
+ entry.header.method = 0; // STORE (not DEFLATE)
44
+ }
45
+ }
46
+ const buffer = zip.toBuffer();
47
+ return buffer;
48
+ },
49
+
50
+ /**
51
+ * Unpack a ZIP file
52
+ */
53
+ async unpack(buffer, options) {
54
+ // Origami generally prefers keeping things as an Uint8Array or ArrayBuffer,
55
+ // but adm-zip only accepts a Buffer.
56
+ if (buffer instanceof Uint8Array || buffer instanceof ArrayBuffer) {
57
+ // @ts-ignore
58
+ buffer = Buffer.from(buffer);
59
+ }
60
+
61
+ const zip = new Zip(buffer);
62
+
63
+ const entries = zip.getEntries();
64
+ const filtered = entries.filter(
65
+ (entry) =>
66
+ !entry.entryName.startsWith("__MACOSX/") &&
67
+ !entry.entryName.endsWith("/"),
68
+ );
69
+ const deflated = Object.fromEntries(
70
+ filtered.map((entry) => [entry.entryName, () => entry.getData()]),
71
+ );
72
+
73
+ // The final tree will include extension handlers and have functions invoked
74
+ // to retrieve data from the ZIP file. While the base map is a SyncMap, the
75
+ // final tree will be async.
76
+ const classFn = HandleExtensionsTransform(
77
+ InvokeFunctionsTransform(SyncMap),
78
+ );
79
+ const result = await Tree.inflatePaths(deflated, { classFn });
80
+
81
+ const parent = options?.parent;
82
+ const globals = parent ? getGlobalsForTree(parent) : null;
83
+ if (globals) {
84
+ result.globals = globals;
85
+ }
86
+
87
+ return result;
88
+ },
89
+ };
90
+
91
+ /** @type {any} */ (zip_handler.pack).key = addExtensionKeyFn(".zip");
92
+
93
+ export default zip_handler;
94
+
95
+ function InvokeFunctionsTransform(Base) {
96
+ return class InvokeFunctions extends Base {
97
+ delete(key) {
98
+ return super.delete(key);
99
+ }
100
+
101
+ get(key) {
102
+ const value = super.get(key);
103
+ return typeof value === "function" ? value() : value;
104
+ }
105
+
106
+ set(key, value) {
107
+ return super.set(key, value);
108
+ }
109
+
110
+ trailingSlashKeys = true;
111
+ };
112
+ }
@@ -0,0 +1,9 @@
1
+ let activeProjectRoot = null;
2
+
3
+ export function get() {
4
+ return activeProjectRoot;
5
+ }
6
+
7
+ export function set(projectRoot) {
8
+ activeProjectRoot = projectRoot;
9
+ }
@@ -0,0 +1,5 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+
3
+ export default function getGlobalsForTree(map) {
4
+ return map ? Tree.root(map).globals : null;
5
+ }
@@ -1,10 +1,9 @@
1
- import { Tree } from "@weborigami/async-tree";
2
- import assignPropertyDescriptors from "../runtime/assignPropertyDescriptors.js";
1
+ import { Tree, assignPropertyDescriptors } from "@weborigami/async-tree";
3
2
  import coreGlobals from "./coreGlobals.js";
4
3
  import projectConfig from "./projectConfig.js";
5
4
 
6
5
  /**
7
- * Return the complete set of globals available to code running in the given
6
+ * Make the complete set of globals available to code running in the given
8
7
  * container. This will be the core globals plus any configuration specified in
9
8
  * the project's config.ori file.
10
9
  *
@@ -12,17 +11,13 @@ import projectConfig from "./projectConfig.js";
12
11
  * container.
13
12
  *
14
13
  * @typedef {import("@weborigami/async-tree").SyncOrAsyncMap} SyncOrAsyncMap
15
- * @param {SyncOrAsyncMap|null} parent
14
+ * @param {SyncOrAsyncMap} parent
16
15
  */
17
- export default async function projectGlobals(parent) {
18
- if (!parent) {
19
- return coreGlobals();
20
- }
21
-
22
- const projectRoot = await Tree.root(parent);
16
+ export default async function initializeGlobalsForTree(parent) {
17
+ const projectRoot = Tree.root(parent);
23
18
  if (!projectRoot.globals) {
24
- // Start with core globals
25
- const globals = await coreGlobals();
19
+ // Start with a copy of the core globals
20
+ const globals = { ...(await coreGlobals()) };
26
21
 
27
22
  if (parent) {
28
23
  // Get config for the given container and add it to the globals. During
@@ -35,7 +30,7 @@ export default async function projectGlobals(parent) {
35
30
  assignPropertyDescriptors(globals, config);
36
31
  }
37
32
 
38
- // Cache globals on project root
33
+ // Store globals on project root
39
34
  projectRoot.globals = globals;
40
35
  }
41
36
 
@@ -92,6 +92,7 @@ const globals = {
92
92
  SuppressedError: globalThis.SuppressedError,
93
93
  Symbol,
94
94
  SyntaxError,
95
+ Temporal: globalThis.Temporal,
95
96
  TextDecoder,
96
97
  TextDecoderStream,
97
98
  TextEncoder,
@@ -10,8 +10,8 @@ import coreGlobals from "./coreGlobals.js";
10
10
  * @typedef {import("@weborigami/async-tree").SyncOrAsyncMap} SyncOrAsyncMap
11
11
  * @param {SyncOrAsyncMap} parent
12
12
  */
13
- export default async function config(parent) {
14
- const projectRoot = await Tree.root(parent);
13
+ export default async function projectConfig(parent) {
14
+ const projectRoot = Tree.root(parent);
15
15
 
16
16
  let configObject = {};
17
17
  const configBuffer = await projectRoot.get("config.ori");
@@ -55,5 +55,7 @@ export default async function projectRootFromPath(dirname) {
55
55
  root = new OrigamiFileMap(dirname);
56
56
  }
57
57
 
58
+ await root.initializeGlobals();
59
+
58
60
  return root;
59
61
  }
@@ -8,11 +8,11 @@ import { pathFromKeys } from "@weborigami/async-tree";
8
8
  * @param {string[]} keys
9
9
  */
10
10
  export default function constructHref(protocol, host, ...keys) {
11
+ let href = host.endsWith("/") ? host.slice(0, -1) : host;
11
12
  const path = pathFromKeys(keys);
12
- if (host.endsWith("/")) {
13
- host = host.slice(0, -1);
13
+ if (path) {
14
+ href += "/" + path;
14
15
  }
15
- let href = [host, path].join("/");
16
16
  if (!href.startsWith(protocol)) {
17
17
  if (!href.startsWith("//")) {
18
18
  href = `//${href}`;
@@ -1,5 +1,6 @@
1
1
  import { trailingSlash } from "@weborigami/async-tree";
2
2
  import HandleExtensionsTransform from "../runtime/HandleExtensionsTransform.js";
3
+ import systemCache from "../runtime/systemCache.js";
3
4
  import constructHref from "./constructHref.js";
4
5
 
5
6
  /**
@@ -12,7 +13,12 @@ import constructHref from "./constructHref.js";
12
13
  * @param {string} host
13
14
  * @param {string[]} keys
14
15
  */
15
- export default function constructSiteTree(protocol, mapClass, host, ...keys) {
16
+ export default async function constructSiteTree(
17
+ protocol,
18
+ mapClass,
19
+ host,
20
+ ...keys
21
+ ) {
16
22
  // If the last key doesn't end in a slash, remove it for now.
17
23
  let lastKey;
18
24
  if (keys.length > 0 && keys.at(-1) && !trailingSlash.has(keys.at(-1))) {
@@ -20,7 +26,10 @@ export default function constructSiteTree(protocol, mapClass, host, ...keys) {
20
26
  }
21
27
 
22
28
  const href = constructHref(protocol, host, ...keys);
23
- let result = new (HandleExtensionsTransform(mapClass))(href);
29
+ const result = await systemCache.getOrInsertComputedAsync(
30
+ href,
31
+ () => new (HandleExtensionsTransform(mapClass))(href),
32
+ );
24
33
 
25
34
  return lastKey ? result.get(lastKey) : result;
26
35
  }
@@ -8,6 +8,6 @@ import constructSiteTree from "./constructSiteTree.js";
8
8
  * @param {string} host
9
9
  * @param {...string} keys
10
10
  */
11
- export default function explore(host, ...keys) {
11
+ export default async function explore(host, ...keys) {
12
12
  return constructSiteTree("https:", ExplorableSiteMap, host, ...keys);
13
13
  }
@@ -8,6 +8,6 @@ import constructSiteTree from "./constructSiteTree.js";
8
8
  * @param {string} host
9
9
  * @param {...string} keys
10
10
  */
11
- export default function explorehttp(host, ...keys) {
11
+ export default async function explorehttp(host, ...keys) {
12
12
  return constructSiteTree("http:", ExplorableSiteMap, host, ...keys);
13
13
  }
@@ -1,18 +1,26 @@
1
- import handleExtension from "../runtime/handleExtension.js";
1
+ import { args, Tree } from "@weborigami/async-tree";
2
+ import handleExtension from "../../src/runtime/handleExtension.js";
2
3
 
3
4
  /**
4
- * Fetch the resource at the given href.
5
- *
6
- * @typedef {import("@weborigami/async-tree").SyncOrAsyncMap} SyncOrAsyncMap
5
+ * Extend the JavaScript `fetch` function to implicity return an ArrayBuffer
6
+ * with an unpack() method if the resource has a known file extension or MIME
7
+ * type.
7
8
  *
8
9
  * @param {string} href
9
- * @param {SyncOrAsyncMap} parent
10
10
  */
11
- export default async function fetchAndHandleExtension(href, parent) {
12
- const response = await fetch(href);
11
+ export default async function fetchAndHandleExtension(href, options, state) {
12
+ if (options && state === undefined) {
13
+ // Options weren't provided
14
+ state = options;
15
+ options = undefined;
16
+ }
17
+
18
+ href = args.string(href, "Origami.fetch");
19
+ const response = await fetch(href, options);
13
20
  if (!response.ok) {
14
21
  return undefined;
15
22
  }
23
+
16
24
  let buffer = await response.arrayBuffer();
17
25
 
18
26
  const mediaType = response.headers.get("Content-Type");
@@ -20,12 +28,16 @@ export default async function fetchAndHandleExtension(href, parent) {
20
28
  /** @type {any} */ (buffer).mediaType = mediaType;
21
29
  }
22
30
 
23
- // Attach any loader defined for the file type.
31
+ // Attach any handler defined for the file type or MIME type.
32
+ const parent = state?.parent;
24
33
  const url = new URL(href);
25
- const filename = url.pathname.split("/").pop();
26
- if (filename) {
27
- buffer = await handleExtension(buffer, filename, parent);
34
+ if (parent) {
35
+ const root = await Tree.root(parent);
36
+ const globals = root.globals;
37
+ const filename = url.pathname.split("/").pop();
38
+ buffer = await handleExtension(buffer, filename, globals, parent);
28
39
  }
29
40
 
30
41
  return buffer;
31
42
  }
43
+ fetchAndHandleExtension.needsState = true;
@@ -22,6 +22,7 @@ export default async function files(...args) {
22
22
  const resolved = path.resolve(basePath, relativePath);
23
23
 
24
24
  const result = new OrigamiFileMap(resolved);
25
+ await result.initializeGlobals();
25
26
  return result;
26
27
  }
27
28
  files.needsState = true;
@@ -1,3 +1,4 @@
1
+ import systemCache from "../runtime/systemCache.js";
1
2
  import constructHref from "./constructHref.js";
2
3
  import fetchAndHandleExtension from "./fetchAndHandleExtension.js";
3
4
 
@@ -11,6 +12,8 @@ import fetchAndHandleExtension from "./fetchAndHandleExtension.js";
11
12
  export default async function http(host, ...keys) {
12
13
  const state = keys.pop();
13
14
  const href = constructHref("http:", host, ...keys);
14
- return fetchAndHandleExtension(href, state.parent);
15
+ return systemCache.getOrInsertComputedAsync(href, () =>
16
+ fetchAndHandleExtension(href, null, state),
17
+ );
15
18
  }
16
19
  http.needsState = true;
@@ -1,3 +1,4 @@
1
+ import systemCache from "../runtime/systemCache.js";
1
2
  import constructHref from "./constructHref.js";
2
3
  import fetchAndHandleExtension from "./fetchAndHandleExtension.js";
3
4
 
@@ -11,6 +12,8 @@ import fetchAndHandleExtension from "./fetchAndHandleExtension.js";
11
12
  export default async function https(host, ...keys) {
12
13
  const state = keys.pop();
13
14
  const href = constructHref("https:", host, ...keys);
14
- return fetchAndHandleExtension(href, state.parent);
15
+ return systemCache.getOrInsertComputedAsync(href, () =>
16
+ fetchAndHandleExtension(href, null, state),
17
+ );
15
18
  }
16
19
  https.needsState = true;
@@ -8,6 +8,6 @@ import constructSiteTree from "./constructSiteTree.js";
8
8
  * @param {string} host
9
9
  * @param {...string} keys
10
10
  */
11
- export default function httpstree(host, ...keys) {
11
+ export default async function httpstree(host, ...keys) {
12
12
  return constructSiteTree("https:", SiteMap, host, ...keys);
13
13
  }
@@ -8,6 +8,6 @@ import constructSiteTree from "./constructSiteTree.js";
8
8
  * @param {string} host
9
9
  * @param {...string} keys
10
10
  */
11
- export default function httptree(host, ...keys) {
11
+ export default async function httptree(host, ...keys) {
12
12
  return constructSiteTree("http:", SiteMap, host, ...keys);
13
13
  }
@@ -1,5 +1,6 @@
1
1
  import { Tree, keysFromPath, pathFromKeys } from "@weborigami/async-tree";
2
2
  import projectRoot from "../project/projectRoot.js";
3
+ import systemCache from "../runtime/systemCache.js";
3
4
 
4
5
  /**
5
6
  * The package: protocol handler
@@ -9,15 +10,27 @@ import projectRoot from "../project/projectRoot.js";
9
10
  export default async function packageProtocol(...args) {
10
11
  const state = args.pop(); // Remaining args are the path
11
12
  const root = await projectRoot(state);
13
+ const path = pathFromKeys(args);
14
+ if (!path) {
15
+ throw new Error("package: protocol requires a package name");
16
+ }
17
+ const href = `package:${path}`;
18
+ const result = await systemCache.getOrInsertComputedAsync(href, () =>
19
+ loadPackage(root, args),
20
+ );
21
+ return result;
22
+ }
23
+ packageProtocol.needsState = true;
12
24
 
25
+ async function loadPackage(root, args) {
13
26
  // Identify the path to the package root
14
- const packageRootKeys = ["node_modules"];
27
+ const packageRootKeys = ["node_modules/"];
15
28
  let name = args.shift();
16
29
  packageRootKeys.push(name);
17
30
  if (name.startsWith("@")) {
18
31
  // First key is an npm organization, add next key as name
19
32
  const nameArg = args.shift();
20
- name += nameArg;
33
+ name = pathFromKeys([name, nameArg]);
21
34
  packageRootKeys.push(nameArg);
22
35
  }
23
36
  const packageRootPath = pathFromKeys(packageRootKeys);
@@ -60,4 +73,3 @@ export default async function packageProtocol(...args) {
60
73
 
61
74
  return result;
62
75
  }
63
- packageProtocol.needsState = true;
@@ -0,0 +1,5 @@
1
+ import { Mixin } from "../../index.ts";
2
+
3
+ declare const AsyncCacheTransform: Mixin<{}>
4
+
5
+ export default AsyncCacheTransform;
@@ -0,0 +1,134 @@
1
+ import enableValueCaching from "./enableValueCaching.js";
2
+ import { cachePathSymbol, noCacheSymbol } from "./symbols.js";
3
+ import systemCache from "./systemCache.js";
4
+ import SystemCacheMap from "./SystemCacheMap.js";
5
+
6
+ /**
7
+ * General-purpose mixin for Origami maps with dependency tracking, used for:
8
+ * files, site resources, and scope references in Origami files
9
+ *
10
+ * This wraps a map's get() and keys() methods to add caching and dependency tracking.
11
+ * It tracks which cached values are downstream of other cached values so that if
12
+ * an upstream value changes, all dependent downstream cached values can be
13
+ * invalidated efficiently.
14
+ *
15
+ * Cache entries look like:
16
+ *
17
+ * key -> {
18
+ * downstreams: Set(path),
19
+ * value
20
+ * }
21
+ *
22
+ * This allows for efficiently evicting all a value and all its downstream
23
+ * dependent cached values.
24
+ *
25
+ * Example project:
26
+ *
27
+ * site.ori loads a.ori and b.ori
28
+ * a.ori loads c.ori
29
+ * b.ori loads c.ori
30
+ * c.ori doesn't load anything
31
+ *
32
+ * Resulting cache:
33
+ *
34
+ * site.ori -> { value: ... }
35
+ * a.ori -> { downstreams: Set(site.ori), value: ... }
36
+ * b.ori -> { downstreams: Set(site.ori), value: ... }
37
+ * c.ori -> { downstreams: Set(a.ori, b.ori), value: ... }
38
+ */
39
+ export default function AsyncCacheTransform(Base) {
40
+ return class AsyncCache extends Base {
41
+ constructor(...args) {
42
+ super(...args);
43
+
44
+ // Expose cache for debugging
45
+ this.cache = systemCache;
46
+ }
47
+
48
+ get cachePath() {
49
+ // @ts-ignore
50
+ return this[cachePathSymbol];
51
+ }
52
+
53
+ cachePathForKey(key) {
54
+ return key === "."
55
+ ? this.cachePath
56
+ : SystemCacheMap.joinPath(this.cachePath, key);
57
+ }
58
+
59
+ async delete(key) {
60
+ const deleted = await super.delete(key);
61
+ if (typeof key === "string") {
62
+ systemCache.delete(this.cachePathForKey(key));
63
+ }
64
+ return deleted;
65
+ }
66
+
67
+ async get(key) {
68
+ if (typeof key !== "string" || key.length === 0) {
69
+ // Non-string keys and non-empty strings can't be cached
70
+ return super.get(key);
71
+ }
72
+ const cachePath = this.cachePathForKey(key);
73
+ const value = await systemCache.getOrInsertComputedAsync(
74
+ cachePath,
75
+ async () => {
76
+ let result = await super.get(key);
77
+ if (result !== undefined) {
78
+ // @ts-ignore
79
+ if (this[noCacheSymbol]) {
80
+ result[noCacheSymbol] = true;
81
+ } else {
82
+ result = enableValueCaching(result, cachePath);
83
+ }
84
+ }
85
+ return result;
86
+ },
87
+ );
88
+ return value;
89
+ }
90
+
91
+ invalidateKeys() {
92
+ const keysPath = this.cachePathForKey("_keys");
93
+ systemCache.delete(keysPath);
94
+ }
95
+
96
+ async *keys() {
97
+ const keysPath = this.cachePathForKey("_keys");
98
+ const keys = await systemCache.getOrInsertComputedAsync(
99
+ keysPath,
100
+ async () => {
101
+ // We can't cache an iterator; convert to array
102
+ const result = [];
103
+ for await (const key of super.keys()) {
104
+ result.push(key);
105
+ }
106
+ return result;
107
+ },
108
+ );
109
+ yield* keys;
110
+ }
111
+
112
+ onKeysChange(key) {
113
+ super.onKeysChange?.(key);
114
+ this.invalidateKeys();
115
+ }
116
+
117
+ onValueChange(key) {
118
+ super.onValueChange?.(key);
119
+ systemCache.delete(this.cachePathForKey(key));
120
+ }
121
+
122
+ async set(key, value) {
123
+ if (typeof key !== "string") {
124
+ return super.set(key, value);
125
+ }
126
+ systemCache.delete(this.cachePathForKey(key));
127
+ if (!this.has(key)) {
128
+ // Adding a new key, need to invalidate cached keys
129
+ this.invalidateKeys();
130
+ }
131
+ super.set(key, value);
132
+ }
133
+ };
134
+ }
@@ -1,5 +1,7 @@
1
1
  import { Mixin } from "../../index.ts";
2
2
 
3
- declare const HandleExtensionsTransform: Mixin<{}>;
3
+ declare const HandleExtensionsTransform: Mixin<{
4
+ initializeGlobals(): Promise<void>;
5
+ }>;
4
6
 
5
7
  export default HandleExtensionsTransform;
@@ -1,3 +1,5 @@
1
+ import getGlobalsForTree from "../project/getGlobalsForTree.js";
2
+ import initializeGlobalsForTree from "../project/initializeGlobalsForTree.js";
1
3
  import handleExtension from "./handleExtension.js";
2
4
 
3
5
  /**
@@ -13,11 +15,25 @@ export default function HandleExtensionsTransform(Base) {
13
15
  return super.delete(key);
14
16
  }
15
17
 
18
+ /**
19
+ * Initialize the globals on the project root. This makes the file handlers
20
+ * available for use from any folder inside the tree.
21
+ *
22
+ * This is an async operation because it can load JavaScript files, so it
23
+ * can't be done in the constructor.
24
+ */
25
+ async initializeGlobals() {
26
+ await initializeGlobalsForTree(this);
27
+ }
28
+
16
29
  get(key) {
30
+ const globals = getGlobalsForTree(this);
17
31
  const value = super.get(key);
18
32
  return value instanceof Promise
19
- ? value.then((resolved) => handleExtension(resolved, key, this))
20
- : handleExtension(value, key, this);
33
+ ? value.then((resolved) =>
34
+ handleExtension(resolved, key, globals, this),
35
+ )
36
+ : handleExtension(value, key, globals, this);
21
37
  }
22
38
 
23
39
  // See delete()