@decohere/flux 0.0.1-dev.110 → 0.0.1-dev.111
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 +1 -1
- package/commonjs/src/serve.command.js +3 -0
- package/commonjs/src/source-module-loader.js +76 -2
- package/commonjs/src/static-imports.js +37 -0
- package/es6/src/serve.command.js +3 -0
- package/es6/src/source-module-loader.js +77 -3
- package/es6/src/static-imports.js +27 -0
- package/package.json +6 -6
- package/types/src/serve.command.d.ts +1 -0
- package/types/src/source-module-loader.d.ts +23 -0
- package/types/src/static-imports.d.ts +5 -0
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
|
-
|
|
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
|
|
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
|
+
};
|
package/es6/src/serve.command.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.0.1-dev.111",
|
|
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.
|
|
29
|
-
"@decohere/forge": "0.0.1-dev.
|
|
30
|
-
"@decohere/mesh": "0.0.1-dev.
|
|
31
|
-
"@decohere/nexus": "0.0.1-dev.
|
|
32
|
-
"@decohere/scribe": "0.0.1-dev.
|
|
28
|
+
"@decohere/crucible": "0.0.1-dev.111",
|
|
29
|
+
"@decohere/forge": "0.0.1-dev.111",
|
|
30
|
+
"@decohere/mesh": "0.0.1-dev.111",
|
|
31
|
+
"@decohere/nexus": "0.0.1-dev.111",
|
|
32
|
+
"@decohere/scribe": "0.0.1-dev.111",
|
|
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
|