@weborigami/language 0.6.17 → 0.7.0-beta.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 (83) hide show
  1. package/index.ts +1 -0
  2. package/main.js +7 -1
  3. package/package.json +6 -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/getSource.js +11 -0
  9. package/src/handlers/htm_handler.js +1 -1
  10. package/src/handlers/js_handler.js +13 -4
  11. package/src/handlers/mediaTypeExtensions.json +15 -0
  12. package/src/handlers/ori_handler.js +8 -7
  13. package/src/handlers/oridocument_handler.js +17 -10
  14. package/src/handlers/processOriExport.js +17 -0
  15. package/src/handlers/tsv_handler.js +1 -1
  16. package/src/handlers/txt_handler.js +4 -2
  17. package/src/handlers/xhtml_handler.js +1 -1
  18. package/src/handlers/yaml_handler.js +6 -3
  19. package/src/project/activeProjectRoot.js +9 -0
  20. package/src/project/getGlobalsForTree.js +5 -0
  21. package/src/project/{projectGlobals.js → initializeGlobalsForTree.js} +8 -13
  22. package/src/project/jsGlobals.js +1 -0
  23. package/src/project/projectConfig.js +2 -2
  24. package/src/project/projectRootFromPath.js +2 -0
  25. package/src/protocols/constructHref.js +3 -3
  26. package/src/protocols/constructSiteTree.js +11 -2
  27. package/src/protocols/explore.js +1 -1
  28. package/src/protocols/explorehttp.js +1 -1
  29. package/src/protocols/fetchAndHandleExtension.js +23 -11
  30. package/src/protocols/files.js +1 -0
  31. package/src/protocols/http.js +4 -1
  32. package/src/protocols/https.js +4 -1
  33. package/src/protocols/httpstree.js +1 -1
  34. package/src/protocols/httptree.js +1 -1
  35. package/src/protocols/package.js +15 -3
  36. package/src/runtime/AsyncCacheTransform.d.ts +5 -0
  37. package/src/runtime/AsyncCacheTransform.js +134 -0
  38. package/src/runtime/HandleExtensionsTransform.d.ts +3 -1
  39. package/src/runtime/HandleExtensionsTransform.js +18 -2
  40. package/src/runtime/OrigamiFileMap.d.ts +5 -2
  41. package/src/runtime/OrigamiFileMap.js +27 -4
  42. package/src/runtime/ScopeMap.js +72 -0
  43. package/src/runtime/SyncCacheTransform.d.ts +8 -0
  44. package/src/runtime/SyncCacheTransform.js +133 -0
  45. package/src/runtime/SystemCacheMap.js +259 -0
  46. package/src/runtime/WatchFilesMixin.js +52 -19
  47. package/src/runtime/enableValueCaching.js +192 -0
  48. package/src/runtime/execute.js +2 -2
  49. package/src/runtime/executionContext.js +7 -0
  50. package/src/runtime/explainReferenceError.js +7 -2
  51. package/src/runtime/expressionObject.js +54 -46
  52. package/src/runtime/handleExtension.js +65 -34
  53. package/src/runtime/interop.js +2 -2
  54. package/src/runtime/mergeTrees.js +1 -1
  55. package/src/runtime/ops.js +28 -33
  56. package/src/runtime/symbols.js +3 -0
  57. package/src/runtime/systemCache.js +3 -0
  58. package/src/runtime/volatile.js +14 -0
  59. package/test/compiler/codeHelpers.js +2 -1
  60. package/test/compiler/optimize.test.js +62 -54
  61. package/test/handlers/ori_handler.test.js +22 -3
  62. package/test/handlers/oridocument_handler.test.js +1 -1
  63. package/test/protocols/https.test.js +19 -0
  64. package/test/protocols/package.test.js +7 -2
  65. package/test/runtime/AsyncCacheTransform.test.js +91 -0
  66. package/test/runtime/OrigamiFileMap.test.js +26 -23
  67. package/test/runtime/ScopeMap.test.js +49 -0
  68. package/test/runtime/SyncCacheTransform.test.js +93 -0
  69. package/test/runtime/SystemCacheMap.test.js +239 -0
  70. package/test/runtime/asyncCalcs.js +28 -0
  71. package/test/runtime/enableValueCaching.test.js +55 -0
  72. package/test/runtime/errors.test.js +53 -30
  73. package/test/runtime/evaluate.test.js +9 -4
  74. package/test/runtime/execute.test.js +6 -1
  75. package/test/runtime/expressionObject.test.js +55 -15
  76. package/test/runtime/fetchAndHandleExtension.test.js +24 -0
  77. package/test/runtime/fixtures/unpack/hello.json +1 -0
  78. package/test/runtime/handleExtension.test.js +12 -1
  79. package/test/runtime/ops.test.js +70 -65
  80. package/test/runtime/syncCalcs.js +27 -0
  81. package/test/runtime/systemCache.test.js +66 -0
  82. package/src/runtime/assignPropertyDescriptors.js +0 -23
  83. package/src/runtime/asyncStorage.js +0 -7
