@decohere/flux 0.0.1-dev.110 → 0.0.1-dev.112

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.
package/README.md CHANGED
@@ -214,7 +214,7 @@ Only source files (`.ts`) that a served entry imports trigger a reboot.
214
214
 
215
215
  **Memory grows over a long session.** Each reboot imports changed modules under a new `?version=` URL, and Node's ES module cache never releases the old versions. That's fine for a working day; if a session runs for days with thousands of reboots, restart it.
216
216
 
217
- Flux uses **Forge’s compiler**, with one SWC config per project built once per run. SWC leaves imports as written; Flux's resolve hook maps each one to its `.ts` source: `@scope/pkg` and `@scope/pkg/sub` through the Mesh aliases (and tsconfig `paths`), `./file.js` and `./file` to `file.ts`, and `./dir` to `dir/index.ts`. Doing it in one place, instead of in SWC (`jsc.paths`, `resolveFully`) and again in the hook, made loading about 20% faster once Node and SWC are warm (`pnpm bench flux-load`, #513). That is a debug rule so you can edit a library and hit HMR without a pre-build or `node_modules` workspace symlink. Release still resolves through real `node_modules` (published `exports` / `main`).
217
+ Flux uses **Forge’s compiler**, with one SWC config per project built once per run. SWC leaves imports as written; Flux's resolve hook maps each one to its `.ts` source: `@scope/pkg` and `@scope/pkg/sub` through the Mesh aliases (and tsconfig `paths`), `./file.js` and `./file` to `file.ts`, and `./dir` to `dir/index.ts`. Doing it in one place, instead of in SWC (`jsc.paths`, `resolveFully`) and again in the hook, made loading about 20% faster once Node and SWC are warm (`pnpm bench flux-load`, #513). Before Node imports an entry, Flux compiles it and the files it imports, following their imports out from the entry, in parallel on SWC's thread pool (#661). The load hook is synchronous and would otherwise compile one file at a time; files the app never imports, such as tests, are not compiled. A file that fails to compile there is compiled again when Node loads it, and that load fails with the file's name, as before. That is a debug rule so you can edit a library and hit HMR without a pre-build or `node_modules` workspace symlink. Release still resolves through real `node_modules` (published `exports` / `main`).
218
218
 
219
219
  Why aliases, and a tree of where a symlink misses `src/index.ts`: [Forge — debug path aliases](../forge/README.md#debug-path-aliases-not-node_modules-links).
220
220
 
@@ -25,6 +25,9 @@ class ServeCommand extends _cli.DecohereCommand {
25
25
  this.addOption(new _cli.ProjectOption());
26
26
  this.option("--reboot-debounce-ms <ms>", "Quiet window in milliseconds after the last affecting save before a reboot", (value)=>(0, _hotreloadserver.resolveRebootDebounceMs)(Number(value)), _hotreloadserver.DEFAULT_REBOOT_DEBOUNCE_MS);
27
27
  }
28
+ runsUntilStopped() {
29
+ return true;
30
+ }
28
31
  /**
29
32
  * Commander passes positional args before options when `[slices...]` is declared.
30
33
  * The CLI host ModuleFactory expects `(opts, cmd)` — normalize here.
@@ -17,6 +17,7 @@ const _mesh = require("@decohere/mesh");
17
17
  const _io = require("@decohere/nexus/io");
18
18
  const _scribe = require("@decohere/scribe");
19
19
  const _rewriteservespecifier = require("./rewrite-serve-specifier.js");
20
+ const _staticimports = require("./static-imports.js");
20
21
  function _interop_require_default(obj) {
21
22
  return obj && obj.__esModule ? obj : {
22
23
  default: obj
@@ -78,6 +79,9 @@ class SourceModuleLoader {
78
79
  buildConfig = "debug";
79
80
  fileInfoMap = new Map();
80
81
  transpileCache = new Map();
82
+ /** Bumped when a file's cached code is dropped, so an up-front compile already running can't put it back. */ transpileVersions = new Map();
83
+ /** The source files each entry's walk reached, so a reboot only walks out from what changed. */ reachedFrom = new Map();
84
+ /** Up-front compiles in flight, shared by entries booting at the same time. */ compilingAhead = new Map();
81
85
  /** SWC options per project, built on first use and kept until the next `configure()`. */ swcConfigs = new Map();
82
86
  aliasPaths = {};
83
87
  aliasBaseUrl = _nodepath.default.resolve(".");
@@ -102,7 +106,9 @@ class SourceModuleLoader {
102
106
  }
103
107
  async configure(options) {
104
108
  this.disposed = false;
105
- this.transpileCache.clear();
109
+ // A new Map, not clear(): an up-front compile from before can't write into this one.
110
+ this.transpileCache = new Map();
111
+ this.reachedFrom = new Map();
106
112
  this.swcConfigs.clear();
107
113
  this.buildConfig = options.buildConfig;
108
114
  this.workspace = options.workspace ?? await (0, _forge.createWorkspace)();
@@ -149,12 +155,14 @@ class SourceModuleLoader {
149
155
  });
150
156
  }
151
157
  async importEntry(absolutePath) {
158
+ await this.compileAhead(_nodepath.default.resolve(absolutePath));
152
159
  const url = this.versionFileUrl(_io.io.pathToFileUrl(_nodepath.default.resolve(absolutePath)));
153
160
  return Promise.resolve(url).then((p)=>/*#__PURE__*/ _interop_require_wildcard(require(p)));
154
161
  }
155
162
  invalidateTranspile(...absolutePaths) {
156
163
  for (const fullPath of absolutePaths){
157
164
  this.transpileCache.delete(fullPath);
165
+ this.transpileVersions.set(fullPath, (this.transpileVersions.get(fullPath) ?? 0) + 1);
158
166
  this.transpiler.invalidateContentCache(new _io.FileEx({
159
167
  fullPath
160
168
  }));
@@ -162,7 +170,8 @@ class SourceModuleLoader {
162
170
  }
163
171
  dispose() {
164
172
  this.disposed = true;
165
- this.transpileCache.clear();
173
+ this.transpileCache = new Map();
174
+ this.reachedFrom = new Map();
166
175
  this.swcConfigs.clear();
167
176
  this.fileInfoMap.clear();
168
177
  this.workspace?.[Symbol.dispose]();
@@ -181,6 +190,71 @@ class SourceModuleLoader {
181
190
  }));
182
191
  return new Map(entries.flat());
183
192
  }
193
+ /**
194
+ * Compiles the entry and the source files it imports, before Node imports it (#661). The load
195
+ * hook is synchronous, so it would compile one file at a time on the main thread; async SWC
196
+ * runs on the thread pool. Walks out from the entry, compiling each file's imports in parallel,
197
+ * so files the app never imports (tests, other apps) are not compiled. A reboot walks out only
198
+ * from the files it reached before that are no longer cached (the saved ones), so their new
199
+ * imports are found without visiting the rest of the graph again.
200
+ *
201
+ * A file that fails here is left out of the cache instead of failing the boot: the load hook
202
+ * then compiles it again and throws as before, naming the file. The walk is a hint; whatever
203
+ * it misses is compiled by the load hook when Node gets to it.
204
+ */ async compileAhead(entryPath) {
205
+ const reached = this.reachedFrom.get(entryPath);
206
+ const starts = reached == null ? [
207
+ entryPath
208
+ ] : [
209
+ ...reached
210
+ ].filter((fullPath)=>!this.transpileCache.has(fullPath));
211
+ const seen = new Set(reached);
212
+ for (const start of starts)seen.delete(start);
213
+ const visit = async (fullPath)=>{
214
+ if (seen.has(fullPath)) return;
215
+ seen.add(fullPath);
216
+ const code = await this.compiledAhead(fullPath);
217
+ if (code == null) return;
218
+ const parentUrl = (0, _nodeurl.pathToFileURL)(fullPath).toString();
219
+ await Promise.all(_staticimports.staticImports.scan(code).map((specifier)=>{
220
+ const target = this.sourcePathOf(specifier, parentUrl);
221
+ return target == null ? undefined : visit(target);
222
+ }));
223
+ };
224
+ await Promise.all(starts.map(visit));
225
+ this.reachedFrom.set(entryPath, seen);
226
+ }
227
+ /** The file's compiled code, from the cache or compiled now; undefined if it isn't a source. */ compiledAhead(fullPath) {
228
+ const fileInfo = this.fileInfoMap.get(fullPath);
229
+ if (fileInfo == null) return Promise.resolve(undefined);
230
+ const cached = this.transpileCache.get(fullPath);
231
+ if (cached != null) return Promise.resolve(cached);
232
+ const inFlight = this.compilingAhead.get(fullPath);
233
+ if (inFlight != null) return inFlight;
234
+ const version = this.transpileVersions.get(fullPath);
235
+ const cache = this.transpileCache;
236
+ const compiling = this.transpiler.transformFileAsync(new _io.FileEx({
237
+ fullPath
238
+ }), _forge.BuildTargets.es6, {}, this.swcConfigFor(fileInfo.project)).then((response)=>{
239
+ if (response.outcome !== "success") return undefined;
240
+ // Saved, reconfigured or disposed while compiling: don't cache stale code.
241
+ const current = !this.disposed && cache === this.transpileCache && version === this.transpileVersions.get(fullPath);
242
+ if (current) cache.set(fullPath, response.code);
243
+ return response.code;
244
+ }).finally(()=>this.compilingAhead.delete(fullPath));
245
+ this.compilingAhead.set(fullPath, compiling);
246
+ return compiling;
247
+ }
248
+ /** The source file a specifier names, mapped the way the resolve hook maps it. */ sourcePathOf(specifier, parentUrl) {
249
+ const url = (0, _rewriteservespecifier.rewriteServeSpecifier)({
250
+ specifier,
251
+ parentUrl,
252
+ isWatchedTs: (filePath)=>this.fileInfoMap.has(filePath),
253
+ resolveBare: (bare)=>_mesh.packageAliases.resolvePath(bare, this.aliasPaths, this.aliasBaseUrl),
254
+ versionFileUrl: (fileUrl)=>fileUrl
255
+ });
256
+ return url == null ? null : (0, _nodeurl.fileURLToPath)(url);
257
+ }
184
258
  loadTs(url) {
185
259
  if (!url.startsWith("file:")) return null;
186
260
  // fileURLToPath, not URL.pathname: decodes %20 and gives Windows paths, so files
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "staticImports", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return staticImports;
9
+ }
10
+ });
11
+ /**
12
+ * The module specifiers a compiled ES module names in `import … from`, `export … from`, bare
13
+ * `import "x"` and literal `import("x")`.
14
+ *
15
+ * Only a hint for compiling ahead (#661): a specifier it misses is compiled when Node loads it,
16
+ * and one it finds by mistake (an import written inside a string) costs one extra compile. So a
17
+ * scan of SWC's output is enough; it never decides what runs.
18
+ */ const STATIC = /\b(?:import|export)\s*(?:[\w$*{},\s]+?\s*from\s*)?["']([^"'\n]+)["']/g;
19
+ const DYNAMIC = /\bimport\s*\(\s*["']([^"'\n]+)["']\s*\)/g;
20
+ function scan(code) {
21
+ const found = new Set();
22
+ for (const pattern of [
23
+ STATIC,
24
+ DYNAMIC
25
+ ]){
26
+ for (const match of code.matchAll(pattern)){
27
+ const specifier = match[1];
28
+ if (specifier != null) found.add(specifier);
29
+ }
30
+ }
31
+ return [
32
+ ...found
33
+ ];
34
+ }
35
+ const staticImports = {
36
+ scan
37
+ };
@@ -15,6 +15,9 @@ export class ServeCommand extends DecohereCommand {
15
15
  this.addOption(new ProjectOption());
16
16
  this.option("--reboot-debounce-ms <ms>", "Quiet window in milliseconds after the last affecting save before a reboot", (value)=>resolveRebootDebounceMs(Number(value)), DEFAULT_REBOOT_DEBOUNCE_MS);
17
17
  }
