@decohere/forge 0.0.1-dev.112 → 0.0.1-dev.113
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 +2 -0
- package/commonjs/src/actions/index.js +1 -0
- package/commonjs/src/actions/paths.action.js +109 -0
- package/commonjs/src/commands/index.js +1 -0
- package/commonjs/src/commands/paths.command.js +24 -0
- package/commonjs/src/extensions/project.extensions.js +1 -1
- package/commonjs/src/factories/swc-config.factory.js +10 -1
- package/commonjs/src/forge.module.js +2 -0
- package/commonjs/src/index.js +4 -0
- package/commonjs/src/packaging/release-exports.js +4 -2
- package/commonjs/src/services/file-writer.service.js +0 -7
- package/commonjs/src/services/incremental-watch.js +24 -0
- package/commonjs/src/services/output-cache.js +39 -0
- package/commonjs/src/services/swc-transpiler.js +40 -119
- package/commonjs/src/services/transpile-cache.js +44 -0
- package/commonjs/src/services/type-emitter.js +13 -6
- package/commonjs/src/types/transform-response.model.js +3 -16
- package/commonjs/src/vitest/forge-build.plugin.js +4 -10
- package/es6/src/actions/index.js +1 -0
- package/es6/src/actions/paths.action.js +86 -0
- package/es6/src/commands/index.js +1 -0
- package/es6/src/commands/paths.command.js +14 -0
- package/es6/src/extensions/project.extensions.js +1 -1
- package/es6/src/factories/swc-config.factory.js +10 -1
- package/es6/src/forge.module.js +2 -0
- package/es6/src/index.js +1 -0
- package/es6/src/packaging/release-exports.js +4 -2
- package/es6/src/services/file-writer.service.js +0 -7
- package/es6/src/services/incremental-watch.js +18 -0
- package/es6/src/services/output-cache.js +29 -0
- package/es6/src/services/swc-transpiler.js +40 -119
- package/es6/src/services/transpile-cache.js +29 -0
- package/es6/src/services/type-emitter.js +13 -6
- package/es6/src/types/transform-response.model.js +0 -5
- package/es6/src/vitest/forge-build.plugin.js +5 -11
- package/package.json +5 -5
- package/types/src/actions/index.d.ts +1 -0
- package/types/src/actions/paths.action.d.ts +28 -0
- package/types/src/commands/index.d.ts +1 -0
- package/types/src/commands/paths.command.d.ts +6 -0
- package/types/src/factories/swc-config.factory.d.ts +1 -1
- package/types/src/forge.module.d.ts +2 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/packaging/release-exports.d.ts +2 -2
- package/types/src/services/file-writer.service.d.ts +0 -1
- package/types/src/services/incremental-watch.d.ts +13 -0
- package/types/src/services/output-cache.d.ts +10 -0
- package/types/src/services/swc-transpiler.d.ts +12 -24
- package/types/src/services/transpile-cache.d.ts +17 -0
- package/types/src/services/type-emitter.d.ts +6 -7
- package/types/src/types/transform-response.model.d.ts +0 -5
- package/types/src/types/transformer.type.d.ts +0 -9
- package/commonjs/src/mixins/caches-outputs.mixin.js +0 -42
- package/commonjs/src/mixins/incremental-transform.mixin.js +0 -34
- package/es6/src/mixins/caches-outputs.mixin.js +0 -32
- package/es6/src/mixins/incremental-transform.mixin.js +0 -24
- package/types/src/mixins/caches-outputs.mixin.d.ts +0 -7
- 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
|
|
31
|
-
const
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
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
|
},
|
package/es6/src/actions/index.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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);
|
package/es6/src/forge.module.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
170
|
-
const
|
|
171
|
-
const
|
|
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
|
-
|
|
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
|
|
183
|
-
return await Promise.all(files.map((file)=>{
|
|
184
|
-
const
|
|
185
|
-
|
|
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.
|
|
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.
|
|
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
|
+
};
|