From 24e8e029ac4d03a8ce4d2615cd4d6ff738a39ebf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:17:21 +0000 Subject: [PATCH 1/5] feat: add static listener diagnostics and runtime listener guard Agent-Logs-Url: https://github.com/ElMassimo/iles/sessions/64e6d452-d6f0-447a-9605-2933041a844f Co-authored-by: ElMassimo <1158253+ElMassimo@users.noreply.github.com> --- packages/iles/jsx-runtime.js | 19 +++++ .../src/client/app/components/DebugPanel.vue | 11 +++ .../src/client/app/composables/devtools.ts | 10 +++ packages/iles/src/client/app/index.ts | 1 + packages/iles/src/client/app/listenerGuard.ts | 49 ++++++++++++ packages/iles/src/node/plugin/plugin.ts | 19 ++++- .../iles/src/node/plugin/remarkWrapIslands.ts | 15 ++++ packages/iles/src/node/plugin/wrap.ts | 74 ++++++++++++++++++- packages/iles/tests/listener-guard.spec.ts | 40 ++++++++++ .../iles/tests/remark-wrap-islands.spec.ts | 34 +++++++++ packages/iles/tests/wrap.spec.ts | 37 ++++++++++ 11 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 packages/iles/src/client/app/listenerGuard.ts create mode 100644 packages/iles/tests/listener-guard.spec.ts create mode 100644 packages/iles/tests/remark-wrap-islands.spec.ts create mode 100644 packages/iles/tests/wrap.spec.ts diff --git a/packages/iles/jsx-runtime.js b/packages/iles/jsx-runtime.js index 75a6cdc3..edbae83d 100644 --- a/packages/iles/jsx-runtime.js +++ b/packages/iles/jsx-runtime.js @@ -5,11 +5,13 @@ import { createStaticVNode as raw, Fragment, } from 'vue' +import { guardListenerCall } from './dist/client/app/listenerGuard' // Internal: Compatibility layer with the automatic JSX runtime of React. // // NOTE: Supports v-slots for consistency with @vue/babel-plugin-jsx. function jsx (type, { children, 'v-slots': vSlots, ...props }) { + wrapListeners(props, type) let slots if (children) { @@ -31,6 +33,23 @@ function jsx (type, { children, 'v-slots': vSlots, ...props }) { return createVNode(type, props, slots) } +function wrapListeners (props, type) { + if (!props || typeof window === 'undefined') return + + for (const [key, value] of Object.entries(props)) { + if (!/^on[A-Z]/.test(key) || typeof value !== 'function') continue + props[key] = (...args) => guardListenerCall( + () => value(...args), + args[0], + { + source: 'mdx', + event: key.slice(2).toLowerCase(), + tag: typeof type === 'string' ? type : type?.name || 'Component', + }, + ) + } +} + // Internal: Extends it to be a stateful component that can perform prop checks. function defineComponent (MDXContent, definition) { return defineVueComponent({ diff --git a/packages/iles/src/client/app/components/DebugPanel.vue b/packages/iles/src/client/app/components/DebugPanel.vue index cba6169b..b640d320 100644 --- a/packages/iles/src/client/app/components/DebugPanel.vue +++ b/packages/iles/src/client/app/components/DebugPanel.vue @@ -11,6 +11,7 @@ export default defineComponent({ const content = ref(null) const open = ref(false) const buttonLabel = computed(() => message.value || 'Debug') + const listenerWarnings = computed(() => (window as any).__ILE_DEVTOOLS__?.getListenerWarnings?.() || []) const cleanPage = computed(() => { const layout = page.value.layoutName || 'false' @@ -54,6 +55,7 @@ export default defineComponent({ cleanPage, copyAll, content, + listenerWarnings, } }, }) @@ -63,6 +65,9 @@ export default defineComponent({

{{ buttonLabel }}Open DevTools to inspect islands 🏝