18
+ runsUntilStopped() {
19
+ return true;
20
+ }
18
21
  /**
19
22
  * Commander passes positional args before options when `[slices...]` is declared.
20
23
  * The CLI host ModuleFactory expects `(opts, cmd)` — normalize here.
@@ -6,13 +6,14 @@ function _ts_decorate(decorators, target, key, desc) {
6
6
  }
7
7
  import { registerHooks } from "node:module";
8
8
  import path from "node:path";
9
- import { fileURLToPath } from "node:url";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
10
  import { injectable } from "@decohere/crucible/decorators";
11
11
  import { BuildTargets, createWorkspace, SwcTranspiler } from "@decohere/forge";
12
12
  import { packageAliases } from "@decohere/mesh";
13
13
  import { FileEx, io } from "@decohere/nexus/io";
14
14
  import { scribe } from "@decohere/scribe";
15
15
  import { rewriteServeSpecifier } from "./rewrite-serve-specifier.js";
16
+ import { staticImports } from "./static-imports.js";
16
17
  export class SourceModuleLoader {
17
18
  logger = scribe.logger(SourceModuleLoader.name);
18
19
  transpiler = new SwcTranspiler();
@@ -22,6 +23,9 @@ export class SourceModuleLoader {
22
23
  buildConfig = "debug";
23
24
  fileInfoMap = new Map();
24
25
  transpileCache = new Map();
26
+ /** Bumped when a file's cached code is dropped, so an up-front compile already running can't put it back. */ transpileVersions = new Map();
27
+ /** The source files each entry's walk reached, so a reboot only walks out from what changed. */ reachedFrom = new Map();
28
+ /** Up-front compiles in flight, shared by entries booting at the same time. */ compilingAhead = new Map();
25
29
  /** SWC options per project, built on first use and kept until the next `configure()`. */ swcConfigs = new Map();
