@decohere/forge 0.0.1-dev.112 → 0.0.1-dev.114

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 (58) hide show
  1. package/README.md +2 -0
  2. package/commonjs/src/actions/index.js +1 -0
  3. package/commonjs/src/actions/paths.action.js +109 -0
  4. package/commonjs/src/commands/index.js +1 -0
  5. package/commonjs/src/commands/paths.command.js +24 -0
  6. package/commonjs/src/extensions/project.extensions.js +1 -1
  7. package/commonjs/src/factories/swc-config.factory.js +10 -1
  8. package/commonjs/src/forge.module.js +2 -0
  9. package/commonjs/src/index.js +4 -0
  10. package/commonjs/src/packaging/release-exports.js +4 -2
  11. package/commonjs/src/services/file-writer.service.js +0 -7
  12. package/commonjs/src/services/incremental-watch.js +24 -0
  13. package/commonjs/src/services/output-cache.js +39 -0
  14. package/commonjs/src/services/swc-transpiler.js +40 -119
  15. package/commonjs/src/services/transpile-cache.js +44 -0
  16. package/commonjs/src/services/type-emitter.js +13 -6
  17. package/commonjs/src/types/transform-response.model.js +3 -16
  18. package/commonjs/src/vitest/forge-build.plugin.js +4 -10
  19. package/es6/src/actions/index.js +1 -0
  20. package/es6/src/actions/paths.action.js +86 -0
  21. package/es6/src/commands/index.js +1 -0
  22. package/es6/src/commands/paths.command.js +14 -0
  23. package/es6/src/extensions/project.extensions.js +1 -1
  24. package/es6/src/factories/swc-config.factory.js +10 -1
  25. package/es6/src/forge.module.js +2 -0
  26. package/es6/src/index.js +1 -0
  27. package/es6/src/packaging/release-exports.js +4 -2
  28. package/es6/src/services/file-writer.service.js +0 -7
  29. package/es6/src/services/incremental-watch.js +18 -0
  30. package/es6/src/services/output-cache.js +29 -0
  31. package/es6/src/services/swc-transpiler.js +40 -119
  32. package/es6/src/services/transpile-cache.js +29 -0
  33. package/es6/src/services/type-emitter.js +13 -6
  34. package/es6/src/types/transform-response.model.js +0 -5
  35. package/es6/src/vitest/forge-build.plugin.js +5 -11
  36. package/package.json +5 -5
  37. package/types/src/actions/index.d.ts +1 -0
  38. package/types/src/actions/paths.action.d.ts +28 -0
  39. package/types/src/commands/index.d.ts +1 -0
  40. package/types/src/commands/paths.command.d.ts +6 -0
  41. package/types/src/factories/swc-config.factory.d.ts +1 -1
  42. package/types/src/forge.module.d.ts +2 -0
  43. package/types/src/index.d.ts +1 -0
  44. package/types/src/packaging/release-exports.d.ts +2 -2
  45. package/types/src/services/file-writer.service.d.ts +0 -1
  46. package/types/src/services/incremental-watch.d.ts +13 -0
  47. package/types/src/services/output-cache.d.ts +10 -0
  48. package/types/src/services/swc-transpiler.d.ts +12 -24
  49. package/types/src/services/transpile-cache.d.ts +17 -0
  50. package/types/src/services/type-emitter.d.ts +6 -7
  51. package/types/src/types/transform-response.model.d.ts +0 -5
  52. package/types/src/types/transformer.type.d.ts +0 -9
  53. package/commonjs/src/mixins/caches-outputs.mixin.js +0 -42
  54. package/commonjs/src/mixins/incremental-transform.mixin.js +0 -34
  55. package/es6/src/mixins/caches-outputs.mixin.js +0 -32
  56. package/es6/src/mixins/incremental-transform.mixin.js +0 -24
  57. package/types/src/mixins/caches-outputs.mixin.d.ts +0 -7
  58. package/types/src/mixins/incremental-transform.mixin.d.ts +0 -13
@@ -24,11 +24,10 @@ _export(exports, {
24
24
  });
25
25
  const _caching = require("@decohere/nexus/caching");
26
26
  const _diagnostics = require("@decohere/nexus/diagnostics");
