From 90891c8220318d7811af0c8ccd54ecfd1f7a82a7 Mon Sep 17 00:00:00 2001 From: themaker Date: Fri, 10 Apr 2026 04:29:47 +0100 Subject: [PATCH 01/14] feat(.changeset, packages/vanjs) robust types for better DOM property resolution --- .changeset/eager-lies-behave.md | 43 ------------ .changeset/funky-humans-open.md | 5 -- .changeset/twelve-trams-shop.md | 7 ++ .zed/settings.json | 2 +- packages/vanjs/src/event-handlers.ts | 2 +- packages/vanjs/src/index.ts | 2 + packages/vanjs/src/van.ts | 100 +++++++++++++++++++++------ 7 files changed, 91 insertions(+), 70 deletions(-) delete mode 100644 .changeset/eager-lies-behave.md delete mode 100644 .changeset/funky-humans-open.md create mode 100644 .changeset/twelve-trams-shop.md diff --git a/.changeset/eager-lies-behave.md b/.changeset/eager-lies-behave.md deleted file mode 100644 index aa02563..0000000 --- a/.changeset/eager-lies-behave.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -"@michthemaker/vanjs": minor ---- - -Add Context API for logical state sharing across component trees - -Introduces `createContext` and `useContext` functions that enable sharing reactive state without prop drilling. Context uses logical scoping (not DOM-based) and integrates seamlessly with VanJS's reactive state system. - -**New exports:** - -- `createContext()`: Creates a new context object with a Provider method -- `useContext(context)`: Retrieves the current context value (must be called within a Provider) - -**Example usage:** - -```typescript -import van, { createContext, useContext } from "@michthemaker/vanjs"; - -const { div, button } = van.tags; -const ThemeContext = createContext<{ color: string }>(); - -const theme = van.state({ color: "blue" }); - -ThemeContext.Provider(theme, () => { - const currentTheme = useContext(ThemeContext); - return div( - () => `Theme: ${currentTheme.val.color}`, - button( - { - onclick: () => (theme.val = { color: "red" }), - }, - "Change Theme" - ) - ); -}); -``` - -**Features:** - -- Shallow reactivity: context values must be VanJS state objects -- Supports nested providers of the same context -- Type-safe with TypeScript generics -- Throws helpful errors when used incorrectly diff --git a/.changeset/funky-humans-open.md b/.changeset/funky-humans-open.md deleted file mode 100644 index bbf69e9..0000000 --- a/.changeset/funky-humans-open.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@michthemaker/vanjs": patch ---- - -Add JSDoc documentation for Context API diff --git a/.changeset/twelve-trams-shop.md b/.changeset/twelve-trams-shop.md new file mode 100644 index 0000000..723a80a --- /dev/null +++ b/.changeset/twelve-trams-shop.md @@ -0,0 +1,7 @@ +--- +"@michthemaker/vanjs": minor +--- + +Added more robust types to van.ts + +Rewrote the vanjs types to better handle type resolution for DOM propperties diff --git a/.zed/settings.json b/.zed/settings.json index fb41076..d683245 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -57,7 +57,7 @@ // settings for all languages "languages": { "TypeScript": { - "language_servers": ["tsgo"] + "language_servers": ["tsgo", "oxfmt"] }, "JavaScript": { "language_servers": ["tsgo", "vtsls"], diff --git a/packages/vanjs/src/event-handlers.ts b/packages/vanjs/src/event-handlers.ts index 6d0bf8e..cda24e2 100644 --- a/packages/vanjs/src/event-handlers.ts +++ b/packages/vanjs/src/event-handlers.ts @@ -13,7 +13,7 @@ import type { StateView } from "./van.ts"; * @example `((e: MouseEvent) => any) | null` -> `MouseEvent` */ -type ExtractEventType = [T] extends [ +export type ExtractEventType = [T] extends [ ((e: infer E extends Event) => any) | null, ] ? E diff --git a/packages/vanjs/src/index.ts b/packages/vanjs/src/index.ts index 5f7a143..05d35a7 100644 --- a/packages/vanjs/src/index.ts +++ b/packages/vanjs/src/index.ts @@ -334,6 +334,8 @@ let van: Van = { (ns: string) => new Proxy(tag, handler(ns)), handler() ) as Van["tags"], + // svgTags: new Proxy(tag, handler("svg")) as Van["svgTags"], + // mathMlTags: new Proxy(tag, handler("math")) as Van["mathMlTags"], hydrate: (dom: T, f: (dom: T) => T | null | undefined): T => ( update(dom as unknown as ChildNode, bind(f as BindingFunc, dom)), dom ), diff --git a/packages/vanjs/src/van.ts b/packages/vanjs/src/van.ts index fe7bf9b..389446c 100644 --- a/packages/vanjs/src/van.ts +++ b/packages/vanjs/src/van.ts @@ -1,9 +1,58 @@ import type { ElementEventHandlers } from "./event-handlers.ts"; -export type { - ElementEventHandlers, - ReactiveEventHandler, -} from "./event-handlers.ts"; +export type * from "./event-handlers.ts"; + +// SVGAnimated union for fast Attributable lookup +type SVGAnimatedTypes = + | SVGAnimatedLength + | SVGAnimatedLengthList + | SVGAnimatedNumber + | SVGAnimatedNumberList + | SVGAnimatedInteger + | SVGAnimatedBoolean + | SVGAnimatedString + | SVGAnimatedAngle + | SVGAnimatedEnumeration + | SVGAnimatedRect + | SVGAnimatedPreserveAspectRatio + | SVGAnimatedTransformList; + +// Readonly/unsettable DOM object union +type UnsettableDOMTypes = + | DOMPointReadOnly + | DOMRectReadOnly + | DOMStringMap + | DOMStringList + | NamedNodeMap + | HTMLCollection + | NodeListOf + | ShadowRoot + | Document + | DocumentFragment + | ValidityState + | TimeRanges + | TextTrackList + | RemotePlayback + | MediaKeys + | MediaError + | FileList + | HTMLElement + | SVGElement + | MathMLElement; + +type Attributable = T extends SVGAnimatedTypes + ? string | number | boolean + : T extends UnsettableDOMTypes | null + ? never + : T extends Date | null + ? string | number | null + : T extends MediaProvider | null + ? MediaProvider | null + : T extends Element | null + ? Element | null + : T extends object + ? never + : T; /** * A reactive state object that automatically triggers updates when its value changes. @@ -45,22 +94,28 @@ export type Primitive = string | number | boolean | bigint; export type PropValue = Primitive | ((e: any) => void) | null; -export type PropValueOrDerived = - | PropValue - | StateView - | (() => PropValue); - -export type Props = Record & { - class?: PropValueOrDerived; +export type PropValueOrDerived = T | StateView | (() => T); +// Symbol brand — invisible in completions (unlike string-keyed brands), but still makes +// Props nominally distinct from ChildDom so tsserver resolves only the props overload when typing `{`. +declare const vanPropsBrand: unique symbol; +export type Props = Record> & { + readonly [vanPropsBrand]?: never; + class?: PropValueOrDerived; is?: string; }; +// Exclude static numeric constants inherited from Node (ATTRIBUTE_NODE, CDATA_SECTION_NODE, etc.) +// which are `number` primitives that pass through Attributable and cause doubled completions in the LSP. export type PropsWithKnownKeys = Partial<{ [K in keyof ElementType as K extends `on${string}` ? ((ev: Event) => any) | null extends ElementType[K] ? never : K - : K]: PropValueOrDerived; + : K extends keyof Node + ? never + : Attributable extends never + ? never + : K]: PropValueOrDerived>; }>; /** @@ -87,13 +142,10 @@ export type ChildDom = | BindingFunc | readonly ChildDom[]; -export type TagFunc = ( - first?: - | (Props & PropsWithKnownKeys & ElementEventHandlers) - | ChildDom - | RefProp, - ...rest: readonly ChildDom[] -) => Result; +export interface TagFunc { + (first?: TagFuncProps, ...rest: readonly ChildDom[]): Result; + (first: ChildDom, ...rest: readonly ChildDom[]): Result; +} /** * HTML tag functions typed for all known HTML elements. @@ -166,9 +218,17 @@ export interface Van { ) => Element; /** - * Tag functions for creating HTML, SVG, and MathML elements. + * Tag functions for creating HTML elements. */ readonly tags: HTMLTags & NamespacedTags; + /** + * Tag functions for creating SVG elements. + */ + readonly svgTags: SVGTags; + /** + * Tag functions for creating MathML elements. + */ + readonly mathMlTags: SVGTags; /** * Hydrates existing DOM with VanJS reactivity. From a12cbd700a3dbbd99c8f43a4ff0e9865291632ef Mon Sep 17 00:00:00 2001 From: themaker Date: Fri, 10 Apr 2026 04:56:39 +0100 Subject: [PATCH 02/14] feat(packages/vanjs) added svg and mathml tags function directly to vanjs interface --- .changeset/deep-brooms-repeat.md | 15 +++++++++++++++ .zed/settings.json | 2 +- apps/examples/plugin-test/src/main.ts | 22 ++++++++++++++++++++++ packages/vanjs/src/index.ts | 10 ++++++++-- packages/vanjs/src/van.ts | 6 +++++- 5 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 .changeset/deep-brooms-repeat.md diff --git a/.changeset/deep-brooms-repeat.md b/.changeset/deep-brooms-repeat.md new file mode 100644 index 0000000..e0c04e6 --- /dev/null +++ b/.changeset/deep-brooms-repeat.md @@ -0,0 +1,15 @@ +--- +"@michthemaker/vanjs": minor +--- + +Separate functions for SVG and MathML Elements added + +```typescript +const { svg, path } = van.svgTags; +const { div, h1, button } = van.tags; + +svg({ + viewBox: "0 0 24 24", + width: "15", +}); +``` diff --git a/.zed/settings.json b/.zed/settings.json index d683245..248c650 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -57,7 +57,7 @@ // settings for all languages "languages": { "TypeScript": { - "language_servers": ["tsgo", "oxfmt"] + "language_servers": ["vtsls", "oxfmt"] }, "JavaScript": { "language_servers": ["tsgo", "vtsls"], diff --git a/apps/examples/plugin-test/src/main.ts b/apps/examples/plugin-test/src/main.ts index f2fc643..4dbd369 100644 --- a/apps/examples/plugin-test/src/main.ts +++ b/apps/examples/plugin-test/src/main.ts @@ -2,6 +2,7 @@ import van, { type Ref } from "@michthemaker/vanjs"; import { Counter } from "./barrel-export"; import { ContextTest } from "./context-test"; +const { svg, path } = van.svgTags; const { div, h1, button } = van.tags; // Component with props - using named export @@ -13,6 +14,27 @@ const App = (props: { name: string }) => { style: "padding: 20px; font-family: sans-serif; max-width: 800px; margin: 0 auto;", }, + svg( + { + xmlns: "http://www.w3.org/2000/svg", + width: "20", + height: "20", + viewBox: "0 0 24 24", + fill: "none", + color: "currentColor", + class: "rotate-180 text-white", + "stroke-width": "2", + }, + path({ + d: "M12 4V20M20 12H4", + stroke: "currentColor", + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: "1.5", + key: "0", + opacity: "undefined", + }) + ), ContextTest(), h1( { diff --git a/packages/vanjs/src/index.ts b/packages/vanjs/src/index.ts index 05d35a7..42f34bf 100644 --- a/packages/vanjs/src/index.ts +++ b/packages/vanjs/src/index.ts @@ -334,8 +334,14 @@ let van: Van = { (ns: string) => new Proxy(tag, handler(ns)), handler() ) as Van["tags"], - // svgTags: new Proxy(tag, handler("svg")) as Van["svgTags"], - // mathMlTags: new Proxy(tag, handler("math")) as Van["mathMlTags"], + svgTags: new Proxy( + tag, + handler("http://www.w3.org/2000/svg") + ) as Van["svgTags"], + mathMlTags: new Proxy( + tag, + handler("http://www.w3.org/1998/Math/MathML") + ) as Van["mathMlTags"], hydrate: (dom: T, f: (dom: T) => T | null | undefined): T => ( update(dom as unknown as ChildNode, bind(f as BindingFunc, dom)), dom ), diff --git a/packages/vanjs/src/van.ts b/packages/vanjs/src/van.ts index 389446c..9843af8 100644 --- a/packages/vanjs/src/van.ts +++ b/packages/vanjs/src/van.ts @@ -127,6 +127,10 @@ export type Ref = { current: T | null }; export type RefProp = { ref?: Ref }; +export type TagFuncProps = Props & + PropsWithKnownKeys & + ElementEventHandlers & { ref?: Ref }; + export type ValidChildDomValue = | Primitive | Node @@ -228,7 +232,7 @@ export interface Van { /** * Tag functions for creating MathML elements. */ - readonly mathMlTags: SVGTags; + readonly mathMlTags: MathMLTags; /** * Hydrates existing DOM with VanJS reactivity. From daabfa066a0c99b013bc9ab8f7b1d1f983fb5927 Mon Sep 17 00:00:00 2001 From: themaker Date: Fri, 10 Apr 2026 05:25:12 +0100 Subject: [PATCH 03/14] feat(packages/vite-plugin-vanjs) better hmr error message container --- .changeset/ten-moles-nail.md | 5 +++++ apps/examples/plugin-test/index.html | 16 ++++++++++++++-- apps/examples/plugin-test/src/main.ts | 10 ++++++---- packages/vite-plugin-vanjs/src/plugin.ts | 8 +++++--- 4 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 .changeset/ten-moles-nail.md diff --git a/.changeset/ten-moles-nail.md b/.changeset/ten-moles-nail.md new file mode 100644 index 0000000..083ff1b --- /dev/null +++ b/.changeset/ten-moles-nail.md @@ -0,0 +1,5 @@ +--- +"@michthemaker/vite-plugin-vanjs": minor +--- + +Better error message container wrapper in hmr diff --git a/apps/examples/plugin-test/index.html b/apps/examples/plugin-test/index.html index 598d39a..597b5b1 100644 --- a/apps/examples/plugin-test/index.html +++ b/apps/examples/plugin-test/index.html @@ -1,12 +1,24 @@ - + Example Repository VanJS + - + diff --git a/apps/examples/plugin-test/src/main.ts b/apps/examples/plugin-test/src/main.ts index 4dbd369..e757c4e 100644 --- a/apps/examples/plugin-test/src/main.ts +++ b/apps/examples/plugin-test/src/main.ts @@ -3,7 +3,7 @@ import { Counter } from "./barrel-export"; import { ContextTest } from "./context-test"; const { svg, path } = van.svgTags; -const { div, h1, button } = van.tags; +const { div, h1, button, style } = van.tags; // Component with props - using named export const App = (props: { name: string }) => { @@ -12,8 +12,10 @@ const App = (props: { name: string }) => { return div( { style: - "padding: 20px; font-family: sans-serif; max-width: 800px; margin: 0 auto;", + "padding-inline: 4px; font-family: sans-serif;; margin: 0; overflow: auto;", + class: "no-scrollbar", }, + // okaythen, svg( { xmlns: "http://www.w3.org/2000/svg", @@ -39,14 +41,14 @@ const App = (props: { name: string }) => { h1( { style: - "color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px;", + "width: 90%; color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px;", ref: ref, }, "VanJS Multi-File HMR Test - me us ", props.name, myName ), - Counter(), + // Counter(), button( { onclick() { diff --git a/packages/vite-plugin-vanjs/src/plugin.ts b/packages/vite-plugin-vanjs/src/plugin.ts index 6d2b00b..8d6ce1d 100644 --- a/packages/vite-plugin-vanjs/src/plugin.ts +++ b/packages/vite-plugin-vanjs/src/plugin.ts @@ -745,14 +745,16 @@ class VanJSHMRRuntime { const stack = error instanceof Error ? (error.stack || '') : ''; const overlay = document.createElement('div'); overlay.id = '__vanjs-hmr-error-overlay'; - overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);color:#fff;z-index:99999;font-family:monospace;padding:40px;box-sizing:border-box;overflow:auto;display:flex;flex-direction:column;'; - overlay.innerHTML = '
' + overlay.className = 'vanjs-hmr-no-scrollbar'; + overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);color:#fff;z-index:99999;font-family:monospace;padding:12px;box-sizing:border-box;overflow:auto;display:flex;flex-direction:column;'; + overlay.innerHTML = '' + + '
' + '
' + '