26
30
  aliasPaths = {};
27
31
  aliasBaseUrl = path.resolve(".");
@@ -46,7 +50,9 @@ export class SourceModuleLoader {
46
50
  }
47
51
  async configure(options) {
48
52
  this.disposed = false;
49
- this.transpileCache.clear();
53
+ // A new Map, not clear(): an up-front compile from before can't write into this one.
54
+ this.transpileCache = new Map();
55
+ this.reachedFrom = new Map();
50
56
  this.swcConfigs.clear();
51
57
  this.buildConfig = options.buildConfig;
52
58
  this.workspace = options.workspace ?? await createWorkspace();
@@ -93,12 +99,14 @@ export class SourceModuleLoader {
93
99
  });
94
100
  }
95
101
  async importEntry(absolutePath) {
102
+ await this.compileAhead(path.resolve(absolutePath));
96
103
  const url = this.versionFileUrl(io.pathToFileUrl(path.resolve(absolutePath)));
97
104
  return import(url);
98
105
  }
99
106
  invalidateTranspile(...absolutePaths) {
100
107
  for (const fullPath of absolutePaths){
101
108
  this.transpileCache.delete(fullPath);
109
+ this.transpileVersions.set(fullPath, (this.transpileVersions.get(fullPath) ?? 0) + 1);
102
110
  this.transpiler.invalidateContentCache(new FileEx({
103
111
  fullPath
104
112
  }));
@@ -106,7 +114,8 @@ export class SourceModuleLoader {
106
114
  }
107
115
  dispose() {
108
116
  this.disposed = true;
109
- this.transpileCache.clear();
117
+ this.transpileCache = new Map();
118
+ this.reachedFrom = new Map();
110
119
  this.swcConfigs.clear();
111
120
  this.fileInfoMap.clear();
112
121
  this.workspace?.[Symbol.dispose]();
@@ -125,6 +134,71 @@ export class SourceModuleLoader {
125
134
  }));
126
135
  return new Map(entries.flat());
127
136
  }