@@ -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()
@@ -2,8 +2,11 @@ import { FileMap } from "@weborigami/async-tree";
2
2
  import EventTargetMixin from "./EventTargetMixin.js";
3
3
  import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
4
4
  import ImportModulesMixin from "./ImportModulesMixin.js";
5
+ import SyncCacheTransform from "./SyncCacheTransform.js";
5
6
  import WatchFilesMixin from "./WatchFilesMixin.js";
6
7
 
7
- export default class OrigamiFileMap extends HandleExtensionsTransform(
8
+ export default class OrigamiFileMap extends SyncCacheTransform(HandleExtensionsTransform(
8
9
  ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileMap)))
9
- ) {}
10
+ )) {
11
+ globals: any;
12
+ }
@@ -1,9 +1,32 @@
1
- import { FileMap } from "@weborigami/async-tree";
1
+ import { FileMap, trailingSlash } from "@weborigami/async-tree";
2
2
  import EventTargetMixin from "./EventTargetMixin.js";
3
3
  import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
4
4
  import ImportModulesMixin from "./ImportModulesMixin.js";
5
+ import SyncCacheTransform from "./SyncCacheTransform.js";
5
6
  import WatchFilesMixin from "./WatchFilesMixin.js";
7
+ import { noCacheSymbol } from "./symbols.js";
6
8
 
7
- export default class OrigamiFileMap extends HandleExtensionsTransform(
8
- ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileMap)))
9
- ) {}
9
+ export default class OrigamiFileMap extends SyncCacheTransform(
10
+ HandleExtensionsTransform(
11
+ ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileMap))),
12
+ ),
13
+ ) {
14
+ get cachePath() {
15
+ return super.cachePath ?? this.path;
16
+ }
17
+
18
+ // Workaround to register file paths in the system cache without trailing
19
+ // slahes. This is so that if someone calls `get("site.ori/")`, the cache path
20
+ // will be "site.ori". It's not clear whether this is the best solution, but
21
+ // hopefully suffices for now.
22
+ cachePathForKey(key) {
23
+ const normalized = trailingSlash.remove(key);
24
+ return super.cachePathForKey(normalized);
25
+ }
26
+
27
+ globals = null;
28
+
29
+ // Don't cache files, just record their dependencies. The OS already caches
30
+ // files, so caching them just consumes memory and may slow things down.
31
+ [noCacheSymbol] = true;
32
+ }
@@ -0,0 +1,72 @@
1
+ import { SyncMap } from "@weborigami/async-tree";
2
+ import path from "node:path";
3
+ import { cachePathSymbol } from "./symbols.js";
4
+ import systemCache from "./systemCache.js";
5
+
6
+ /**
7
+ * Return a sync map of all values in scope for the given sync source map.
8
+ *
9
+ * @param {SyncMap} source
10
+ */
11
+ export default class ScopeMap extends SyncMap {
12
+ constructor(source) {
13
+ super();
14
+ this.source = source;
15
+ this.trailingSlashKeys = source.trailingSlashKeys;
16
+ }
17
+
18
+ get(key) {
19
+ let value;
20
+
21
+ // Starting with this map, search up its parent hierarchy
22
+ let current = this.source;
23
+ while (current) {
24
+ // If the keys of this folder change, the scope request for this key could
25
+ // return a different value, so a change in keys needs to invalidate the
26
+ // value. Whether or not the get() request below succeeds, track the keys
27
+ // of this folder as an upstream dependency of the value being requested.
28
+ if (current[cachePathSymbol]) {
29
+ const folderKeysPath = path.join(current[cachePathSymbol], "_keys");
30
+ systemCache.trackCurrentDependency(folderKeysPath);
31
+ }
32
+
33
+ value = current.get(key);
34
+ if (value !== undefined) {
35
+ break;
36
+ }
37
+ current = current.parent;
38
+ }
39
+
40
+ return value;
41
+ }
42
+
43
+ // Collect all keys for this tree and all parents
44
+ keys() {
45
+ const scopeKeys = new Set();
46
+ let current = this.source;
47
+ while (current) {
48
+ for (const key of current.keys()) {
49
+ scopeKeys.add(key);
50
+ }
51
+ current = current.parent;
52
+ }
53
+ return scopeKeys[Symbol.iterator]();
54
+ }
55
+
56
+ // Collect all keys for this tree and all parents.
57
+ //
58
+ // This method exists for debugging purposes, as it's helpful to be able to
59
+ // quickly flatten and view the entire scope chain.
60
+ get trees() {
61
+ const result = [];
62
+
63
+ /** @type {SyncMap|null} */
64
+ let current = this.source;
65
+ while (current) {
66
+ result.push(current);
67
+ current = "parent" in current ? current.parent : null;
68
+ }
69
+
70
+ return result;
71
+ }
72
+ }
@@ -0,0 +1,8 @@
1
+ import { Mixin } from "../../index.ts";
2
+
3
+ declare const SyncCacheTransform: Mixin<{
4
+ get cachePath(): string;
5
+ cachePathForKey(key: string): string;
6
+ }>
7
+
8
+ export default SyncCacheTransform;