@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 CHANGED
@@ -21,7 +21,7 @@ export default defineConfig({
21
21
  plugins: [
22
22
  macroforgePlugin({
23
23
  generateTypes: true,
24
- typesOutputDir: 'src/types/generated',
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: 'src/types/generated',
62
+ typesOutputDir: '.macroforge/types',
63
63
  emitMetadata: true,
64
64
  }),
65
65
  ],
package/package.json CHANGED
@@ -1,28 +1,33 @@
1
1
  {
2
- "name": "@macroforge/vite-plugin",
3
- "version": "0.1.42",
4
- "type": "module",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
2
+ "dependencies": {
3
+ "@macroforge/shared": "^0.1.63",
4
+ "macroforge": "^0.1.63"
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
- "dist"
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/rymskip/macroforge-ts.git"
25
+ "url": "git+https://github.com/macroforge-ts/vite-plugin.git"
13
26
  },
14
27
  "scripts": {
15
- "build": "tsc -p tsconfig.json",
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/shared": "^0.1.42",
22
- "macroforge": "^0.1.42"
28
+ "test": "node --test tests/**/*.test.js"
23
29
  },
24
- "devDependencies": {
25
- "typescript": "^5.9.3",
26
- "vite": "^7.2.4"
27
- }
30
+ "type": "module",
31
+ "types": "src/index.d.ts",
32
+ "version": "0.1.45"
28
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;