@macroforge/vite-plugin 0.1.40 → 0.1.43
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 -2
- package/package.json +24 -18
- package/src/index.d.ts +21 -0
- package/src/index.js +558 -0
- package/dist/index.d.ts +0 -186
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -692
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ export default defineConfig({
|
|
|
21
21
|
plugins: [
|
|
22
22
|
macroforgePlugin({
|
|
23
23
|
generateTypes: true,
|
|
24
|
-
typesOutputDir: '
|
|
24
|
+
typesOutputDir: '.macroforge/types',
|
|
25
25
|
emitMetadata: true,
|
|
26
26
|
}),
|
|
27
27
|
],
|
|
@@ -59,7 +59,7 @@ export default defineConfig({
|
|
|
59
59
|
plugins: [
|
|
60
60
|
macroforgePlugin({
|
|
61
61
|
generateTypes: true,
|
|
62
|
-
typesOutputDir: '
|
|
62
|
+
typesOutputDir: '.macroforge/types',
|
|
63
63
|
emitMetadata: true,
|
|
64
64
|
}),
|
|
65
65
|
],
|
package/package.json
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"@macroforge/shared": "^0.1.61",
|
|
4
|
+
"macroforge": "^0.1.61"
|
|
5
|
+
},
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@types/node": "^22.0.0"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./src/index.js",
|
|
12
|
+
"types": "./src/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
7
15
|
"files": [
|
|
8
|
-
"
|
|
16
|
+
"src"
|
|
9
17
|
],
|
|
18
|
+
"main": "src/index.js",
|
|
19
|
+
"name": "@macroforge/vite-plugin",
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
22
|
+
},
|
|
10
23
|
"repository": {
|
|
11
24
|
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/
|
|
25
|
+
"url": "git+https://github.com/macroforge-ts/vite-plugin.git"
|
|
13
26
|
},
|
|
14
27
|
"scripts": {
|
|
15
|
-
"
|
|
16
|
-
"clean": "rm -rf dist",
|
|
17
|
-
"cleanbuild": "npm run clean && npm run build",
|
|
18
|
-
"test": "npm run build && node --test tests/**/*.test.js"
|
|
19
|
-
},
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"macroforge": "^0.1.40"
|
|
28
|
+
"test": "node --test tests/**/*.test.js"
|
|
22
29
|
},
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
30
|
+
"type": "module",
|
|
31
|
+
"types": "src/index.d.ts",
|
|
32
|
+
"version": "0.1.43"
|
|
27
33
|
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a Vite plugin for Macroforge compile-time macro expansion.
|
|
5
|
+
*
|
|
6
|
+
* Configuration is loaded from `macroforge.config.js` (or .ts/.mjs/.cjs).
|
|
7
|
+
* Vite-specific options can be set under the `vite` key in the config file.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // vite.config.ts
|
|
12
|
+
* import { macroforge } from '@macroforge/vite-plugin';
|
|
13
|
+
*
|
|
14
|
+
* export default defineConfig({
|
|
15
|
+
* plugins: [macroforge()],
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function macroforge(): Promise<Plugin<any>>;
|
|
20
|
+
|
|
21
|
+
export default macroforge;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @macroforge/vite-plugin
|
|
3
|
+
*
|
|
4
|
+
* Vite plugin for Macroforge compile-time TypeScript macro expansion.
|
|
5
|
+
*
|
|
6
|
+
* This plugin integrates Macroforge's Rust-based macro expander into the Vite build pipeline,
|
|
7
|
+
* enabling compile-time code generation through `@derive` decorators. It processes TypeScript
|
|
8
|
+
* files during the build, expands macros, generates type definitions, and emits metadata.
|
|
9
|
+
*
|
|
10
|
+
* All configuration is loaded from `macroforge.config.js` (or .ts/.mjs/.cjs).
|
|
11
|
+
* Vite-specific options can be set under the `vite` key in the config file.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // vite.config.ts
|
|
16
|
+
* import { defineConfig } from 'vite';
|
|
17
|
+
* import { macroforge } from '@macroforge/vite-plugin';
|
|
18
|
+
*
|
|
19
|
+
* export default defineConfig({
|
|
20
|
+
* plugins: [macroforge()],
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* // macroforge.config.ts
|
|
27
|
+
* export default {
|
|
28
|
+
* keepDecorators: false,
|
|
29
|
+
* vite: {
|
|
30
|
+
* generateTypes: true, // Generate .d.ts files (default: true)
|
|
31
|
+
* typesOutputDir: ".macroforge/types", // Types output dir (default: ".macroforge/types")
|
|
32
|
+
* emitMetadata: true, // Emit metadata JSON (default: true)
|
|
33
|
+
* metadataOutputDir: ".macroforge/meta", // Metadata output dir (default: ".macroforge/meta")
|
|
34
|
+
* },
|
|
35
|
+
* };
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @packageDocumentation
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { createRequire } from "module";
|
|
42
|
+
import * as fs from "fs";
|
|
43
|
+
import * as path from "path";
|
|
44
|
+
import { collectExternalDecoratorModules, loadMacroConfig } from "@macroforge/shared";
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const moduleRequire = createRequire(import.meta.url);
|
|
48
|
+
|
|
49
|
+
/** @type {typeof import('typescript') | undefined} */
|
|
50
|
+
let tsModule;
|
|
51
|
+
try {
|
|
52
|
+
tsModule = moduleRequire("typescript");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
tsModule = undefined;
|
|
55
|
+
console.warn(
|
|
56
|
+
"[@macroforge/vite-plugin] TypeScript not found. Generated .d.ts files will be skipped."
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @type {Map<string, import('typescript').CompilerOptions>} */
|
|
61
|
+
const compilerOptionsCache = new Map();
|
|
62
|
+
|
|
63
|
+
/** @type {NodeJS.Require | undefined} */
|
|
64
|
+
let cachedRequire;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Ensures that `require()` is available in the current execution context.
|
|
68
|
+
* @returns {Promise<NodeRequire>}
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
async function ensureRequire() {
|
|
72
|
+
if (typeof require !== "undefined") {
|
|
73
|
+
return require;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!cachedRequire) {
|
|
77
|
+
const { createRequire } = await import("module");
|
|
78
|
+
cachedRequire = /** @type {NodeJS.Require} */ (createRequire(process.cwd() + "/"));
|
|
79
|
+
// @ts-ignore - Expose on globalThis so native runtime loaders can use it
|
|
80
|
+
globalThis.require = cachedRequire;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return cachedRequire;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Retrieves and normalizes TypeScript compiler options for declaration emission.
|
|
88
|
+
* @param {string} projectRoot - The project root directory
|
|
89
|
+
* @returns {import('typescript').CompilerOptions | undefined}
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
function getCompilerOptions(projectRoot) {
|
|
93
|
+
if (!tsModule) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const cached = compilerOptionsCache.get(projectRoot);
|
|
97
|
+
if (cached) {
|
|
98
|
+
return cached;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @type {string | undefined} */
|
|
102
|
+
let configPath;
|
|
103
|
+
try {
|
|
104
|
+
configPath = tsModule.findConfigFile(
|
|
105
|
+
projectRoot,
|
|
106
|
+
tsModule.sys.fileExists,
|
|
107
|
+
"tsconfig.json"
|
|
108
|
+
);
|
|
109
|
+
} catch {
|
|
110
|
+
configPath = undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @type {import('typescript').CompilerOptions} */
|
|
114
|
+
let options;
|
|
115
|
+
if (configPath) {
|
|
116
|
+
const configFile = tsModule.readConfigFile(configPath, tsModule.sys.readFile);
|
|
117
|
+
if (configFile.error) {
|
|
118
|
+
const formatted = tsModule.formatDiagnosticsWithColorAndContext(
|
|
119
|
+
[configFile.error],
|
|
120
|
+
{
|
|
121
|
+
getCurrentDirectory: () => projectRoot,
|
|
122
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
123
|
+
getNewLine: () => tsModule.sys.newLine,
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
console.warn(
|
|
127
|
+
`[@macroforge/vite-plugin] Failed to read tsconfig at ${configPath}\n${formatted}`
|
|
128
|
+
);
|
|
129
|
+
options = {};
|
|
130
|
+
} else {
|
|
131
|
+
const parsed = tsModule.parseJsonConfigFileContent(
|
|
132
|
+
configFile.config,
|
|
133
|
+
tsModule.sys,
|
|
134
|
+
path.dirname(configPath)
|
|
135
|
+
);
|
|
136
|
+
options = parsed.options;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
options = {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Normalize options for declaration-only emission
|
|
143
|
+
/** @type {import('typescript').CompilerOptions} */
|
|
144
|
+
const normalized = {
|
|
145
|
+
...options,
|
|
146
|
+
declaration: true,
|
|
147
|
+
emitDeclarationOnly: true,
|
|
148
|
+
noEmitOnError: false,
|
|
149
|
+
incremental: false,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Remove output path options to allow programmatic control
|
|
153
|
+
delete normalized.outDir;
|
|
154
|
+
delete normalized.outFile;
|
|
155
|
+
|
|
156
|
+
// Apply sensible defaults for modern TypeScript projects
|
|
157
|
+
normalized.moduleResolution ??= tsModule.ModuleResolutionKind.Bundler;
|
|
158
|
+
normalized.module ??= tsModule.ModuleKind.ESNext;
|
|
159
|
+
normalized.target ??= tsModule.ScriptTarget.ESNext;
|
|
160
|
+
normalized.strict ??= true;
|
|
161
|
+
normalized.skipLibCheck ??= true;
|
|
162
|
+
|
|
163
|
+
compilerOptionsCache.set(projectRoot, normalized);
|
|
164
|
+
return normalized;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generates TypeScript declaration files from in-memory source code.
|
|
169
|
+
* @param {string} code - The macro-expanded TypeScript source code
|
|
170
|
+
* @param {string} fileName - The original file path
|
|
171
|
+
* @param {string} projectRoot - The project root directory
|
|
172
|
+
* @returns {string | undefined}
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
function emitDeclarationsFromCode(code, fileName, projectRoot) {
|
|
176
|
+
if (!tsModule) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const compilerOptions = getCompilerOptions(projectRoot);
|
|
181
|
+
if (!compilerOptions) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const normalizedFileName = path.resolve(fileName);
|
|
186
|
+
const sourceText = code;
|
|
187
|
+
const compilerHost = tsModule.createCompilerHost(compilerOptions, true);
|
|
188
|
+
|
|
189
|
+
// Override getSourceFile to serve in-memory code for the target file
|
|
190
|
+
compilerHost.getSourceFile = (requestedFileName, languageVersion) => {
|
|
191
|
+
if (path.resolve(requestedFileName) === normalizedFileName) {
|
|
192
|
+
return tsModule.createSourceFile(
|
|
193
|
+
requestedFileName,
|
|
194
|
+
sourceText,
|
|
195
|
+
languageVersion,
|
|
196
|
+
true
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const text = tsModule.sys.readFile(requestedFileName);
|
|
200
|
+
return text !== undefined
|
|
201
|
+
? tsModule.createSourceFile(requestedFileName, text, languageVersion, true)
|
|
202
|
+
: undefined;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Override readFile to serve in-memory code for the target file
|
|
206
|
+
compilerHost.readFile = (requestedFileName) => {
|
|
207
|
+
return path.resolve(requestedFileName) === normalizedFileName
|
|
208
|
+
? sourceText
|
|
209
|
+
: tsModule.sys.readFile(requestedFileName);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Override fileExists to report the virtual file as existing
|
|
213
|
+
compilerHost.fileExists = (requestedFileName) => {
|
|
214
|
+
return (
|
|
215
|
+
path.resolve(requestedFileName) === normalizedFileName ||
|
|
216
|
+
tsModule.sys.fileExists(requestedFileName)
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Capture emitted declaration content
|
|
221
|
+
/** @type {string | undefined} */
|
|
222
|
+
let output;
|
|
223
|
+
const writeFile = (/** @type {string} */ outputName, /** @type {string} */ text) => {
|
|
224
|
+
if (outputName.endsWith(".d.ts")) {
|
|
225
|
+
output = text;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const program = tsModule.createProgram(
|
|
230
|
+
[normalizedFileName],
|
|
231
|
+
compilerOptions,
|
|
232
|
+
compilerHost
|
|
233
|
+
);
|
|
234
|
+
const emitResult = program.emit(undefined, writeFile, undefined, true);
|
|
235
|
+
|
|
236
|
+
// Log diagnostics if emission was skipped due to errors
|
|
237
|
+
if (emitResult.emitSkipped && emitResult.diagnostics.length > 0) {
|
|
238
|
+
const formatted = tsModule.formatDiagnosticsWithColorAndContext(
|
|
239
|
+
emitResult.diagnostics,
|
|
240
|
+
{
|
|
241
|
+
getCurrentDirectory: () => projectRoot,
|
|
242
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
243
|
+
getNewLine: () => tsModule.sys.newLine,
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
console.warn(
|
|
247
|
+
`[@macroforge/vite-plugin] Declaration emit failed for ${path.relative(
|
|
248
|
+
projectRoot,
|
|
249
|
+
fileName
|
|
250
|
+
)}\n${formatted}`
|
|
251
|
+
);
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return output;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Creates a Vite plugin for Macroforge compile-time macro expansion.
|
|
260
|
+
*
|
|
261
|
+
* Configuration is loaded from `macroforge.config.js` (or .ts/.mjs/.cjs).
|
|
262
|
+
* Vite-specific options can be set under the `vite` key in the config file.
|
|
263
|
+
*
|
|
264
|
+
* @return {Promise<import('vite').Plugin>}
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* // vite.config.ts
|
|
269
|
+
* import { macroforge } from '@macroforge/vite-plugin';
|
|
270
|
+
*
|
|
271
|
+
* export default defineConfig({
|
|
272
|
+
* plugins: [macroforge()],
|
|
273
|
+
* });
|
|
274
|
+
* ```
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* // macroforge.config.ts
|
|
279
|
+
* export default {
|
|
280
|
+
* keepDecorators: false,
|
|
281
|
+
* vite: {
|
|
282
|
+
* generateTypes: true,
|
|
283
|
+
* typesOutputDir: ".macroforge/types",
|
|
284
|
+
* emitMetadata: true,
|
|
285
|
+
* metadataOutputDir: ".macroforge/meta",
|
|
286
|
+
* },
|
|
287
|
+
* };
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
export async function macroforge() {
|
|
291
|
+
/**
|
|
292
|
+
* Reference to the loaded Macroforge Rust binary module.
|
|
293
|
+
* @type {{ expandSync: Function, loadConfig?: (content: string, filepath: string) => any } | undefined}
|
|
294
|
+
*/
|
|
295
|
+
let rustTransformer;
|
|
296
|
+
|
|
297
|
+
// Load the Rust binary first
|
|
298
|
+
try {
|
|
299
|
+
rustTransformer = moduleRequire("macroforge");
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.warn(
|
|
302
|
+
"[@macroforge/vite-plugin] Rust binary not found. Please run `npm run build:rust` first."
|
|
303
|
+
);
|
|
304
|
+
console.warn(error);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Load config upfront (passing Rust transformer for foreign type parsing)
|
|
308
|
+
const macroConfig = loadMacroConfig(process.cwd(), rustTransformer?.loadConfig);
|
|
309
|
+
|
|
310
|
+
if (macroConfig.hasForeignTypes) {
|
|
311
|
+
console.log(
|
|
312
|
+
"[@macroforge/vite-plugin] Loaded config with foreign types from:",
|
|
313
|
+
macroConfig.configPath
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Vite options resolved from config (with defaults)
|
|
318
|
+
/** @type {boolean} */
|
|
319
|
+
let generateTypes = true;
|
|
320
|
+
/** @type {string} */
|
|
321
|
+
let typesOutputDir = ".macroforge/types";
|
|
322
|
+
/** @type {boolean} */
|
|
323
|
+
let emitMetadata = true;
|
|
324
|
+
/** @type {string} */
|
|
325
|
+
let metadataOutputDir = ".macroforge/meta";
|
|
326
|
+
|
|
327
|
+
// Load vite-specific options from the config file
|
|
328
|
+
if (macroConfig.configPath) {
|
|
329
|
+
try {
|
|
330
|
+
const configModule = await import(macroConfig.configPath);
|
|
331
|
+
const userConfig = configModule.default || configModule;
|
|
332
|
+
const viteConfig = userConfig.vite;
|
|
333
|
+
|
|
334
|
+
if (viteConfig) {
|
|
335
|
+
if (viteConfig.generateTypes !== undefined) {
|
|
336
|
+
generateTypes = viteConfig.generateTypes;
|
|
337
|
+
}
|
|
338
|
+
if (viteConfig.typesOutputDir !== undefined) {
|
|
339
|
+
typesOutputDir = viteConfig.typesOutputDir;
|
|
340
|
+
}
|
|
341
|
+
if (viteConfig.emitMetadata !== undefined) {
|
|
342
|
+
emitMetadata = viteConfig.emitMetadata;
|
|
343
|
+
}
|
|
344
|
+
if (viteConfig.metadataOutputDir !== undefined) {
|
|
345
|
+
metadataOutputDir = viteConfig.metadataOutputDir;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`[@macroforge/vite-plugin] Failed to load config from ${macroConfig.configPath}: ${error.message}`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** @type {string} */
|
|
356
|
+
let projectRoot;
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Ensures a directory exists, creating it recursively if necessary.
|
|
360
|
+
* @param {string} dir
|
|
361
|
+
*/
|
|
362
|
+
function ensureDir(dir) {
|
|
363
|
+
if (!fs.existsSync(dir)) {
|
|
364
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Writes generated TypeScript declaration files to the configured output directory.
|
|
370
|
+
* @param {string} id - The absolute path of the source file
|
|
371
|
+
* @param {string} types - The generated declaration file content
|
|
372
|
+
*/
|
|
373
|
+
function writeTypeDefinitions(id, types) {
|
|
374
|
+
const relativePath = path.relative(projectRoot, id);
|
|
375
|
+
const parsed = path.parse(relativePath);
|
|
376
|
+
const outputBase = path.join(projectRoot, typesOutputDir, parsed.dir);
|
|
377
|
+
ensureDir(outputBase);
|
|
378
|
+
const targetPath = path.join(outputBase, `${parsed.name}.d.ts`);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const existing = fs.existsSync(targetPath)
|
|
382
|
+
? fs.readFileSync(targetPath, "utf-8")
|
|
383
|
+
: null;
|
|
384
|
+
if (existing !== types) {
|
|
385
|
+
fs.writeFileSync(targetPath, types, "utf-8");
|
|
386
|
+
console.log(
|
|
387
|
+
`[@macroforge/vite-plugin] Wrote types for ${relativePath} -> ${path.relative(projectRoot, targetPath)}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error(
|
|
392
|
+
`[@macroforge/vite-plugin] Failed to write type definitions for ${id}:`,
|
|
393
|
+
error
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Writes macro intermediate representation (IR) metadata to JSON files.
|
|
400
|
+
* @param {string} id - The absolute path of the source file
|
|
401
|
+
* @param {string} metadata - The macro IR metadata as a JSON string
|
|
402
|
+
*/
|
|
403
|
+
function writeMetadata(id, metadata) {
|
|
404
|
+
const relativePath = path.relative(projectRoot, id);
|
|
405
|
+
const parsed = path.parse(relativePath);
|
|
406
|
+
const outputBase = path.join(projectRoot, metadataOutputDir, parsed.dir);
|
|
407
|
+
ensureDir(outputBase);
|
|
408
|
+
const targetPath = path.join(outputBase, `${parsed.name}.macro-ir.json`);
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const existing = fs.existsSync(targetPath)
|
|
412
|
+
? fs.readFileSync(targetPath, "utf-8")
|
|
413
|
+
: null;
|
|
414
|
+
if (existing !== metadata) {
|
|
415
|
+
fs.writeFileSync(targetPath, metadata, "utf-8");
|
|
416
|
+
console.log(
|
|
417
|
+
`[@macroforge/vite-plugin] Wrote metadata for ${relativePath} -> ${path.relative(projectRoot, targetPath)}`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error(
|
|
422
|
+
`[@macroforge/vite-plugin] Failed to write metadata for ${id}:`,
|
|
423
|
+
error
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Formats transformation errors into user-friendly messages.
|
|
430
|
+
* @param {unknown} error
|
|
431
|
+
* @param {string} id
|
|
432
|
+
* @returns {string}
|
|
433
|
+
*/
|
|
434
|
+
function formatTransformError(error, id) {
|
|
435
|
+
const relative = projectRoot ? path.relative(projectRoot, id) || id : id;
|
|
436
|
+
if (error instanceof Error) {
|
|
437
|
+
const details =
|
|
438
|
+
error.stack && error.stack.includes(error.message)
|
|
439
|
+
? error.stack
|
|
440
|
+
: `${error.message}\n${error.stack ?? ""}`;
|
|
441
|
+
return `[@macroforge/vite-plugin] Failed to transform ${relative}\n${details}`.trim();
|
|
442
|
+
}
|
|
443
|
+
return `[@macroforge/vite-plugin] Failed to transform ${relative}: ${String(error)}`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** @type {import('vite').Plugin} */
|
|
447
|
+
const plugin = {
|
|
448
|
+
name: "@macroforge/vite-plugin",
|
|
449
|
+
enforce: "pre",
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* @param {{ root: string }} config
|
|
453
|
+
*/
|
|
454
|
+
configResolved(config) {
|
|
455
|
+
projectRoot = config.root;
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* @param {string} code
|
|
460
|
+
* @param {string} id
|
|
461
|
+
*/
|
|
462
|
+
async transform(code, id) {
|
|
463
|
+
// Ensure require() is available for native module loading
|
|
464
|
+
await ensureRequire();
|
|
465
|
+
|
|
466
|
+
// Only transform TypeScript files
|
|
467
|
+
if (!id.endsWith(".ts") && !id.endsWith(".tsx")) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Skip node_modules by default
|
|
472
|
+
if (id.includes("node_modules")) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Skip already-expanded files
|
|
477
|
+
if (id.includes(".expanded.")) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Check if Rust transformer is available
|
|
482
|
+
if (!rustTransformer || !rustTransformer.expandSync) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
// Collect external decorator modules from macro imports
|
|
488
|
+
const externalDecoratorModules = collectExternalDecoratorModules(
|
|
489
|
+
code,
|
|
490
|
+
moduleRequire
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
// Perform macro expansion via the Rust binary
|
|
494
|
+
const result = rustTransformer.expandSync(code, id, {
|
|
495
|
+
keepDecorators: macroConfig.keepDecorators,
|
|
496
|
+
externalDecoratorModules,
|
|
497
|
+
configPath: macroConfig.configPath,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Report diagnostics from macro expansion
|
|
501
|
+
for (const diag of result.diagnostics) {
|
|
502
|
+
if (diag.level === "error") {
|
|
503
|
+
const message = `Macro error at ${id}:${diag.start ?? "?"}-${diag.end ?? "?"}: ${diag.message}`;
|
|
504
|
+
/** @type {any} */ (this).error(message);
|
|
505
|
+
} else {
|
|
506
|
+
console.warn(
|
|
507
|
+
`[@macroforge/vite-plugin] ${diag.level}: ${diag.message}`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (result && result.code) {
|
|
513
|
+
// Remove macro-only imports so SSR output doesn't load native bindings
|
|
514
|
+
result.code = result.code.replace(
|
|
515
|
+
/\/\*\*\s*import\s+macro[\s\S]*?\*\/\s*/gi,
|
|
516
|
+
""
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
// Generate type definitions if enabled
|
|
520
|
+
if (generateTypes) {
|
|
521
|
+
const emitted = emitDeclarationsFromCode(
|
|
522
|
+
result.code,
|
|
523
|
+
id,
|
|
524
|
+
projectRoot
|
|
525
|
+
);
|
|
526
|
+
if (emitted) {
|
|
527
|
+
writeTypeDefinitions(id, emitted);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Write macro IR metadata if enabled
|
|
532
|
+
if (emitMetadata && result.metadata) {
|
|
533
|
+
writeMetadata(id, result.metadata);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
code: result.code,
|
|
538
|
+
map: null,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
} catch (error) {
|
|
542
|
+
// Re-throw Vite plugin errors to preserve plugin attribution
|
|
543
|
+
if (error && typeof error === "object" && "plugin" in error) {
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
// Format and report other errors
|
|
547
|
+
const message = formatTransformError(error, id);
|
|
548
|
+
/** @type {any} */ (this).error(message);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return null;
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
return plugin;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export default macroforge;
|