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