@macroforge/typescript-plugin 0.1.36 → 0.1.38

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
@@ -59,17 +59,17 @@ npm install @macroforge/typescript-plugin
59
59
 
60
60
  ### Functions
61
61
 
62
+ - **`parseMacroImportComments`** - Parses macro import comments to extract macro name to module path mappings.
63
+ - **`getExternalManifest`** - Attempts to load the manifest from an external macro package.
64
+ - **`getExternalMacroInfo`** - Looks up macro info from an external package manifest.
65
+ - **`getExternalDecoratorInfo`** - Looks up decorator info from an external package manifest.
62
66
  - **`findDeriveAtPosition`** - Finds a macro name within `@derive(...)` decorators at a given cursor position.
67
+ - **`findDeriveKeywordAtPosition`** - Finds the `@derive` keyword at a given cursor position.
63
68
  - **`findDecoratorAtPosition`** - Finds a field decorator (like `@serde` or `@debug`) at a given cursor position.
64
- - **`getMacroHoverInfo`** - const lastCommentEnd = beforeMatch.lastIndexOf("*/
69
+ - **`findEnclosingDeriveContext`** - const lastCommentEnd = beforeMatch.lastIndexOf("*/
70
+ - **`getMacroHoverInfo`** - Generates hover information (QuickInfo) for macros and decorators at a cursor position.
65
71
  - **`shouldProcess`** - Determines whether a file should be processed for macro expansion.
66
- - **`hasMacroDirectives`** - Performs a quick check to determine if a file contains any macro-related directives.
67
- - **`loadMacroConfig`** - Whether to preserve decorator syntax in the expanded output.
68
- - **`init`** - Main plugin factory function conforming to the TypeScript Language Service Plugin API.
69
- - **`create`** - Creates the plugin instance for a TypeScript project.
70
- - **`processFile`** - Processes a file through macro expansion via the native Rust plugin.
71
- - **`toPlainDiagnostic`** - Converts a TypeScript diagnostic to a plain object for the native plugin.
72
- - ... and 1 more
72
+ - ... and 7 more
73
73
 
74
74
  ### Types
75
75
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAmdrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,iBAAS,IAAI,CAAC,OAAO,EAAE;IAAE,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE;mBAcxB,EAAE,CAAC,MAAM,CAAC,gBAAgB;EAg+DjD;AAED,SAAS,IAAI,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAw1BrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,iBAAS,IAAI,CAAC,OAAO,EAAE;IAAE,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE;mBAcxB,EAAE,CAAC,MAAM,CAAC,gBAAgB;EAg+DjD;AAED,SAAS,IAAI,CAAC"}
package/dist/index.js CHANGED
@@ -102,6 +102,99 @@ function getMacroManifest() {
102
102
  return null;
103
103
  }
104
104
  }
105
+ /**
106
+ * Parses macro import comments to extract macro name to module path mappings.
107
+ *
108
+ * Macroforge supports importing external macros using a special JSDoc comment syntax:
109
+ * `/** import macro {MacroName, Another} from "@scope/package"; *​/`
110
+ *
111
+ * @param text - The source text to search for import comments
112
+ * @returns A Map of macro name to module path
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * const text = `/** import macro {Gigaform, CustomMacro} from "@playground/macro"; *​/`;
117
+ * parseMacroImportComments(text);
118
+ * // => Map { "Gigaform" => "@playground/macro", "CustomMacro" => "@playground/macro" }
119
+ * ```
120
+ */
121
+ function parseMacroImportComments(text) {
122
+ const imports = new Map();
123
+ const pattern = /\/\*\*\s*import\s+macro\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/gi;
124
+ let match;
125
+ while ((match = pattern.exec(text)) !== null) {
126
+ const names = match[1]
127
+ .split(",")
128
+ .map((n) => n.trim())
129
+ .filter(Boolean);
130
+ const modulePath = match[2];
131
+ for (const name of names) {
132
+ imports.set(name, modulePath);
133
+ }
134
+ }
135
+ return imports;
136
+ }
137
+ /**
138
+ * Cache for external macro package manifests.
139
+ * Maps package path to its manifest (or null if failed to load).
140
+ */
141
+ const externalManifestCache = new Map();
142
+ /**
143
+ * Attempts to load the manifest from an external macro package.
144
+ *
145
+ * External macro packages (like `@playground/macro`) export their own
146
+ * `__macroforgeGetManifest()` function that provides macro metadata
147
+ * including descriptions.
148
+ *
149
+ * @param modulePath - The package path (e.g., "@playground/macro")
150
+ * @returns The macro manifest, or null if loading failed
151
+ */
152
+ function getExternalManifest(modulePath) {
153
+ if (externalManifestCache.has(modulePath)) {
154
+ return externalManifestCache.get(modulePath) ?? null;
155
+ }
156
+ try {
157
+ // Try to require the external package
158
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
159
+ const pkg = require(modulePath);
160
+ if (typeof pkg.__macroforgeGetManifest === "function") {
161
+ const manifest = pkg.__macroforgeGetManifest();
162
+ externalManifestCache.set(modulePath, manifest);
163
+ return manifest;
164
+ }
165
+ }
166
+ catch {
167
+ // Package not found or doesn't export manifest
168
+ }
169
+ externalManifestCache.set(modulePath, null);
170
+ return null;
171
+ }
172
+ /**
173
+ * Looks up macro info from an external package manifest.
174
+ *
175
+ * @param macroName - The macro name to look up
176
+ * @param modulePath - The package path
177
+ * @returns The macro manifest entry, or null if not found
178
+ */
179
+ function getExternalMacroInfo(macroName, modulePath) {
180
+ const manifest = getExternalManifest(modulePath);
181
+ if (!manifest)
182
+ return null;
183
+ return (manifest.macros.find((m) => m.name.toLowerCase() === macroName.toLowerCase()) ?? null);
184
+ }
185
+ /**
186
+ * Looks up decorator info from an external package manifest.
187
+ *
188
+ * @param decoratorName - The decorator name to look up
189
+ * @param modulePath - The package path
190
+ * @returns The decorator manifest entry, or null if not found
191
+ */
192
+ function getExternalDecoratorInfo(decoratorName, modulePath) {
193
+ const manifest = getExternalManifest(modulePath);
194
+ if (!manifest)
195
+ return null;
196
+ return (manifest.decorators.find((d) => d.export.toLowerCase() === decoratorName.toLowerCase()) ?? null);
197
+ }
105
198
  /**
106
199
  * Finds a macro name within `@derive(...)` decorators at a given cursor position.
107
200
  *
@@ -164,6 +257,41 @@ function findDeriveAtPosition(text, position) {
164
257
  }
165
258
  return null;
166
259
  }
260
+ /**
261
+ * Finds the `@derive` keyword at a given cursor position.
262
+ * This matches the literal "@derive" text before the opening parenthesis,
263
+ * allowing hover documentation on the directive keyword itself.
264
+ *
265
+ * @param text - The source text to search
266
+ * @param position - The cursor position as a 0-indexed character offset
267
+ * @returns An object with start/end positions, or `null` if not on @derive keyword
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * // Given text: "/** @derive(Debug) *​/"
272
+ * findDeriveKeywordAtPosition(text, 5);
273
+ * // => { start: 4, end: 11 } // covers "@derive"
274
+ *
275
+ * // Position on "Debug" (inside parens) returns null
276
+ * findDeriveKeywordAtPosition(text, 12);
277
+ * // => null
278
+ * ```
279
+ *
280
+ * @see {@link findDeriveAtPosition} - For macro names inside @derive()
281
+ */
282
+ function findDeriveKeywordAtPosition(text, position) {
283
+ // Match @derive only when followed by ( to distinguish from other uses
284
+ const deriveKeywordPattern = /@derive(?=\s*\()/gi;
285
+ let match;
286
+ while ((match = deriveKeywordPattern.exec(text)) !== null) {
287
+ const start = match.index; // Position of @
288
+ const end = start + "@derive".length;
289
+ if (position >= start && position < end) {
290
+ return { start, end };
291
+ }
292
+ }
293
+ return null;
294
+ }
167
295
  /**
168
296
  * Finds a field decorator (like `@serde` or `@debug`) at a given cursor position.
169
297
  *
@@ -221,12 +349,54 @@ function findDecoratorAtPosition(text, position) {
221
349
  }
222
350
  return null;
223
351
  }
352
+ /**
353
+ * Finds what `@derive` macros apply to code at a given position.
354
+ *
355
+ * This function uses a heuristic: it finds the nearest `@derive(...)` decorator
356
+ * that appears before the given position. This is useful for determining which
357
+ * macros might be responsible for a particular field decorator.
358
+ *
359
+ * @param text - The source text to search
360
+ * @param position - The cursor position as a 0-indexed character offset
361
+ * @returns An array of macro names from the enclosing @derive, or `null` if not found
362
+ *
363
+ * @example
364
+ * ```typescript
365
+ * const text = `/** @derive(Debug, Serialize) *​/
366
+ * class User {
367
+ * @serde({ skip: true })
368
+ * password: string;
369
+ * }`;
370
+ *
371
+ * // Position on @serde
372
+ * findEnclosingDeriveContext(text, text.indexOf("@serde"));
373
+ * // => ["Debug", "Serialize"]
374
+ * ```
375
+ */
376
+ function findEnclosingDeriveContext(text, position) {
377
+ const beforePosition = text.substring(0, position);
378
+ const derivePattern = /@derive\s*\(\s*([^)]+)\s*\)/gi;
379
+ let lastMatch = null;
380
+ let match;
381
+ while ((match = derivePattern.exec(beforePosition)) !== null) {
382
+ lastMatch = match;
383
+ }
384
+ if (lastMatch) {
385
+ const macros = lastMatch[1]
386
+ .split(",")
387
+ .map((m) => m.trim())
388
+ .filter(Boolean);
389
+ return macros;
390
+ }
391
+ return null;
392
+ }
224
393
  /**
225
394
  * Generates hover information (QuickInfo) for macros and decorators at a cursor position.
226
395
  *
227
396
  * This function provides IDE hover tooltips for Macroforge-specific syntax:
228
- * - Macro names within `@derive(...)` JSDoc decorators
229
- * - Field decorators like `@serde`, `@debug`, etc.
397
+ * - The `@derive` keyword itself
398
+ * - Macro names within `@derive(...)` JSDoc decorators (both built-in and external)
399
+ * - Field decorators like `@serde`, `@debug`, and custom decorators from external macros
230
400
  *
231
401
  * @param text - The source text to analyze
232
402
  * @param position - The cursor position as a 0-indexed character offset
@@ -236,42 +406,93 @@ function findDecoratorAtPosition(text, position) {
236
406
  *
237
407
  * @remarks
238
408
  * The function checks positions in the following order:
239
- * 1. First, check if cursor is on a macro name within `@derive(...)` via {@link findDeriveAtPosition}
240
- * 2. Then, check if cursor is on a field decorator via {@link findDecoratorAtPosition}
409
+ * 1. Check if cursor is on the `@derive` keyword via {@link findDeriveKeywordAtPosition}
410
+ * 2. Check if cursor is on a macro name within `@derive(...)` via {@link findDeriveAtPosition}
411
+ * - First checks built-in manifest via {@link getMacroManifest}
412
+ * - Then checks external macro imports via {@link parseMacroImportComments}
413
+ * - Falls back to generic hover for unknown macros
414
+ * 3. Check if cursor is on a field decorator via {@link findDecoratorAtPosition}
415
+ * - First checks built-in manifest (macros and decorators)
416
+ * - Then checks external package manifests via {@link getExternalDecoratorInfo}
417
+ * - Falls back to generic hover showing enclosing derive context
418
+ *
419
+ * For external macros (imported via `/** import macro {Name} from "package"; * /`),
420
+ * the function attempts to load the external package's manifest to retrieve
421
+ * descriptions and documentation. See {@link getExternalMacroInfo}.
241
422
  *
242
423
  * The returned QuickInfo includes:
243
- * - `kind`: Always `functionElement` (displayed as a function in the IDE)
424
+ * - `kind`: `keyword` for @derive, `functionElement` for macros/decorators
244
425
  * - `textSpan`: The highlighted range in the editor
245
426
  * - `displayParts`: The formatted display text (e.g., "@derive(Debug)")
246
427
  * - `documentation`: The macro/decorator description from the manifest
247
428
  *
248
429
  * @example
249
430
  * ```typescript
431
+ * // Hovering over "@derive" keyword
432
+ * const info = getMacroHoverInfo(text, 4, ts);
433
+ * // Returns QuickInfo with documentation about the derive directive
434
+ *
250
435
  * // Hovering over "Debug" in "@derive(Debug, Clone)"
251
436
  * const info = getMacroHoverInfo(text, 14, ts);
252
437
  * // Returns QuickInfo with:
253
438
  * // - displayParts: "@derive(Debug)"
254
439
  * // - documentation: "Generates a fmt_debug() method for debugging output"
255
440
  *
441
+ * // Hovering over external macro "Gigaform" in "@derive(Gigaform)"
442
+ * const info = getMacroHoverInfo(text, 14, ts);
443
+ * // Returns QuickInfo with description loaded from @playground/macro package
444
+ *
256
445
  * // Hovering over "@serde" field decorator
257
446
  * const info = getMacroHoverInfo(text, 5, ts);
258
447
  * // Returns QuickInfo with:
259
448
  * // - displayParts: "@serde"
260
449
  * // - documentation: "Serialization/deserialization field options"
450
+ *
451
+ * // Hovering over "@hiddenController" from external Gigaform macro
452
+ * const info = getMacroHoverInfo(text, 5, ts);
453
+ * // Returns QuickInfo with docs loaded from external package manifest
261
454
  * ```
262
455
  *
456
+ * @see {@link findDeriveKeywordAtPosition} - Locates the @derive keyword
263
457
  * @see {@link findDeriveAtPosition} - Locates macro names in @derive decorators
264
458
  * @see {@link findDecoratorAtPosition} - Locates field decorators
265
- * @see {@link getMacroManifest} - Provides macro/decorator metadata
459
+ * @see {@link findEnclosingDeriveContext} - Finds macros that apply to a position
460
+ * @see {@link getMacroManifest} - Provides built-in macro/decorator metadata
461
+ * @see {@link getExternalMacroInfo} - Provides external macro metadata
462
+ * @see {@link getExternalDecoratorInfo} - Provides external decorator metadata
266
463
  */
267
464
  function getMacroHoverInfo(text, position, tsModule) {
268
465
  const manifest = getMacroManifest();
269
- if (!manifest)
270
- return null;
271
- // Check for @derive(MacroName) in JSDoc comments
466
+ // 1. Check if hovering on @derive keyword itself
467
+ const deriveKeyword = findDeriveKeywordAtPosition(text, position);
468
+ if (deriveKeyword) {
469
+ return {
470
+ kind: tsModule.ScriptElementKind.keyword,
471
+ kindModifiers: "",
472
+ textSpan: {
473
+ start: deriveKeyword.start,
474
+ length: deriveKeyword.end - deriveKeyword.start,
475
+ },
476
+ displayParts: [{ text: "@derive", kind: "keyword" }],
477
+ documentation: [
478
+ {
479
+ text: "Derive directive - applies compile-time macros to generate methods and implementations.\n\n" +
480
+ "**Usage:** `/** @derive(MacroName, AnotherMacro) */`\n\n" +
481
+ "**Built-in macros:** Debug, Clone, Default, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize\n\n" +
482
+ "External macros can be imported using:\n" +
483
+ '`/** import macro {Name} from "package"; */`',
484
+ kind: "text",
485
+ },
486
+ ],
487
+ };
488
+ }
489
+ // Parse external macro imports for later use
490
+ const externalMacros = parseMacroImportComments(text);
491
+ // 2. Check for @derive(MacroName) in JSDoc comments
272
492
  const deriveMatch = findDeriveAtPosition(text, position);
273
493
  if (deriveMatch) {
274
- const macroInfo = manifest.macros.get(deriveMatch.macroName.toLowerCase());
494
+ // 2a. Check built-in manifest
495
+ const macroInfo = manifest?.macros.get(deriveMatch.macroName.toLowerCase());
275
496
  if (macroInfo) {
276
497
  return {
277
498
  kind: tsModule.ScriptElementKind.functionElement,
@@ -290,12 +511,63 @@ function getMacroHoverInfo(text, position, tsModule) {
290
511
  : [],
291
512
  };
292
513
  }
514
+ // 2b. Check external macro imports
515
+ const modulePath = externalMacros.get(deriveMatch.macroName);
516
+ if (modulePath) {
517
+ // Try to get detailed info from the external package manifest
518
+ const externalMacroInfo = getExternalMacroInfo(deriveMatch.macroName, modulePath);
519
+ const description = externalMacroInfo?.description
520
+ ? externalMacroInfo.description
521
+ : "This macro is loaded from an external package at compile time.";
522
+ return {
523
+ kind: tsModule.ScriptElementKind.functionElement,
524
+ kindModifiers: "external",
525
+ textSpan: {
526
+ start: deriveMatch.start,
527
+ length: deriveMatch.end - deriveMatch.start,
528
+ },
529
+ displayParts: [
530
+ { text: "@derive(", kind: "punctuation" },
531
+ { text: externalMacroInfo?.name ?? deriveMatch.macroName, kind: "functionName" },
532
+ { text: ")", kind: "punctuation" },
533
+ ],
534
+ documentation: [
535
+ {
536
+ text: `**External macro** from \`${modulePath}\`\n\n${description}`,
537
+ kind: "text",
538
+ },
539
+ ],
540
+ };
541
+ }
542
+ // 2c. Fallback for unknown/unrecognized macros
543
+ return {
544
+ kind: tsModule.ScriptElementKind.functionElement,
545
+ kindModifiers: "",
546
+ textSpan: {
547
+ start: deriveMatch.start,
548
+ length: deriveMatch.end - deriveMatch.start,
549
+ },
550
+ displayParts: [
551
+ { text: "@derive(", kind: "punctuation" },
552
+ { text: deriveMatch.macroName, kind: "functionName" },
553
+ { text: ")", kind: "punctuation" },
554
+ ],
555
+ documentation: [
556
+ {
557
+ text: `**Macro:** ${deriveMatch.macroName}\n\n` +
558
+ "This macro is not in the built-in manifest. If it's a custom macro, " +
559
+ "ensure it's imported using:\n\n" +
560
+ `\`/** import macro {${deriveMatch.macroName}} from "your-package"; */\``,
561
+ kind: "text",
562
+ },
563
+ ],
564
+ };
293
565
  }
294
- // Check for @decorator patterns
566
+ // 3. Check for @decorator patterns
295
567
  const decoratorMatch = findDecoratorAtPosition(text, position);
296
568
  if (decoratorMatch) {
297
- // Check if it's a macro name
298
- const macroInfo = manifest.macros.get(decoratorMatch.name.toLowerCase());
569
+ // 3a. Check if it's a built-in macro name
570
+ const macroInfo = manifest?.macros.get(decoratorMatch.name.toLowerCase());
299
571
  if (macroInfo) {
300
572
  return {
301
573
  kind: tsModule.ScriptElementKind.functionElement,
@@ -313,8 +585,8 @@ function getMacroHoverInfo(text, position, tsModule) {
313
585
  : [],
314
586
  };
315
587
  }
316
- // Check if it's a decorator
317
- const decoratorInfo = manifest.decorators.get(decoratorMatch.name.toLowerCase());
588
+ // 3b. Check if it's a built-in decorator
589
+ const decoratorInfo = manifest?.decorators.get(decoratorMatch.name.toLowerCase());
318
590
  if (decoratorInfo && decoratorInfo.docs) {
319
591
  return {
320
592
  kind: tsModule.ScriptElementKind.functionElement,
@@ -330,6 +602,62 @@ function getMacroHoverInfo(text, position, tsModule) {
330
602
  documentation: [{ text: decoratorInfo.docs, kind: "text" }],
331
603
  };
332
604
  }
605
+ // 3c. Check if this decorator is in a macro context (for external/custom decorators)
606
+ const enclosingMacros = findEnclosingDeriveContext(text, decoratorMatch.start);
607
+ if (enclosingMacros && enclosingMacros.length > 0) {
608
+ // Find which external macro might define this decorator
609
+ const likelySourceMacro = enclosingMacros.find((m) => externalMacros.has(m));
610
+ if (likelySourceMacro) {
611
+ const modulePath = externalMacros.get(likelySourceMacro);
612
+ // Try to get detailed decorator info from the external package
613
+ const externalDecoratorInfo = modulePath
614
+ ? getExternalDecoratorInfo(decoratorMatch.name, modulePath)
615
+ : null;
616
+ const description = externalDecoratorInfo?.docs
617
+ ? externalDecoratorInfo.docs
618
+ : "This decorator configures field-level behavior for the macro.";
619
+ return {
620
+ kind: tsModule.ScriptElementKind.functionElement,
621
+ kindModifiers: "external",
622
+ textSpan: {
623
+ start: decoratorMatch.start,
624
+ length: decoratorMatch.end - decoratorMatch.start,
625
+ },
626
+ displayParts: [
627
+ { text: "@", kind: "punctuation" },
628
+ { text: externalDecoratorInfo?.export ?? decoratorMatch.name, kind: "functionName" },
629
+ ],
630
+ documentation: [
631
+ {
632
+ text: `**Field decorator** from \`${likelySourceMacro}\` macro (\`${modulePath}\`)\n\n` +
633
+ description,
634
+ kind: "text",
635
+ },
636
+ ],
637
+ };
638
+ }
639
+ // Fallback: Generic decorator in macro context
640
+ return {
641
+ kind: tsModule.ScriptElementKind.functionElement,
642
+ kindModifiers: "",
643
+ textSpan: {
644
+ start: decoratorMatch.start,
645
+ length: decoratorMatch.end - decoratorMatch.start,
646
+ },
647
+ displayParts: [
648
+ { text: "@", kind: "punctuation" },
649
+ { text: decoratorMatch.name, kind: "functionName" },
650
+ ],
651
+ documentation: [
652
+ {
653
+ text: `**Field decorator:** ${decoratorMatch.name}\n\n` +
654
+ `Used with @derive(${enclosingMacros.join(", ")}).\n` +
655
+ "This decorator configures field-level behavior for the applied macros.",
656
+ kind: "text",
657
+ },
658
+ ],
659
+ };
660
+ }
333
661
  }
334
662
  return null;
335
663
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@macroforge/typescript-plugin",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "TypeScript language service plugin that augments classes decorated with @derive to include macro-generated methods.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "test": "bun run build && node --test tests/**/*.test.js"
34
34
  },
35
35
  "dependencies": {
36
- "macroforge": "^0.1.36"
36
+ "macroforge": "^0.1.38"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "typescript": ">=5.0.0"