@decohere/forge 0.0.1-dev.111 → 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 +42 -113
- 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 +25 -40
- 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 +42 -113
- 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 +26 -41
- 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 +14 -20
- 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/types/src/vitest/forge-build.plugin.d.ts +0 -2
- 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
|
@@ -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);
|
|
@@ -117,126 +97,75 @@ export class SwcTranspiler extends mixwith(CachesOutputs, IncrementalTransform)
|
|
|
117
97
|
}
|
|
118
98
|
}
|
|
119
99
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
|
|
100
|
+
* Compiles `content` as the source of `file`, without reading the file: for callers that
|
|
101
|
+
* already hold the code, such as the Vitest plugin, which must compile what Vite passes it
|
|
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) {
|
|
123
106
|
const swcOptions = this.#swcConfigFactory.createFileConfig(file, baseConfig);
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
content = await fs.promises.readFile(file.fullPath, "utf8");
|
|
127
|
-
} catch (error) {
|
|
128
|
-
return TransformResponse.Error({
|
|
129
|
-
file,
|
|
130
|
-
messages: [
|
|
131
|
-
String(error)
|
|
132
|
-
],
|
|
133
|
-
config: swcOptions
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
if (content === "") return SwcTranspiler.skippedEmpty(file, swcOptions);
|
|
107
|
+
if (content === "") return SwcTranspiler.skippedEmpty(file, swcOptions, project);
|
|
137
108
|
try {
|
|
138
109
|
const transformed = await transform(content, swcOptions);
|
|
139
110
|
if (transformed == null) {
|
|
140
111
|
throw new Error("swc.transform returned no result");
|
|
141
112
|
}
|
|
142
|
-
return SwcTranspiler.succeeded(file, buildTarget, swcOptions, content, transformed.code);
|
|
113
|
+
return SwcTranspiler.succeeded(file, buildTarget, swcOptions, content, transformed.code, project);
|
|
143
114
|
} catch (error) {
|
|
144
115
|
return TransformResponse.Error({
|
|
145
116
|
file,
|
|
146
117
|
messages: [
|
|
147
118
|
String(error)
|
|
148
119
|
],
|
|
149
|
-
config: swcOptions
|
|
120
|
+
config: swcOptions,
|
|
121
|
+
project
|
|
150
122
|
});
|
|
151
123
|
}
|
|
152
124
|
}
|
|
153
|
-
static skippedEmpty(file, swcOptions) {
|
|
125
|
+
static skippedEmpty(file, swcOptions, project) {
|
|
154
126
|
return TransformResponse.Skipped({
|
|
155
127
|
file,
|
|
156
128
|
messages: "File is empty",
|
|
157
|
-
config: swcOptions
|
|
129
|
+
config: swcOptions,
|
|
130
|
+
project
|
|
158
131
|
});
|
|
159
132
|
}
|
|
160
|
-
static succeeded(file, buildTarget, swcOptions, content, code) {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
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;
|
|
164
137
|
return TransformResponse.Success({
|
|
165
138
|
file,
|
|
166
139
|
code,
|
|
167
140
|
sourceContent: content,
|
|
168
141
|
target: buildTarget,
|
|
169
142
|
fileName,
|
|
170
|
-
|
|
143
|
+
outputDir: outputPath ? path.dirname(outputPath) : undefined,
|
|
144
|
+
outputPath,
|
|
145
|
+
config: swcOptions,
|
|
146
|
+
project
|
|
171
147
|
});
|
|
172
148
|
}
|
|
173
149
|
async transformTarget(files, buildTarget, options, project, onOutput) {
|
|
174
|
-
const baseConfig = this
|
|
175
|
-
return await Promise.all(files.map((file)=>{
|
|
176
|
-
const
|
|
177
|
-
|
|
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;
|
|
178
156
|
}));
|
|
179
157
|
}
|
|
180
|
-
async runTransform(file, target, swcOptions, project, onOutput) {
|
|
181
|
-
const content = await this.getFileContent(file);
|
|
182
|
-
if (content === "") {
|
|
183
|
-
const skipped = TransformResponse.Skipped({
|
|
184
|
-
file,
|
|
185
|
-
messages: "File is empty",
|
|
186
|
-
config: swcOptions,
|
|
187
|
-
project
|
|
188
|
-
});
|
|
189
|
-
onOutput?.(skipped);
|
|
190
|
-
return skipped;
|
|
191
|
-
}
|
|
192
|
-
try {
|
|
193
|
-
const filePath = file.relativePath;
|
|
194
|
-
const extName = path.extname(filePath);
|
|
195
|
-
const fileName = filePath.replace(new RegExp(`${extName}$`), ".js");
|
|
196
|
-
const outBaseDir = project?.getOutDir(target);
|
|
197
|
-
const outputPath = outBaseDir ? path.join(outBaseDir, fileName) : undefined;
|
|
198
|
-
const outputDir = outputPath ? path.dirname(outputPath) : undefined;
|
|
199
|
-
const transformed = await transform(content, swcOptions);
|
|
200
|
-
if (transformed == null) {
|
|
201
|
-
throw new Error("swc.transform returned no result");
|
|
202
|
-
}
|
|
203
|
-
const success = TransformResponse.Success({
|
|
204
|
-
file,
|
|
205
|
-
code: transformed.code,
|
|
206
|
-
sourceContent: content,
|
|
207
|
-
target,
|
|
208
|
-
fileName,
|
|
209
|
-
outputDir,
|
|
210
|
-
outputPath,
|
|
211
|
-
config: swcOptions,
|
|
212
|
-
project
|
|
213
|
-
});
|
|
214
|
-
onOutput?.(success);
|
|
215
|
-
return success;
|
|
216
|
-
} catch (error) {
|
|
217
|
-
const failure = TransformResponse.Error({
|
|
218
|
-
file,
|
|
219
|
-
messages: [
|
|
220
|
-
String(error)
|
|
221
|
-
],
|
|
222
|
-
config: swcOptions,
|
|
223
|
-
project
|
|
224
|
-
});
|
|
225
|
-
onOutput?.(failure);
|
|
226
|
-
return failure;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
158
|
async onIncrementalFileEvent(evt) {
|
|
230
159
|
const { file, project } = evt;
|
|
231
160
|
if (evt.eventName === io.WatchEvents.unlink) {
|
|
232
|
-
this.
|
|
161
|
+
this.#outputs.delete(file.fullPath);
|
|
233
162
|
return [];
|
|
234
163
|
}
|
|
235
164
|
if (!project.isSourceFile(file)) {
|
|
236
165
|
return [];
|
|
237
166
|
}
|
|
238
167
|
const freshContent = await file.getContent();
|
|
239
|
-
const cached = this.
|
|
168
|
+
const cached = this.#outputs.get(file.fullPath);
|
|
240
169
|
if (cached != null && cached[0]?.sourceContent === freshContent) {
|
|
241
170
|
return [];
|
|
242
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
|
+
};
|
|
@@ -9,11 +9,10 @@ function _ts_metadata(k, v) {
|
|
|
9
9
|
}
|
|
10
10
|
import { invalidateCache } from "@decohere/nexus/caching";
|
|
11
11
|
import { Trace } from "@decohere/nexus/diagnostics";
|
|
12
|
-
import { mixwith } from "@decohere/nexus/extensibility";
|
|
13
12
|
import { io } from "@decohere/nexus/io";
|
|
14
13
|
import "../extensions/project.extensions.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { IncrementalWatch } from "./incremental-watch.js";
|
|
15
|
+
import { OutputCache } from "./output-cache.js";
|
|
17
16
|
import { TsProgramHost } from "./ts-program-host.js";
|
|
18
17
|
import { TypeEmissionProgram } from "./type-emission-program.js";
|
|
19
18
|
import { BuildTargets } from "../types/build-target.type.js";
|
|
@@ -45,12 +44,20 @@ import { BuildTargets } from "../types/build-target.type.js";
|
|
|
45
44
|
/** Compiler options enforced for `forge typecheck` — never emit JS or declarations. */ export const REQUIRED_TYPE_CHECK_OPTIONS = {
|
|
46
45
|
noEmit: true
|
|
47
46
|
};
|
|
48
|
-
export class TypeEmitter
|
|
47
|
+
export class TypeEmitter {
|
|
49
48
|
buildTargets = [
|
|
50
49
|
BuildTargets.types
|
|
51
50
|
];
|
|
52
51
|
/** Prepared programs from init(), consumed once by the next transformProject() call. */ #programs;
|
|
53
52
|
#host = new TsProgramHost();
|
|
53
|
+
#outputs = new OutputCache();
|
|
54
|
+
#incremental = new IncrementalWatch();
|
|
55
|
+
get incrementalTransform$() {
|
|
56
|
+
return this.#incremental.results$;
|
|
57
|
+
}
|
|
58
|
+
watch(workspace) {
|
|
59
|
+
this.#incremental.watch(workspace, (evt)=>this.onIncrementalFileEvent(evt));
|
|
60
|
+
}
|
|
54
61
|
/**
|
|
55
62
|
* Phase 1: fetch files, create TypeScript programs sequentially.
|
|
56
63
|
* Called before transformProject() so that the heavy createProgram work
|
|
@@ -69,7 +76,7 @@ export class TypeEmitter extends mixwith(CachesOutputs, IncrementalTransform) {
|
|
|
69
76
|
return [];
|
|
70
77
|
}
|
|
71
78
|
const responses = (await Promise.all(programs.map((program)=>program.emit(filter, onOutput)))).flat();
|
|
72
|
-
this.
|
|
79
|
+
this.#outputs.record(responses);
|
|
73
80
|
return responses;
|
|
74
81
|
}
|
|
75
82
|
/**
|
|
@@ -123,7 +130,7 @@ export class TypeEmitter extends mixwith(CachesOutputs, IncrementalTransform) {
|
|
|
123
130
|
async onIncrementalFileEvent(evt) {
|
|
124
131
|
const { file, project } = evt;
|
|
125
132
|
if (evt.eventName === io.WatchEvents.unlink) {
|
|
126
|
-
this.
|
|
133
|
+
this.#outputs.delete(file.fullPath);
|
|
127
134
|
return [];
|
|
128
135
|
}
|
|
129
136
|
if (!project.isSourceFile(file)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { WorkspaceFactory } from "@decohere/mesh";
|
|
3
|
+
import { sourceExtensions, WorkspaceFactory } from "@decohere/mesh";
|
|
4
4
|
import { FileEx } from "@decohere/nexus/io";
|
|
5
5
|
import { ConsoleAdapter, scribe } from "@decohere/scribe";
|
|
6
6
|
import { SwcTranspiler } from "../services/swc-transpiler.js";
|
|
@@ -9,11 +9,6 @@ import { WorkspaceDefaults } from "../workspace.config.js";
|
|
|
9
9
|
// Above fatal (60) so all standard levels are nooped when logLevel is "silent"
|
|
10
10
|
const SILENT_LEVEL = 999;
|
|
11
11
|
const PLUGIN_NAME = "forge-vitest-source";
|
|
12
|
-
const TS_EXTENSIONS = [
|
|
13
|
-
".ts",
|
|
14
|
-
".tsx",
|
|
15
|
-
".mts"
|
|
16
|
-
];
|
|
17
12
|
export function forgeBuild(options) {
|
|
18
13
|
const buildConfig = options?.buildConfig ?? "debug";
|
|
19
14
|
const workspaceRoot = options?.workspaceRoot ?? process.cwd();
|
|
@@ -28,9 +23,11 @@ export function forgeBuild(options) {
|
|
|
28
23
|
}
|
|
29
24
|
});
|
|
30
25
|
const logger = loggerFactory(PLUGIN_NAME);
|
|
31
|
-
const skipBuild = options?.skipBuild ?? false;
|
|
32
26
|
const transpiler = new SwcTranspiler();
|
|
33
|
-
|
|
27
|
+
/**
|
|
28
|
+
* SWC options per project (tsconfig, `.swcrc`, path aliases), built once: rebuilding them for
|
|
29
|
+
* every file was most of the plugin's per-file cost, as it was in flux (#513, #517).
|
|
30
|
+
*/ const baseConfigs = new Map();
|
|
34
31
|
const transformCache = new Map();
|
|
35
32
|
let workspace = null;
|
|
36
33
|
const initWorkspace = async ()=>{
|
|
@@ -41,10 +38,17 @@ export function forgeBuild(options) {
|
|
|
41
38
|
rootDir: workspaceRoot
|
|
42
39
|
});
|
|
43
40
|
};
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
const baseConfigFor = (project)=>{
|
|
42
|
+
const cached = baseConfigs.get(project);
|
|
43
|
+
if (cached != null) return cached;
|
|
44
|
+
const config = transpiler.createBaseConfig(BuildTargets.es6, {
|
|
45
|
+
aliasPaths: true,
|
|
46
|
+
tsConfig: project?.getTsConfigSync(),
|
|
47
|
+
userConfig: project?.getSwcConfigSync(),
|
|
48
|
+
workspace: workspace ?? undefined
|
|
49
|
+
});
|
|
50
|
+
baseConfigs.set(project, config);
|
|
51
|
+
return config;
|
|
48
52
|
};
|
|
49
53
|
return {
|
|
50
54
|
name: PLUGIN_NAME,
|
|
@@ -53,51 +57,32 @@ export function forgeBuild(options) {
|
|
|
53
57
|
logger.debug("Plugin loaded");
|
|
54
58
|
},
|
|
55
59
|
resolveId (source, importer) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
return candidates.find((c)=>existsSync(c)) ?? null;
|
|
60
|
+
// `./a.js` is `a.ts` or `a.tsx`, `./a.mjs` is `a.mts`, `./a.cjs` is `a.cts` (#518).
|
|
61
|
+
if (importer != null && /\.[mc]?jsx?$/.test(source) && source.startsWith(".")) {
|
|
62
|
+
const written = path.resolve(path.dirname(importer), source);
|
|
63
|
+
return sourceExtensions.candidates(written).find((c)=>existsSync(c)) ?? null;
|
|
61
64
|
}
|
|
62
65
|
return null;
|
|
63
66
|
},
|
|
64
67
|
watchChange (id) {
|
|
65
|
-
const file = new FileEx({
|
|
66
|
-
fullPath: id
|
|
67
|
-
});
|
|
68
68
|
transformCache.delete(id);
|
|
69
|
-
transpiler.invalidateContentCache(file);
|
|
70
69
|
},
|
|
71
70
|
async transform (code, id) {
|
|
72
71
|
if (id.includes("node_modules") || !/[.](ts|tsx|mts|cts)$/.test(id)) {
|
|
73
72
|
return null;
|
|
74
73
|
}
|
|
75
|
-
await initWorkspace();
|
|
76
|
-
const project = await workspace?.findProject(id, buildConfig);
|
|
77
|
-
if (project != null && !skipBuild) {
|
|
78
|
-
preWarmProject(project);
|
|
79
|
-
}
|
|
80
74
|
const cached = transformCache.get(id);
|
|
81
75
|
if (cached !== undefined && cached.content === code) {
|
|
82
76
|
return cached.result;
|
|
83
77
|
}
|
|
84
|
-
|
|
78
|
+
await initWorkspace();
|
|
79
|
+
const project = await workspace?.findProject(id, buildConfig);
|
|
80
|
+
// The code Vite passes, not a read of the file: no content cache can serve an older
|
|
81
|
+
// version, and there is no pre-warm compiling files ahead from disk (#517).
|
|
82
|
+
const response = await transpiler.transformCode(code, new FileEx({
|
|
85
83
|
fullPath: id,
|
|
86
84
|
content: code
|
|
87
|
-
});
|
|
88
|
-
if (cached !== undefined) {
|
|
89
|
-
// Content changed for a previously-seen file — invalidate the transpiler's
|
|
90
|
-
// FILE_CONTENT cache so the fresh content from Vite is used.
|
|
91
|
-
// The VS Code Vitest extension disables Vite's chokidar watcher
|
|
92
|
-
// (server.watch: null) and drives re-runs via vitest.watcher.onFileChange(),
|
|
93
|
-
// bypassing watchChange entirely — so we cannot rely on watchChange alone.
|
|
94
|
-
transpiler.invalidateContentCache(file);
|
|
95
|
-
}
|
|
96
|
-
const [response] = await transpiler.transformFile(file, BuildTargets.es6, {
|
|
97
|
-
aliasPaths: true,
|
|
98
|
-
tsConfig: project ? await project.getTsConfig() : undefined,
|
|
99
|
-
workspace: workspace ?? undefined
|
|
100
|
-
});
|
|
85
|
+
}), BuildTargets.es6, baseConfigFor(project));
|
|
101
86
|
if (!response || response.outcome !== "success") return null;
|
|
102
87
|
const result = {
|
|
103
88
|
code: response.code,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decohere/forge",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.113",
|
|
4
4
|
"description": "TypeScript monorepo build system — zero config, fast compilation, streamlined developer workflow",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@decohere/crucible": "0.0.1-dev.
|
|
33
|
-
"@decohere/mesh": "0.0.1-dev.
|
|
34
|
-
"@decohere/nexus": "0.0.1-dev.
|
|
35
|
-
"@decohere/scribe": "0.0.1-dev.
|
|
32
|
+
"@decohere/crucible": "0.0.1-dev.113",
|
|
33
|
+
"@decohere/mesh": "0.0.1-dev.113",
|
|
34
|
+
"@decohere/nexus": "0.0.1-dev.113",
|
|
35
|
+
"@decohere/scribe": "0.0.1-dev.113",
|
|
36
36
|
"@swc/core": "^1.11.11",
|
|
37
37
|
"async": "^3.2.6",
|
|
38
38
|
"parse-imports": "^2.2.1",
|
|
@@ -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,28 @@
|
|
|
1
|
+
import { Workspace } from "@decohere/mesh";
|
|
2
|
+
import type { Action } from "@decohere/nexus/cli";
|
|
3
|
+
import { ActionResponse } from "@decohere/nexus/cli";
|
|
4
|
+
export declare const PATHS_FILE = "tsconfig.paths.json";
|
|
5
|
+
export type PathsOptions = {
|
|
6
|
+
/** Fail if the file is missing or out of date, instead of writing it. */
|
|
7
|
+
check?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type PathsResponse = {
|
|
10
|
+
file: string;
|
|
11
|
+
changed: boolean;
|
|
12
|
+
succeeded: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Writes `tsconfig.paths.json`: tsconfig `paths` for every workspace package and the subpaths it
|
|
17
|
+
* exports, from Mesh's aliases (#518). Without `node_modules` links, `tsc` finds workspace
|
|
18
|
+
* packages only through `paths`; generating them keeps them in step with what Flux, the Vitest
|
|
19
|
+
* plugin and builds resolve. The root tsconfig extends the file.
|
|
20
|
+
*/
|
|
21
|
+
export declare class Paths implements Action<PathsOptions> {
|
|
22
|
+
#private;
|
|
23
|
+
private readonly workspace;
|
|
24
|
+
constructor(workspace: Workspace);
|
|
25
|
+
run(options: PathsOptions): Promise<ActionResponse<PathsResponse>>;
|
|
26
|
+
/** The file's content: convention and exported subpaths only, never the tsconfig's own paths. */
|
|
27
|
+
static render(workspace: Workspace): string;
|
|
28
|
+
}
|
|
@@ -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";
|
|
@@ -8,7 +8,7 @@ export declare class SwcConfigFactory {
|
|
|
8
8
|
* Call once per (project, target) and then use createFileConfig() per file.
|
|
9
9
|
*/
|
|
10
10
|
createBaseConfig(buildTarget: TranspileTarget, configOptions: SwcConfigOptions): Options;
|
|
11
|
-
/** Stamps a per-file filename onto a pre-built base config. */
|
|
11
|
+
/** Stamps a per-file filename onto a pre-built base config; `.tsx` also turns on JSX (#518). */
|
|
12
12
|
createFileConfig(file: FileEx, baseConfig: Options): Options;
|
|
13
13
|
private static nonemptyPaths;
|
|
14
14
|
/**
|
|
@@ -19,6 +19,7 @@ export declare class ForgeModule extends ModuleManifest {
|
|
|
19
19
|
reportDependencies: typeof Actions.ReportDependencies;
|
|
20
20
|
version: typeof Actions.Version;
|
|
21
21
|
tag: typeof Actions.Tag;
|
|
22
|
+
paths: typeof Actions.Paths;
|
|
22
23
|
typecheck: typeof Actions.TypeCheck;
|
|
23
24
|
commitFiles: typeof Actions.CommitFiles;
|
|
24
25
|
link: typeof Actions.Link;
|
|
@@ -32,6 +33,7 @@ export declare class ForgeModule extends ModuleManifest {
|
|
|
32
33
|
package: typeof Commands.PackageCommand;
|
|
33
34
|
version: typeof Commands.VersionCommand;
|
|
34
35
|
tag: typeof Commands.TagCommand;
|
|
36
|
+
paths: typeof Commands.PathsCommand;
|
|
35
37
|
typecheck: typeof Commands.TypeCheckCommand;
|
|
36
38
|
link: typeof Commands.LinkCommand;
|
|
37
39
|
unlink: typeof Commands.UnlinkCommand;
|
package/types/src/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { ForgeError } from "./errors.js";
|
|
|
3
3
|
export { ForgeModule } from "./forge.module.js";
|
|
4
4
|
export type { TranspileOptions } from "./services/swc-transpiler.js";
|
|
5
5
|
export { SwcTranspiler } from "./services/swc-transpiler.js";
|
|
6
|
+
export { transpileCache } from "./services/transpile-cache.js";
|
|
6
7
|
export type { BuildTarget, TranspileTarget } from "./types/build-target.type.js";
|
|
7
8
|
export { BuildTargets } from "./types/build-target.type.js";
|
|
8
9
|
export type { ForgeConfiguration } from "./types/configuration.type.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PackageJson } from "@decohere/mesh";
|
|
2
2
|
/** Release `exports`: subpath (`.`, `./testing`) to a file path inside the release output. */
|
|
3
3
|
export type ReleaseExports = Record<string, string>;
|
|
4
|
-
/** `src/main.ts` becomes `src/main.js`, with any leading `./` dropped. */
|
|
4
|
+
/** `src/main.ts` becomes `src/main.js` (`.mts` `.mjs`, `.cts` `.cjs`), with any leading `./` dropped. */
|
|
5
5
|
declare function toJs(filePath: string): string;
|
|
6
6
|
/**
|
|
7
7
|
* The `exports` a library needs once only its `es6/` output is shipped: the library's own
|
|
@@ -3,7 +3,6 @@ import "../extensions/project.extensions.js";
|
|
|
3
3
|
import type { TransformResponse } from "../types/transform-response.model.js";
|
|
4
4
|
export declare class FileWriterService {
|
|
5
5
|
#private;
|
|
6
|
-
initialize(): Promise<void>;
|
|
7
6
|
warmup(projects: Project[]): Promise<void>;
|
|
8
7
|
enqueueOutput(response: TransformResponse): void;
|
|
9
8
|
enqueueOutputs(responses: TransformResponse[]): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProjectFileEvent, Workspace } from "@decohere/mesh";
|
|
2
|
+
import type { Observable } from "rxjs";
|
|
3
|
+
import type { TransformResponse } from "../types/transform-response.model.js";
|
|
4
|
+
/**
|
|
5
|
+
* Runs a transformer's handler for every workspace file change and emits what it built. The
|
|
6
|
+
* handler invalidates the transformer's own caches, rebuilds, and returns the responses (empty
|
|
7
|
+
* for a deletion or when nothing was built).
|
|
8
|
+
*/
|
|
9
|
+
export declare class IncrementalWatch {
|
|
10
|
+
#private;
|
|
11
|
+
get results$(): Observable<TransformResponse[]>;
|
|
12
|
+
watch(workspace: Workspace, handler: (evt: ProjectFileEvent) => Promise<TransformResponse[]>): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TransformResponse } from "../types/transform-response.model.js";
|
|
2
|
+
/** The outputs each source file last built to, so watch mode can delete them when it goes. */
|
|
3
|
+
export declare class OutputCache {
|
|
4
|
+
#private;
|
|
5
|
+
get(sourcePath: string): TransformResponse[] | undefined;
|
|
6
|
+
/** Records the successful outputs, replacing what each of their source files had before. */
|
|
7
|
+
record(responses: TransformResponse[]): void;
|
|
8
|
+
/** Deletes the files a source built to and forgets them. */
|
|
9
|
+
delete(sourcePath: string): void;
|
|
10
|
+
}
|