{{ cleanPage }}
+
+{{ listenerWarnings.map((item: any) => item.message).join('\n\n') }}
+    
@@ -160,4 +165,10 @@ export default defineComponent({ .block + .block { margin-top: 8px; } + +.warning { + border-top-color: #EF4444; + color: #FCA5A5; + white-space: pre-wrap; +} diff --git a/packages/iles/src/client/app/composables/devtools.ts b/packages/iles/src/client/app/composables/devtools.ts index 9a730182..439e46fa 100644 --- a/packages/iles/src/client/app/composables/devtools.ts +++ b/packages/iles/src/client/app/composables/devtools.ts @@ -16,6 +16,7 @@ const HYDRATION_LAYER_ID = 'iles:hydration' let lastUsedIslandId = 0 const islandsById = reactive>({}) const islands = computed(() => Object.values(islandsById)) +const listenerWarnings = reactive([]) const strategyLabels: Record = { 'client:idle': 'whenIdle', @@ -88,6 +89,15 @@ const devtools = { console.info(`🏝 hydrated ${component}`, el, slots) } }, + + reportStaticListenerWarning (warning: any) { + if (listenerWarnings.some(item => item.key === warning.key)) return + listenerWarnings.push({ time: Date.now(), ...warning }) + }, + + getListenerWarnings () { + return listenerWarnings + }, } ;(window as any).__ILE_DEVTOOLS__ = devtools diff --git a/packages/iles/src/client/app/index.ts b/packages/iles/src/client/app/index.ts index d175c9e7..afe6cf2e 100644 --- a/packages/iles/src/client/app/index.ts +++ b/packages/iles/src/client/app/index.ts @@ -1,6 +1,7 @@ import { createApp as createClientApp, createSSRApp, ref } from 'vue' import { createMemoryHistory, createRouter as createVueRouter, createWebHistory } from 'vue-router' import { createHead } from '@unhead/vue' +import './listenerGuard' import routes from '@islands/routes' import config from '@islands/app-config' diff --git a/packages/iles/src/client/app/listenerGuard.ts b/packages/iles/src/client/app/listenerGuard.ts new file mode 100644 index 00000000..e272e1f7 --- /dev/null +++ b/packages/iles/src/client/app/listenerGuard.ts @@ -0,0 +1,49 @@ +interface ListenerWarningDetails { + source?: string + event?: string + tag?: string + line?: number + column?: number +} + +type DevtoolsApi = { + reportStaticListenerWarning?: (payload: any) => void +} + +const warned = new Set() + +function getEventTarget (event: any) { + const target = event?.composedPath?.()?.[0] || event?.target + return target as Element | null +} + +function hasIslandContext (target: Element | null) { + return Boolean(target?.closest?.('ile-root[hydrated]')) +} + +export function guardListenerCall (handler: () => T, event: Event, details: ListenerWarningDetails = {}): T { + const target = getEventTarget(event) + if (!hasIslandContext(target)) { + const key = [ + details.source || 'listener', + details.event || event?.type || 'event', + details.tag || target?.nodeName || 'unknown', + details.line || 0, + details.column || 0, + ].join(':') + + if (!warned.has(key)) { + warned.add(key) + const location = details.line ? `:${details.line}:${details.column || 0}` : '' + const message = `[iles] Event listener '${details.event || event?.type || 'event'}' on <${details.tag || 'unknown'}>${location} ran without island context. Wrap this component in or move interaction into an island.` + console.error(message, { details, event, target }) + ;(window as any).__ILE_DEVTOOLS__?.reportStaticListenerWarning?.({ key, message, details, target, eventType: event?.type }) + } + } + + return handler() +} + +if (typeof window !== 'undefined') + (window as any).__ILE_GUARD_LISTENER_CALL__ = guardListenerCall + diff --git a/packages/iles/src/node/plugin/plugin.ts b/packages/iles/src/node/plugin/plugin.ts index e3749217..e0757710 100644 --- a/packages/iles/src/node/plugin/plugin.ts +++ b/packages/iles/src/node/plugin/plugin.ts @@ -134,8 +134,23 @@ export default function IslandsPlugins (appConfig: AppConfig): PluginOption[] { if (query.vue !== undefined && query.type === 'script-client') return 'export default {}; if (import.meta.hot) import.meta.hot.accept()' - if (isSFCMain(path, query) && code.includes('client:') && code.includes(' console.warn(message), + }) + + if (sfcMain && shouldProcessListeners && code.includes(' console.warn(message), + }) }, }, { diff --git a/packages/iles/src/node/plugin/remarkWrapIslands.ts b/packages/iles/src/node/plugin/remarkWrapIslands.ts index f9172ffe..6f3cb8bb 100644 --- a/packages/iles/src/node/plugin/remarkWrapIslands.ts +++ b/packages/iles/src/node/plugin/remarkWrapIslands.ts @@ -26,6 +26,9 @@ export default ({ config }: { config: AppConfig }) => async (ast: any, file: any wrapWithIsland(strategy, node, resolveComponentImport) return SKIP } + + if (isJsxElement(node)) + warnStaticListeners(node, file.path) }) const componentsToImport = await Promise.all(componentPromises) @@ -42,6 +45,18 @@ export default ({ config }: { config: AppConfig }) => async (ast: any, file: any } } +function warnStaticListeners (node: MdxJsxFlowElement | MdxJsxTextElement, filename: string) { + const tagName = node.name || 'Component' + for (const attr of node.attributes) { + if ('name' in attr && /^on[A-Z]/.test(String(attr.name))) { + const point = (attr as any).position?.start || (node as any).position?.start + const line = point?.line || 0 + const column = point?.column || 0 + console.warn(`[iles] Listener '${attr.name}' on <${tagName}> in ${filename}:${line}:${column} will be static at runtime unless wrapped in .`) + } + } +} + function isJsxElement (node: Node): node is MdxJsxFlowElement | MdxJsxTextElement { return node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement' } diff --git a/packages/iles/src/node/plugin/wrap.ts b/packages/iles/src/node/plugin/wrap.ts index 48e59d0d..c693c6ab 100644 --- a/packages/iles/src/node/plugin/wrap.ts +++ b/packages/iles/src/node/plugin/wrap.ts @@ -2,7 +2,7 @@ import MagicString from 'magic-string' import type { SFCBlock } from 'vue/compiler-sfc' import { parse } from 'vue/compiler-sfc' import type { ComponentInfo, PublicPluginAPI as ComponentsApi } from 'unplugin-vue-components/types' -import type { ElementNode, RootNode, TemplateChildNode } from '@vue/compiler-core' +import type { DirectiveNode, ElementNode, RootNode, TemplateChildNode } from '@vue/compiler-core' import type { AppConfig } from '../shared' import { pascalCase, isString, debug } from './utils' import { parseImports, parseExports } from './parse' @@ -15,6 +15,20 @@ interface SfcRootNode extends RootNode { export const unresolvedIslandKey = '__viteIslandComponent' +export interface ListenerWarning { + event: string + tag: string + line: number + column: number + message: string +} + +export interface WrapIslandsOptions { + analyzeListeners?: boolean + wrapListeners?: boolean + warn?: (warning: ListenerWarning) => void +} + export async function wrapLayout (code: string, filename: string) { const { descriptor: { template }, errors } = parse(code, { filename }) if (errors.length > 0 || !template || !isString(template.attrs.layout)) return @@ -38,7 +52,7 @@ export async function wrapLayout (code: string, filename: string) { const scriptClientRE = /]*\bclient:[^>]*)>([^]*?)<\/script>/ -export async function wrapIslandsInSFC (config: AppConfig, code: string, filename: string) { +export async function wrapIslandsInSFC (config: AppConfig, code: string, filename: string, options: WrapIslandsOptions = {}) { code = code.replace(scriptClientRE, (_, attrs, content) => `${content}`) @@ -69,6 +83,9 @@ export async function wrapIslandsInSFC (config: AppConfig, code: string, filenam const elements = sfcRootNode.children.filter((n: any) => n.tag) as ElementNode[] + if (options.analyzeListeners || options.wrapListeners) + processSFCListeners(elements, s, filename, options) + for (const child of elements) { await visitSFCNode(child, s, resolveComponentImport) } @@ -97,6 +114,59 @@ export async function wrapIslandsInSFC (config: AppConfig, code: string, filenam } } +function processSFCListeners (elements: ElementNode[], s: MagicString, filename: string, options: WrapIslandsOptions) { + const walk = (node: ElementNode, insideIsland = false) => { + const hasClientDirective = node.props.some(prop => 'name' in prop && prop.name.startsWith('client:')) + const withinIsland = insideIsland || hasClientDirective || node.tag === 'Island' + + for (const prop of node.props) { + if (!isOnDirective(prop)) continue + const event = getEventName(prop) + const location = prop.loc.start + const warning: ListenerWarning = { + event, + tag: node.tag, + line: location.line, + column: location.column, + message: `[iles] Listener '${event}' on <${node.tag}> in ${filename}:${location.line}:${location.column} will be static at runtime unless wrapped in .`, + } + + if (!withinIsland && options.analyzeListeners) + options.warn?.(warning) + + if (options.wrapListeners && prop.exp?.loc?.source) + s.overwrite(prop.exp.loc.start.offset, prop.exp.loc.end.offset, guardedVueExpression(prop.exp.loc.source, warning)) + } + + for (const child of node.children || []) { + if ('tag' in (child as any)) + walk(child as ElementNode, withinIsland) + } + } + + for (const element of elements) + walk(element) +} + +function guardedVueExpression (expression: string, warning: ListenerWarning) { + const details = JSON.stringify({ + source: 'vue', + ...warning, + }) + const run = `{ const __ile_handler = (${expression}); return typeof __ile_handler === 'function' ? __ile_handler($event) : __ile_handler }` + return `($event) => (window.__ILE_GUARD_LISTENER_CALL__ ? window.__ILE_GUARD_LISTENER_CALL__(() => ${run}, $event, ${details}) : (() => ${run})())` +} + +function isOnDirective (prop: any): prop is DirectiveNode { + return prop?.type === 7 && prop?.name === 'on' +} + +function getEventName (prop: DirectiveNode) { + if (prop.arg?.type === 4) + return prop.arg.content || 'event' + return 'event' +} + async function visitSFCNode (node: ElementNode, s: MagicString, resolveComponentImport: (strategy: string, tag: string) => Promise) { const strategy = 'props' in node && node.props.find(prop => prop.name.startsWith('client:'))?.name diff --git a/packages/iles/tests/listener-guard.spec.ts b/packages/iles/tests/listener-guard.spec.ts new file mode 100644 index 00000000..357cb5b3 --- /dev/null +++ b/packages/iles/tests/listener-guard.spec.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from 'vite-plus/test' + +import { guardListenerCall } from '@client/app/listenerGuard' + +describe('listener guard', () => { + test('calls original handler when no island context and reports once', () => { + const errors: any[] = [] + const previousConsoleError = console.error + console.error = (...args: any[]) => { errors.push(args) } + ;(globalThis as any).window = { + __ILE_DEVTOOLS__: { reportStaticListenerWarning: () => {} }, + } + + const event = { type: 'click', target: { nodeName: 'BUTTON', closest: () => null } } as any + let called = 0 + guardListenerCall(() => ++called, event, { source: 'vue', event: 'click', tag: 'button', line: 1, column: 2 }) + guardListenerCall(() => ++called, event, { source: 'vue', event: 'click', tag: 'button', line: 1, column: 2 }) + + console.error = previousConsoleError + expect(called).toBe(2) + expect(errors.length).toBe(1) + }) + + test('does not report when inside an island', () => { + const errors: any[] = [] + const previousConsoleError = console.error + console.error = (...args: any[]) => { errors.push(args) } + ;(globalThis as any).window = { + __ILE_DEVTOOLS__: { reportStaticListenerWarning: () => {} }, + } + + const event = { type: 'click', target: { closest: () => ({}) } } as any + const result = guardListenerCall(() => 42, event, { source: 'mdx', event: 'click', tag: 'button' }) + + console.error = previousConsoleError + expect(result).toBe(42) + expect(errors.length).toBe(0) + }) +}) + diff --git a/packages/iles/tests/remark-wrap-islands.spec.ts b/packages/iles/tests/remark-wrap-islands.spec.ts new file mode 100644 index 00000000..11b9a7f3 --- /dev/null +++ b/packages/iles/tests/remark-wrap-islands.spec.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from 'vite-plus/test' +import type { AppConfig } from 'iles' + +import remarkWrapIslands from '@node/plugin/remarkWrapIslands' + +describe('remarkWrapIslands listener diagnostics', () => { + test('warns for mdx listeners outside islands', async () => { + const warnings: string[] = [] + const previousWarn = console.warn + console.warn = (message: any) => warnings.push(String(message)) + + const plugin = remarkWrapIslands({ + config: { + namedPlugins: { components: { api: {} } }, + } as any as AppConfig, + }) + + const ast: any = { + type: 'root', + children: [{ + type: 'mdxJsxFlowElement', + name: 'button', + position: { start: { line: 2, column: 3 } }, + attributes: [{ type: 'mdxJsxAttribute', name: 'onClick', position: { start: { line: 2, column: 10 } } }], + }], + } + await plugin(ast, { path: '/src/pages/demo.mdx' }) + + console.warn = previousWarn + expect(warnings[0]).toContain("Listener 'onClick'") + expect(warnings[0]).toContain('/src/pages/demo.mdx') + }) +}) + diff --git a/packages/iles/tests/wrap.spec.ts b/packages/iles/tests/wrap.spec.ts new file mode 100644 index 00000000..a4356e5a --- /dev/null +++ b/packages/iles/tests/wrap.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from 'vite-plus/test' +import type { AppConfig } from 'iles' + +import { wrapIslandsInSFC } from '@node/plugin/wrap' + +const minimalConfig = { + namedPlugins: { + components: { + api: { + findComponent: async () => ({ from: '/comp.vue', name: 'default' }), + stringifyImport: () => `import __ile_components_0 from '/comp.vue'`, + }, + }, + pages: { api: { isPage: () => true } }, + }, + resolvePath: async (path: string) => path, +} as any as AppConfig + +describe('wrapIslandsInSFC listener diagnostics', () => { + test('warns for listeners outside islands', async () => { + const warnings: string[] = [] + await wrapIslandsInSFC(minimalConfig, ``, '/src/pages/a.vue', { + analyzeListeners: true, + warn: warning => warnings.push(warning.message), + }) + expect(warnings[0]).toContain("Listener 'click'") + expect(warnings[0]).toContain('/src/pages/a.vue') + }) + + test('wraps listener handlers', async () => { + const result = await wrapIslandsInSFC(minimalConfig, ``, '/src/pages/a.vue', { + wrapListeners: true, + }) + expect(result?.code).toContain('__ILE_GUARD_LISTENER_CALL__') + }) +}) + From e182ca44b14004f75bf94f538d445a2809b4d15b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:18:45 +0000 Subject: [PATCH 2/5] fix: avoid dist import in jsx runtime listener wrapper Agent-Logs-Url: https://github.com/ElMassimo/iles/sessions/64e6d452-d6f0-447a-9605-2933041a844f Co-authored-by: ElMassimo <1158253+ElMassimo@users.noreply.github.com> --- packages/iles/jsx-runtime.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/iles/jsx-runtime.js b/packages/iles/jsx-runtime.js index edbae83d..4e29b69e 100644 --- a/packages/iles/jsx-runtime.js +++ b/packages/iles/jsx-runtime.js @@ -5,7 +5,6 @@ import { createStaticVNode as raw, Fragment, } from 'vue' -import { guardListenerCall } from './dist/client/app/listenerGuard' // Internal: Compatibility layer with the automatic JSX runtime of React. // @@ -50,6 +49,11 @@ function wrapListeners (props, type) { } } +function guardListenerCall (handler, event, details) { + const guard = typeof window !== 'undefined' && window.__ILE_GUARD_LISTENER_CALL__ + return guard ? guard(handler, event, details) : handler() +} + // Internal: Extends it to be a stateful component that can perform prop checks. function defineComponent (MDXContent, definition) { return defineVueComponent({ From cab75a0690647628443f8d7e8735f527595a4a34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:21:37 +0000 Subject: [PATCH 3/5] fix: address review feedback and sanitize vue listener wrapping Agent-Logs-Url: https://github.com/ElMassimo/iles/sessions/64e6d452-d6f0-447a-9605-2933041a844f Co-authored-by: ElMassimo <1158253+ElMassimo@users.noreply.github.com> --- packages/iles/jsx-runtime.js | 2 +- packages/iles/src/client/app/components/DebugPanel.vue | 4 +++- packages/iles/src/client/app/listenerGuard.ts | 1 - packages/iles/src/node/plugin/wrap.ts | 8 ++++---- packages/iles/tests/listener-guard.spec.ts | 1 - packages/iles/tests/remark-wrap-islands.spec.ts | 1 - packages/iles/tests/wrap.spec.ts | 1 - 7 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/iles/jsx-runtime.js b/packages/iles/jsx-runtime.js index 4e29b69e..2035e3e1 100644 --- a/packages/iles/jsx-runtime.js +++ b/packages/iles/jsx-runtime.js @@ -42,7 +42,7 @@ function wrapListeners (props, type) { args[0], { source: 'mdx', - event: key.slice(2).toLowerCase(), + event: key.slice(2), tag: typeof type === 'string' ? type : type?.name || 'Component', }, ) diff --git a/packages/iles/src/client/app/components/DebugPanel.vue b/packages/iles/src/client/app/components/DebugPanel.vue index b640d320..d5eaea62 100644 --- a/packages/iles/src/client/app/components/DebugPanel.vue +++ b/packages/iles/src/client/app/components/DebugPanel.vue @@ -12,6 +12,7 @@ export default defineComponent({ const open = ref(false) const buttonLabel = computed(() => message.value || 'Debug') const listenerWarnings = computed(() => (window as any).__ILE_DEVTOOLS__?.getListenerWarnings?.() || []) + const formattedListenerWarnings = computed(() => listenerWarnings.value.map((item: any) => item.message).join('\n\n')) const cleanPage = computed(() => { const layout = page.value.layoutName || 'false' @@ -56,6 +57,7 @@ export default defineComponent({ copyAll, content, listenerWarnings, + formattedListenerWarnings, } }, }) @@ -66,7 +68,7 @@ export default defineComponent({

{{ buttonLabel }}Open DevTools to inspect islands 🏝

{{ cleanPage }}
-{{ listenerWarnings.map((item: any) => item.message).join('\n\n') }}
+{{ formattedListenerWarnings }}
     
diff --git a/packages/iles/src/client/app/listenerGuard.ts b/packages/iles/src/client/app/listenerGuard.ts index e272e1f7..7032fe78 100644 --- a/packages/iles/src/client/app/listenerGuard.ts +++ b/packages/iles/src/client/app/listenerGuard.ts @@ -46,4 +46,3 @@ export function guardListenerCall (handler: () => T, event: Event, details: L if (typeof window !== 'undefined') (window as any).__ILE_GUARD_LISTENER_CALL__ = guardListenerCall - diff --git a/packages/iles/src/node/plugin/wrap.ts b/packages/iles/src/node/plugin/wrap.ts index c693c6ab..1ea45e24 100644 --- a/packages/iles/src/node/plugin/wrap.ts +++ b/packages/iles/src/node/plugin/wrap.ts @@ -135,7 +135,7 @@ function processSFCListeners (elements: ElementNode[], s: MagicString, filename: options.warn?.(warning) if (options.wrapListeners && prop.exp?.loc?.source) - s.overwrite(prop.exp.loc.start.offset, prop.exp.loc.end.offset, guardedVueExpression(prop.exp.loc.source, warning)) + wrapVueExpression(s, prop.exp.loc.start.offset, prop.exp.loc.end.offset, warning) } for (const child of node.children || []) { @@ -148,13 +148,13 @@ function processSFCListeners (elements: ElementNode[], s: MagicString, filename: walk(element) } -function guardedVueExpression (expression: string, warning: ListenerWarning) { +function wrapVueExpression (s: MagicString, start: number, end: number, warning: ListenerWarning) { const details = JSON.stringify({ source: 'vue', ...warning, }) - const run = `{ const __ile_handler = (${expression}); return typeof __ile_handler === 'function' ? __ile_handler($event) : __ile_handler }` - return `($event) => (window.__ILE_GUARD_LISTENER_CALL__ ? window.__ILE_GUARD_LISTENER_CALL__(() => ${run}, $event, ${details}) : (() => ${run})())` + s.appendLeft(start, `($event) => window.__ILE_GUARD_LISTENER_CALL__(() => { const _ileHandler = (`) + s.appendRight(end, `); return typeof _ileHandler === 'function' ? _ileHandler($event) : _ileHandler }, $event, ${details})`) } function isOnDirective (prop: any): prop is DirectiveNode { diff --git a/packages/iles/tests/listener-guard.spec.ts b/packages/iles/tests/listener-guard.spec.ts index 357cb5b3..df9a0c55 100644 --- a/packages/iles/tests/listener-guard.spec.ts +++ b/packages/iles/tests/listener-guard.spec.ts @@ -37,4 +37,3 @@ describe('listener guard', () => { expect(errors.length).toBe(0) }) }) - diff --git a/packages/iles/tests/remark-wrap-islands.spec.ts b/packages/iles/tests/remark-wrap-islands.spec.ts index 11b9a7f3..f19ca954 100644 --- a/packages/iles/tests/remark-wrap-islands.spec.ts +++ b/packages/iles/tests/remark-wrap-islands.spec.ts @@ -31,4 +31,3 @@ describe('remarkWrapIslands listener diagnostics', () => { expect(warnings[0]).toContain('/src/pages/demo.mdx') }) }) - diff --git a/packages/iles/tests/wrap.spec.ts b/packages/iles/tests/wrap.spec.ts index a4356e5a..84bedcaa 100644 --- a/packages/iles/tests/wrap.spec.ts +++ b/packages/iles/tests/wrap.spec.ts @@ -34,4 +34,3 @@ describe('wrapIslandsInSFC listener diagnostics', () => { expect(result?.code).toContain('__ILE_GUARD_LISTENER_CALL__') }) }) - From 211b80da95703ad5925cd76b6ef36fd60bfc0b19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:33:14 +0000 Subject: [PATCH 4/5] fix: remove unused DevtoolsApi type causing ci typecheck failure Agent-Logs-Url: https://github.com/ElMassimo/iles/sessions/545ca0d6-6a77-467e-bdf9-1f97c2701110 Co-authored-by: ElMassimo <1158253+ElMassimo@users.noreply.github.com> --- packages/iles/src/client/app/listenerGuard.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/iles/src/client/app/listenerGuard.ts b/packages/iles/src/client/app/listenerGuard.ts index 7032fe78..9ea3d406 100644 --- a/packages/iles/src/client/app/listenerGuard.ts +++ b/packages/iles/src/client/app/listenerGuard.ts @@ -6,10 +6,6 @@ interface ListenerWarningDetails { column?: number } -type DevtoolsApi = { - reportStaticListenerWarning?: (payload: any) => void -} - const warned = new Set() function getEventTarget (event: any) { From eb24c464c87e1e1bb1f9358ff0a078549e7c9423 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:05:22 +0000 Subject: [PATCH 5/5] fix: use relative import in listener guard spec for vitest resolution Agent-Logs-Url: https://github.com/ElMassimo/iles/sessions/520dc749-530a-41db-8db4-1ac568094e62 Co-authored-by: ElMassimo <1158253+ElMassimo@users.noreply.github.com> --- packages/iles/tests/listener-guard.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/iles/tests/listener-guard.spec.ts b/packages/iles/tests/listener-guard.spec.ts index df9a0c55..5b782ff7 100644 --- a/packages/iles/tests/listener-guard.spec.ts +++ b/packages/iles/tests/listener-guard.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vite-plus/test' -import { guardListenerCall } from '@client/app/listenerGuard' +import { guardListenerCall } from '../src/client/app/listenerGuard' describe('listener guard', () => { test('calls original handler when no island context and reports once', () => {