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

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.
@@ -25,6 +25,9 @@ class BuildCommand extends _cli.DecohereCommand {
25
25
  this.option("--watch", "Watches for changes in the source dir", false);
26
26
  this.option("--file <file>", "File to build");
27
27
  }
28
+ runsUntilStopped(options) {
29
+ return options.watch === true;
30
+ }
28
31
  run = async (actions, options)=>{
29
32
  const responses = await (0, _auto.default)({
30
33
  clean: async ()=>await actions.clean.run(options),
@@ -49,6 +49,9 @@ class LinkCommand extends _cli.DecohereCommand {
49
49
  this.option("--target-dir <dir>", "Path to the consumer project root");
50
50
  this.option("--watch", "Watch for changes after linking", false);
51
51
  }
52
+ runsUntilStopped(options) {
53
+ return options.watch === true;
54
+ }
52
55
  run = async (actions, options)=>{
53
56
  const responses = await (0, _auto.default)({
54
57
  clean: async ()=>await actions.clean.run(options),
@@ -114,29 +114,55 @@ class SwcTranspiler extends (0, _extensibility.mixwith)(_cachesoutputsmixin.Cach
114
114
  config: swcOptions
115
115
  });
116
116
  }
117
- if (content === "") {
118
- return _transformresponsemodel.TransformResponse.Skipped({
119
- file,
120
- messages: "File is empty",
121
- config: swcOptions
122
- });
123
- }
117
+ if (content === "") return SwcTranspiler.skippedEmpty(file, swcOptions);
124
118
  try {
125
119
  const transformed = (0, _core.transformSync)(content, swcOptions);
126
120
  if (transformed == null) {
127
121
  throw new Error("swc.transformSync returned no result");
128
122
  }
129
- const filePath = file.relativePath;
130
- const extName = _nodepath.default.extname(filePath);
131
- const fileName = filePath.replace(new RegExp(`${extName}$`), ".js");
132
- return _transformresponsemodel.TransformResponse.Success({
123
+ return SwcTranspiler.succeeded(file, buildTarget, swcOptions, content, transformed.code);
124
+ } catch (error) {
125
+ return _transformresponsemodel.TransformResponse.Error({
133
126
  file,
134
- code: transformed.code,
135
- sourceContent: content,
136
- target: buildTarget,
137
- fileName,
127
+ messages: [
128
+ String(error)
129
+ ],
130
+ config: swcOptions
131
+ });
132
+ }
133
+ }
134
+ /**
135
+ * Async variant of {@link transformFileSync}, with the same result for the same file. SWC
136
+ * runs it on libuv's thread pool, so many calls in flight compile in parallel (#661).
137
+ */ async transformFileAsync(file, buildTarget, options = {}, baseConfig = this.createBaseConfig(buildTarget, options)) {
138
+ let content;
139
+ try {
140
+ content = await _nodefs.default.promises.readFile(file.fullPath, "utf8");
141
+ } catch (error) {
142
+ const swcOptions = this.#swcConfigFactory.createFileConfig(file, baseConfig);
143
+ return _transformresponsemodel.TransformResponse.Error({
144
+ file,
145
+ messages: [
146
+ String(error)
147
+ ],
138
148
  config: swcOptions
139
149
  });
150
+ }
151
+ return this.transformCode(content, file, buildTarget, baseConfig);
152
+ }
153
+ /**
154
+ * Compiles `content` as the source of `file`, without reading the file: for callers that
155
+ * already hold the code, such as the Vitest plugin, which must compile what Vite passes it
156
+ * rather than an earlier read of the disk (#517). Async SWC, on the thread pool.
157
+ */ async transformCode(content, file, buildTarget, baseConfig) {
158
+ const swcOptions = this.#swcConfigFactory.createFileConfig(file, baseConfig);
159
+ if (content === "") return SwcTranspiler.skippedEmpty(file, swcOptions);
160
+ try {
161
+ const transformed = await (0, _core.transform)(content, swcOptions);
162
+ if (transformed == null) {
163
+ throw new Error("swc.transform returned no result");
164
+ }
165
+ return SwcTranspiler.succeeded(file, buildTarget, swcOptions, content, transformed.code);
140
166
  } catch (error) {
141
167
  return _transformresponsemodel.TransformResponse.Error({
142
168
  file,
@@ -147,6 +173,26 @@ class SwcTranspiler extends (0, _extensibility.mixwith)(_cachesoutputsmixin.Cach
147
173
  });
148
174
  }
149
175
  }
176
+ static skippedEmpty(file, swcOptions) {
177
+ return _transformresponsemodel.TransformResponse.Skipped({
178
+ file,
179
+ messages: "File is empty",
180
+ config: swcOptions
181
+ });
182
+ }
183
+ static succeeded(file, buildTarget, swcOptions, content, code) {
184
+ const filePath = file.relativePath;
185
+ const extName = _nodepath.default.extname(filePath);
186
+ const fileName = filePath.replace(new RegExp(`${extName}$`), ".js");
187
+ return _transformresponsemodel.TransformResponse.Success({
188
+ file,
189
+ code,
190
+ sourceContent: content,
191
+ target: buildTarget,
192
+ fileName,
193
+ config: swcOptions
194
+ });
195
+ }
150
196
  async transformTarget(files, buildTarget, options, project, onOutput) {
151
197
  const baseConfig = this.#swcConfigFactory.createBaseConfig(buildTarget, this.withAliasContext(options));
152
198
  return await Promise.all(files.map((file)=>{
@@ -43,9 +43,11 @@ function forgeBuild(options) {
43
43
  }
44
44
  });
45
45
  const logger = loggerFactory(PLUGIN_NAME);
46
- const skipBuild = options?.skipBuild ?? false;
47
46
  const transpiler = new _swctranspiler.SwcTranspiler();
48
- const warmedProjects = new Set();
47
+ /**
48
+ * SWC options per project (tsconfig, `.swcrc`, path aliases), built once: rebuilding them for
49
+ * every file was most of the plugin's per-file cost, as it was in flux (#513, #517).
50
+ */ const baseConfigs = new Map();
49
51
  const transformCache = new Map();
50
52
  let workspace = null;
51
53
  const initWorkspace = async ()=>{
@@ -56,10 +58,17 @@ function forgeBuild(options) {
56
58
  rootDir: workspaceRoot
57
59
  });
58
60
  };
59
- const preWarmProject = (project)=>{
60
- if (warmedProjects.has(project.projectDirectory)) return;
61
- warmedProjects.add(project.projectDirectory);
62
- void transpiler.transformProject(project).then(()=>logger.debug(`Pre-warm complete for ${project.name}`)).catch((err)=>logger.error(`Pre-warm error for ${project.name}: ${err.message}`));
61
+ const baseConfigFor = (project)=>{
62
+ const cached = baseConfigs.get(project);
63
+ if (cached != null) return cached;
64
+ const config = transpiler.createBaseConfig(_buildtargettype.BuildTargets.es6, {
65
+ aliasPaths: true,
66
+ tsConfig: project?.getTsConfigSync(),
67
+ userConfig: project?.getSwcConfigSync(),
68
+ workspace: workspace ?? undefined
69
+ });
70
+ baseConfigs.set(project, config);
71
+ return config;
63
72
  };
64
73
  return {
65
74
  name: PLUGIN_NAME,
@@ -77,42 +86,24 @@ function forgeBuild(options) {
77
86
  return null;
78
87
  },
79
88
  watchChange (id) {
80
- const file = new _io.FileEx({
81
- fullPath: id
82
- });
83
89
  transformCache.delete(id);
84
- transpiler.invalidateContentCache(file);
85
90
  },
86
91
  async transform (code, id) {
87
92
  if (id.includes("node_modules") || !/[.](ts|tsx|mts|cts)$/.test(id)) {
88
93
  return null;
89
94
  }
90
- await initWorkspace();
91
- const project = await workspace?.findProject(id, buildConfig);
92
- if (project != null && !skipBuild) {
93
- preWarmProject(project);
94
- }
95
95
  const cached = transformCache.get(id);
96
96
  if (cached !== undefined && cached.content === code) {
97
97
  return cached.result;
98
98
  }
99
- const file = new _io.FileEx({
99
+ await initWorkspace();
100
+ const project = await workspace?.findProject(id, buildConfig);
101
+ // The code Vite passes, not a read of the file: no content cache can serve an older
102
+ // version, and there is no pre-warm compiling files ahead from disk (#517).
103
+ const response = await transpiler.transformCode(code, new _io.FileEx({
100
104
  fullPath: id,
101
105
  content: code
102
- });
103
- if (cached !== undefined) {
104
- // Content changed for a previously-seen file — invalidate the transpiler's
105
- // FILE_CONTENT cache so the fresh content from Vite is used.
106
- // The VS Code Vitest extension disables Vite's chokidar watcher
107
- // (server.watch: null) and drives re-runs via vitest.watcher.onFileChange(),
108
- // bypassing watchChange entirely — so we cannot rely on watchChange alone.
109
- transpiler.invalidateContentCache(file);
110
- }
111
- const [response] = await transpiler.transformFile(file, _buildtargettype.BuildTargets.es6, {
112
- aliasPaths: true,
113
- tsConfig: project ? await project.getTsConfig() : undefined,
114
- workspace: workspace ?? undefined
115
- });
106
+ }), _buildtargettype.BuildTargets.es6, baseConfigFor(project));
116
107
  if (!response || response.outcome !== "success") return null;
117
108
  const result = {
118
109
  code: response.code,
@@ -10,6 +10,9 @@ export class BuildCommand extends DecohereCommand {
10
10
  this.option("--watch", "Watches for changes in the source dir", false);
11
11
  this.option("--file <file>", "File to build");
12
12
  }
13
+ runsUntilStopped(options) {
14
+ return options.watch === true;
15
+ }
13
16
  run = async (actions, options)=>{
14
17
  const responses = await auto({
15
18
  clean: async ()=>await actions.clean.run(options),
@@ -26,6 +26,9 @@ export class LinkCommand extends DecohereCommand {
26
26
  this.option("--target-dir <dir>", "Path to the consumer project root");
27
27
  this.option("--watch", "Watch for changes after linking", false);
28
28
  }
29
+ runsUntilStopped(options) {
30
+ return options.watch === true;
31
+ }
29
32
  run = async (actions, options)=>{
30
33
  const responses = await auto({
31
34
  clean: async ()=>await actions.clean.run(options),
@@ -99,29 +99,55 @@ export class SwcTranspiler extends mixwith(CachesOutputs, IncrementalTransform)
99
99
  config: swcOptions
100
100
  });
101
101
  }
102
- if (content === "") {
103
- return TransformResponse.Skipped({
104
- file,
105
- messages: "File is empty",
106
- config: swcOptions
107
- });
108
- }
102
+ if (content === "") return SwcTranspiler.skippedEmpty(file, swcOptions);
109
103
  try {
110
104
  const transformed = transformSync(content, swcOptions);
111
105
  if (transformed == null) {
112
106
  throw new Error("swc.transformSync returned no result");
113
107
  }
114
- const filePath = file.relativePath;
115
- const extName = path.extname(filePath);
116
- const fileName = filePath.replace(new RegExp(`${extName}$`), ".js");
117
- return TransformResponse.Success({
108
+ return SwcTranspiler.succeeded(file, buildTarget, swcOptions, content, transformed.code);
109
+ } catch (error) {
110
+ return TransformResponse.Error({
118
111
  file,
119
- code: transformed.code,
120
- sourceContent: content,
121
- target: buildTarget,
122
- fileName,
112
+ messages: [
113
+ String(error)
114
+ ],
115
+ config: swcOptions
116
+ });
117
+ }
118
+ }
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
+ ],
123
133
  config: swcOptions
124
134
  });
135
+ }
136
+ return this.transformCode(content, file, buildTarget, baseConfig);
137
+ }
138
+ /**
139
+ * Compiles `content` as the source of `file`, without reading the file: for callers that
140
+ * 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) {
143
+ const swcOptions = this.#swcConfigFactory.createFileConfig(file, baseConfig);
144
+ if (content === "") return SwcTranspiler.skippedEmpty(file, swcOptions);
145
+ try {
146
+ const transformed = await transform(content, swcOptions);
147
+ if (transformed == null) {
148
+ throw new Error("swc.transform returned no result");
149
+ }
150
+ return SwcTranspiler.succeeded(file, buildTarget, swcOptions, content, transformed.code);
125
151
  } catch (error) {
126
152
  return TransformResponse.Error({
127
153
  file,
@@ -132,6 +158,26 @@ export class SwcTranspiler extends mixwith(CachesOutputs, IncrementalTransform)
132
158
  });
133
159
  }
134
160
  }
161
+ static skippedEmpty(file, swcOptions) {
162
+ return TransformResponse.Skipped({
163
+ file,
164
+ messages: "File is empty",
165
+ config: swcOptions
166
+ });
167
+ }
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");
172
+ return TransformResponse.Success({
173
+ file,
174
+ code,
175
+ sourceContent: content,
176
+ target: buildTarget,
177
+ fileName,
178
+ config: swcOptions
179
+ });
180
+ }
135
181
  async transformTarget(files, buildTarget, options, project, onOutput) {
136
182
  const baseConfig = this.#swcConfigFactory.createBaseConfig(buildTarget, this.withAliasContext(options));
137
183
  return await Promise.all(files.map((file)=>{
@@ -28,9 +28,11 @@ export function forgeBuild(options) {
28
28
  }
29
29
  });
30
30
  const logger = loggerFactory(PLUGIN_NAME);
31
- const skipBuild = options?.skipBuild ?? false;
32
31
  const transpiler = new SwcTranspiler();
33
- const warmedProjects = new Set();
32
+ /**
33
+ * SWC options per project (tsconfig, `.swcrc`, path aliases), built once: rebuilding them for
34
+ * every file was most of the plugin's per-file cost, as it was in flux (#513, #517).
35
+ */ const baseConfigs = new Map();
34
36
  const transformCache = new Map();
35
37
  let workspace = null;
36
38
  const initWorkspace = async ()=>{
@@ -41,10 +43,17 @@ export function forgeBuild(options) {
41
43
  rootDir: workspaceRoot
42
44
  });
43
45
  };
44
- const preWarmProject = (project)=>{
45
- if (warmedProjects.has(project.projectDirectory)) return;
46
- warmedProjects.add(project.projectDirectory);
47
- void transpiler.transformProject(project).then(()=>logger.debug(`Pre-warm complete for ${project.name}`)).catch((err)=>logger.error(`Pre-warm error for ${project.name}: ${err.message}`));
46
+ const baseConfigFor = (project)=>{
47
+ const cached = baseConfigs.get(project);
48
+ if (cached != null) return cached;
49
+ const config = transpiler.createBaseConfig(BuildTargets.es6, {
50
+ aliasPaths: true,
51
+ tsConfig: project?.getTsConfigSync(),
52
+ userConfig: project?.getSwcConfigSync(),
53
+ workspace: workspace ?? undefined
54
+ });
55
+ baseConfigs.set(project, config);
56
+ return config;
48
57
  };
49
58
  return {
50
59
  name: PLUGIN_NAME,
@@ -62,42 +71,24 @@ export function forgeBuild(options) {
62
71
  return null;
63
72
  },
64
73
  watchChange (id) {
65
- const file = new FileEx({
66
- fullPath: id
67
- });
68
74
  transformCache.delete(id);
69
- transpiler.invalidateContentCache(file);
70
75
  },
71
76
  async transform (code, id) {
72
77
  if (id.includes("node_modules") || !/[.](ts|tsx|mts|cts)$/.test(id)) {
73
78
  return null;
74
79
  }
75
- await initWorkspace();
76
- const project = await workspace?.findProject(id, buildConfig);
77
- if (project != null && !skipBuild) {
78
- preWarmProject(project);
79
- }
80
80
  const cached = transformCache.get(id);
81
81
  if (cached !== undefined && cached.content === code) {
82
82
  return cached.result;
83
83
  }
84
- const file = new FileEx({
84
+ await initWorkspace();
85
+ const project = await workspace?.findProject(id, buildConfig);
86
+ // The code Vite passes, not a read of the file: no content cache can serve an older
87
+ // version, and there is no pre-warm compiling files ahead from disk (#517).
88
+ const response = await transpiler.transformCode(code, new FileEx({
85
89
  fullPath: id,
86
90
  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
- });
91
+ }), BuildTargets.es6, baseConfigFor(project));
101
92
  if (!response || response.outcome !== "success") return null;
102
93
  const result = {
103
94
  code: response.code,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/forge",
3
- "version": "0.0.1-dev.110",
3
+ "version": "0.0.1-dev.112",
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.110",
33
- "@decohere/mesh": "0.0.1-dev.110",
34
- "@decohere/nexus": "0.0.1-dev.110",
35
- "@decohere/scribe": "0.0.1-dev.110",
32
+ "@decohere/crucible": "0.0.1-dev.112",
33
+ "@decohere/mesh": "0.0.1-dev.112",
34
+ "@decohere/nexus": "0.0.1-dev.112",
35
+ "@decohere/scribe": "0.0.1-dev.112",
36
36
  "@swc/core": "^1.11.11",
37
37
  "async": "^3.2.6",
38
38
  "parse-imports": "^2.2.1",
@@ -12,5 +12,6 @@ export type BuildActions = {
12
12
  };
13
13
  export declare class BuildCommand extends DecohereCommand<BuildActions> {
14
14
  constructor(name?: string);
15
+ runsUntilStopped(options: Record<string, unknown>): boolean;
15
16
  run: (actions: BuildActions, options: MergeOptions<BuildActions>) => Promise<CommandResponse<BuildActions>>;
16
17
  }
@@ -19,6 +19,7 @@ type UnlinkActions = {
19
19
  export declare class LinkCommand extends DecohereCommand<LinkActions> {
20
20
  get actions(): (typeof Build | typeof Clean | typeof Copy | typeof Link | typeof Watch)[];
21
21
  constructor(name?: string);
22
+ runsUntilStopped(options: Record<string, unknown>): boolean;
22
23
  run: (actions: LinkActions, options: MergeOptions<LinkActions>) => Promise<CommandResponse<LinkActions>>;
23
24
  }
24
25
  export declare class UnlinkCommand extends DecohereCommand<UnlinkActions> {
@@ -37,6 +37,19 @@ export declare class SwcTranspiler extends SwcTranspiler_base implements Transfo
37
37
  * Pass `baseConfig` from {@link createBaseConfig} to skip rebuilding it for every file.
38
38
  */
39
39
  transformFileSync(file: FileEx, buildTarget: TranspileTarget, options?: TranspileOptions, baseConfig?: Options): TransformResponse;
40
+ /**
41
+ * Async variant of {@link transformFileSync}, with the same result for the same file. SWC
42
+ * runs it on libuv's thread pool, so many calls in flight compile in parallel (#661).
43
+ */
44
+ transformFileAsync(file: FileEx, buildTarget: TranspileTarget, options?: TranspileOptions, baseConfig?: Options): Promise<TransformResponse>;
45
+ /**
46
+ * Compiles `content` as the source of `file`, without reading the file: for callers that
47
+ * already hold the code, such as the Vitest plugin, which must compile what Vite passes it
48
+ * rather than an earlier read of the disk (#517). Async SWC, on the thread pool.
49
+ */
50
+ transformCode(content: string, file: FileEx, buildTarget: TranspileTarget, baseConfig: Options): Promise<TransformResponse>;
51
+ private static skippedEmpty;
52
+ private static succeeded;
40
53
  private transformTarget;
41
54
  private runTransform;
42
55
  protected onIncrementalFileEvent(evt: ProjectFileEvent): Promise<TransformResponse[]>;
@@ -2,8 +2,6 @@ export type ForgeBuildOptions = {
2
2
  buildConfig?: string;
3
3
  logLevel?: string;
4
4
  workspaceRoot?: string;
5
- /** Skip the build pre-warm entirely (e.g. when a pre-built artifact is already on disk). */
6
- skipBuild?: boolean;
7
5
  };
8
6
  export declare function forgeBuild(options?: ForgeBuildOptions): {
9
7
  name: string;