137
+ /**
138
+ * Compiles the entry and the source files it imports, before Node imports it (#661). The load
139
+ * hook is synchronous, so it would compile one file at a time on the main thread; async SWC
140
+ * runs on the thread pool. Walks out from the entry, compiling each file's imports in parallel,
141
+ * so files the app never imports (tests, other apps) are not compiled. A reboot walks out only
142
+ * from the files it reached before that are no longer cached (the saved ones), so their new
143
+ * imports are found without visiting the rest of the graph again.
144
+ *
145
+ * A file that fails here is left out of the cache instead of failing the boot: the load hook
146
+ * then compiles it again and throws as before, naming the file. The walk is a hint; whatever
147
+ * it misses is compiled by the load hook when Node gets to it.
148
+ */ async compileAhead(entryPath) {
149
+ const reached = this.reachedFrom.get(entryPath);
150
+ const starts = reached == null ? [
151
+ entryPath
152
+ ] : [
153
+ ...reached
154
+ ].filter((fullPath)=>!this.transpileCache.has(fullPath));
155
+ const seen = new Set(reached);
156
+ for (const start of starts)seen.delete(start);
157
+ const visit = async (fullPath)=>{
158
+ if (seen.has(fullPath)) return;
159
+ seen.add(fullPath);
160
+ const code = await this.compiledAhead(fullPath);
161
+ if (code == null) return;
162
+ const parentUrl = pathToFileURL(fullPath).toString();
163
+ await Promise.all(staticImports.scan(code).map((specifier)=>{
164
+ const target = this.sourcePathOf(specifier, parentUrl);
165
+ return target == null ? undefined : visit(target);
166
+ }));
167
+ };
168
+ await Promise.all(starts.map(visit));
169
+ this.reachedFrom.set(entryPath, seen);
170
+ }
171
+ /** The file's compiled code, from the cache or compiled now; undefined if it isn't a source. */ compiledAhead(fullPath) {
172
+ const fileInfo = this.fileInfoMap.get(fullPath);
173
+ if (fileInfo == null) return Promise.resolve(undefined);
174
+ const cached = this.transpileCache.get(fullPath);
175
+ if (cached != null) return Promise.resolve(cached);
176
+ const inFlight = this.compilingAhead.get(fullPath);
177
+ if (inFlight != null) return inFlight;
178
+ const version = this.transpileVersions.get(fullPath);
179
+ const cache = this.transpileCache;
180
+ const compiling = this.transpiler.transformFileAsync(new FileEx({
181
+ fullPath
182
+ }), BuildTargets.es6, {}, this.swcConfigFor(fileInfo.project)).then((response)=>{
183
+ if (response.outcome !== "success") return undefined;
184
+ // Saved, reconfigured or disposed while compiling: don't cache stale code.
185
+ const current = !this.disposed && cache === this.transpileCache && version === this.transpileVersions.get(fullPath);
186
+ if (current) cache.set(fullPath, response.code);
187
+ return response.code;
188
+ }).finally(()=>this.compilingAhead.delete(fullPath));
189
+ this.compilingAhead.set(fullPath, compiling);
190
+ return compiling;
191
+ }
192
+ /** The source file a specifier names, mapped the way the resolve hook maps it. */ sourcePathOf(specifier, parentUrl) {
193
+ const url = rewriteServeSpecifier({
194
+ specifier,
195
+ parentUrl,
196
+ isWatchedTs: (filePath)=>this.fileInfoMap.has(filePath),
197
+ resolveBare: (bare)=>packageAliases.resolvePath(bare, this.aliasPaths, this.aliasBaseUrl),
198
+ versionFileUrl: (fileUrl)=>fileUrl
199
+ });
200
+ return url == null ? null : fileURLToPath(url);
201
+ }
128
202
  loadTs(url) {
129
203
  if (!url.startsWith("file:")) return null;
130
204
  // fileURLToPath, not URL.pathname: decodes %20 and gives Windows paths, so files
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The module specifiers a compiled ES module names in `import … from`, `export … from`, bare
3
+ * `import "x"` and literal `import("x")`.
4
+ *
5
+ * Only a hint for compiling ahead (#661): a specifier it misses is compiled when Node loads it,
6
+ * and one it finds by mistake (an import written inside a string) costs one extra compile. So a
7
+ * scan of SWC's output is enough; it never decides what runs.
8
+ */ const STATIC = /\b(?:import|export)\s*(?:[\w$*{},\s]+?\s*from\s*)?["']([^"'\n]+)["']/g;
9
+ const DYNAMIC = /\bimport\s*\(\s*["']([^"'\n]+)["']\s*\)/g;
10
+ function scan(code) {
11
+ const found = new Set();
12
+ for (const pattern of [
13
+ STATIC,
14
+ DYNAMIC
15
+ ]){
16
+ for (const match of code.matchAll(pattern)){
17
+ const specifier = match[1];
18
+ if (specifier != null) found.add(specifier);
19
+ }
20
+ }
21
+ return [
22
+ ...found
23
+ ];
24
+ }
25
+ export const staticImports = {
26
+ scan
27
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/flux",
3
- "version": "0.0.1-dev.110",
3
+ "version": "0.0.1-dev.112",
4
4
  "description": "Hot module reload server for Node.js ESM applications",
5
5
  "keywords": [
6
6
  "typescript",
@@ -25,11 +25,11 @@
25
25
  "test": "echo \"Error: no test specified\" && exit 1"
26
26
  },
27
27
  "dependencies": {
28
- "@decohere/crucible": "0.0.1-dev.110",
29
- "@decohere/forge": "0.0.1-dev.110",
30
- "@decohere/mesh": "0.0.1-dev.110",
31
- "@decohere/nexus": "0.0.1-dev.110",
32
- "@decohere/scribe": "0.0.1-dev.110",
28
+ "@decohere/crucible": "0.0.1-dev.112",
29
+ "@decohere/forge": "0.0.1-dev.112",
30
+ "@decohere/mesh": "0.0.1-dev.112",
31
+ "@decohere/nexus": "0.0.1-dev.112",
32
+ "@decohere/scribe": "0.0.1-dev.112",
33
33
  "commander": "^13.1.0",
34
34
  "rxjs": "^7.8.2"
35
35
  },
@@ -3,6 +3,7 @@ import { ServeAction } from "./serve.action.js";
3
3
  export declare class ServeCommand extends DecohereCommand {
4
4
  get actions(): (typeof ServeAction)[];
5
5
  constructor(name: string);
6
+ runsUntilStopped(): boolean;
6
7
  /**
7
8
  * Commander passes positional args before options when `[slices...]` is declared.
8
9
  * The CLI host ModuleFactory expects `(opts, cmd)` — normalize here.
@@ -30,6 +30,12 @@ export declare class SourceModuleLoader {
30
30
  private buildConfig;
31
31
  private fileInfoMap;
32
32
  private transpileCache;
33
+ /** Bumped when a file's cached code is dropped, so an up-front compile already running can't put it back. */
34
+ private transpileVersions;
35
+ /** The source files each entry's walk reached, so a reboot only walks out from what changed. */
36
+ private reachedFrom;
37
+ /** Up-front compiles in flight, shared by entries booting at the same time. */
38
+ private compilingAhead;
33
39
  /** SWC options per project, built on first use and kept until the next `configure()`. */
34
40
  private swcConfigs;
35
41
  private aliasPaths;
@@ -47,6 +53,23 @@ export declare class SourceModuleLoader {
47
53
  invalidateTranspile(...absolutePaths: string[]): void;
48
54
  dispose(): void;
49
55
  private buildFileInfoMap;
56
+ /**
57
+ * Compiles the entry and the source files it imports, before Node imports it (#661). The load
58
+ * hook is synchronous, so it would compile one file at a time on the main thread; async SWC
59
+ * runs on the thread pool. Walks out from the entry, compiling each file's imports in parallel,
60
+ * so files the app never imports (tests, other apps) are not compiled. A reboot walks out only
61
+ * from the files it reached before that are no longer cached (the saved ones), so their new
62
+ * imports are found without visiting the rest of the graph again.
63
+ *
64
+ * A file that fails here is left out of the cache instead of failing the boot: the load hook
65
+ * then compiles it again and throws as before, naming the file. The walk is a hint; whatever
66
+ * it misses is compiled by the load hook when Node gets to it.
67
+ */
68
+ private compileAhead;
69
+ /** The file's compiled code, from the cache or compiled now; undefined if it isn't a source. */
70
+ private compiledAhead;
71
+ /** The source file a specifier names, mapped the way the resolve hook maps it. */
72
+ private sourcePathOf;
50
73
  private loadTs;
51
74
  /**
52
75
  * The project's SWC options, built once: rebuilding them for every file was about a quarter
@@ -0,0 +1,5 @@
1
+ declare function scan(code: string): string[];
2
+ export declare const staticImports: {
3
+ scan: typeof scan;
4
+ };
5
+ export {};