HMR Error in ' + slotId + '

' + '' + '
' + '

Old DOM preserved. Fix the error and save to retry.

' - + '
'
+      + '
'
       + this.escapeHtml(message) + '\\n\\n' + this.escapeHtml(stack) + '
'; const dismiss = () => this.dismissErrorOverlay(); overlay.querySelector('#__vanjs-hmr-dismiss')?.addEventListener('click', dismiss); From a916ab5617456958b5761e01b2870f88af13c8c1 Mon Sep 17 00:00:00 2001 From: themaker Date: Sat, 11 Apr 2026 21:12:17 +0100 Subject: [PATCH 04/14] feat(packages/vite-plugin-vanjs) add support for svgTags and mathml tags detection for vite hmr --- apps/examples/plugin-test/src/App.ts | 60 ++++++++++++++ .../examples/plugin-test/src/barrel-export.ts | 1 - apps/examples/plugin-test/src/context-test.ts | 36 +++------ apps/examples/plugin-test/src/counter.ts | 80 ------------------- apps/examples/plugin-test/src/main.ts | 72 +++-------------- apps/examples/plugin-test/vite.config.ts | 2 +- packages/vite-plugin-vanjs/src/plugin.ts | 33 +++++--- 7 files changed, 103 insertions(+), 181 deletions(-) create mode 100644 apps/examples/plugin-test/src/App.ts delete mode 100644 apps/examples/plugin-test/src/barrel-export.ts delete mode 100644 apps/examples/plugin-test/src/counter.ts diff --git a/apps/examples/plugin-test/src/App.ts b/apps/examples/plugin-test/src/App.ts new file mode 100644 index 0000000..d479a9c --- /dev/null +++ b/apps/examples/plugin-test/src/App.ts @@ -0,0 +1,60 @@ +import van, { type Ref } from "@michthemaker/vanjs"; +import {} from "./context-test"; + +const { svg, path } = van.svgTags; +const { div, h1, button } = van.tags; + +// Component with props - using named export +const App = (props: { name: string }) => { + const myName = van.state("Mich"); + const ref: Ref = { current: null }; + return div( + { + style: + "padding-inline: 4px; font-family: sans-serif;; margin: 0; overflow: auto;", + class: "no-scrollbar", + }, + // okaythen, + svg( + { + xmlns: "http://www.w3.org/2000/svg", + width: "20", + height: "20", + viewBox: "0 0 24 24", + fill: "none", + color: "currentColor", + class: "rotate-180 text-white", + "stroke-width": "2", + }, + path({ + d: "M12 4V20M20 12H4", + stroke: "currentColor", + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: "1.5", + key: "0", + opacity: "undefined", + }) + ), + h1( + { + style: + "width: 90%; color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px;", + ref: ref, + }, + "VanJS Multi-File HMR Test - me us ", + props.name, + myName + ), + button( + { + onclick() { + myName.val = "Michthemaker"; + }, + }, + "Click them" + ) + ); +}; + +export { App }; diff --git a/apps/examples/plugin-test/src/barrel-export.ts b/apps/examples/plugin-test/src/barrel-export.ts deleted file mode 100644 index 8a55baa..0000000 --- a/apps/examples/plugin-test/src/barrel-export.ts +++ /dev/null @@ -1 +0,0 @@ -export { Counter } from "./counter"; diff --git a/apps/examples/plugin-test/src/context-test.ts b/apps/examples/plugin-test/src/context-test.ts index bf23770..7dba61f 100644 --- a/apps/examples/plugin-test/src/context-test.ts +++ b/apps/examples/plugin-test/src/context-test.ts @@ -1,33 +1,15 @@ import van, { createContext, useContext } from "@michthemaker/vanjs"; -const { div, button } = van.tags; -const PopoverContext = createContext(); +const QueryContext = createContext<{ name: string }>(); -const ContextTest = (): any => { - const value = van.state(2); - const value2 = van.state(89); +const QueryContextProvider = (childrenFn: () => any) => { + const state = van.state({ name: "kashiba" }); + return QueryContext.Provider(state, childrenFn); +}; - return div( - { style: "padding: 2px;" }, - PopoverContext.Provider(value, () => { - const value = useContext(PopoverContext); - return [ - value.val, - PopoverContext.Provider(value2, () => { - const value = useContext(PopoverContext); - return [ - () => value.val, - button( - { - onclick: () => (value.val = Math.random() * 200), - }, - "Change Second one" - ), - ]; - }), - ]; - }) - ); +const useQueryFt = () => { + const { val: queryContext } = useContext(QueryContext); + return queryContext; }; -export { ContextTest }; +export { QueryContextProvider, useQueryFt }; diff --git a/apps/examples/plugin-test/src/counter.ts b/apps/examples/plugin-test/src/counter.ts deleted file mode 100644 index b7284fa..0000000 --- a/apps/examples/plugin-test/src/counter.ts +++ /dev/null @@ -1,80 +0,0 @@ -import van from "@michthemaker/vanjs"; -const { div, h1, p, button, input } = van.tags; - -const Counter = (): any => { - const counter = van.state({ number: 9 }); - const textInput = van.state("Edit Me!"); - - // Test van.derive - now preserved across HMR with createDerived - const doubled = van.derive(() => counter.val.number * 2); - const tripled = van.derive(() => counter.val.number * 3); - - return div( - { style: "padding: 2px;" }, - - // Counter section - div( - { - style: - "margin-bottom: 20px; border: 2px solid #4CAF50; border-radius: 8px; padding: 16px;", - }, - h1("Counter testing meee waitng for u "), - p(() => `Count: ${counter.val.number} + name`), - p(() => `Doubled (inline): ${counter.val.number * 2}`), - p(() => `derived 😉 Doubled : ${doubled.val}`), - p(() => `Tripled (derived): ${tripled.val}`), - button( - { - onclick: () => counter.val.number++, - style: - "margin: 5px; padding: 10px 20px; cursor: pointer; background-color: #4CAF50; color: white; border: none; border-radius: 4px;", - }, - "Increment" - ), - button( - { - onclick: () => counter.val.number--, - style: - "margin: 5px; padding: 10px 20px; cursor: pointer; background-color: #FF9800; color: white; border: none; border-radius: 4px;", - }, - "Decrement" - ), - button( - { - onclick: () => { - counter.val = { number: 0 }; - }, - style: - "margin: 5px; padding: 10px 20px; cursor: pointer; background: #f44336; color: white; border: none; border-radius: 4px;", - }, - "Reset" - ) - ), - - // Text input section - div( - { - style: - "margin-bottom: 20px; border: 2px solid #2196F3; border-radius: 8px; padding: 16px; display: none;", - }, - h1("Text Input "), - input({ - type: "text", - value: () => textInput.val, - oninput: (e: any) => { - textInput.val = e.target.value; - }, - style: - "padding: 8px; font-size: 16px; width: 300px; border: 1px solid #ccc; border-radius: 4px;", - }), - p(() => `You typed: me ls`), - p(() => `Length: ${textInput.val.length}`) - ) - ); -}; - -export const OtherName = (): any => { - return div("name"); -}; - -export { Counter }; diff --git a/apps/examples/plugin-test/src/main.ts b/apps/examples/plugin-test/src/main.ts index e757c4e..a94914a 100644 --- a/apps/examples/plugin-test/src/main.ts +++ b/apps/examples/plugin-test/src/main.ts @@ -1,65 +1,11 @@ -import van, { type Ref } from "@michthemaker/vanjs"; -import { Counter } from "./barrel-export"; -import { ContextTest } from "./context-test"; +import van from "@michthemaker/vanjs"; +import { App } from "./App.ts"; +import { QueryContextProvider } from "./context-test.ts"; -const { svg, path } = van.svgTags; -const { div, h1, button, style } = van.tags; +const root = document.body!; -// Component with props - using named export -const App = (props: { name: string }) => { - const myName = van.state("Mich"); - const ref: Ref = { current: null }; - return div( - { - style: - "padding-inline: 4px; font-family: sans-serif;; margin: 0; overflow: auto;", - class: "no-scrollbar", - }, - // okaythen, - svg( - { - xmlns: "http://www.w3.org/2000/svg", - width: "20", - height: "20", - viewBox: "0 0 24 24", - fill: "none", - color: "currentColor", - class: "rotate-180 text-white", - "stroke-width": "2", - }, - path({ - d: "M12 4V20M20 12H4", - stroke: "currentColor", - strokeLinecap: "round", - strokeLinejoin: "round", - strokeWidth: "1.5", - key: "0", - opacity: "undefined", - }) - ), - ContextTest(), - h1( - { - style: - "width: 90%; color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px;", - ref: ref, - }, - "VanJS Multi-File HMR Test - me us ", - props.name, - myName - ), - // Counter(), - button( - { - onclick() { - myName.val = "Michthemaker"; - }, - }, - "Click me" - ) - ); -}; - -van.add(document.body, App({ name: "Mice" })); - -export default App; +van.add( + root, + // QueryContextProvider(() => App({ name: "okolo" })) + App({ name: "okolo" }) +); diff --git a/apps/examples/plugin-test/vite.config.ts b/apps/examples/plugin-test/vite.config.ts index 456a04e..13a2cbf 100644 --- a/apps/examples/plugin-test/vite.config.ts +++ b/apps/examples/plugin-test/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ }, }, plugins: [ - inspect(), + inspect({}), vanjs({ hmr: { smartStateChecking: true, diff --git a/packages/vite-plugin-vanjs/src/plugin.ts b/packages/vite-plugin-vanjs/src/plugin.ts index 8d6ce1d..7615ad4 100644 --- a/packages/vite-plugin-vanjs/src/plugin.ts +++ b/packages/vite-plugin-vanjs/src/plugin.ts @@ -101,7 +101,8 @@ function detectComponents(code: string): ComponentInfo[] { // Extract tag names from van.tags destructuring const tagNames = new Set(); - const tagsDestructurePattern = /const\s*\{\s*([^}]+)\s*\}\s*=\s*van\.tags/g; + const tagsDestructurePattern = + /const\s*\{\s*([^}]+)\s*\}\s*=\s*van\.\w*[Tt]ags/g; while ((match = tagsDestructurePattern.exec(code)) !== null) { const names = match[1].split(",").map((s) => s.trim()); for (const name of names) { @@ -223,8 +224,15 @@ function detectComponents(code: string): ComponentInfo[] { // Skip non-components (lowercase first letter) if (!/^[A-Z]/.test(internalName)) continue; - // Skip already detected by Pattern 1 or 2 - if (components.some((c) => c.name === internalName)) continue; + // If already detected by Pattern 1 or 2, backfill exportStatementRange so + // transformSubmoduleComponents knows to remove the export { } statement + const alreadyDetected = components.find((c) => c.name === internalName); + if (alreadyDetected) { + if (!alreadyDetected.exportStatementRange) { + alreadyDetected.exportStatementRange = exportStatementRange; + } + continue; + } // Locate the plain const declaration const constDeclPattern = new RegExp( @@ -396,7 +404,16 @@ function transformSubmoduleComponents( removedExportRanges.add(rangeKey); } // Ensure $$__hmr__Name is exported so newModule.$$__hmr__Name works in HMR accept - s.prependLeft(declarationStart, "export "); + // Guard: don't double-prepend if the declaration was already `export const` + console.log( + "`" + code.slice(declarationStart, declarationStart + 35) + "`", + "i am the code you are looking for" + ); + const isAlreadyExported = + code.slice(declarationStart, declarationStart + 6) === "export"; + if (!isAlreadyExported) { + s.prependLeft(declarationStart, "export "); + } s.append( `\nexport const ${exportedName} = (props) => __VAN_HMR__.registerRender('${slotId}', ${hmrName}, props);\n` ); @@ -745,16 +762,14 @@ class VanJSHMRRuntime { const stack = error instanceof Error ? (error.stack || '') : ''; const overlay = document.createElement('div'); overlay.id = '__vanjs-hmr-error-overlay'; - overlay.className = 'vanjs-hmr-no-scrollbar'; - overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);color:#fff;z-index:99999;font-family:monospace;padding:12px;box-sizing:border-box;overflow:auto;display:flex;flex-direction:column;'; - overlay.innerHTML = '' - + '
' + overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);color:#fff;z-index:99999;font-family:monospace;padding:40px;box-sizing:border-box;overflow:auto;display:flex;flex-direction:column;'; + overlay.innerHTML = '
' + '
' + '

