feat(plugin): experimental <script main> SFC block - #347
Conversation
Group a component's main-thread code in a dedicated <script main> block instead of marking every function with a 'main thread' directive. The block is lowered by a pure SFC-text rewrite in a pre-vue-loader (enforce: 'pre') on .vue files: every top-level function gets the 'main thread' directive injected and the block merges into <script setup> (or becomes one). vue-loader's experimentalInlineMatchResource bakes pre-loaders into every ?vue&type= sub-request, so the connector, script, and template sub-modules all parse the same rewritten source on both layers, keeping the SWC worklet _wkltId content hashes in sync between the BG context objects and MT LEPUS registrations. Downstream semantics (value capture, MainThreadRef, runOnMainThread/runOnBackground, shared modules) are unchanged. Lowering rules: top-level functions become worklets; imports merge into the setup scope with exact-duplicate specifiers deduplicated; other variable declarations stay background-evaluated (captured by closure); top-level side effects and exports are compile errors; HTML comments mentioning <script main> are ignored by the block scanner. Includes unit tests (including an end-to-end compiler-sfc + SWC pass asserting matching BG/MT worklet hashes), a script-main-block entry in the main-thread example (verified: both worklets register with matching ids in the built bundle), and EN/ZH guide docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGiiwQGSBEokk6ggrt45Fv
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
CI note: the Lint job failure is pre-existing on the base branch, not from this diff — #346 already carries the one-line fix (moving the suppression onto the parameter line). Once that lands on Generated by Claude Code |
Add a dedicated guide page for the experimental <script main> syntax
style (EN/ZH, sidebar entries, cross-links from the main-thread-script
guide and both tutorials), and migrate three existing MTS examples from
per-function 'main thread' directives to the block style as living
proof it works:
- main-thread/shared-module: MT tap handler calling a
`with { runtime: 'shared' }` import directly on the main thread
- swiper/SwiperMTS: three touch handlers sharing MainThreadRef state
- gallery/GalleryComplete: worklet-calling-worklet MTS scrollbar
Verification (Lynx for Web harness, headless Chromium + web-core):
- shared-module: tap cycles box color #4FC3F7 → #81C784 → #FFB74D via
the MT handler (pixel-sampled)
- gallery: the MTS scrollbar thumb tracks the auto-scrolling list
- script-main-block: MT tap → runOnBackground round trip re-renders
the tap counter, with pixel-exact behavior parity against the
directive-style cross-thread-calls twin
- all three migrations also compile to worklet bodies byte-identical
to their directive-style originals (registrations diffed after
normalizing content-hash ids), so behavior is preserved by
construction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGiiwQGSBEokk6ggrt45Fv
|
CI is fully green now: #346's lint fix landed on Generated by Claude Code |
Experiment for #314 — a dedicated
<script main>SFC block so a component's main-thread code lives in one place instead of per-function'main thread'directives:How it works
The block is lowered by a pure SFC-text rewrite in a pre-vue-loader (
enforce: 'pre'on.vue), keeping the whole existing worklet pipeline untouched:<script main>(declaration,const f = () => {},const f = function () {}, including expression-body arrows) gets the'main thread'directive injected as its first body statement.<script setup>(or the block becomes the setup block if none exists), so worklets capture setup bindings by closure and the template sees the functions as ordinary setup bindings (:main-thread-bindtap="onTap").{ _wkltId }context objects, MT layer emitsregisterWorkletInternal()calls.The subtle part is dual-layer hash consistency: vue-loader's
experimentalInlineMatchResourcebakes the remaining loader chain (pre-loaders included) into every?vue&type=sub-request, so the connector, script, and template sub-modules all parse the same rewritten source on both layers —_wkltIdcontent hashes stay in sync by construction. The rewrite is also idempotent (once lowered, no<script main>block remains).Semantics
<script main>const/letuseMainThreadRef()for MT state)<script main setup>, two main blocks,langmismatch,srcErrors strip the block from the output (never letting vue-loader misparse it as an Options API script) and surface via
emitError.Docs
<script main>Block" (EN + ZH) covering syntax, semantics, a migration guide, style trade-offs, and limitations; added to the sidebar with anexperimentaltag.Example migrations (proof the style is usable)
Three existing MTS examples were migrated from directives to the block style, each exercising a distinct capability:
main-thread/shared-modulewith { runtime: 'shared' }import called directly on the MT inside the blockswiper/SwiperMTSMainThreadRefstate across block functionsgallery/GalleryCompleteonScrollMTS→adjustScrollbarMTS) +SystemInfoPlus the new
script-main-blockentry in the main-thread example (across-thread-callstwin in the new syntax).Verification
@vue/compiler-sfc+ both SWC worklet transforms asserting the BG_wkltIdequals the MT registration hash.registerWorkletInternalfunction bodies are identical after normalizing content-hash ids — behavior preserved by construction.@lynx-js/web-core, pixel-level assertions through the closed shadow DOM):shared-moduletap cycles the box color#4FC3F7 → #81C784 → #FFB74Dvia the MT handler;gallery's MTS scrollbar thumb tracks the auto-scrolling list;script-main-block's MT tap →runOnBackgroundround trip re-renders the tap counter with pixel-exact parity against the directive-stylecross-thread-callstwin. (TherunOnMainThreadcolor leg is a no-op in the current web-core environment for both styles — pre-existing upstream behavior, not a block regression. SwiperMTS can't be pixel-tested on web because its image/linear layout doesn't render in the harness even pre-migration; it's covered by the compiled-equivalence check.)Notes / open questions
<script main>as proposed in Feature request: native<script main>SFC block instead of per-function 'main thread' directive #314.<script main-thread>would be more explicit; easy to alias.const mainRef = ref()inside the main block expecting an MT ref. This implementation keeps such statements background-evaluated (consistent with today's capture semantics) and documentsuseMainThreadRef()instead — auto-rewritingref()felt too magical for a first pass.types/volar-plugincould learn it in a follow-up.vue-sfc-script-extractor): a</script>inside a string literal of the block isn't supported. HTML comments mentioning<script main>are handled.@babel/parser(+@babel/types, types-only) as avue-lynxdependency for the lowering — already present in the tree via@vue/compiler-core.Changeset included (
vue-lynxminor, example packages patch).Closes #314
🤖 Generated with Claude Code
https://claude.ai/code/session_01VGiiwQGSBEokk6ggrt45Fv