@decohere/flux 0.0.1-dev.119 → 0.0.1-dev.12

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 (47) hide show
  1. package/commonjs/src/flux.command.js +3 -3
  2. package/commonjs/src/flux.module.js +8 -9
  3. package/commonjs/src/hot-reload-server.js +219 -307
  4. package/commonjs/src/index.js +2 -34
  5. package/commonjs/src/serve.action.js +7 -78
  6. package/commonjs/src/serve.command.js +4 -35
  7. package/es6/src/flux.command.js +3 -3
  8. package/es6/src/flux.module.js +8 -9
  9. package/es6/src/hot-reload-server.js +175 -287
  10. package/es6/src/index.js +3 -5
  11. package/es6/src/serve.action.js +9 -73
  12. package/es6/src/serve.command.js +4 -35
  13. package/package.json +6 -8
  14. package/types/src/flux.command.d.ts +2 -2
  15. package/types/src/flux.module.d.ts +4 -6
  16. package/types/src/hot-reload-server.d.ts +23 -57
  17. package/types/src/index.d.ts +1 -8
  18. package/types/src/serve.action.d.ts +3 -13
  19. package/types/src/serve.command.d.ts +1 -7
  20. package/README.md +0 -273
  21. package/commonjs/src/deferred-script-runner.js +0 -56
  22. package/commonjs/src/flux.js +0 -56
  23. package/commonjs/src/rewrite-serve-specifier.js +0 -61
  24. package/commonjs/src/run.action.js +0 -47
  25. package/commonjs/src/run.command.js +0 -65
  26. package/commonjs/src/slice-serve.js +0 -130
  27. package/commonjs/src/source-module-loader.js +0 -378
  28. package/commonjs/src/static-imports.js +0 -37
  29. package/commonjs/src/transpile-disk-cache.js +0 -148
  30. package/es6/src/deferred-script-runner.js +0 -46
  31. package/es6/src/flux.js +0 -41
  32. package/es6/src/rewrite-serve-specifier.js +0 -51
  33. package/es6/src/run.action.js +0 -37
  34. package/es6/src/run.command.js +0 -55
  35. package/es6/src/slice-serve.js +0 -105
  36. package/es6/src/source-module-loader.js +0 -322
  37. package/es6/src/static-imports.js +0 -27
  38. package/es6/src/transpile-disk-cache.js +0 -142
  39. package/types/src/deferred-script-runner.d.ts +0 -18
  40. package/types/src/flux.d.ts +0 -43
  41. package/types/src/rewrite-serve-specifier.d.ts +0 -14
  42. package/types/src/run.action.d.ts +0 -15
  43. package/types/src/run.command.d.ts +0 -11
  44. package/types/src/slice-serve.d.ts +0 -52
  45. package/types/src/source-module-loader.d.ts +0 -96
  46. package/types/src/static-imports.d.ts +0 -5
  47. package/types/src/transpile-disk-cache.d.ts +0 -38
@@ -1,46 +0,0 @@
1
- function _ts_decorate(decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- }
7
- import { spawn } from "node:child_process";
8
- import { injectable } from "@decohere/crucible/decorators";
9
- import { detectPackageManager, packageManagerFromString } from "@decohere/devkit/interop";
10
- import { scribe } from "@decohere/scribe";
11
- export class DeferredScriptRunner {
12
- logger = scribe.logger(DeferredScriptRunner.name);
13
- children = [];
14
- start(options) {
15
- const pm = detectPackageManager(options.workspaceRoot) ?? packageManagerFromString("pnpm");
16
- this.logger.info(`deferred ${options.label} — running ${pm.name} run ${options.script} in ${options.cwd}`);
17
- const child = spawn(pm.name, [
18
- "run",
19
- options.script
20
- ], {
21
- cwd: options.cwd,
22
- stdio: "inherit",
23
- env: process.env,
24
- shell: false
25
- });
26
- child.on("exit", (code, signal)=>{
27
- if (signal) return;
28
- if (code != null && code !== 0) {
29
- this.logger.warn(`deferred ${options.label} exited with ${code}`);
30
- }
31
- });
32
- this.children.push(child);
33
- return child;
34
- }
35
- async [Symbol.asyncDispose]() {
36
- for (const child of this.children){
37
- if (!child.killed) {
38
- child.kill("SIGTERM");
39
- }
40
- }
41
- this.children.length = 0;
42
- }
43
- }
44
- DeferredScriptRunner = _ts_decorate([
45
- injectable()
46
- ], DeferredScriptRunner);
package/es6/src/flux.js DELETED
@@ -1,41 +0,0 @@
1
- import path from "node:path";
2
- import { HotReloadServer } from "./hot-reload-server.js";
3
- import { SourceModuleLoader } from "./source-module-loader.js";
4
- /** Process-level one-shot loader so a second `run()` reconfigures instead of stacking hooks. */ const runLoader = new SourceModuleLoader();
5
- async function run(entry, options = {}) {
6
- const trimmed = entry.trim();
7
- if (trimmed == null || trimmed === "") {
8
- throw new Error("flux.run requires an entry file path");
9
- }
10
- const absoluteEntry = path.resolve(trimmed);
11
- await runLoader.configure({
12
- workspace: options.workspace,
13
- buildConfig: options.configuration ?? "debug"
14
- });
15
- runLoader.registerHooks();
16
- process.argv = [
17
- process.execPath,
18
- absoluteEntry,
19
- ...options.args ?? []
20
- ];
21
- return runLoader.importEntry(absoluteEntry);
22
- }
23
- async function serve(options = {}) {
24
- const server = new HotReloadServer();
25
- await server.start({
26
- buildConfig: options.configuration ?? "debug",
27
- project: options.project,
28
- projects: options.projects,
29
- workspace: options.workspace,
30
- rebootDebounceMs: options.rebootDebounceMs
31
- });
32
- return server;
33
- }
34
- function dispose() {
35
- runLoader.dispose();
36
- }
37
- export const flux = {
38
- run,
39
- serve,
40
- dispose
41
- };
@@ -1,51 +0,0 @@
1
- import path from "node:path";
2
- import { fileURLToPath, pathToFileURL } from "node:url";
3
- import { packageAliases, sourceExtensions } from "@decohere/workspace";
4
- /**
5
- * Map a resolve-hook specifier to a versioned watched `.ts` file URL.
6
- * Covers relative `.js` / `.ts` / extensionless imports, directory imports (`./models` is
7
- * `./models/index.ts`), and bare workspace package names and subpaths. Flux compiles without
8
- * SWC's own path resolution, so this is the only place imports are mapped to sources (#513).
9
- */ export function rewriteServeSpecifier(options) {
10
- const { specifier, parentUrl, isWatchedTs, resolveBare, versionFileUrl } = options;
11
- const relativeTs = resolveRelativeWatchedTs(specifier, parentUrl, isWatchedTs);
12
- if (relativeTs != null) {
13
- return versionFileUrl(pathToFileURL(relativeTs).toString());
14
- }
15
- const bareTarget = resolveBare(specifier);
16
- const bareTs = bareTarget == null ? null : watchedTs(bareTarget, isWatchedTs);
17
- if (bareTs != null) {
18
- return versionFileUrl(pathToFileURL(bareTs).toString());
19
- }
20
- return null;
21
- }
22
- /**
23
- * The watched source file a path names: itself, or the source its extension stands for (`.js` is
24
- * `.ts` or `.tsx`, `.mjs` is `.mts`, `.cjs` is `.cts`, none adds `.ts` or `.tsx`), else the
25
- * `index` source of the directory it names. A file wins over a directory of the same name.
26
- */ function watchedTs(filePath, isWatchedTs) {
27
- const withoutSlash = filePath.replace(/[/\\]+$/, "");
28
- // The workspace alias lookup may have added `.ts` to a subpath that names a directory (`@lib/models`
29
- // arrives as `…/models.ts`), so the directory is the path without any extension.
30
- const directory = withoutSlash.replace(/\.([mc]?[jt]sx?)$/, "");
31
- const candidates = [
32
- ...sourceExtensions.candidates(withoutSlash),
33
- ...sourceExtensions.candidates(directory),
34
- ...sourceExtensions.candidates(path.join(directory, "index"))
35
- ];
36
- return candidates.find((candidate)=>isWatchedTs(candidate)) ?? null;
37
- }
38
- function resolveRelativeWatchedTs(specifier, parentUrl, isWatchedTs) {
39
- if (packageAliases.isBareSpecifier(specifier)) return null;
40
- if (!(specifier.startsWith(".") || specifier.startsWith("/"))) return null;
41
- const parentClean = parentUrl.split("?")[0];
42
- if (parentClean == null || parentClean === "") return null;
43
- let resolved;
44
- try {
45
- resolved = new URL(specifier, parentClean);
46
- } catch {
47
- return null;
48
- }
49
- if (resolved.protocol !== "file:") return null;
50
- return watchedTs(fileURLToPath(resolved), isWatchedTs);
51
- }
@@ -1,37 +0,0 @@
1
- function _ts_decorate(decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- }
7
- function _ts_metadata(k, v) {
8
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
- }
10
- import { injectable } from "@decohere/crucible/decorators";
11
- import { Workspace } from "@decohere/workspace";
12
- import { ActionResponse } from "@decohere/devkit/cli";
13
- import { scribe } from "@decohere/scribe";
14
- import { flux } from "./flux.js";
15
- export class RunAction {
16
- workspace;
17
- logger = scribe.logger(RunAction.name);
18
- constructor(workspace){
19
- this.workspace = workspace;
20
- }
21
- async run(options) {
22
- this.logger.trace(`Running ${options.entry}`);
23
- await flux.run(options.entry, {
24
- configuration: options.configuration,
25
- args: options.args,
26
- workspace: this.workspace
27
- });
28
- return ActionResponse.Success(RunAction.name);
29
- }
30
- }
31
- RunAction = _ts_decorate([
32
- injectable(),
33
- _ts_metadata("design:type", Function),
34
- _ts_metadata("design:paramtypes", [
35
- typeof Workspace === "undefined" ? Object : Workspace
36
- ])
37
- ], RunAction);
@@ -1,55 +0,0 @@
1
- import { ConfigurationOption, DecohereCommand } from "@decohere/devkit/cli";
2
- import { RunAction } from "./run.action.js";
3
- export class RunCommand extends DecohereCommand {
4
- get actions() {
5
- return [
6
- RunAction
7
- ];
8
- }
9
- constructor(name){
10
- super(name);
11
- this.description("Compile and run a TypeScript entry on the fly (no watch / HMR)");
12
- this.argument("<entry>", "Path to the TypeScript entry file");
13
- this.argument("[args...]", "Arguments forwarded to the entry after rewriting argv");
14
- this.addOption(new ConfigurationOption("debug"));
15
- }
16
- /**
17
- * Commander passes positionals before options when `<entry> [args...]` is declared.
18
- * The CLI host ModuleFactory expects `(opts, cmd)` — normalize and stash entry/args on opts.
19
- */ action(fn) {
20
- return super.action((...args)=>{
21
- const cmd = args.at(-1);
22
- const opts = args.at(-2);
23
- const { entry, entryArgs } = normalizeRunPositionals(args.slice(0, -2));
24
- cmd.setOptionValue("entry", entry);
25
- cmd.setOptionValue("args", entryArgs);
26
- return fn(opts, cmd);
27
- });
28
- }
29
- }
30
- /** Commander yields `[entry, args[]]` or sometimes a flat list depending on arity. */ function normalizeRunPositionals(positionals) {
31
- if (positionals.length === 0) {
32
- return {
33
- entry: "",
34
- entryArgs: []
35
- };
36
- }
37
- const first = positionals[0];
38
- if (typeof first !== "string" || first === "") {
39
- return {
40
- entry: "",
41
- entryArgs: []
42
- };
43
- }
44
- const second = positionals[1];
45
- if (Array.isArray(second)) {
46
- return {
47
- entry: first,
48
- entryArgs: second.filter((s)=>typeof s === "string")
49
- };
50
- }
51
- return {
52
- entry: first,
53
- entryArgs: positionals.slice(1).filter((s)=>typeof s === "string" && s !== "")
54
- };
55
- }
@@ -1,105 +0,0 @@
1
- /**
2
- * Directory-slice helpers for `flux serve <dir>…` — classify workspace packages as
3
- * Flux apps, Nixpacks-style script deferrals, or libraries to skip.
4
- */ /** Preferred package scripts for foreign toolchains (Nixpacks-style). */ export const DEFER_SCRIPT_CANDIDATES = [
5
- "dev",
6
- "serve"
7
- ];
8
- /**
9
- * Normalize a CLI slice token to a `projects/…` prefix without a trailing slash.
10
- * `funding` and `projects/funding` both become `projects/funding`.
11
- */ export function normalizeSlicePrefix(slice) {
12
- const trimmed = slice.replaceAll("\\", "/").replace(/\/+$/, "");
13
- if (trimmed === "" || trimmed === ".") {
14
- throw new Error("Slice directory must be a non-empty path");
15
- }
16
- if (trimmed === "projects" || trimmed.startsWith("projects/")) {
17
- return trimmed;
18
- }
19
- return `projects/${trimmed}`;
20
- }
21
- /**
22
- * True when `projectDirectory` belongs to the slice token.
23
- *
24
- * Matches:
25
- * - nested: `projects/funding`, `projects/funding/api`
26
- * - flat dotted package dirs: `projects/funding.web` for token `funding`
27
- *
28
- * Does not match siblings that only share a string prefix (`funding` vs `funding-fee.api`).
29
- */ export function isUnderSlice(projectDirectory, slicePrefix) {
30
- const dir = projectDirectory.replaceAll("\\", "/").replace(/\/+$/, "");
31
- const prefix = slicePrefix.replaceAll("\\", "/").replace(/\/+$/, "");
32
- if (dir === prefix || dir.startsWith(`${prefix}/`)) {
33
- return true;
34
- }
35
- // Flat monorepo packages: projects/<module>.<role>
36
- return dir.startsWith(`${prefix}.`);
37
- }
38
- export function projectsInSlice(projects, slice) {
39
- const prefix = normalizeSlicePrefix(slice);
40
- return projects.filter((p)=>isUnderSlice(p.projectDirectory, prefix));
41
- }
42
- /** Union of packages under any of the slice tokens (deduped by package name). */ export function projectsInSlices(projects, slices) {
43
- const byName = new Map();
44
- for (const slice of slices){
45
- for (const project of projectsInSlice(projects, slice)){
46
- byName.set(project.name, project);
47
- }
48
- }
49
- return [
50
- ...byName.values()
51
- ];
52
- }
53
- /** Script invokes Flux (in-process serve stays on Flux, not deferred). */ export function isFluxOwnedScript(script) {
54
- return /\bflux\b/i.test(script);
55
- }
56
- /**
57
- * First Nixpacks-style serve script that is not Flux-owned.
58
- * Prefers `dev`, then `serve`.
59
- */ export function resolveDeferScript(scripts) {
60
- for (const name of DEFER_SCRIPT_CANDIDATES){
61
- const body = scripts[name];
62
- if (body != null && body !== "" && !isFluxOwnedScript(body)) {
63
- return name;
64
- }
65
- }
66
- return undefined;
67
- }
68
- /** Convention: Flux Node apps use `main` ending in `main.ts` / `main.tsx`. */ export function isFluxAppMain(main) {
69
- const normalized = main.replaceAll("\\", "/");
70
- return /(^|\/)main\.tsx?$/.test(normalized);
71
- }
72
- /**
73
- * Classify one package. Foreign serve scripts win over `main.ts` (Vite apps
74
- * often have a main entry but must not be HMR-booted by Flux).
75
- */ export function classifySliceProject(project) {
76
- if (resolveDeferScript(project.scripts) != null) {
77
- return "defer";
78
- }
79
- if (isFluxAppMain(project.main)) {
80
- return "flux";
81
- }
82
- return "skip";
83
- }
84
- export function classifySliceProjects(projects, slices) {
85
- const tokens = typeof slices === "string" ? [
86
- slices
87
- ] : [
88
- ...slices
89
- ];
90
- const inSlice = projectsInSlices(projects, tokens);
91
- const flux = [];
92
- const defer = [];
93
- const skip = [];
94
- for (const project of inSlice){
95
- const kind = classifySliceProject(project);
96
- if (kind === "flux") flux.push(project);
97
- else if (kind === "defer") defer.push(project);
98
- else skip.push(project);
99
- }
100
- return {
101
- flux,
102
- defer,
103
- skip
104
- };
105
- }
@@ -1,322 +0,0 @@
1
- function _ts_decorate(decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- }
7
- import { readFileSync } from "node:fs";
8
- import { registerHooks } from "node:module";
9
- import path from "node:path";
10
- import { fileURLToPath, pathToFileURL } from "node:url";
11
- import { injectable } from "@decohere/crucible/decorators";
12
- import { BuildTargets, createWorkspace, SwcTranspiler } from "@decohere/forge";
13
- import { packageAliases, sourceExtensions } from "@decohere/workspace";
14
- import { FileEx, io } from "@decohere/core/io";
15
- import { scribe } from "@decohere/scribe";
16
- import { rewriteServeSpecifier } from "./rewrite-serve-specifier.js";
17
- import { staticImports } from "./static-imports.js";
18
- import { TranspileDiskCache } from "./transpile-disk-cache.js";
19
- export class SourceModuleLoader {
20
- logger = scribe.logger(SourceModuleLoader.name);
21
- transpiler = new SwcTranspiler();
22
- disposed = false;
23
- hooksRegistered = false;
24
- workspace;
25
- buildConfig = "debug";
26
- fileInfoMap = new Map();
27
- transpileCache = new Map();
28
- /** Bumped when a file's cached code is dropped, so an up-front compile already running can't put it back. */ transpileVersions = new Map();
29
- /** The source files each entry's walk reached, so a reboot only walks out from what changed. */ reachedFrom = new Map();
30
- /** Compiled code on disk across runs (#514); null when caching is off. */ diskCache = null;
31
- /** Up-front compiles in flight, shared by entries booting at the same time. */ compilingAhead = new Map();
32
- /** SWC options per project, built on first use and kept until the next `configure()`. */ swcConfigs = new Map();
33
- commonJsConfigs = new Map();
34
- aliasPaths = {};
35
- aliasBaseUrl = path.resolve(".");
36
- versionFileUrl = (fileUrl)=>fileUrl;
37
- onResolved;
38
- getWorkspace() {
39
- return this.workspace;
40
- }
41
- hasSourceFile(absolutePath) {
42
- return this.fileInfoMap.has(absolutePath);
43
- }
44
- sourceFilePaths() {
45
- return this.fileInfoMap.keys();
46
- }
47
- setSourceFile(absolutePath, project) {
48
- this.fileInfoMap.set(absolutePath, {
49
- project
50
- });
51
- }
52
- async configure(options) {
53
- this.disposed = false;
54
- // A new Map, not clear(): an up-front compile from before can't write into this one.
55
- this.transpileCache = new Map();
56
- this.reachedFrom = new Map();
57
- this.swcConfigs.clear();
58
- this.commonJsConfigs.clear();
59
- this.buildConfig = options.buildConfig;
60
- this.workspace = options.workspace ?? await createWorkspace();
61
- this.versionFileUrl = options.versionFileUrl ?? ((fileUrl)=>fileUrl);
62
- this.onResolved = options.onResolved;
63
- this.fileInfoMap = await this.buildFileInfoMap(this.buildConfig);
64
- this.diskCache = TranspileDiskCache.forWorkspace(this.workspace.rootDir);
65
- // In the background; it turns the cache off, with a warning, rather than throw.
66
- void this.diskCache?.pruneIfDue();
67
- const aliases = this.workspace.getPackageAliases(this.buildConfig);
68
- this.aliasBaseUrl = aliases.baseUrl;
69
- this.aliasPaths = aliases.paths;
70
- }
71
- registerHooks() {
72
- if (this.hooksRegistered) return;
73
- this.hooksRegistered = true;
74
- // Compiled code carries inline source maps; Node only applies them to stack traces when
75
- // this is on. Without it, errors point at compiled line numbers (#506).
76
- process.setSourceMapsEnabled(true);
77
- registerHooks({
78
- resolve: (specifier, context, next)=>{
79
- if (this.disposed) return next(specifier, context);
80
- const parentUrl = context.parentURL ?? "";
81
- const rewritten = rewriteServeSpecifier({
82
- specifier,
83
- parentUrl,
84
- isWatchedTs: (filePath)=>this.fileInfoMap.has(filePath),
85
- resolveBare: (bare)=>packageAliases.resolvePath(bare, this.aliasPaths, this.aliasBaseUrl),
86
- versionFileUrl: this.versionFileUrl
87
- }) ?? specifier;
88
- const result = next(rewritten, context);
89
- this.onResolved?.(result, parentUrl);
90
- return result;
91
- },
92
- load: (url, context, next)=>{
93
- if (this.disposed) return next(url, context);
94
- const result = this.loadTs(url);
95
- if (result != null) {
96
- return {
97
- format: result.format,
98
- source: result.source,
99
- shortCircuit: true
100
- };
101
- }
102
- return next(url, context);
103
- }
104
- });
105
- }
106
- async importEntry(absolutePath) {
107
- await this.compileAhead(path.resolve(absolutePath));
108
- const url = this.versionFileUrl(io.pathToFileUrl(path.resolve(absolutePath)));
109
- return import(url);
110
- }
111
- invalidateTranspile(...absolutePaths) {
112
- for (const fullPath of absolutePaths){
113
- this.transpileCache.delete(fullPath);
114
- this.transpileVersions.set(fullPath, (this.transpileVersions.get(fullPath) ?? 0) + 1);
115
- }
116
- }
117
- dispose() {
118
- this.disposed = true;
119
- this.transpileCache = new Map();
120
- this.reachedFrom = new Map();
121
- this.swcConfigs.clear();
122
- this.commonJsConfigs.clear();
123
- this.fileInfoMap.clear();
124
- this.workspace?.[Symbol.dispose]();
125
- this.workspace = undefined;
126
- }
127
- async buildFileInfoMap(buildConfig) {
128
- const projects = this.workspace?.getProjects(buildConfig) ?? [];
129
- const entries = await Promise.all(projects.map(async (project)=>{
130
- const files = await project.getAllFiles();
131
- return files.map((file)=>[
132
- file.fullPath,
133
- {
134
- project
135
- }
136
- ]);
137
- }));
138
- return new Map(entries.flat());
139
- }
140
- /**
141
- * Compiles the entry and the source files it imports, before Node imports it (#661). The load
142
- * hook is synchronous, so it would compile one file at a time on the main thread; async SWC
143
- * runs on the thread pool. Walks out from the entry, compiling each file's imports in parallel,
144
- * so files the app never imports (tests, other apps) are not compiled. A reboot walks out only
145
- * from the files it reached before that are no longer cached (the saved ones), so their new
146
- * imports are found without visiting the rest of the graph again.
147
- *
148
- * A file that fails here is left out of the cache instead of failing the boot: the load hook
149
- * then compiles it again and throws as before, naming the file. The walk is a hint; whatever
150
- * it misses is compiled by the load hook when Node gets to it.
151
- */ async compileAhead(entryPath) {
152
- const reached = this.reachedFrom.get(entryPath);
153
- const starts = reached == null ? [
154
- entryPath
155
- ] : [
156
- ...reached
157
- ].filter((fullPath)=>!this.transpileCache.has(fullPath));
158
- const seen = new Set(reached);
159
- for (const start of starts)seen.delete(start);
160
- const visit = async (fullPath)=>{
161
- if (seen.has(fullPath)) return;
162
- seen.add(fullPath);
163
- const code = await this.compiledAhead(fullPath);
164
- if (code == null) return;
165
- const parentUrl = pathToFileURL(fullPath).toString();
166
- await Promise.all(staticImports.scan(code).map((specifier)=>{
167
- const target = this.sourcePathOf(specifier, parentUrl);
168
- return target == null ? undefined : visit(target);
169
- }));
170
- };
171
- await Promise.all(starts.map(visit));
172
- this.reachedFrom.set(entryPath, seen);
173
- }
174
- /** The file's compiled code, from the cache or compiled now; undefined if it isn't a source. */ compiledAhead(fullPath) {
175
- const fileInfo = this.fileInfoMap.get(fullPath);
176
- if (fileInfo == null) return Promise.resolve(undefined);
177
- const cached = this.transpileCache.get(fullPath);
178
- if (cached != null) return Promise.resolve(cached);
179
- const inFlight = this.compilingAhead.get(fullPath);
180
- if (inFlight != null) return inFlight;
181
- const version = this.transpileVersions.get(fullPath);
182
- const cache = this.transpileCache;
183
- const compiling = this.compileAsync(fullPath, fileInfo.project).then((code)=>{
184
- if (code == null) return undefined;
185
- // Saved, reconfigured or disposed while compiling: don't cache stale code.
186
- const current = !this.disposed && cache === this.transpileCache && version === this.transpileVersions.get(fullPath);
187
- if (current) cache.set(fullPath, code);
188
- return code;
189
- }).finally(()=>this.compilingAhead.delete(fullPath));
190
- this.compilingAhead.set(fullPath, compiling);
191
- return compiling;
192
- }
193
- /** The source file a specifier names, mapped the way the resolve hook maps it. */ sourcePathOf(specifier, parentUrl) {
194
- const url = rewriteServeSpecifier({
195
- specifier,
196
- parentUrl,
197
- isWatchedTs: (filePath)=>this.fileInfoMap.has(filePath),
198
- resolveBare: (bare)=>packageAliases.resolvePath(bare, this.aliasPaths, this.aliasBaseUrl),
199
- versionFileUrl: (fileUrl)=>fileUrl
200
- });
201
- return url == null ? null : fileURLToPath(url);
202
- }
203
- loadTs(url) {
204
- if (!url.startsWith("file:")) return null;
205
- // fileURLToPath, not URL.pathname: decodes %20 and gives Windows paths, so files
206
- // whose path has a space still match the workspace's package list (#511).
207
- const cleanPath = fileURLToPath(url);
208
- const fileInfo = this.fileInfoMap.get(cleanPath);
209
- if (fileInfo == null) return null;
210
- const format = sourceExtensions.isCommonJs(cleanPath) ? "commonjs" : "module";
211
- const cached = this.transpileCache.get(cleanPath);
212
- if (cached != null) return {
213
- source: cached,
214
- format
215
- };
216
- const code = this.compileSync(cleanPath, fileInfo.project);
217
- this.transpileCache.set(cleanPath, code);
218
- return {
219
- source: code,
220
- format
221
- };
222
- }
223
- /**
224
- * Compiles a file for the load hook, through the disk cache (#514). Throws, naming the file,
225
- * when it can't be read or doesn't compile.
226
- */ compileSync(fullPath, project) {
227
- const file = new FileEx({
228
- fullPath
229
- });
230
- const baseConfig = this.swcConfigFor(project, fullPath);
231
- let source;
232
- try {
233
- source = readFileSync(fullPath, "utf8");
234
- } catch (error) {
235
- this.logger.error(`Failed to transpile ${fullPath}`, error);
236
- throw new Error(`Failed to transpile ${fullPath}`, {
237
- cause: error
238
- });
239
- }
240
- const key = this.diskCache?.keyFor(fullPath, source, baseConfig);
241
- const hit = key == null ? undefined : this.diskCache?.readSync(key);
242
- if (hit != null) return hit;
243
- const response = this.transpiler.transformCodeSync(source, file, BuildTargets.es6, baseConfig);
244
- if (response.outcome !== "success") {
245
- this.logger.error(`Failed to transpile ${fullPath}`, response);
246
- throw new Error(`Failed to transpile ${fullPath}`);
247
- }
248
- if (key != null) this.diskCache?.writeSync(key, response.code);
249
- return response.code;
250
- }
251
- /**
252
- * Compiles a file ahead, through the disk cache; undefined when it doesn't compile, which the
253
- * load hook then reports (see {@link compileAhead}). The source and the cache entry are read
254
- * synchronously: for small files that is far cheaper than a trip through libuv's thread pool,
255
- * which async SWC needs for the compiles that miss (#514).
256
- */ async compileAsync(fullPath, project) {
257
- const file = new FileEx({
258
- fullPath
259
- });
260
- const baseConfig = this.swcConfigFor(project, fullPath);
261
- let source;
262
- try {
263
- source = readFileSync(fullPath, "utf8");
264
- } catch {
265
- // Left to the load hook, which reads it again and fails naming it, as for a file
266
- // that doesn't compile.
267
- return undefined;
268
- }
269
- const key = this.diskCache?.keyFor(fullPath, source, baseConfig);
270
- const hit = key == null ? undefined : this.diskCache?.readSync(key);
271
- if (hit != null) return hit;
272
- const response = await this.transpiler.transformCode(source, file, BuildTargets.es6, baseConfig);
273
- if (response.outcome !== "success") return undefined;
274
- if (key != null) await this.diskCache?.write(key, response.code);
275
- return response.code;
276
- }
277
- /**
278
- * The project's SWC options, built once: rebuilding them for every file was about a quarter
279
- * of load time (#513). Import specifiers are left as written, without `jsc.paths` or
280
- * `resolveFully`: the resolve hook maps them to source files (aliases, `.js` to `.ts`,
281
- * directory indexes), so SWC does not resolve them a second time against the disk.
282
- */ swcConfigFor(project, fullPath) {
283
- const config = this.esModuleConfigFor(project);
284
- if (!sourceExtensions.isCommonJs(fullPath)) return config;
285
- // `.cts` is CommonJS: Node loads it with `require` semantics, so SWC emits CommonJS (#518).
286
- // Kept per project, like the ES module options, so the disk cache hashes them once.
287
- const cached = this.commonJsConfigs.get(project);
288
- if (cached != null) return cached;
289
- const commonJs = {
290
- ...config,
291
- module: {
292
- type: "commonjs"
293
- }
294
- };
295
- this.commonJsConfigs.set(project, commonJs);
296
- return commonJs;
297
- }
298
- esModuleConfigFor(project) {
299
- const cached = this.swcConfigs.get(project);
300
- if (cached != null) return cached;
301
- const base = this.transpiler.createBaseConfig(BuildTargets.es6, {
302
- tsConfig: project.getTsConfigSync(),
303
- userConfig: project.getSwcConfigSync(),
304
- aliasPaths: false,
305
- workspace: this.workspace
306
- });
307
- // `module` is the ES module config (`type: "es6"` above); the spread loses the narrowing.
308
- const module = {
309
- ...base.module,
310
- resolveFully: false
311
- };
312
- const config = {
313
- ...base,
314
- module
315
- };
316
- this.swcConfigs.set(project, config);
317
- return config;
318
- }
319
- }
320
- SourceModuleLoader = _ts_decorate([
321
- injectable()
322
- ], SourceModuleLoader);