HMR Error in ' + slotId + '

' + '' + '
' + '

Old DOM preserved. Fix the error and save to retry.

' - + '
'
+      + '
'
       + this.escapeHtml(message) + '\\n\\n' + this.escapeHtml(stack) + '
'; const dismiss = () => this.dismissErrorOverlay(); overlay.querySelector('#__vanjs-hmr-dismiss')?.addEventListener('click', dismiss); From 3df3a23c12146ecec90388e916202733e88b0568 Mon Sep 17 00:00:00 2001 From: themaker Date: Sat, 11 Apr 2026 23:01:59 +0100 Subject: [PATCH 05/14] fix(packages/vite-plugin-vanjs, examples/plugin-test) fix comment line `export` interfering with `const Component` in vite hmr --- apps/examples/plugin-test/src/App.ts | 8 +++- apps/examples/plugin-test/src/main.ts | 7 +--- packages/vite-plugin-vanjs/src/plugin.ts | 49 ++++++++++++++++-------- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/apps/examples/plugin-test/src/App.ts b/apps/examples/plugin-test/src/App.ts index d479a9c..34b37b4 100644 --- a/apps/examples/plugin-test/src/App.ts +++ b/apps/examples/plugin-test/src/App.ts @@ -5,7 +5,7 @@ const { svg, path } = van.svgTags; const { div, h1, button } = van.tags; // Component with props - using named export -const App = (props: { name: string }) => { +export const App = (props: { name: string }) => { const myName = van.state("Mich"); const ref: Ref = { current: null }; return div( @@ -57,4 +57,8 @@ const App = (props: { name: string }) => { ); }; -export { App }; +const Foo = () => { + return h1("name"); +}; + +export { Foo as Bar }; diff --git a/apps/examples/plugin-test/src/main.ts b/apps/examples/plugin-test/src/main.ts index a94914a..7c977b4 100644 --- a/apps/examples/plugin-test/src/main.ts +++ b/apps/examples/plugin-test/src/main.ts @@ -1,11 +1,6 @@ import van from "@michthemaker/vanjs"; import { App } from "./App.ts"; -import { QueryContextProvider } from "./context-test.ts"; const root = document.body!; -van.add( - root, - // QueryContextProvider(() => App({ name: "okolo" })) - App({ name: "okolo" }) -); +van.add(root, App({ name: "okolo" })); diff --git a/packages/vite-plugin-vanjs/src/plugin.ts b/packages/vite-plugin-vanjs/src/plugin.ts index 7615ad4..9e0286e 100644 --- a/packages/vite-plugin-vanjs/src/plugin.ts +++ b/packages/vite-plugin-vanjs/src/plugin.ts @@ -26,6 +26,7 @@ interface ComponentInfo { name: string; exportedName: string; isDefault: boolean; + isExportConst: boolean; declarationStart: number; nameStart: number; nameEnd: number; @@ -159,7 +160,7 @@ function detectComponents(code: string): ComponentInfo[] { // Pattern 1: export const Name = (...) => ... const exportConstPattern = - /export\s+const\s+([A-Z][a-zA-Z0-9]*)\s*=\s*(?:async\s+)?(?:<[^>]*>\s*)?(\([^)]*\)|[a-zA-Z_][a-zA-Z0-9_]*)(?:\s*:\s*[^{=>][^=>]*?)?\s*=>/g; + /export[^\S\n]+const[^\S\n]+([A-Z][a-zA-Z0-9]*)\s*=\s*(?:async\s+)?(?:<[^>]*>\s*)?(\([^)]*\)|[a-zA-Z_][a-zA-Z0-9_]*)(?:\s*:\s*[^{=>][^=>]*?)?\s*=>/g; while ((match = exportConstPattern.exec(code)) !== null) { const name = match[1]; const body = extractFunctionBody(match.index + match[0].length); @@ -170,6 +171,7 @@ function detectComponents(code: string): ComponentInfo[] { name, exportedName: name, isDefault, + isExportConst: true, declarationStart: match.index, nameStart, nameEnd: nameStart + name.length, @@ -193,6 +195,7 @@ function detectComponents(code: string): ComponentInfo[] { name, exportedName: name, isDefault: true, + isExportConst: false, declarationStart: match.index, nameStart, nameEnd: nameStart + name.length, @@ -230,6 +233,8 @@ function detectComponents(code: string): ComponentInfo[] { if (alreadyDetected) { if (!alreadyDetected.exportStatementRange) { alreadyDetected.exportStatementRange = exportStatementRange; + // Update exportedName in case of `export { Foo as Bar }` + alreadyDetected.exportedName = exportedName; } continue; } @@ -246,6 +251,7 @@ function detectComponents(code: string): ComponentInfo[] { const body = extractFunctionBody( constMatch.index + constMatch[0].length ); + if (usesVanTags(body)) { const declarationStart = constMatch.index; const nameStart = @@ -255,6 +261,7 @@ function detectComponents(code: string): ComponentInfo[] { name, exportedName, isDefault: false, + isExportConst: false, declarationStart, nameStart, nameEnd, @@ -385,6 +392,7 @@ function transformSubmoduleComponents( name, exportedName, isDefault, + isExportConst, declarationStart, nameStart, nameEnd, @@ -393,32 +401,38 @@ function transformSubmoduleComponents( const slotId = `${relPath}:${name}`; const hmrName = `$$__hmr__${name}`; - // Rename the original component declaration in-place - s.overwrite(nameStart, nameEnd, hmrName); - if (exportStatementRange) { - // Pattern 3: plain const + separate export { } statement + // Pattern 3: plain const (or export const) + separate export { } statement + + // 1. Remove the export { } block (deduplicated for multi-export statements) const rangeKey = `${exportStatementRange.start}:${exportStatementRange.end}`; if (!removedExportRanges.has(rangeKey)) { s.remove(exportStatementRange.start, exportStatementRange.end); removedExportRanges.add(rangeKey); } - // Ensure $$__hmr__Name is exported so newModule.$$__hmr__Name works in HMR accept - // Guard: don't double-prepend if the declaration was already `export const` - console.log( - "`" + code.slice(declarationStart, declarationStart + 35) + "`", - "i am the code you are looking for" - ); - const isAlreadyExported = - code.slice(declarationStart, declarationStart + 6) === "export"; - if (!isAlreadyExported) { - s.prependLeft(declarationStart, "export "); + + // 2. Rename + ensure exported in one overwrite to avoid MagicString conflicts + if (!isExportConst) { + // `const Name` → `export const $$__hmr__Name` + const sClone = s.slice(nameStart - "const ".length, nameEnd); + console.log(sClone, `I am sclone`); + s.overwrite( + nameStart - "const ".length, + nameEnd, + `export const ${hmrName}` + ); + } else { + // `export const Name` → `export const $$__hmr__Name` (just rename) + s.overwrite(nameStart, nameEnd, hmrName); } + + // 3. Append public wrapper s.append( `\nexport const ${exportedName} = (props) => __VAN_HMR__.registerRender('${slotId}', ${hmrName}, props);\n` ); } else if (isDefault) { - // Pattern 2: plain const + export default + // Pattern 2: plain const + export default Name + s.overwrite(nameStart, nameEnd, hmrName); const isAlreadyExported = code.slice(declarationStart, declarationStart + 6) === "export"; if (!isAlreadyExported) { @@ -434,7 +448,8 @@ function transformSubmoduleComponents( ); } } else { - // Pattern 1: export const — append wrapper using exportedName + // Pattern 1: export const Name — rename + append wrapper + s.overwrite(nameStart, nameEnd, hmrName); s.append( `\nexport const ${exportedName} = (props) => __VAN_HMR__.registerRender('${slotId}', ${hmrName}, props);\n` ); From 6f992e065ffe811105df3a9a4ea7fe26098d065d Mon Sep 17 00:00:00 2001 From: themaker Date: Sun, 12 Apr 2026 10:51:47 +0100 Subject: [PATCH 06/14] fix(packages/vite-plugin-vanjs, examples/plugin-test) adding support for vanjs context support in vite hmr --- apps/examples/plugin-test/src/App.ts | 75 ++++++++--------------- apps/examples/plugin-test/src/GifModal.ts | 13 ++++ packages/vite-plugin-vanjs/src/plugin.ts | 60 ++++++++++++++++-- 3 files changed, 92 insertions(+), 56 deletions(-) create mode 100644 apps/examples/plugin-test/src/GifModal.ts diff --git a/apps/examples/plugin-test/src/App.ts b/apps/examples/plugin-test/src/App.ts index 34b37b4..bf97771 100644 --- a/apps/examples/plugin-test/src/App.ts +++ b/apps/examples/plugin-test/src/App.ts @@ -1,64 +1,39 @@ import van, { type Ref } from "@michthemaker/vanjs"; -import {} from "./context-test"; +import { QueryContextProvider, useQueryFt } from "./context-test"; +import { GifModal } from "./GifModal"; -const { svg, path } = van.svgTags; -const { div, h1, button } = van.tags; +const { div, h1, p } = van.tags; // Component with props - using named export export const App = (props: { name: string }) => { const myName = van.state("Mich"); const ref: Ref = { current: null }; - return div( - { - style: - "padding-inline: 4px; font-family: sans-serif;; margin: 0; overflow: auto;", - class: "no-scrollbar", - }, - // okaythen, - svg( - { - xmlns: "http://www.w3.org/2000/svg", - width: "20", - height: "20", - viewBox: "0 0 24 24", - fill: "none", - color: "currentColor", - class: "rotate-180 text-white", - "stroke-width": "2", - }, - path({ - d: "M12 4V20M20 12H4", - stroke: "currentColor", - strokeLinecap: "round", - strokeLinejoin: "round", - strokeWidth: "1.5", - key: "0", - opacity: "undefined", - }) - ), - h1( + return QueryContextProvider(() => + div( { style: - "width: 90%; color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px;", - ref: ref, + "padding-inline: 4px; font-family: sans-serif;; margin: 0; overflow: auto;", + class: "no-scrollbar", }, - "VanJS Multi-File HMR Test - me us ", - props.name, - myName - ), - button( - { - onclick() { - myName.val = "Michthemaker"; + h1( + { + style: + "width: 90%; color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px;", + ref: ref, }, - }, - "Click them" + "VanJS Multi-File HMR Test - me usaa and meee ", + props.name, + myName + ), + // the below uses useQueryFt which calls useContext underneath, see ./context-test.ts file if needed + // when i save this file exporting the GifModal, there is an error saying useContext must be called within a Provider + GifModal(), + // the below uses useQueryFt which calls useContext underneath, see ./context-test.ts file if needed + // when i save this exact file, everything works fine, no error + () => { + const me = useQueryFt(); + return me.name; + } ) ); }; - -const Foo = () => { - return h1("name"); -}; - -export { Foo as Bar }; diff --git a/apps/examples/plugin-test/src/GifModal.ts b/apps/examples/plugin-test/src/GifModal.ts new file mode 100644 index 0000000..3aa8d4f --- /dev/null +++ b/apps/examples/plugin-test/src/GifModal.ts @@ -0,0 +1,13 @@ +// just a file using dummy context to test context with hmr + +import van from "@michthemaker/vanjs"; +import { useQueryFt } from "./context-test"; + +const { p } = van.tags; + +const GifModal = () => { + const queryContext = useQueryFt(); + return p(queryContext.name, "name is usss and them too"); +}; + +export { GifModal }; diff --git a/packages/vite-plugin-vanjs/src/plugin.ts b/packages/vite-plugin-vanjs/src/plugin.ts index 9e0286e..8dc792f 100644 --- a/packages/vite-plugin-vanjs/src/plugin.ts +++ b/packages/vite-plugin-vanjs/src/plugin.ts @@ -275,6 +275,40 @@ function detectComponents(code: string): ComponentInfo[] { return components; } +/** Detect local name of `createContext` imported from @michthemaker/vanjs */ +function detectCreateContextLocalName(code: string): string | null { + const match = + /import\s+(?:\w+\s*,\s*)?\{[^}]*\bcreateContext\s*(?:as\s+(\w+))?[^}]*\}\s*from\s+['"]@michthemaker\/vanjs['"]/.exec( + code + ); + if (!match) return null; + // if aliased: `createContext as foo` → foo, else → createContext + const aliasMatch = /\bcreateContext\s+as\s+(\w+)/.exec(match[0]); + return aliasMatch ? aliasMatch[1] : "createContext"; +} + +/** Transform createContext() → __VAN_HMR__.createContext('hmrId') */ +function transformCreateContext( + ctx: TransformContext, + localName: string +): void { + const { code, s, relPath, getLineCol } = ctx; + const pattern = new RegExp( + `\\b${localName}\\s*(?:<[^>]*>)?\\s*\\(\\s*\\)`, + "g" + ); + let match; + while ((match = pattern.exec(code)) !== null) { + const start = match.index; + const hmrId = `${relPath}:${getLineCol(start)}`; + s.overwrite( + start, + start + match[0].length, + `__VAN_HMR__.createContext('${hmrId}')` + ); + } +} + /** Inject the HMR runtime import */ function injectHmrImport(s: MagicString): void { s.prepend(`import { __VAN_HMR__ } from 'virtual:vanjs-hmr-runtime';\n`); @@ -414,8 +448,6 @@ function transformSubmoduleComponents( // 2. Rename + ensure exported in one overwrite to avoid MagicString conflicts if (!isExportConst) { // `const Name` → `export const $$__hmr__Name` - const sClone = s.slice(nameStart - "const ".length, nameEnd); - console.log(sClone, `I am sclone`); s.overwrite( nameStart - "const ".length, nameEnd, @@ -501,16 +533,19 @@ export function vanjsRefresh(options: VanJSHMROptions = {}): Plugin { }, transform(code, id) { - // Skip non-matching files + // Skip non-matching files and virtual modules + if (id.startsWith("\0")) return null; if (!include.test(id) || exclude.test(id)) return null; + const hasCreateContext = + code.includes("createContext") && code.includes("@michthemaker/vanjs"); if ( !code.includes("van.state") && !code.includes("van.tags") && - !code.includes("van.derive") + !code.includes("van.derive") && + !hasCreateContext ) { return null; } - const s = new MagicString(code); const relPath = posix .normalize(id.replace(projectRoot, "").replace(/\\/g, "/")) @@ -528,6 +563,9 @@ export function vanjsRefresh(options: VanJSHMROptions = {}): Plugin { // === Common transformations === transformVanState(ctx); // transformVanDerive(ctx); + const createContextLocalName = detectCreateContextLocalName(code); + if (createContextLocalName) + transformCreateContext(ctx, createContextLocalName); const components = detectComponents(code); injectHmrImport(s); @@ -570,10 +608,11 @@ export function vanjsRefresh(options: VanJSHMROptions = {}): Plugin { // Source of truth: apps/examples/plugin-test/src/hmr-runtime.ts // ============================================ const HMR_RUNTIME_CODE = (shapeMatching = true) => ` -import van from "@michthemaker/vanjs"; +import van, { createContext as __vanCreateContext } from "@michthemaker/vanjs"; class VanJSHMRRuntime { stateRegistry = new Map(); + contextRegistry = new Map(); // derivedRegistry = new Map(); renderSlots = new Map(); currentStateContext = null; @@ -648,6 +687,15 @@ class VanJSHMRRuntime { return state; } + createContext(hmrId) { + if (this.contextRegistry.has(hmrId)) { + return this.contextRegistry.get(hmrId); + } + const ctx = __vanCreateContext(); + this.contextRegistry.set(hmrId, ctx); + return ctx; + } + /** * @dev it's like we won't create derived anymore */ From 1df36df72953d2aa1e7f9f6cb1109beb0136bfa6 Mon Sep 17 00:00:00 2001 From: themaker Date: Sun, 12 Apr 2026 10:54:30 +0100 Subject: [PATCH 07/14] fix(packages/vite-plugin-vanjs, examples/plugin-test) adding support for vanjs context support in vite hmr --- packages/vite-plugin-vanjs/src/plugin.ts | 44 +++++++++++++----------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/vite-plugin-vanjs/src/plugin.ts b/packages/vite-plugin-vanjs/src/plugin.ts index 8dc792f..ce155eb 100644 --- a/packages/vite-plugin-vanjs/src/plugin.ts +++ b/packages/vite-plugin-vanjs/src/plugin.ts @@ -820,27 +820,29 @@ class VanJSHMRRuntime { } showErrorOverlay(slotId, error) { - this.dismissErrorOverlay(); - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? (error.stack || '') : ''; - const overlay = document.createElement('div'); - overlay.id = '__vanjs-hmr-error-overlay'; - overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);color:#fff;z-index:99999;font-family:monospace;padding:40px;box-sizing:border-box;overflow:auto;display:flex;flex-direction:column;'; - overlay.innerHTML = '
' - + '
' - + '

