Skip to content

feat: AST-based compiler macro system for extendComponentMeta#116

Open
hendrikheil wants to merge 1 commit into
nuxt-content:mainfrom
hendrikheil:feat/compiler-macro-extend-meta
Open

feat: AST-based compiler macro system for extendComponentMeta#116
hendrikheil wants to merge 1 commit into
nuxt-content:mainfrom
hendrikheil:feat/compiler-macro-extend-meta

Conversation

@hendrikheil

Copy link
Copy Markdown

Summary

  • Replaces the fragile regex+eval hack for extendComponentMeta with proper AST-based parsing via oxc-parser + oxc-walker
  • Adds extendMetaFunctions module option to register named compiler macros, each with an optional transform hook
  • Macro calls are stripped from browser output by the Vite plugin (true zero-runtime compiler macros)
  • Custom macro names get auto-generated declare function type declarations

New API

// nuxt.config.ts
componentMeta: {
  extendMetaFunctions: [
    { name: 'extendComponentMeta' },
    { name: 'extendProps', transform: (extracted) => ({ _studio: extracted }) }
  ]
}
<!-- In a component -->
<script setup>
extendProps({ filePath: 'file-picker' })
</script>

Produces component.meta._studio = { filePath: 'file-picker' } — with no runtime cost.

Changes

  • src/parser/macro-extractor.ts — new file: extractMacroMeta() and stripMacroCalls() using oxc AST
  • src/types/module.ts — new ExtendMetaFunction interface, extendMetaFunctions option
  • src/parser/meta-parser.ts — replaces regex+eval with extractMacroMeta
  • src/utils/unplugin.ts — adds Vite transform hook to strip macro calls
  • src/module.ts — wires defaults, generates type declarations for custom macro names
  • src/parser/index.ts — adds extendMetaFunctions support to standalone parser

Test plan

  • Existing extendComponentMeta test passes (backward compatible)
  • New extendProps with transform test passes (meta._studio.filePath === 'file-picker')
  • Verify macro calls are absent from browser bundle output

🤖 Generated with Claude Code

@hendrikheil hendrikheil force-pushed the feat/compiler-macro-extend-meta branch from 22a8396 to ef01cc7 Compare May 22, 2026 14:32
@pkg-pr-new

pkg-pr-new Bot commented May 22, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/nuxt-component-meta@116

commit: 05f350d

@atinux atinux requested a review from farnabaz June 3, 2026 11:25
Comment thread src/parser/macro-extractor.ts Outdated
*/
function extractScriptContent(code: string, filename: string): string {
if (!filename.endsWith('.vue')) return code
const { descriptor } = parseSfc(code, { filename, ignoreEmpty: false })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I test the code in playground I got this error:

Cannot find package 'velocityjs' imported from /Users/.../projects/nuxt/nuxt-component-meta/playground/.nuxt/dev/index.mjs

Which comes from '@vue/compiler-sfc'. Since we are using scf compiler just to get the <script> tag content here, I believe using regex will be safe and faster and reduce dependencies.

I'll give it a try

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried it — regex extraction works and drops @vue/compiler-sfc entirely. Benchmarks:

before (sfc) after (regex) speedup
script extraction ~0.0009 ms ~0.0001 ms 8.7×
full pipeline (literal arg) 0.636 ms 0.556 ms 1.14×

End-to-end gain is modest (pipeline is dominated by jiti.evalModule), but the crash is gone and we shed a dependency. Only caveat: the regex won't handle a </script> inside a string/comment like the full parser would — a non-issue for top-level macro statements.

Benchmarks added under test/*.bench.ts (pnpm test:bench).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally went with compiler-sfc to stay within ecosystem tooling to ensure we're as compatible as possible.

I think since the performance improvement is modest, we should maybe try to fix the issue you encountered in the playground instead of switching to the regex?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@farnabaz I looked into your issue and I think we could easily just solve this by adding @vue/compiler-sfc to externals.
This will emit some warnings still, but not cause any errors now.

These warnings are unlikely to be avoidable unless we explicitly suppress them as the playground imports our usually build-time only code at runtime (

import { getComponentMeta } from "../../../src/parser"
).

I'd argue that switching back and accepting the warnings is a better approach?

@farnabaz farnabaz Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hendrikheil Sorry for the late response
I understand your argument, but we only need the text inside <script> tag, using whole SFC parser to get string content is not ideal IMO.

Have you tested the current state of PR, is there some issues in the behavior?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested it. There is one real issue right now, generic components. A <script setup> whose
generic attribute contains > (e.g. generic="T extends Record<string, unknown>")
breaks the current regex. <script\b[^>]*> stops at the first > inside the type,
oxc then fails to parse, and the macro is silently dropped. @vue/compiler-sfc
handles that.

That said, the regex can too be adjusted to support it too:

const SCRIPT_BLOCK_RE = /<script\b(?:"[^"]*"|'[^']*'|[^>])*>([\s\S]*?)<\/script>/gi

Simple / generic / multi-block all pass with that. The only case neither handles is a
literal </script> inside a string. However, that's an HTML tokenization rule, so
compiler-sfc truncates there too.

So with the tweak the regex matches compiler-sfc for every realistic case - you've
convinced me, no need for the full SFC parser (right now). I'll apply the patch and add tests for
the generic + multi-block cases.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you think we should keep the bench tests?
I think in general we could clean the pr at least slimming / removing most of the comments?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure lets clean it up, appreciate if you take care of it.
Also do you mind squashing all commit into a verified commit? new Github organization rule 🙏

@hendrikheil hendrikheil Jul 2, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing, works for me, I'll clean the PR up.

I also had Fable bake up a PoC downstream in nuxt-studio to demo what this enables 🔥

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to go, I think this can be merged and released?

Replaces the regex+eval extraction of `extendComponentMeta` with a
compiler macro system built on oxc-parser and oxc-walker.

- Add `extendMetaFunctions` option to register named compiler macros,
  each with an optional `transform` hook applied to the extracted argument
- New `src/parser/macro-extractor.ts` handles AST-based extraction
  (extractMacroMeta) and browser-output stripping (stripMacroCalls)
- Macro arguments support object/array literals and imported const
  references, evaluated at build time via jiti
- Vite unplugin gains a transform hook that strips macro calls from
  browser output, making them true zero-runtime compiler macros
- Custom macro names get global `declare function` type declarations
  in the generated component-meta.d.ts
- Standalone parser (src/parser/index.ts) also supports extendMetaFunctions

Co-Authored-By: Claude <noreply@anthropic.com>
@hendrikheil hendrikheil force-pushed the feat/compiler-macro-extend-meta branch from 5eaf124 to 1e6bcc6 Compare July 2, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants