@@ -2,7 +2,7 @@ import MagicString from 'magic-string'
22import type { SFCBlock } from 'vue/compiler-sfc'
33import { parse } from 'vue/compiler-sfc'
44import type { ComponentInfo , PublicPluginAPI as ComponentsApi } from 'unplugin-vue-components/types'
5- import type { ElementNode , RootNode , TemplateChildNode } from '@vue/compiler-core'
5+ import type { DirectiveNode , ElementNode , RootNode , TemplateChildNode } from '@vue/compiler-core'
66import type { AppConfig } from '../shared'
77import { pascalCase , isString , debug } from './utils'
88import { parseImports , parseExports } from './parse'
@@ -15,6 +15,20 @@ interface SfcRootNode extends RootNode {
1515
1616export const unresolvedIslandKey = '__viteIslandComponent'
1717
18+ export interface ListenerWarning {
19+ event : string
20+ tag : string
21+ line : number
22+ column : number
23+ message : string
24+ }
25+
26+ export interface WrapIslandsOptions {
27+ analyzeListeners ?: boolean
28+ wrapListeners ?: boolean
29+ warn ?: ( warning : ListenerWarning ) => void
30+ }
31+
1832export async function wrapLayout ( code : string , filename : string ) {
1933 const { descriptor : { template } , errors } = parse ( code , { filename } )
2034 if ( errors . length > 0 || ! template || ! isString ( template . attrs . layout ) ) return
@@ -38,7 +52,7 @@ export async function wrapLayout (code: string, filename: string) {
3852
3953const scriptClientRE = / < s c r i p t \b ( [ ^ > ] * \b c l i e n t : [ ^ > ] * ) > ( [ ^ ] * ?) < \/ s c r i p t > /
4054
41- export async function wrapIslandsInSFC ( config : AppConfig , code : string , filename : string ) {
55+ export async function wrapIslandsInSFC ( config : AppConfig , code : string , filename : string , options : WrapIslandsOptions = { } ) {
4256 code = code . replace ( scriptClientRE , ( _ , attrs , content ) =>
4357 `<script-client${ attrs } >${ content } </script-client>` )
4458
@@ -69,6 +83,9 @@ export async function wrapIslandsInSFC (config: AppConfig, code: string, filenam
6983
7084 const elements = sfcRootNode . children . filter ( ( n : any ) => n . tag ) as ElementNode [ ]
7185
86+ if ( options . analyzeListeners || options . wrapListeners )
87+ processSFCListeners ( elements , s , filename , options )
88+
7289 for ( const child of elements ) {
7390 await visitSFCNode ( child , s , resolveComponentImport )
7491 }
@@ -97,6 +114,59 @@ export async function wrapIslandsInSFC (config: AppConfig, code: string, filenam
97114 }
98115}
99116
117+ function processSFCListeners ( elements : ElementNode [ ] , s : MagicString , filename : string , options : WrapIslandsOptions ) {
118+ const walk = ( node : ElementNode , insideIsland = false ) => {
119+ const hasClientDirective = node . props . some ( prop => 'name' in prop && prop . name . startsWith ( 'client:' ) )
120+ const withinIsland = insideIsland || hasClientDirective || node . tag === 'Island'
121+
122+ for ( const prop of node . props ) {
123+ if ( ! isOnDirective ( prop ) ) continue
124+ const event = getEventName ( prop )
125+ const location = prop . loc . start
126+ const warning : ListenerWarning = {
127+ event,
128+ tag : node . tag ,
129+ line : location . line ,
130+ column : location . column ,
131+ message : `[iles] Listener '${ event } ' on <${ node . tag } > in ${ filename } :${ location . line } :${ location . column } will be static at runtime unless wrapped in <Island client:...>.` ,
132+ }
133+
134+ if ( ! withinIsland && options . analyzeListeners )
135+ options . warn ?.( warning )
136+
137+ if ( options . wrapListeners && prop . exp ?. loc ?. source )
138+ s . overwrite ( prop . exp . loc . start . offset , prop . exp . loc . end . offset , guardedVueExpression ( prop . exp . loc . source , warning ) )
139+ }
140+
141+ for ( const child of node . children || [ ] ) {
142+ if ( 'tag' in ( child as any ) )
143+ walk ( child as ElementNode , withinIsland )
144+ }
145+ }
146+
147+ for ( const element of elements )
148+ walk ( element )
149+ }
150+
151+ function guardedVueExpression ( expression : string , warning : ListenerWarning ) {
152+ const details = JSON . stringify ( {
153+ source : 'vue' ,
154+ ...warning ,
155+ } )
156+ const run = `{ const __ile_handler = (${ expression } ); return typeof __ile_handler === 'function' ? __ile_handler($event) : __ile_handler }`
157+ return `($event) => (window.__ILE_GUARD_LISTENER_CALL__ ? window.__ILE_GUARD_LISTENER_CALL__(() => ${ run } , $event, ${ details } ) : (() => ${ run } )())`
158+ }
159+
160+ function isOnDirective ( prop : any ) : prop is DirectiveNode {
161+ return prop ?. type === 7 && prop ?. name === 'on'
162+ }
163+
164+ function getEventName ( prop : DirectiveNode ) {
165+ if ( prop . arg ?. type === 4 )
166+ return prop . arg . content || 'event'
167+ return 'event'
168+ }
169+
100170async function visitSFCNode ( node : ElementNode , s : MagicString , resolveComponentImport : ( strategy : string , tag : string ) => Promise < ComponentInfo > ) {
101171 const strategy = 'props' in node
102172 && node . props . find ( prop => prop . name . startsWith ( 'client:' ) ) ?. name
0 commit comments