HMR Error in ' + slotId + '

' - + '' - + '
' - + '

Old DOM preserved. Fix the error and save to retry.

' - + '
'
-      + this.escapeHtml(message) + '\\n\\n' + this.escapeHtml(stack) + '
'; - const dismiss = () => this.dismissErrorOverlay(); - overlay.querySelector('#__vanjs-hmr-dismiss')?.addEventListener('click', dismiss); - const onKey = (e) => { if (e.key === 'Escape') { dismiss(); document.removeEventListener('keydown', onKey); } }; - document.addEventListener('keydown', onKey); - document.body.appendChild(overlay); - this.errorOverlay = overlay; - } + this.dismissErrorOverlay(); + const message = error instanceof Error ? error.message : String(error); + const stack = error instanceof Error ? (error.stack || '') : ''; + const overlay = document.createElement('div'); + overlay.id = '__vanjs-hmr-error-overlay'; + overlay.className = 'vanjs-hmr-no-scrollbar'; + overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);color:#fff;z-index:99999;font-family:monospace;padding:12px;box-sizing:border-box;overflow:auto;display:flex;flex-direction:column;'; + overlay.innerHTML = '' + + '
' + + '
' + + '

HMR Error in ' + slotId + '

' + + '' + + '
' + + '

Old DOM preserved. Fix the error and save to retry.