27
- const _extensibility = require("@decohere/nexus/extensibility");
28
27
  const _io = require("@decohere/nexus/io");
29
28
  require("../extensions/project.extensions.js");
30
- const _cachesoutputsmixin = require("../mixins/caches-outputs.mixin.js");
31
- const _incrementaltransformmixin = require("../mixins/incremental-transform.mixin.js");
29
+ const _incrementalwatch = require("./incremental-watch.js");
30
+ const _outputcache = require("./output-cache.js");
32
31
  const _tsprogramhost = require("./ts-program-host.js");
33
32
  const _typeemissionprogram = require("./type-emission-program.js");
34
33
  const _buildtargettype = require("../types/build-target.type.js");
@@ -55,12 +54,20 @@ const REQUIRED_TYPE_EMITTER_OPTIONS = {
55
54
  const REQUIRED_TYPE_CHECK_OPTIONS = {
56
55
  noEmit: true
57
56
  };
58
- class TypeEmitter extends (0, _extensibility.mixwith)(_cachesoutputsmixin.CachesOutputs, _incrementaltransformmixin.IncrementalTransform) {
57
+ class TypeEmitter {
59
58
  buildTargets = [
60
59
  _buildtargettype.BuildTargets.types
61
60
  ];
62
61
  /** Prepared programs from init(), consumed once by the next transformProject() call. */ #programs;
63
62
  #host = new _tsprogramhost.TsProgramHost();
63
+ #outputs = new _outputcache.OutputCache();
64
+ #incremental = new _incrementalwatch.IncrementalWatch();
65
+ get incrementalTransform$() {
66
+ return this.#incremental.results$;
67
+ }
68
+ watch(workspace) {
69
+ this.#incremental.watch(workspace, (evt)=>this.onIncrementalFileEvent(evt));
70
+ }
64
71
  /**
65
72
  * Phase 1: fetch files, create TypeScript programs sequentially.
66
73
  * Called before transformProject() so that the heavy createProgram work
@@ -79,7 +86,7 @@ class TypeEmitter extends (0, _extensibility.mixwith)(_cachesoutputsmixin.Caches
79
86
  return [];
80
87
  }
81
88
  const responses = (await Promise.all(programs.map((program)=>program.emit(filter, onOutput)))).flat();
82
- this.cacheOutputs(responses);
89
+ this.#outputs.record(responses);
83
90
  return responses;
84
91
  }
85
92
  /**
@@ -133,7 +140,7 @@ class TypeEmitter extends (0, _extensibility.mixwith)(_cachesoutputsmixin.Caches
133
140
  async onIncrementalFileEvent(evt) {
134
141
  const { file, project } = evt;
135
142
  if (evt.eventName === _io.io.WatchEvents.unlink) {
136
- this.deleteCachedOutputs(file.fullPath);
143
+ this.#outputs.delete(file.fullPath);
137
144
  return [];
138
145
  }
139
146
  if (!project.isSourceFile(file)) {
@@ -2,25 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- function _export(target, all) {
6
- for(var name in all)Object.defineProperty(target, name, {
7
- enumerable: true,
8
- get: Object.getOwnPropertyDescriptor(all, name).get
9
- });
10
- }
11
- _export(exports, {
12
- get TransformOutcomes () {
13
- return TransformOutcomes;
14
- },
15
- get TransformResponse () {
5
+ Object.defineProperty(exports, "TransformResponse", {
6
+ enumerable: true,
7
+ get: function() {
16
8
  return TransformResponse;
17
9
  }
18
10
  });
19
- const TransformOutcomes = {
20
- success: "success",
21
- failure: "failure",
22
- skipped: "skipped"
23
- };
24
11
  class TransformResponse {
25
12
  static Success(data) {
26
13
  return new TransformResponse({
@@ -24,11 +24,6 @@ function _interop_require_default(obj) {
24
24
  // Above fatal (60) so all standard levels are nooped when logLevel is "silent"
25
25
  const SILENT_LEVEL = 999;
26
26
  const PLUGIN_NAME = "forge-vitest-source";
27
- const TS_EXTENSIONS = [
28
- ".ts",
29
- ".tsx",
30
- ".mts"
31
- ];
32
27
  function forgeBuild(options) {
33
28
  const buildConfig = options?.buildConfig ?? "debug";
34
29
  const workspaceRoot = options?.workspaceRoot ?? process.cwd();
@@ -77,11 +72,10 @@ function forgeBuild(options) {
77
72
  logger.debug("Plugin loaded");
78
73
  },
79
74
  resolveId (source, importer) {
80
- if (importer != null && source.endsWith(".js") && source.startsWith(".")) {
81
- const importerDirectory = _nodepath.default.dirname(importer);
82
- const withoutExtension = source.slice(0, -3);
83
- const candidates = TS_EXTENSIONS.map((ext)=>_nodepath.default.resolve(importerDirectory, `${withoutExtension}${ext}`));
84
- return candidates.find((c)=>(0, _nodefs.existsSync)(c)) ?? null;
75
+ // `./a.js` is `a.ts` or `a.tsx`, `./a.mjs` is `a.mts`, `./a.cjs` is `a.cts` (#518).
76
+ if (importer != null && /\.[mc]?jsx?$/.test(source) && source.startsWith(".")) {
77
+ const written = _nodepath.default.resolve(_nodepath.default.dirname(importer), source);
78
+ return _mesh.sourceExtensions.candidates(written).find((c)=>(0, _nodefs.existsSync)(c)) ?? null;
85
79
  }
86
80
  return null;
87
81
  },
@@ -7,6 +7,7 @@ export * from "./dependency-sync.action.js";
7
7
  export * from "./docker.action.js";
8
8
  export * from "./link.action.js";
9
9
  export * from "./package.action.js";
10
+ export * from "./paths.action.js";
10
11
  export * from "./publish.action.js";
11
12
  export * from "./sync-lock-file.action.js";
12
13
  export * from "./tag.action.js";
@@ -0,0 +1,86 @@
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 { existsSync } from "node:fs";
11
+ import { readFile, writeFile } from "node:fs/promises";
12
+ import path from "node:path";
13
+ import { injectable } from "@decohere/crucible/decorators";
14
+ import { packageAliases, Workspace } from "@decohere/mesh";
15
+ import { ActionResponse } from "@decohere/nexus/cli";
16
+ import { scribe } from "@decohere/scribe";
17
+ export const PATHS_FILE = "tsconfig.paths.json";
18
+ export class Paths {
19
+ workspace;
20
+ #logger = scribe.logger(Paths.name);
21
+ constructor(workspace){
22
+ this.workspace = workspace;
23
+ }
24
+ async run(options) {
25
+ const file = path.join(this.workspace.rootDir, PATHS_FILE);
26
+ const content = Paths.render(this.workspace);
27
+ const current = existsSync(file) ? await readFile(file, "utf8") : null;
28
+ const changed = current !== content;
29
+ if (options.check) {
30
+ if (!changed) {
31
+ return ActionResponse.Success(Paths.name, [
32
+ {
33
+ file,
34
+ changed,
35
+ succeeded: true
36
+ }
37
+ ]);
38
+ }
39
+ const error = `${PATHS_FILE} is out of date. Run \`forge paths\` to update it.`;
40
+ this.#logger.error(error);
41
+ return ActionResponse.Error(Paths.name, [
42
+ {
43
+ file,
44
+ changed,
45
+ succeeded: false,
46
+ error
47
+ }
48
+ ]);
49
+ }
50
+ if (changed) {
51
+ await writeFile(file, content);
52
+ this.#logger.info(`Wrote ${PATHS_FILE}`);
53
+ }
54
+ return ActionResponse.Success(Paths.name, [
55
+ {
56
+ file,
57
+ changed,
58
+ succeeded: true
59
+ }
60
+ ]);
61
+ }
62
+ /** The file's content: convention and exported subpaths only, never the tsconfig's own paths. */ static render(workspace) {
63
+ const rootDir = path.resolve(workspace.rootDir);
64
+ const { paths } = packageAliases.build(workspace.getProjects("debug"), {
65
+ baseUrl: rootDir
66
+ });
67
+ const sorted = Object.fromEntries(Object.keys(paths).sort().map((key)=>[
68
+ key,
69
+ (paths[key] ?? []).map((target)=>`./${target}`)
70
+ ]));
71
+ const json = {
72
+ $comment: "Generated by `forge paths` from the workspace packages. Do not edit.",
73
+ compilerOptions: {
74
+ paths: sorted
75
+ }
76
+ };
77
+ return `${JSON.stringify(json, null, 2)}\n`;
78
+ }
79
+ }
80
+ Paths = _ts_decorate([
81
+ injectable(),
82
+ _ts_metadata("design:type", Function),
83
+ _ts_metadata("design:paramtypes", [
84
+ typeof Workspace === "undefined" ? Object : Workspace
85
+ ])
86
+ ], Paths);
@@ -4,6 +4,7 @@ export * from "./docker.command.js";
4
4
  export * from "./forge.command.js";
5
5
  export * from "./link.command.js";
6
6
  export * from "./package.command.js";
7
+ export * from "./paths.command.js";
7
8
  export * from "./publish.command.js";
8
9
  export * from "./tag.command.js";
9
10
  export * from "./typecheck.command.js";
@@ -0,0 +1,14 @@
1
+ import { DecohereCommand } from "@decohere/nexus/cli";
2
+ import { Paths } from "../actions/paths.action.js";
3
+ export class PathsCommand extends DecohereCommand {
4
+ get actions() {
5
+ return [
6
+ Paths
7
+ ];
8
+ }
9
+ constructor(name){
10
+ super(name);
11
+ this.description("Write tsconfig.paths.json: tsconfig paths for the workspace packages and their exported subpaths");
12
+ this.option("--check", "Fail if tsconfig.paths.json is missing or out of date", false);
13
+ }
14
+ }
@@ -37,7 +37,7 @@ export class ProjectExtensions {
37
37
  }
38
38
  static swcConfigPaths(project) {
39
39
  return [
40
- path.join(project.projectDirectory, SWC_CONFIG),
40
+ path.join(project.projectRoot, SWC_CONFIG),
41
41
  path.join(project.rootDir, SWC_CONFIG)
42
42
  ];
43
43
  }
@@ -58,9 +58,18 @@ export class SwcConfigFactory {
58
58
  mergeArrays: false
59
59
  }, defaultOptions, options, userConfig ?? {});
60
60
  }
61
- /** Stamps a per-file filename onto a pre-built base config. */ createFileConfig(file, baseConfig) {
61
+ /** Stamps a per-file filename onto a pre-built base config; `.tsx` also turns on JSX (#518). */ createFileConfig(file, baseConfig) {
62
+ const tsx = file.fullPath.endsWith(".tsx");
63
+ const jsc = tsx && baseConfig.jsc?.parser?.syntax === "typescript" ? {
64
+ ...baseConfig.jsc,
65
+ parser: {
66
+ ...baseConfig.jsc.parser,
67
+ tsx: true
68
+ }
69
+ } : baseConfig.jsc;
62
70
  const config = {
63
71
  ...baseConfig,
72
+ jsc,
64
73
  filename: file.fullPath
65
74
  };
66
75
  this.validate(config);
@@ -25,6 +25,7 @@ export class ForgeModule extends ModuleManifest {
25
25
  reportDependencies: Actions.ReportDependencies,
26
26
  version: Actions.Version,
27
27
  tag: Actions.Tag,
28
+ paths: Actions.Paths,
28
29
  typecheck: Actions.TypeCheck,
29
30
  commitFiles: Actions.CommitFiles,
30
31
  link: Actions.Link,
@@ -38,6 +39,7 @@ export class ForgeModule extends ModuleManifest {
38
39
  package: Commands.PackageCommand,
39
40
  version: Commands.VersionCommand,
40
41
  tag: Commands.TagCommand,
42
+ paths: Commands.PathsCommand,
41
43
  typecheck: Commands.TypeCheckCommand,
42
44
  link: Commands.LinkCommand,
43
45
  unlink: Commands.UnlinkCommand
package/es6/src/index.js CHANGED
@@ -2,5 +2,6 @@ export { createWorkspace } from "./create-workspace.js";
2
2
  export { ForgeError } from "./errors.js";
3
3
  export { ForgeModule } from "./forge.module.js";
4
4
  export { SwcTranspiler } from "./services/swc-transpiler.js";
5
+ export { transpileCache } from "./services/transpile-cache.js";
5
6
  export { BuildTargets } from "./types/build-target.type.js";
6
7
  export { Debug as DebugConfiguration, DefaultConfigurations, Link as LinkConfiguration, Release as ReleaseConfiguration, WorkspaceDefaults } from "./workspace.config.js";
@@ -1,11 +1,13 @@
1
+ import { sourceExtensions } from "@decohere/mesh";
1
2
  const ES6_PREFIX = "./es6/";
2
3
  /** Condition names that can point at runnable ESM, most specific first. `types` never does. */ const RUNTIME_CONDITIONS = [
3
4
  "import",
4
5
  "node",
5
6
  "default"
6
7
  ];
7
- /** `src/main.ts` becomes `src/main.js`, with any leading `./` dropped. */ function toJs(filePath) {
8
- return filePath.replace(/^\.\//, "").replace(/\.ts$/, ".js").replace(/\.mts$/, ".mjs");
8
+ /** `src/main.ts` becomes `src/main.js` (`.mts` `.mjs`, `.cts` `.cjs`), with any leading `./` dropped. */ function toJs(filePath) {
9
+ const file = filePath.replace(/^\.\//, "");
10
+ return sourceExtensions.isSource(file) ? sourceExtensions.outputPath(file) : file;
9
11
  }
10
12
  /**
11
13
  * The `exports` a library needs once only its `es6/` output is shipped: the library's own
@@ -14,19 +14,12 @@ export class FileWriterService {
14
14
  #artifactByPath = new Map();
15
15
  #warmupSignature = "";
16
16
  #warmupPromise;
17
- #initialized = false;
18
17
  #pendingWrites = [];
19
18
  #activeWorkers = 0;
20
19
  #drainResolvers = [];
21
20
  #ensuredDirs = new Set();
22
21
  #ensureDirPromises = new Map();
23
22
  #workerError;
24
- async initialize() {
25
- if (this.#initialized) {
26
- return;
27
- }
28
- this.#initialized = true;
29
- }
30
23
  async warmup(projects) {
31
24
  const outputDirs = this.getOutputDirs(projects);
32
25
  const signature = outputDirs.join("|");
@@ -0,0 +1,18 @@
1
+ import { Subject } from "rxjs";
2
+ /**
3
+ * Runs a transformer's handler for every workspace file change and emits what it built. The
4
+ * handler invalidates the transformer's own caches, rebuilds, and returns the responses (empty
5
+ * for a deletion or when nothing was built).
6
+ */ export class IncrementalWatch {
7
+ #results = new Subject();
8
+ get results$() {
9
+ return this.#results.asObservable();
10
+ }
11
+ watch(workspace, handler) {
12
+ workspace.projectFileChanged$.subscribe((evt)=>{
13
+ void handler(evt).then((responses)=>{
14
+ this.#results.next(responses);
15
+ });
16
+ });
17
+ }
18
+ }
@@ -0,0 +1,29 @@
1
+ import { io } from "@decohere/nexus/io";
2
+ /** The outputs each source file last built to, so watch mode can delete them when it goes. */ export class OutputCache {
3
+ #bySource = new Map();
4
+ get(sourcePath) {
5
+ return this.#bySource.get(sourcePath);
6
+ }
7
+ /** Records the successful outputs, replacing what each of their source files had before. */ record(responses) {
8
+ const bySource = new Map();
9
+ for (const response of responses){
10
+ if (response.outcome !== "success") continue;
11
+ const group = bySource.get(response.file.fullPath) ?? [];
12
+ group.push(response);
13
+ bySource.set(response.file.fullPath, group);
14
+ }
15
+ for (const [sourcePath, group] of bySource){
16
+ this.#bySource.set(sourcePath, group);
17
+ }
18
+ }
19
+ /** Deletes the files a source built to and forgets them. */ delete(sourcePath) {
20
+ const outputs = this.#bySource.get(sourcePath);
21
+ if (outputs == null) return;
22
+ for (const response of outputs){
23
+ if (response.outputPath != null) {
24
+ void io.deleteFile(response.outputPath);
25
+ }
26
+ }
27
+ this.#bySource.delete(sourcePath);
28
+ }
29
+ }
@@ -7,29 +7,33 @@ function _ts_decorate(decorators, target, key, desc) {
7
7
  function _ts_metadata(k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  }
10
- import fs from "node:fs";
11
10
  import path from "node:path";
11
+ import { sourceExtensions } from "@decohere/mesh";
12
12
  import { cache, invalidateCache } from "@decohere/nexus/caching";
13
13
  import { Trace } from "@decohere/nexus/diagnostics";
14
- import { mixwith } from "@decohere/nexus/extensibility";
15
14
  import { io } from "@decohere/nexus/io";
16
15
  import { transform, transformSync } from "@swc/core";
17
16
  import { BuildTargets } from "../types/build-target.type.js";
18
17
  import "../extensions/project.extensions.js";
19
18
  import { SwcConfigFactory } from "../factories/swc-config.factory.js";
20
- import { CachesOutputs } from "../mixins/caches-outputs.mixin.js";
21
- import { IncrementalTransform } from "../mixins/incremental-transform.mixin.js";
22
19
  import { TransformResponse } from "../types/transform-response.model.js";
23
- export class SwcTranspiler extends mixwith(CachesOutputs, IncrementalTransform) {
20
+ import { IncrementalWatch } from "./incremental-watch.js";
21
+ import { OutputCache } from "./output-cache.js";
22
+ export class SwcTranspiler {
24
23
  buildTargets = [
25
24
  BuildTargets.es6,
26
25
  BuildTargets.commonjs
27
26
  ];
28
27
  #swcConfigFactory = new SwcConfigFactory();
29
28
  #workspace;
29
+ #outputs = new OutputCache();
30
+ #incremental = new IncrementalWatch();
31
+ get incrementalTransform$() {
32
+ return this.#incremental.results$;
33
+ }
30
34
  watch(workspace) {
31
35
  this.#workspace = workspace;
32
- super.watch(workspace);
36
+ this.#incremental.watch(workspace, (evt)=>this.onIncrementalFileEvent(evt));
33
37
  }
34
38
  withAliasContext(options = {}) {
35
39
  return {
@@ -63,42 +67,18 @@ export class SwcTranspiler extends mixwith(CachesOutputs, IncrementalTransform)
63
67
  return responses.flat();
64
68
  }));
65
69
  const flat = responses.flat();
66
- this.cacheOutputs(flat);
70
+ this.#outputs.record(flat);
67
71
  return flat;
68
72
  }
69
- async transformFile(fileArrayLike, buildTarget, options = {}) {
70
- const files = Array.isArray(fileArrayLike) ? fileArrayLike : [
71
- fileArrayLike
72
- ];
73
- const responses = this.transformTarget(files, buildTarget, options);
74
- return responses;
75
- }
76
73
  /**
77
74
  * The SWC options for one project and target, without a filename. Building them reads the
78
75
  * workspace's aliases and merges configs, so a caller that transpiles many files builds them
79
- * once and passes them to {@link transformFileSync}.
76
+ * once and passes them to {@link transformCode}.
80
77
  */ createBaseConfig(buildTarget, options = {}) {
81
78
  return this.#swcConfigFactory.createBaseConfig(buildTarget, this.withAliasContext(options));
82
79
  }
83
- /**
84
- * Synchronous, single-file variant of `transformFile`.
85
- * Reads the file with `fs.readFileSync` and transpiles via `swc.transformSync`.
86
- * Intended for callers that cannot await (e.g. Node.js `registerHooks` load hook).
87
- * Pass `baseConfig` from {@link createBaseConfig} to skip rebuilding it for every file.
88
- */ transformFileSync(file, buildTarget, options = {}, baseConfig = this.createBaseConfig(buildTarget, options)) {
80
+ /** Sync variant of {@link transformCode}, for the load hook, which cannot await (#514). */ transformCodeSync(content, file, buildTarget, baseConfig) {
89
81
  const swcOptions = this.#swcConfigFactory.createFileConfig(file, baseConfig);
90
- let content;
91
- try {
92
- content = fs.readFileSync(file.fullPath, "utf8");
93
- } catch (error) {
94
- return TransformResponse.Error({
95
- file,
96
- messages: [
97
- String(error)
98
- ],
99
- config: swcOptions
100
- });
101
- }
102
82
  if (content === "") return SwcTranspiler.skippedEmpty(file, swcOptions);
103
83
  try {
104
84
  const transformed = transformSync(content, swcOptions);
@@ -116,135 +96,76 @@ export class SwcTranspiler extends mixwith(CachesOutputs, IncrementalTransform)
116
96
  });
117
97
  }
118
98
  }
119
- /**
120
- * Async variant of {@link transformFileSync}, with the same result for the same file. SWC
121
- * runs it on libuv's thread pool, so many calls in flight compile in parallel (#661).
122
- */ async transformFileAsync(file, buildTarget, options = {}, baseConfig = this.createBaseConfig(buildTarget, options)) {
123
- let content;
124
- try {
125
- content = await fs.promises.readFile(file.fullPath, "utf8");
126
- } catch (error) {
127
- const swcOptions = this.#swcConfigFactory.createFileConfig(file, baseConfig);
128
- return TransformResponse.Error({
129
- file,
130
- messages: [
131
- String(error)
132
- ],
133
- config: swcOptions
134
- });
135
- }
136
- return this.transformCode(content, file, buildTarget, baseConfig);
137
- }
138
99
  /**
139
100
  * Compiles `content` as the source of `file`, without reading the file: for callers that
140
101
  * already hold the code, such as the Vitest plugin, which must compile what Vite passes it
141
- * rather than an earlier read of the disk (#517). Async SWC, on the thread pool.
142
- */ async transformCode(content, file, buildTarget, baseConfig) {
102
+ * rather than an earlier read of the disk (#517). Async SWC, on the thread pool, so many
103
+ * calls in flight compile in parallel (#661). With a `project`, the response carries where
104
+ * the build writes the output.
105
+ */ async transformCode(content, file, buildTarget, baseConfig, project) {
143
106
  const swcOptions = this.#swcConfigFactory.createFileConfig(file, baseConfig);
144
- if (content === "") return SwcTranspiler.skippedEmpty(file, swcOptions);
107
+ if (content === "") return SwcTranspiler.skippedEmpty(file, swcOptions, project);
145
108
  try {
146
109
  const transformed = await transform(content, swcOptions);
147
110
  if (transformed == null) {
148
111
  throw new Error("swc.transform returned no result");
149
112
  }
150
- return SwcTranspiler.succeeded(file, buildTarget, swcOptions, content, transformed.code);
113
+ return SwcTranspiler.succeeded(file, buildTarget, swcOptions, content, transformed.code, project);
151
114
  } catch (error) {
152
115
  return TransformResponse.Error({
153
116
  file,
154
117
  messages: [
155
118
  String(error)
156
119
  ],
157
- config: swcOptions
120
+ config: swcOptions,
121
+ project
158
122
  });
159
123
  }
160
124
  }
161
- static skippedEmpty(file, swcOptions) {
125
+ static skippedEmpty(file, swcOptions, project) {
162
126
  return TransformResponse.Skipped({
163
127
  file,
164
128
  messages: "File is empty",
165
- config: swcOptions
129
+ config: swcOptions,
130
+ project
166
131
  });
167
132
  }
168
- static succeeded(file, buildTarget, swcOptions, content, code) {
169
- const filePath = file.relativePath;
170
- const extName = path.extname(filePath);
171
- const fileName = filePath.replace(new RegExp(`${extName}$`), ".js");
133
+ static succeeded(file, buildTarget, swcOptions, content, code, project) {
134
+ const fileName = sourceExtensions.outputPath(file.relativePath);
135
+ const outBaseDir = project?.getOutDir(buildTarget);
136
+ const outputPath = outBaseDir ? path.join(outBaseDir, fileName) : undefined;
172
137
  return TransformResponse.Success({
173
138
  file,
174
139
  code,
175
140
  sourceContent: content,
176
141
  target: buildTarget,
177
142
  fileName,
178
- config: swcOptions
143
+ outputDir: outputPath ? path.dirname(outputPath) : undefined,
144
+ outputPath,
145
+ config: swcOptions,
146
+ project
179
147
  });
180
148
  }
181
149
  async transformTarget(files, buildTarget, options, project, onOutput) {
182
- const baseConfig = this.#swcConfigFactory.createBaseConfig(buildTarget, this.withAliasContext(options));
183
- return await Promise.all(files.map((file)=>{
184
- const swcOptions = this.#swcConfigFactory.createFileConfig(file, baseConfig);
185
- return this.runTransform(file, buildTarget, swcOptions, project, onOutput);
150
+ const baseConfig = this.createBaseConfig(buildTarget, options);
151
+ return await Promise.all(files.map(async (file)=>{
152
+ const content = await this.getFileContent(file);
153
+ const response = await this.transformCode(content, file, buildTarget, baseConfig, project);
154
+ onOutput?.(response);
155
+ return response;
186
156
  }));
187
157
  }
188
- async runTransform(file, target, swcOptions, project, onOutput) {
189
- const content = await this.getFileContent(file);
190
- if (content === "") {
191
- const skipped = TransformResponse.Skipped({
192
- file,
193
- messages: "File is empty",
194
- config: swcOptions,
195
- project
196
- });
197
- onOutput?.(skipped);
198
- return skipped;
199
- }
200
- try {
201
- const filePath = file.relativePath;
202
- const extName = path.extname(filePath);
203
- const fileName = filePath.replace(new RegExp(`${extName}$`), ".js");
204
- const outBaseDir = project?.getOutDir(target);
205
- const outputPath = outBaseDir ? path.join(outBaseDir, fileName) : undefined;
206
- const outputDir = outputPath ? path.dirname(outputPath) : undefined;
207
- const transformed = await transform(content, swcOptions);
208
- if (transformed == null) {
209
- throw new Error("swc.transform returned no result");
210
- }
211
- const success = TransformResponse.Success({
212
- file,
213
- code: transformed.code,
214
- sourceContent: content,
215
- target,
216
- fileName,
217
- outputDir,
218
- outputPath,
219
- config: swcOptions,
220
- project
221
- });
222
- onOutput?.(success);
223
- return success;
224
- } catch (error) {
225
- const failure = TransformResponse.Error({
226
- file,
227
- messages: [
228
- String(error)
229
- ],
230
- config: swcOptions,
231
- project
232
- });
233
- onOutput?.(failure);
234
- return failure;
235
- }
236
- }
237
158
  async onIncrementalFileEvent(evt) {
238
159
  const { file, project } = evt;
239
160
  if (evt.eventName === io.WatchEvents.unlink) {
240
- this.deleteCachedOutputs(file.fullPath);
161
+ this.#outputs.delete(file.fullPath);
241
162
  return [];
242
163
  }
243
164
  if (!project.isSourceFile(file)) {
244
165
  return [];
245
166
  }
246
167
  const freshContent = await file.getContent();
247
- const cached = this.outputCache.get(file.fullPath);
168
+ const cached = this.#outputs.get(file.fullPath);
248
169
  if (cached != null && cached[0]?.sourceContent === freshContent) {
249
170
  return [];
250
171
  }
@@ -0,0 +1,29 @@
1
+ import { existsSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { version as swcVersion } from "@swc/core";
5
+ /**
6
+ * Where compiled code is cached on disk (#514), or null when caching is off:
7
+ *
8
+ * - `DECOHERE_CACHE=0` turns it off.
9
+ * - `DECOHERE_CACHE_DIR` puts it in that directory.
10
+ * - Otherwise `<root>/node_modules/.cache/decohere`, where tools keep caches, or when the
11
+ * workspace has no `node_modules`, `$XDG_CACHE_HOME/decohere`, then the OS temp directory.
12
+ *
13
+ * Flux keeps its entries in the `flux` folder inside it; `forge clean` removes the folder.
14
+ */ function directory(rootDir, env = process.env) {
15
+ if (env.DECOHERE_CACHE === "0") return null;
16
+ if (env.DECOHERE_CACHE_DIR != null && env.DECOHERE_CACHE_DIR !== "") {
17
+ return path.resolve(env.DECOHERE_CACHE_DIR);
18
+ }
19
+ const nodeModules = path.join(rootDir, "node_modules");
20
+ if (existsSync(nodeModules)) return path.join(nodeModules, ".cache", "decohere");
21
+ if (env.XDG_CACHE_HOME != null && env.XDG_CACHE_HOME !== "") {
22
+ return path.join(env.XDG_CACHE_HOME, "decohere");
23
+ }
24
+ return path.join(os.tmpdir(), "decohere");
25
+ }
26
+ /** The SWC version compiling: part of every cache key, so upgrading SWC misses. */ export const transpileCache = {
27
+ directory,
28
+ swcVersion
29
+ };