@macroforge/vite-plugin 0.1.42 → 0.1.45
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 -19
- 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 -565
package/dist/index.js
DELETED
|
@@ -1,565 +0,0 @@
|
|
|
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
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* // vite.config.ts
|
|
13
|
-
* import { defineConfig } from 'vite';
|
|
14
|
-
* import macroforgePlugin from '@macroforge/vite-plugin';
|
|
15
|
-
*
|
|
16
|
-
* export default defineConfig({
|
|
17
|
-
* plugins: [
|
|
18
|
-
* macroforgePlugin({
|
|
19
|
-
* generateTypes: true,
|
|
20
|
-
* typesOutputDir: 'src/types/generated',
|
|
21
|
-
* emitMetadata: true,
|
|
22
|
-
* }),
|
|
23
|
-
* ],
|
|
24
|
-
* });
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* @packageDocumentation
|
|
28
|
-
*/
|
|
29
|
-
import { createRequire } from "module";
|
|
30
|
-
import * as fs from "fs";
|
|
31
|
-
import * as path from "path";
|
|
32
|
-
import { collectExternalDecoratorModules, loadMacroConfig, } from "@macroforge/shared";
|
|
33
|
-
const moduleRequire = createRequire(import.meta.url);
|
|
34
|
-
let tsModule;
|
|
35
|
-
try {
|
|
36
|
-
tsModule = moduleRequire("typescript");
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
tsModule = undefined;
|
|
40
|
-
console.warn("[@macroforge/vite-plugin] TypeScript not found. Generated .d.ts files will be skipped.");
|
|
41
|
-
}
|
|
42
|
-
const compilerOptionsCache = new Map();
|
|
43
|
-
let cachedRequire;
|
|
44
|
-
/**
|
|
45
|
-
* Ensures that `require()` is available in the current execution context.
|
|
46
|
-
*
|
|
47
|
-
* @remarks
|
|
48
|
-
* This function handles the ESM/CommonJS interoperability problem. In pure ESM environments,
|
|
49
|
-
* `require` is not defined, but some native modules (like the Macroforge Rust binary) may
|
|
50
|
-
* depend on it being available. This function:
|
|
51
|
-
*
|
|
52
|
-
* 1. Returns the existing `require` if already available (CommonJS context)
|
|
53
|
-
* 2. Creates a synthetic `require` using Node's `createRequire` API (ESM context)
|
|
54
|
-
* 3. Exposes the created `require` on `globalThis` for native runtime loaders
|
|
55
|
-
* 4. Caches the result to avoid redundant creation
|
|
56
|
-
*
|
|
57
|
-
* @returns A Promise resolving to a Node.js `require` function
|
|
58
|
-
*
|
|
59
|
-
* @internal
|
|
60
|
-
*/
|
|
61
|
-
async function ensureRequire() {
|
|
62
|
-
if (typeof require !== "undefined") {
|
|
63
|
-
return require;
|
|
64
|
-
}
|
|
65
|
-
if (!cachedRequire) {
|
|
66
|
-
const { createRequire } = await import("module");
|
|
67
|
-
cachedRequire = createRequire(process.cwd() + "/");
|
|
68
|
-
// Expose on globalThis so native runtime loaders can use it
|
|
69
|
-
globalThis.require = cachedRequire;
|
|
70
|
-
}
|
|
71
|
-
return cachedRequire;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Retrieves and normalizes TypeScript compiler options for declaration emission.
|
|
75
|
-
*
|
|
76
|
-
* @remarks
|
|
77
|
-
* This function reads the project's `tsconfig.json` and adjusts the compiler options
|
|
78
|
-
* specifically for generating declaration files from macro-expanded code. The function:
|
|
79
|
-
*
|
|
80
|
-
* 1. Locates `tsconfig.json` using TypeScript's built-in config file discovery
|
|
81
|
-
* 2. Parses and validates the configuration
|
|
82
|
-
* 3. Normalizes options for declaration-only emission
|
|
83
|
-
* 4. Caches results per project root for performance
|
|
84
|
-
*
|
|
85
|
-
* **Forced Options:**
|
|
86
|
-
* - `declaration: true` - Enable declaration file output
|
|
87
|
-
* - `emitDeclarationOnly: true` - Only emit `.d.ts` files, not JavaScript
|
|
88
|
-
* - `noEmitOnError: false` - Continue emission even with type errors
|
|
89
|
-
* - `incremental: false` - Disable incremental compilation
|
|
90
|
-
*
|
|
91
|
-
* **Default Options** (applied if not specified in tsconfig):
|
|
92
|
-
* - `moduleResolution: Bundler` - Modern bundler-style resolution
|
|
93
|
-
* - `module: ESNext` - ES module output
|
|
94
|
-
* - `target: ESNext` - Latest ECMAScript target
|
|
95
|
-
* - `strict: true` - Enable strict type checking
|
|
96
|
-
* - `skipLibCheck: true` - Skip type checking of declaration files
|
|
97
|
-
*
|
|
98
|
-
* **Removed Options:**
|
|
99
|
-
* - `outDir` and `outFile` - Removed to allow programmatic output control
|
|
100
|
-
*
|
|
101
|
-
* @param projectRoot - The project root directory to search for tsconfig.json
|
|
102
|
-
*
|
|
103
|
-
* @returns Normalized compiler options, or `undefined` if TypeScript is not available
|
|
104
|
-
*
|
|
105
|
-
* @internal
|
|
106
|
-
*/
|
|
107
|
-
function getCompilerOptions(projectRoot) {
|
|
108
|
-
if (!tsModule) {
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
const cached = compilerOptionsCache.get(projectRoot);
|
|
112
|
-
if (cached) {
|
|
113
|
-
return cached;
|
|
114
|
-
}
|
|
115
|
-
let configPath;
|
|
116
|
-
try {
|
|
117
|
-
configPath = tsModule.findConfigFile(projectRoot, tsModule.sys.fileExists, "tsconfig.json");
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
configPath = undefined;
|
|
121
|
-
}
|
|
122
|
-
let options;
|
|
123
|
-
if (configPath) {
|
|
124
|
-
const configFile = tsModule.readConfigFile(configPath, tsModule.sys.readFile);
|
|
125
|
-
if (configFile.error) {
|
|
126
|
-
const formatted = tsModule.formatDiagnosticsWithColorAndContext([configFile.error], {
|
|
127
|
-
getCurrentDirectory: () => projectRoot,
|
|
128
|
-
getCanonicalFileName: (fileName) => fileName,
|
|
129
|
-
getNewLine: () => tsModule.sys.newLine,
|
|
130
|
-
});
|
|
131
|
-
console.warn(`[@macroforge/vite-plugin] Failed to read tsconfig at ${configPath}\n${formatted}`);
|
|
132
|
-
options = {};
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
const parsed = tsModule.parseJsonConfigFileContent(configFile.config, tsModule.sys, path.dirname(configPath));
|
|
136
|
-
options = parsed.options;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
options = {};
|
|
141
|
-
}
|
|
142
|
-
// Normalize options for declaration-only emission
|
|
143
|
-
const normalized = {
|
|
144
|
-
...options,
|
|
145
|
-
declaration: true,
|
|
146
|
-
emitDeclarationOnly: true,
|
|
147
|
-
noEmitOnError: false,
|
|
148
|
-
incremental: false,
|
|
149
|
-
};
|
|
150
|
-
// Remove output path options to allow programmatic control
|
|
151
|
-
delete normalized.outDir;
|
|
152
|
-
delete normalized.outFile;
|
|
153
|
-
// Apply sensible defaults for modern TypeScript projects
|
|
154
|
-
normalized.moduleResolution ??= tsModule.ModuleResolutionKind.Bundler;
|
|
155
|
-
normalized.module ??= tsModule.ModuleKind.ESNext;
|
|
156
|
-
normalized.target ??= tsModule.ScriptTarget.ESNext;
|
|
157
|
-
normalized.strict ??= true;
|
|
158
|
-
normalized.skipLibCheck ??= true;
|
|
159
|
-
compilerOptionsCache.set(projectRoot, normalized);
|
|
160
|
-
return normalized;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Generates TypeScript declaration files (`.d.ts`) from in-memory source code.
|
|
164
|
-
*
|
|
165
|
-
* @remarks
|
|
166
|
-
* This function creates a virtual TypeScript compilation environment to emit declaration
|
|
167
|
-
* files from macro-expanded code. It sets up a custom compiler host that serves the
|
|
168
|
-
* transformed source from memory while delegating other file operations to the filesystem.
|
|
169
|
-
*
|
|
170
|
-
* **Virtual Compiler Host:**
|
|
171
|
-
* The custom compiler host intercepts file operations for the target file:
|
|
172
|
-
* - `getSourceFile`: Returns the in-memory code for the target file, filesystem for others
|
|
173
|
-
* - `readFile`: Returns the in-memory code for the target file, filesystem for others
|
|
174
|
-
* - `fileExists`: Reports the target file as existing even though it's virtual
|
|
175
|
-
*
|
|
176
|
-
* This approach allows generating declarations for transformed code without writing
|
|
177
|
-
* intermediate files to disk.
|
|
178
|
-
*
|
|
179
|
-
* **Error Handling:**
|
|
180
|
-
* If declaration emission fails (e.g., due to type errors in the transformed code),
|
|
181
|
-
* diagnostics are formatted and logged as warnings, and `undefined` is returned.
|
|
182
|
-
*
|
|
183
|
-
* @param code - The macro-expanded TypeScript source code
|
|
184
|
-
* @param fileName - The original file path (used for module resolution and output naming)
|
|
185
|
-
* @param projectRoot - The project root directory (used for diagnostic formatting)
|
|
186
|
-
*
|
|
187
|
-
* @returns The generated declaration file content, or `undefined` if emission failed
|
|
188
|
-
* or TypeScript is not available
|
|
189
|
-
*
|
|
190
|
-
* @internal
|
|
191
|
-
*/
|
|
192
|
-
function emitDeclarationsFromCode(code, fileName, projectRoot) {
|
|
193
|
-
if (!tsModule) {
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
const compilerOptions = getCompilerOptions(projectRoot);
|
|
197
|
-
if (!compilerOptions) {
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
const normalizedFileName = path.resolve(fileName);
|
|
201
|
-
const sourceText = code;
|
|
202
|
-
const compilerHost = tsModule.createCompilerHost(compilerOptions, true);
|
|
203
|
-
// Override getSourceFile to serve in-memory code for the target file
|
|
204
|
-
compilerHost.getSourceFile = (requestedFileName, languageVersion) => {
|
|
205
|
-
if (path.resolve(requestedFileName) === normalizedFileName) {
|
|
206
|
-
return tsModule.createSourceFile(requestedFileName, sourceText, languageVersion, true);
|
|
207
|
-
}
|
|
208
|
-
const text = tsModule.sys.readFile(requestedFileName);
|
|
209
|
-
return text !== undefined
|
|
210
|
-
? tsModule.createSourceFile(requestedFileName, text, languageVersion, true)
|
|
211
|
-
: undefined;
|
|
212
|
-
};
|
|
213
|
-
// Override readFile to serve in-memory code for the target file
|
|
214
|
-
compilerHost.readFile = (requestedFileName) => {
|
|
215
|
-
return path.resolve(requestedFileName) === normalizedFileName
|
|
216
|
-
? sourceText
|
|
217
|
-
: tsModule.sys.readFile(requestedFileName);
|
|
218
|
-
};
|
|
219
|
-
// Override fileExists to report the virtual file as existing
|
|
220
|
-
compilerHost.fileExists = (requestedFileName) => {
|
|
221
|
-
return (path.resolve(requestedFileName) === normalizedFileName ||
|
|
222
|
-
tsModule.sys.fileExists(requestedFileName));
|
|
223
|
-
};
|
|
224
|
-
// Capture emitted declaration content
|
|
225
|
-
let output;
|
|
226
|
-
const writeFile = (outputName, text) => {
|
|
227
|
-
if (outputName.endsWith(".d.ts")) {
|
|
228
|
-
output = text;
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
const program = tsModule.createProgram([normalizedFileName], compilerOptions, compilerHost);
|
|
232
|
-
const emitResult = program.emit(undefined, writeFile, undefined, true);
|
|
233
|
-
// Log diagnostics if emission was skipped due to errors
|
|
234
|
-
if (emitResult.emitSkipped && emitResult.diagnostics.length > 0) {
|
|
235
|
-
const formatted = tsModule.formatDiagnosticsWithColorAndContext(emitResult.diagnostics, {
|
|
236
|
-
getCurrentDirectory: () => projectRoot,
|
|
237
|
-
getCanonicalFileName: (fileName) => fileName,
|
|
238
|
-
getNewLine: () => tsModule.sys.newLine,
|
|
239
|
-
});
|
|
240
|
-
console.warn(`[@macroforge/vite-plugin] Declaration emit failed for ${path.relative(projectRoot, fileName)}\n${formatted}`);
|
|
241
|
-
return undefined;
|
|
242
|
-
}
|
|
243
|
-
return output;
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Creates a Vite plugin for Macroforge compile-time macro expansion.
|
|
247
|
-
*
|
|
248
|
-
* @remarks
|
|
249
|
-
* This is the main entry point for integrating Macroforge into a Vite build pipeline.
|
|
250
|
-
* The plugin:
|
|
251
|
-
*
|
|
252
|
-
* 1. **Runs early** (`enforce: "pre"`) to transform code before other plugins
|
|
253
|
-
* 2. **Processes TypeScript files** (`.ts` and `.tsx`) excluding `node_modules`
|
|
254
|
-
* 3. **Expands macros** using the Macroforge Rust binary via `expandSync()`
|
|
255
|
-
* 4. **Generates type definitions** for transformed code (optional, default: enabled)
|
|
256
|
-
* 5. **Emits metadata** about macro transformations (optional, default: enabled)
|
|
257
|
-
*
|
|
258
|
-
* **Plugin Lifecycle:**
|
|
259
|
-
* - `configResolved`: Initializes project root, loads config, and attempts to load Rust binary
|
|
260
|
-
* - `transform`: Processes each TypeScript file through the macro expander
|
|
261
|
-
*
|
|
262
|
-
* **Error Handling:**
|
|
263
|
-
* - If the Rust binary is not available, files pass through unchanged
|
|
264
|
-
* - Macro expansion errors are reported via Vite's `this.error()` mechanism
|
|
265
|
-
* - TypeScript emission errors are logged as warnings
|
|
266
|
-
*
|
|
267
|
-
* @param options - Plugin configuration options
|
|
268
|
-
*
|
|
269
|
-
* @returns A Vite plugin instance
|
|
270
|
-
*
|
|
271
|
-
* @public
|
|
272
|
-
*
|
|
273
|
-
* @example
|
|
274
|
-
* ```typescript
|
|
275
|
-
* // Basic usage
|
|
276
|
-
* import macroforgePlugin from '@macroforge/vite-plugin';
|
|
277
|
-
*
|
|
278
|
-
* export default defineConfig({
|
|
279
|
-
* plugins: [macroforgePlugin()],
|
|
280
|
-
* });
|
|
281
|
-
* ```
|
|
282
|
-
*
|
|
283
|
-
* @example
|
|
284
|
-
* ```typescript
|
|
285
|
-
* // With custom options
|
|
286
|
-
* import macroforgePlugin from '@macroforge/vite-plugin';
|
|
287
|
-
*
|
|
288
|
-
* export default defineConfig({
|
|
289
|
-
* plugins: [
|
|
290
|
-
* macroforgePlugin({
|
|
291
|
-
* generateTypes: true,
|
|
292
|
-
* typesOutputDir: 'src/types/generated',
|
|
293
|
-
* emitMetadata: false,
|
|
294
|
-
* }),
|
|
295
|
-
* ],
|
|
296
|
-
* });
|
|
297
|
-
* ```
|
|
298
|
-
*/
|
|
299
|
-
function napiMacrosPlugin(options = {}) {
|
|
300
|
-
/**
|
|
301
|
-
* Reference to the loaded Macroforge Rust binary module.
|
|
302
|
-
* Contains the `expandSync` function for synchronous macro expansion.
|
|
303
|
-
* Will be `undefined` if the binary failed to load.
|
|
304
|
-
*/
|
|
305
|
-
let rustTransformer;
|
|
306
|
-
/** The resolved Vite project root directory */
|
|
307
|
-
let projectRoot;
|
|
308
|
-
/** Loaded configuration from macroforge.json */
|
|
309
|
-
let macroConfig = { keepDecorators: false };
|
|
310
|
-
// Resolve options with defaults
|
|
311
|
-
const generateTypes = options.generateTypes !== false; // Default to true
|
|
312
|
-
const typesOutputDir = options.typesOutputDir || "src/macros/generated";
|
|
313
|
-
const emitMetadata = options.emitMetadata !== false;
|
|
314
|
-
const metadataOutputDir = options.metadataOutputDir || typesOutputDir;
|
|
315
|
-
/**
|
|
316
|
-
* Ensures a directory exists, creating it recursively if necessary.
|
|
317
|
-
*
|
|
318
|
-
* @param dir - The directory path to ensure exists
|
|
319
|
-
*/
|
|
320
|
-
function ensureDir(dir) {
|
|
321
|
-
if (!fs.existsSync(dir)) {
|
|
322
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Writes generated TypeScript declaration files to the configured output directory.
|
|
327
|
-
*
|
|
328
|
-
* @remarks
|
|
329
|
-
* Preserves the source file's directory structure within the output directory.
|
|
330
|
-
* Implements change detection to avoid unnecessary file writes - only writes
|
|
331
|
-
* if the content differs from the existing file (or the file doesn't exist).
|
|
332
|
-
*
|
|
333
|
-
* Output path formula:
|
|
334
|
-
* `{projectRoot}/{typesOutputDir}/{relative/path/to/source}/{filename}.d.ts`
|
|
335
|
-
*
|
|
336
|
-
* @param id - The absolute path of the source file
|
|
337
|
-
* @param types - The generated declaration file content
|
|
338
|
-
*/
|
|
339
|
-
function writeTypeDefinitions(id, types) {
|
|
340
|
-
const relativePath = path.relative(projectRoot, id);
|
|
341
|
-
const parsed = path.parse(relativePath);
|
|
342
|
-
const outputBase = path.join(projectRoot, typesOutputDir, parsed.dir);
|
|
343
|
-
ensureDir(outputBase);
|
|
344
|
-
const targetPath = path.join(outputBase, `${parsed.name}.d.ts`);
|
|
345
|
-
try {
|
|
346
|
-
const existing = fs.existsSync(targetPath)
|
|
347
|
-
? fs.readFileSync(targetPath, "utf-8")
|
|
348
|
-
: null;
|
|
349
|
-
if (existing !== types) {
|
|
350
|
-
fs.writeFileSync(targetPath, types, "utf-8");
|
|
351
|
-
console.log(`[@macroforge/vite-plugin] Wrote types for ${relativePath} -> ${path.relative(projectRoot, targetPath)}`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
catch (error) {
|
|
355
|
-
console.error(`[@macroforge/vite-plugin] Failed to write type definitions for ${id}:`, error);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Writes macro intermediate representation (IR) metadata to JSON files.
|
|
360
|
-
*
|
|
361
|
-
* @remarks
|
|
362
|
-
* Preserves the source file's directory structure within the output directory.
|
|
363
|
-
* Implements change detection to avoid unnecessary file writes - only writes
|
|
364
|
-
* if the content differs from the existing file (or the file doesn't exist).
|
|
365
|
-
*
|
|
366
|
-
* Output path formula:
|
|
367
|
-
* `{projectRoot}/{metadataOutputDir}/{relative/path/to/source}/{filename}.macro-ir.json`
|
|
368
|
-
*
|
|
369
|
-
* The metadata contains information about which macros were applied and their
|
|
370
|
-
* transformation results, useful for debugging and tooling integration.
|
|
371
|
-
*
|
|
372
|
-
* @param id - The absolute path of the source file
|
|
373
|
-
* @param metadata - The macro IR metadata as a JSON string
|
|
374
|
-
*/
|
|
375
|
-
function writeMetadata(id, metadata) {
|
|
376
|
-
const relativePath = path.relative(projectRoot, id);
|
|
377
|
-
const parsed = path.parse(relativePath);
|
|
378
|
-
const outputBase = path.join(projectRoot, metadataOutputDir, parsed.dir);
|
|
379
|
-
ensureDir(outputBase);
|
|
380
|
-
const targetPath = path.join(outputBase, `${parsed.name}.macro-ir.json`);
|
|
381
|
-
try {
|
|
382
|
-
const existing = fs.existsSync(targetPath)
|
|
383
|
-
? fs.readFileSync(targetPath, "utf-8")
|
|
384
|
-
: null;
|
|
385
|
-
if (existing !== metadata) {
|
|
386
|
-
fs.writeFileSync(targetPath, metadata, "utf-8");
|
|
387
|
-
console.log(`[@macroforge/vite-plugin] Wrote metadata for ${relativePath} -> ${path.relative(projectRoot, targetPath)}`);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
catch (error) {
|
|
391
|
-
console.error(`[@macroforge/vite-plugin] Failed to write metadata for ${id}:`, error);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Formats transformation errors into user-friendly messages.
|
|
396
|
-
*
|
|
397
|
-
* @remarks
|
|
398
|
-
* Handles both Error instances and unknown error types. For Error instances,
|
|
399
|
-
* includes the full stack trace if available. Paths are made relative to the
|
|
400
|
-
* project root for readability.
|
|
401
|
-
*
|
|
402
|
-
* @param error - The caught error (can be any type)
|
|
403
|
-
* @param id - The absolute path of the file that failed to transform
|
|
404
|
-
*
|
|
405
|
-
* @returns A formatted error message string with plugin prefix
|
|
406
|
-
*/
|
|
407
|
-
function formatTransformError(error, id) {
|
|
408
|
-
const relative = projectRoot ? path.relative(projectRoot, id) || id : id;
|
|
409
|
-
if (error instanceof Error) {
|
|
410
|
-
const details = error.stack && error.stack.includes(error.message)
|
|
411
|
-
? error.stack
|
|
412
|
-
: `${error.message}\n${error.stack ?? ""}`;
|
|
413
|
-
return `[@macroforge/vite-plugin] Failed to transform ${relative}\n${details}`.trim();
|
|
414
|
-
}
|
|
415
|
-
return `[@macroforge/vite-plugin] Failed to transform ${relative}: ${String(error)}`;
|
|
416
|
-
}
|
|
417
|
-
return {
|
|
418
|
-
/**
|
|
419
|
-
* The unique identifier for this plugin.
|
|
420
|
-
* Used by Vite for plugin ordering and error reporting.
|
|
421
|
-
*/
|
|
422
|
-
name: "@macroforge/vite-plugin",
|
|
423
|
-
/**
|
|
424
|
-
* Run this plugin before other plugins.
|
|
425
|
-
*
|
|
426
|
-
* @remarks
|
|
427
|
-
* Macro expansion must happen early in the build pipeline so that
|
|
428
|
-
* subsequent plugins (like TypeScript compilation) see the expanded code.
|
|
429
|
-
*/
|
|
430
|
-
enforce: "pre",
|
|
431
|
-
/**
|
|
432
|
-
* Hook called when Vite config has been resolved.
|
|
433
|
-
*
|
|
434
|
-
* @remarks
|
|
435
|
-
* Performs plugin initialization:
|
|
436
|
-
* 1. Stores the resolved project root directory
|
|
437
|
-
* 2. Loads the Macroforge configuration from `macroforge.json`
|
|
438
|
-
* 3. Attempts to load the Rust macro expander binary
|
|
439
|
-
*
|
|
440
|
-
* If the Rust binary fails to load, a warning is logged but the plugin
|
|
441
|
-
* continues to function (files will pass through unchanged).
|
|
442
|
-
*
|
|
443
|
-
* @param config - The resolved Vite configuration
|
|
444
|
-
*/
|
|
445
|
-
configResolved(config) {
|
|
446
|
-
projectRoot = config.root;
|
|
447
|
-
// Load the Rust binary first
|
|
448
|
-
try {
|
|
449
|
-
rustTransformer = moduleRequire("macroforge");
|
|
450
|
-
}
|
|
451
|
-
catch (error) {
|
|
452
|
-
console.warn("[@macroforge/vite-plugin] Rust binary not found. Please run `npm run build:rust` first.");
|
|
453
|
-
console.warn(error);
|
|
454
|
-
}
|
|
455
|
-
// Load config (passing Rust transformer for foreign type parsing)
|
|
456
|
-
macroConfig = loadMacroConfig(projectRoot, rustTransformer?.loadConfig);
|
|
457
|
-
if (macroConfig.hasForeignTypes) {
|
|
458
|
-
console.log("[@macroforge/vite-plugin] Loaded config with foreign types from:", macroConfig.configPath);
|
|
459
|
-
}
|
|
460
|
-
},
|
|
461
|
-
/**
|
|
462
|
-
* Transform hook for processing TypeScript files through the macro expander.
|
|
463
|
-
*
|
|
464
|
-
* @remarks
|
|
465
|
-
* This is the core of the plugin. For each TypeScript file (`.ts` or `.tsx`):
|
|
466
|
-
*
|
|
467
|
-
* 1. **Filtering**: Skips non-TypeScript files and `node_modules`
|
|
468
|
-
* 2. **Expansion**: Calls the Rust binary's `expandSync()` function
|
|
469
|
-
* 3. **Diagnostics**: Reports errors via `this.error()`, logs warnings
|
|
470
|
-
* 4. **Post-processing**: Removes macro-only imports to prevent SSR issues
|
|
471
|
-
* 5. **Type Generation**: Optionally generates `.d.ts` files
|
|
472
|
-
* 6. **Metadata Emission**: Optionally writes macro IR JSON files
|
|
473
|
-
*
|
|
474
|
-
* **Return Value:**
|
|
475
|
-
* - Returns `null` if the file should not be transformed (not TS, in node_modules, etc.)
|
|
476
|
-
* - Returns `{ code, map }` with the transformed code (source maps not yet supported)
|
|
477
|
-
*
|
|
478
|
-
* **Error Handling:**
|
|
479
|
-
* - Macro expansion errors are reported via Vite's error mechanism
|
|
480
|
-
* - Vite plugin errors are re-thrown to preserve plugin attribution
|
|
481
|
-
* - Other errors are formatted and reported
|
|
482
|
-
*
|
|
483
|
-
* @param code - The source code to transform
|
|
484
|
-
* @param id - The absolute file path
|
|
485
|
-
*
|
|
486
|
-
* @returns Transformed code and source map, or null if no transformation needed
|
|
487
|
-
*/
|
|
488
|
-
async transform(code, id) {
|
|
489
|
-
// Ensure require() is available for native module loading
|
|
490
|
-
await ensureRequire();
|
|
491
|
-
// Only transform TypeScript files
|
|
492
|
-
if (!id.endsWith(".ts") && !id.endsWith(".tsx")) {
|
|
493
|
-
return null;
|
|
494
|
-
}
|
|
495
|
-
// Skip node_modules by default
|
|
496
|
-
if (id.includes("node_modules")) {
|
|
497
|
-
return null;
|
|
498
|
-
}
|
|
499
|
-
// Check if Rust transformer is available
|
|
500
|
-
if (!rustTransformer || !rustTransformer.expandSync) {
|
|
501
|
-
// Return unchanged if transformer not available
|
|
502
|
-
return null;
|
|
503
|
-
}
|
|
504
|
-
try {
|
|
505
|
-
// Collect external decorator modules from macro imports
|
|
506
|
-
const externalDecoratorModules = collectExternalDecoratorModules(code, moduleRequire);
|
|
507
|
-
// Perform macro expansion via the Rust binary
|
|
508
|
-
const result = rustTransformer.expandSync(code, id, {
|
|
509
|
-
keepDecorators: macroConfig.keepDecorators,
|
|
510
|
-
externalDecoratorModules,
|
|
511
|
-
configPath: macroConfig.configPath,
|
|
512
|
-
});
|
|
513
|
-
// Report diagnostics from macro expansion
|
|
514
|
-
for (const diag of result.diagnostics) {
|
|
515
|
-
if (diag.level === "error") {
|
|
516
|
-
const message = `Macro error at ${id}:${diag.start ?? "?"}-${diag.end ?? "?"}: ${diag.message}`;
|
|
517
|
-
this.error(message);
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
// Log warnings and info messages
|
|
521
|
-
console.warn(`[@macroforge/vite-plugin] ${diag.level}: ${diag.message}`);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
if (result && result.code) {
|
|
525
|
-
// TODO: Needs complete overhaul and dynamic attribute removal NO HARDCODING
|
|
526
|
-
// Decorator removal is currently handled by the Rust binary based on keepDecorators config
|
|
527
|
-
// if (!macroConfig.keepDecorators) {
|
|
528
|
-
// result.code = result.code
|
|
529
|
-
// .replace(/\/\*\*\s*@derive[\s\S]*?\*\/\s*/gi, "")
|
|
530
|
-
// .replace(/\/\*\*\s*@debug[\s\S]*?\*\/\s*/gi, "");
|
|
531
|
-
// }
|
|
532
|
-
// Remove macro-only imports so SSR output doesn't load native bindings
|
|
533
|
-
// These imports are only needed at compile-time for type checking
|
|
534
|
-
result.code = result.code.replace(/\/\*\*\s*import\s+macro[\s\S]*?\*\/\s*/gi, "");
|
|
535
|
-
// Generate type definitions if enabled
|
|
536
|
-
if (generateTypes) {
|
|
537
|
-
const emitted = emitDeclarationsFromCode(result.code, id, projectRoot);
|
|
538
|
-
if (emitted) {
|
|
539
|
-
writeTypeDefinitions(id, emitted);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
// Write macro IR metadata if enabled
|
|
543
|
-
if (emitMetadata && result.metadata) {
|
|
544
|
-
writeMetadata(id, result.metadata);
|
|
545
|
-
}
|
|
546
|
-
return {
|
|
547
|
-
code: result.code,
|
|
548
|
-
map: null, // expandSync does not generate source maps yet
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
catch (error) {
|
|
553
|
-
// Re-throw Vite plugin errors to preserve plugin attribution
|
|
554
|
-
if (error && typeof error === "object" && "plugin" in error) {
|
|
555
|
-
throw error;
|
|
556
|
-
}
|
|
557
|
-
// Format and report other errors
|
|
558
|
-
const message = formatTransformError(error, id);
|
|
559
|
-
this.error(message);
|
|
560
|
-
}
|
|
561
|
-
return null;
|
|
562
|
-
},
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
export default napiMacrosPlugin;
|