' + + '
'
+        + this.escapeHtml(message) + '\\n\\n' + this.escapeHtml(stack) + '
'; + const dismiss = () => this.dismissErrorOverlay(); + overlay.querySelector('#__vanjs-hmr-dismiss')?.addEventListener('click', dismiss); + const onKey = (e) => { if (e.key === 'Escape') { dismiss(); document.removeEventListener('keydown', onKey); } }; + document.addEventListener('keydown', onKey); + document.body.appendChild(overlay); + this.errorOverlay = overlay; + } dismissErrorOverlay() { if (this.errorOverlay) { this.errorOverlay.remove(); this.errorOverlay = null; } From f9249d14aed6d668b69d6588953c0c5a8479ad8b Mon Sep 17 00:00:00 2001 From: themaker Date: Sun, 12 Apr 2026 11:17:25 +0100 Subject: [PATCH 08/14] fix(packages/vite-plugin-vanjs, examples/plugin-test) adding support for vanjs context support in vite hmr --- packages/vanjs/src/utils/context.ts | 6 ++++- packages/vite-plugin-vanjs/src/plugin.ts | 30 +++++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/vanjs/src/utils/context.ts b/packages/vanjs/src/utils/context.ts index 83bdcdb..8d4f8d8 100644 --- a/packages/vanjs/src/utils/context.ts +++ b/packages/vanjs/src/utils/context.ts @@ -14,7 +14,11 @@ export type Context = { ) => ChildDom; }; -const contextStacks = new Map, StateView[]>(); +/** + * @internal do not use this in app + * reserved from framework/vite plugin authors + */ +export const contextStacks = new Map, StateView[]>(); /** * Creates a context object for sharing state without prop drilling. diff --git a/packages/vite-plugin-vanjs/src/plugin.ts b/packages/vite-plugin-vanjs/src/plugin.ts index ce155eb..1881884 100644 --- a/packages/vite-plugin-vanjs/src/plugin.ts +++ b/packages/vite-plugin-vanjs/src/plugin.ts @@ -608,7 +608,7 @@ export function vanjsRefresh(options: VanJSHMROptions = {}): Plugin { // Source of truth: apps/examples/plugin-test/src/hmr-runtime.ts // ============================================ const HMR_RUNTIME_CODE = (shapeMatching = true) => ` -import van, { createContext as __vanCreateContext } from "@michthemaker/vanjs"; +import van, { createContext as __vanCreateContext, contextStacks as __contextStacks } from "@michthemaker/vanjs"; class VanJSHMRRuntime { stateRegistry = new Map(); @@ -803,7 +803,14 @@ class VanJSHMRRuntime { const startMarker = new Comment('hmr:' + id + ':start'); const endMarker = new Comment('hmr:' + id + ':end'); - this.renderSlots.set(id, { startMarker, endMarker, props }); + + // Snapshot active context values at render time + const contextSnapshot = new Map(); + for (const [ctx, stack] of __contextStacks.entries()) { + if (stack.length > 0) contextSnapshot.set(ctx, stack[stack.length - 1]); + } + + this.renderSlots.set(id, { startMarker, endMarker, props, contextSnapshot }); const element = fn(props); this.currentInstanceId = prevInstanceId; @@ -889,7 +896,24 @@ class VanJSHMRRuntime { if (props !== undefined) slot.props = props; try { - const newElement = fn(slot.props); + // Replay context snapshot so useContext works outside original call tree + const snapshot = slot.contextSnapshot; + if (snapshot) { + for (const [ctx, value] of snapshot.entries()) { + if (!__contextStacks.has(ctx)) __contextStacks.set(ctx, []); + __contextStacks.get(ctx).push(value); + } + } + let newElement; + try { + newElement = fn(slot.props); + } finally { + if (snapshot) { + for (const [ctx] of snapshot.entries()) { + __contextStacks.get(ctx)?.pop(); + } + } + } this.currentInstanceId = prevInstanceId; parent.insertBefore(newElement, endMarker); this.dismissErrorOverlay(); From 36ce0eeb76feaf73c36b86a605b269b5359b2761 Mon Sep 17 00:00:00 2001 From: themaker Date: Sun, 12 Apr 2026 11:19:58 +0100 Subject: [PATCH 09/14] fix(packages/vite-plugin-vanjs, examples/plugin-test) adding support for vanjs context support in vite hmr x2 --- apps/examples/plugin-test/src/App.ts | 4 ---- apps/examples/plugin-test/src/GifModal.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/examples/plugin-test/src/App.ts b/apps/examples/plugin-test/src/App.ts index bf97771..e261f72 100644 --- a/apps/examples/plugin-test/src/App.ts +++ b/apps/examples/plugin-test/src/App.ts @@ -25,11 +25,7 @@ export const App = (props: { name: string }) => { props.name, myName ), - // the below uses useQueryFt which calls useContext underneath, see ./context-test.ts file if needed - // when i save this file exporting the GifModal, there is an error saying useContext must be called within a Provider GifModal(), - // the below uses useQueryFt which calls useContext underneath, see ./context-test.ts file if needed - // when i save this exact file, everything works fine, no error () => { const me = useQueryFt(); return me.name; diff --git a/apps/examples/plugin-test/src/GifModal.ts b/apps/examples/plugin-test/src/GifModal.ts index 3aa8d4f..cc4c8c7 100644 --- a/apps/examples/plugin-test/src/GifModal.ts +++ b/apps/examples/plugin-test/src/GifModal.ts @@ -7,7 +7,7 @@ const { p } = van.tags; const GifModal = () => { const queryContext = useQueryFt(); - return p(queryContext.name, "name is usss and them too"); + return p(queryContext.name, "name is usss and them four"); }; export { GifModal }; From e32c20f83c63cf85cc3a8be43d2d7cf9aaa66745 Mon Sep 17 00:00:00 2001 From: themaker Date: Sun, 12 Apr 2026 11:29:49 +0100 Subject: [PATCH 10/14] fix(packages/vite-plugin-vanjs, examples/plugin-test) adding support for vanjs context support in vite hmr x3 --- apps/examples/plugin-test/src/App.ts | 70 +++++++++++++------ apps/examples/plugin-test/src/GifModal.ts | 2 +- apps/examples/plugin-test/src/context-test.ts | 15 +++- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/apps/examples/plugin-test/src/App.ts b/apps/examples/plugin-test/src/App.ts index e261f72..e1ff426 100644 --- a/apps/examples/plugin-test/src/App.ts +++ b/apps/examples/plugin-test/src/App.ts @@ -1,35 +1,63 @@ import van, { type Ref } from "@michthemaker/vanjs"; -import { QueryContextProvider, useQueryFt } from "./context-test"; +import { + QueryContextProvider, + ThemeContextProvider, + useQueryFt, + useTheme, +} from "./context-test"; import { GifModal } from "./GifModal"; -const { div, h1, p } = van.tags; +const { div, h1, p, span, button } = van.tags; -// Component with props - using named export +// Edge case 1: nested providers (ThemeContext inside QueryContext) +// Expected: both contexts available to deep children on rerender export const App = (props: { name: string }) => { const myName = van.state("Mich"); const ref: Ref = { current: null }; + return QueryContextProvider(() => - div( - { - style: - "padding-inline: 4px; font-family: sans-serif;; margin: 0; overflow: auto;", - class: "no-scrollbar", - }, - h1( + ThemeContextProvider(() => + div( { style: - "width: 90%; color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px;", - ref: ref, + "padding-inline: 4px; font-family: sans-serif; margin: 0; overflow: auto;", + class: "no-scrollbar", }, - "VanJS Multi-File HMR Test - me usaa and meee ", - props.name, - myName - ), - GifModal(), - () => { - const me = useQueryFt(); - return me.name; - } + h1( + { + style: + "width: 90%; color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px;", + ref: ref, + }, + "VanJS HMR Context Edge Cases — ", + props.name, + " ", + myName + ), + + // Edge case 1: child component using ONE context (QueryContext) + // Save GifModal.ts → should rerender fine with QueryContext snapshot + GifModal(), + + // Edge case 2: inline fn using BOTH contexts simultaneously + // Save App.ts → should have both QueryContext + ThemeContext in snapshot + () => { + const query = useQueryFt(); + const theme = useTheme(); + return span( + { style: `color: ${theme.color};` }, + `query=${query.name} dark=${theme.dark}` + ); + }, + + // Edge case 3: same context used twice in same tree + // Both reads should return same value + () => { + const a = useQueryFt(); + const b = useQueryFt(); + return p(`same context yeah twice: ${a.name} === ${b.name}`); + } + ) ) ); }; diff --git a/apps/examples/plugin-test/src/GifModal.ts b/apps/examples/plugin-test/src/GifModal.ts index cc4c8c7..e3cd679 100644 --- a/apps/examples/plugin-test/src/GifModal.ts +++ b/apps/examples/plugin-test/src/GifModal.ts @@ -7,7 +7,7 @@ const { p } = van.tags; const GifModal = () => { const queryContext = useQueryFt(); - return p(queryContext.name, "name is usss and them four"); + return p(queryContext.name, "name is usss and them eight"); }; export { GifModal }; diff --git a/apps/examples/plugin-test/src/context-test.ts b/apps/examples/plugin-test/src/context-test.ts index 7dba61f..47926c8 100644 --- a/apps/examples/plugin-test/src/context-test.ts +++ b/apps/examples/plugin-test/src/context-test.ts @@ -1,15 +1,26 @@ import van, { createContext, useContext } from "@michthemaker/vanjs"; const QueryContext = createContext<{ name: string }>(); +const ThemeContext = createContext<{ color: string; dark: boolean }>(); const QueryContextProvider = (childrenFn: () => any) => { - const state = van.state({ name: "kashiba" }); + const state = van.state({ name: "omaeba" }); return QueryContext.Provider(state, childrenFn); }; +const ThemeContextProvider = (childrenFn: () => any) => { + const state = van.state({ color: "blue", dark: false }); + return ThemeContext.Provider(state, childrenFn); +}; + const useQueryFt = () => { const { val: queryContext } = useContext(QueryContext); return queryContext; }; -export { QueryContextProvider, useQueryFt }; +const useTheme = () => { + const { val: theme } = useContext(ThemeContext); + return theme; +}; + +export { QueryContextProvider, ThemeContextProvider, useQueryFt, useTheme }; From 2806423f7eda873073d416f2f5b38a41d9b9cd87 Mon Sep 17 00:00:00 2001 From: themaker Date: Sun, 12 Apr 2026 15:29:03 +0100 Subject: [PATCH 11/14] fix(packages/vite-plugin-vanjs, examples/plugin-test) adding support for vanjs context support in vite hmr x4 more edge case tests done --- apps/examples/plugin-test/src/App.ts | 11 ++++-- apps/examples/plugin-test/src/UserProfile.ts | 34 +++++++++++++++++++ apps/examples/plugin-test/src/context-test.ts | 8 ++++- 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 apps/examples/plugin-test/src/UserProfile.ts diff --git a/apps/examples/plugin-test/src/App.ts b/apps/examples/plugin-test/src/App.ts index e1ff426..1bc8000 100644 --- a/apps/examples/plugin-test/src/App.ts +++ b/apps/examples/plugin-test/src/App.ts @@ -6,6 +6,7 @@ import { useTheme, } from "./context-test"; import { GifModal } from "./GifModal"; +import { UserProfile } from "./UserProfile"; const { div, h1, p, span, button } = van.tags; @@ -51,12 +52,16 @@ export const App = (props: { name: string }) => { }, // Edge case 3: same context used twice in same tree - // Both reads should return same value () => { const a = useQueryFt(); const b = useQueryFt(); - return p(`same context yeah twice: ${a.name} === ${b.name}`); - } + return p(`same context twice: ${a.name} === ${b.name}`); + }, + + // Edge case 4: component that owns its OWN Provider internally + // UserBadge inside UserProfile uses UserContext + // Save UserProfile.ts → UserBadge should rerender with UserContext snapshot + UserProfile() ) ) ); diff --git a/apps/examples/plugin-test/src/UserProfile.ts b/apps/examples/plugin-test/src/UserProfile.ts new file mode 100644 index 0000000..d9a4242 --- /dev/null +++ b/apps/examples/plugin-test/src/UserProfile.ts @@ -0,0 +1,34 @@ +// Edge case: component that owns its own Context.Provider +// and has a deeply nested consumer in same file + +import van from "@michthemaker/vanjs"; +import { UserContext, useUser } from "./context-test"; + +const { div, p, span, strong } = van.tags; + +// Deep consumer — no Provider above it in its own file +// relies on snapshot replay during HMR rerender +const UserBadge = () => { + const user = useUser(); + return span( + { style: "background: #eee; padding: 4px 8px; border-radius: 4px;" }, + strong(user.role + "me and you"), + " — ", + user.username + ); +}; + +// Owns the Provider + renders UserBadge inside it +const UserProfile = () => { + const userState = van.state({ username: "michthemaker", role: "admin" }); + return UserContext.Provider(userState, () => + div( + { style: "border: 1px solid #ccc; padding: 12px; margin-top: 12px;" }, + p("UserProfile component owns this Provider"), + UserBadge(), + p(() => `role is: ${userState.val.role}`) + ) + ); +}; + +export { UserProfile, UserBadge }; diff --git a/apps/examples/plugin-test/src/context-test.ts b/apps/examples/plugin-test/src/context-test.ts index 47926c8..c921e10 100644 --- a/apps/examples/plugin-test/src/context-test.ts +++ b/apps/examples/plugin-test/src/context-test.ts @@ -2,9 +2,10 @@ import van, { createContext, useContext } from "@michthemaker/vanjs"; const QueryContext = createContext<{ name: string }>(); const ThemeContext = createContext<{ color: string; dark: boolean }>(); +export const UserContext = createContext<{ username: string; role: string }>(); const QueryContextProvider = (childrenFn: () => any) => { - const state = van.state({ name: "omaeba" }); + const state = van.state({ name: "kashiba" }); return QueryContext.Provider(state, childrenFn); }; @@ -23,4 +24,9 @@ const useTheme = () => { return theme; }; +export const useUser = () => { + const { val: user } = useContext(UserContext); + return user; +}; + export { QueryContextProvider, ThemeContextProvider, useQueryFt, useTheme }; From e593c95da5fe53f8fe8434e9280c0fca0eab9be8 Mon Sep 17 00:00:00 2001 From: themaker Date: Sun, 12 Apr 2026 15:34:54 +0100 Subject: [PATCH 12/14] fix(packages/vite-plugin-vanjs/src/plugin.ts) stray debug log removed --- packages/vite-plugin-vanjs/src/plugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vite-plugin-vanjs/src/plugin.ts b/packages/vite-plugin-vanjs/src/plugin.ts index 1881884..c39be9d 100644 --- a/packages/vite-plugin-vanjs/src/plugin.ts +++ b/packages/vite-plugin-vanjs/src/plugin.ts @@ -651,7 +651,6 @@ class VanJSHMRRuntime { // Shape changed — reset in place to keep derived subscriptions intact this.log('summary', '[VanJS HMR] 🔄 State shape changed for "' + hmrId + '" — resetting'); preserved.val = initial; - console.log(preserved) return preserved; } const state = this.originalVanState(initial); From 858ccba7039a830e410ca367718f4864cb2bec54 Mon Sep 17 00:00:00 2001 From: themaker Date: Sun, 12 Apr 2026 16:38:34 +0100 Subject: [PATCH 13/14] fix(packages/create-van-app) bump deps version in create-van-app --- .changeset/deep-brooms-repeat.md | 18 ++++++++++++++---- .changeset/pink-frogs-enjoy.md | 7 +++++++ .changeset/ten-moles-nail.md | 4 +++- .changeset/twelve-trams-shop.md | 4 ++-- .changeset/wet-masks-slide.md | 8 ++++++++ .changeset/wicked-meteors-turn.md | 8 ++++++++ .../template-js-css/package.json | 4 ++-- .../template-js-tailwind/package.json | 4 ++-- .../template-ts-css/package.json | 4 ++-- .../template-ts-tailwind/package.json | 4 ++-- 10 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 .changeset/pink-frogs-enjoy.md create mode 100644 .changeset/wet-masks-slide.md create mode 100644 .changeset/wicked-meteors-turn.md diff --git a/.changeset/deep-brooms-repeat.md b/.changeset/deep-brooms-repeat.md index e0c04e6..0e73bd0 100644 --- a/.changeset/deep-brooms-repeat.md +++ b/.changeset/deep-brooms-repeat.md @@ -2,14 +2,24 @@ "@michthemaker/vanjs": minor --- -Separate functions for SVG and MathML Elements added +Added dedicated SVG and MathML element support + +VanJS now provides specialized tag creation for SVG and MathML elements through `van.svgTags` and `van.mathmlTags`, giving you proper type safety and element creation for graphics and mathematical markup alongside regular HTML elements. ```typescript -const { svg, path } = van.svgTags; +const { svg, path, circle } = van.svgTags; const { div, h1, button } = van.tags; +const { math, mi, mo } = van.mathmlTags; +// Create SVG graphics svg({ viewBox: "0 0 24 24", - width: "15", -}); + width: "24", + height: "24" +}, + path({ d: "M12 2L2 7v10c0 5.55 3.84 10 9 10s9-4.45 9-10V7l-10-5z" }) +); + +// Create mathematical notation +math(mi("x"), mo("+"), mi("y")); ``` diff --git a/.changeset/pink-frogs-enjoy.md b/.changeset/pink-frogs-enjoy.md new file mode 100644 index 0000000..84c32c5 --- /dev/null +++ b/.changeset/pink-frogs-enjoy.md @@ -0,0 +1,7 @@ +--- +"@michthemaker/vite-plugin-vanjs": patch +--- + +Fixed duplicate export statements during hot reload + +The Vite plugin was incorrectly duplicating export statements like `export { App }` when processing components for hot module replacement. This has been resolved, ensuring clean exports without duplication. diff --git a/.changeset/ten-moles-nail.md b/.changeset/ten-moles-nail.md index 083ff1b..96ec942 100644 --- a/.changeset/ten-moles-nail.md +++ b/.changeset/ten-moles-nail.md @@ -2,4 +2,6 @@ "@michthemaker/vite-plugin-vanjs": minor --- -Better error message container wrapper in hmr +Improved error display during hot module replacement + +Error messages during development now display in a better-styled container during hot module replacement, making debugging issues easier to spot and read. diff --git a/.changeset/twelve-trams-shop.md b/.changeset/twelve-trams-shop.md index 723a80a..47290d0 100644 --- a/.changeset/twelve-trams-shop.md +++ b/.changeset/twelve-trams-shop.md @@ -2,6 +2,6 @@ "@michthemaker/vanjs": minor --- -Added more robust types to van.ts +Improved type definitions for DOM element properties -Rewrote the vanjs types to better handle type resolution for DOM propperties +Rewrote the VanJS type system to provide better type resolution when setting attributes and properties on DOM elements. This gives you more accurate autocompletion and compile-time checks when using `van.tags`. diff --git a/.changeset/wet-masks-slide.md b/.changeset/wet-masks-slide.md new file mode 100644 index 0000000..602924a --- /dev/null +++ b/.changeset/wet-masks-slide.md @@ -0,0 +1,8 @@ +--- +"@michthemaker/vite-plugin-vanjs": minor +--- + +Improved context support during Hot Module Replacement (HMR) + +- **Stable contexts across reloads**: `createContext()` is intercepted and keyed by `hmrId`, so the same context object is reused between HMR updates. +- **Context snapshot + replay**: active context stacks are captured on `registerRender` and replayed on `rerender`, allowing `useContext` to keep working even when called outside the original render call tree. diff --git a/.changeset/wicked-meteors-turn.md b/.changeset/wicked-meteors-turn.md new file mode 100644 index 0000000..d6cb6f1 --- /dev/null +++ b/.changeset/wicked-meteors-turn.md @@ -0,0 +1,8 @@ +--- +"@michthemaker/vite-plugin-vanjs": minor +--- + +Better handling for multiple and aliased exports + +- Multiple named exports like `export { Foo, Bar }` are now correctly preserved during transformation. +- Aliased exports such as `export { Foo as Bar }` now generate the correct wrapper and transform logic. diff --git a/packages/create-van-app/template-js-css/package.json b/packages/create-van-app/template-js-css/package.json index cd09d8e..b7db04e 100644 --- a/packages/create-van-app/template-js-css/package.json +++ b/packages/create-van-app/template-js-css/package.json @@ -9,10 +9,10 @@ "preview": "vite preview" }, "dependencies": { - "@michthemaker/vanjs": "^0.2.0" + "@michthemaker/vanjs": "^0.4.0" }, "devDependencies": { - "@michthemaker/vite-plugin-vanjs": "^0.1.2", + "@michthemaker/vite-plugin-vanjs": "^0.2.0", "@types/node": "^25.3.0", "vite": "latest" } diff --git a/packages/create-van-app/template-js-tailwind/package.json b/packages/create-van-app/template-js-tailwind/package.json index 1e37c3f..94c8a89 100644 --- a/packages/create-van-app/template-js-tailwind/package.json +++ b/packages/create-van-app/template-js-tailwind/package.json @@ -9,12 +9,12 @@ "preview": "vite preview" }, "dependencies": { - "@michthemaker/vanjs": "^0.2.0", + "@michthemaker/vanjs": "^0.4.0", "clsx": "^2.1.1", "tailwind-merge": "^2.6.0" }, "devDependencies": { - "@michthemaker/vite-plugin-vanjs": "^0.1.2", + "@michthemaker/vite-plugin-vanjs": "^0.2.0", "autoprefixer": "^10.4.21", "postcss": "^8.5.6", "tailwindcss": "^3.4.9", diff --git a/packages/create-van-app/template-ts-css/package.json b/packages/create-van-app/template-ts-css/package.json index adcfb7f..a592aca 100644 --- a/packages/create-van-app/template-ts-css/package.json +++ b/packages/create-van-app/template-ts-css/package.json @@ -10,10 +10,10 @@ "check": "tsc -b --noEmit" }, "dependencies": { - "@michthemaker/vanjs": "^0.2.0" + "@michthemaker/vanjs": "^0.4.0" }, "devDependencies": { - "@michthemaker/vite-plugin-vanjs": "^0.1.2", + "@michthemaker/vite-plugin-vanjs": "^0.2.0", "@types/node": "^25.3.0", "typescript": "latest", "vite": "latest" diff --git a/packages/create-van-app/template-ts-tailwind/package.json b/packages/create-van-app/template-ts-tailwind/package.json index d8ba04b..453dbcf 100644 --- a/packages/create-van-app/template-ts-tailwind/package.json +++ b/packages/create-van-app/template-ts-tailwind/package.json @@ -10,12 +10,12 @@ "check": "tsc -b --noEmit" }, "dependencies": { - "@michthemaker/vanjs": "^0.2.0", + "@michthemaker/vanjs": "^0.4.0", "clsx": "^2.1.1", "tailwind-merge": "^2.6.0" }, "devDependencies": { - "@michthemaker/vite-plugin-vanjs": "^0.1.2", + "@michthemaker/vite-plugin-vanjs": "^0.2.0", "@types/node": "^25.3.0", "autoprefixer": "^10.4.21", "postcss": "^8.5.6", From 99fa633f33b0fba00cd376bdc07c86e039764171 Mon Sep 17 00:00:00 2001 From: themaker Date: Sun, 12 Apr 2026 16:44:12 +0100 Subject: [PATCH 14/14] fix(.changeset/*.md) added `bump deps version in create-van-app changeset` for create-van-app --- .changeset/small-moles-arrive.md | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .changeset/small-moles-arrive.md diff --git a/.changeset/small-moles-arrive.md b/.changeset/small-moles-arrive.md new file mode 100644 index 0000000..9a27758 --- /dev/null +++ b/.changeset/small-moles-arrive.md @@ -0,0 +1,68 @@ +--- +"create-van-app": major +--- + +Scaffold Your First Van Project using `create-van-app` + +> **Compatibility Note:** +> Create Van App requires [Node.js](https://nodejs.org/en/) version 20.19+, 22.12+. However, some templates require a higher Node.js version to work, please upgrade if your package manager warns about it. + +With NPM: + +```bash +npm create van-app@latest +``` + +With Yarn: + +```bash +yarn create van-app +``` + +With PNPM: + +```bash +pnpm create van-app +``` + +With Bun: + +```bash +bun create van-app +``` + +With Deno: + +```bash +deno init --npm van-app +``` + +Then follow the prompts! + +You can also directly specify the project name and the template you want to use via additional command line options. For example, to scaffold a VanJS + Tailwind project, run: + +```bash +# npm 7+, extra double-dash is needed: +npm create van-app@latest my-van-app -- --template vanjs-ts-tailwind + +# yarn +yarn create van-app my-van-app --template vanjs-ts-tailwind + +# pnpm +pnpm create van-app my-van-app --template vanjs-ts-tailwind + +# Bun +bun create van-app my-van-app --template vanjs-ts-tailwind + +# Deno +deno init --npm van-app my-van-app --template vanjs-ts-tailwind +``` + +Currently supported template presets include: + +- `vanjs-ts` + `tailwind` ← default +- `vanjs-ts` + `css` +- `vanjs` + `tailwind` +- `vanjs` + `css` + +You can use `.` for the project name to scaffold in the current directory.