diff --git a/knip.config.ts b/knip.config.ts index e50d449..9f7fa4c 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -17,17 +17,15 @@ const config: KnipConfig = { 'packages/react/*': { entry: ['stories/*.stories.tsx'], }, - // knip's storybook plugin doesn't know the community solid framework, so - // the harness config is declared an entry by hand. jest-dom is loaded via - // a setup file vite-plugin-solid injects by bare specifier. + // knip's storybook plugin doesn't know the community solid framework; + // jest-dom is loaded via a setup file vite-plugin-solid injects. 'packages/solid': { entry: ['.storybook/main.ts', '.storybook/manager.ts'], ignoreDependencies: ['@testing-library/jest-dom'], }, 'packages/solid/*': { entry: ['stories/*.stories.tsx'], - // The babel presets are referenced by name (strings) in each package's - // tsdown.config.ts — invisible to import analysis. + // The babel presets are referenced as strings in tsdown.config.ts. ignoreDependencies: ['babel-preset-solid', '@babel/preset-typescript'], }, }, diff --git a/packages/solid/dialog/src/context.ts b/packages/solid/dialog/src/context.ts index ce6eafc..2d1ebb5 100644 --- a/packages/solid/dialog/src/context.ts +++ b/packages/solid/dialog/src/context.ts @@ -2,30 +2,21 @@ import { createContext, useContext, type Accessor, type Context } from 'solid-js import type { DialogApi, DialogMachine } from '@dunky.dev/dialog' export interface DialogContextValue { - // The connected api as a fine-grained store proxy — reading a field in JSX - // or an effect subscribes to exactly that leaf. + // Fine-grained store proxy: reading a field subscribes to exactly that leaf. api: DialogApi machine: DialogMachine - // Nesting level (1 = top-level). Decides the topmost dialog of a stack for - // Escape, focus, and assistive-tech containment. + // Nesting level (1 = top-level); decides the topmost dialog of a stack. depth: number - // The element the Portal teleported into, or null for the page body — - // Content scopes the scroll lock to it. An accessor so the Portal's prop - // stays live: the root provides null; Portal re-provides the context with - // the field filled in. + // The Portal's container (null = page body); an accessor so it stays live. container: Accessor - // The rendered Backdrop element, shared because Backdrop and Content are - // sibling parts: Content's stack entry excepts its own backdrop from the - // containment so it stays pressable while its dialog is topmost. A plain - // mutable box, not a signal: the layer walk reads it synchronously inside - // the same settle that mounts the backdrop, before a signal write would - // commit. + // The rendered Backdrop, shared so Content's stack entry can except it from + // the containment. A plain box, not a signal: the layer walk reads it in the + // same settle that mounts the backdrop, before a signal write would commit. backdropRef: { current: HTMLDivElement | null } } -// The `null` default keeps the root's parent-lookup non-throwing (depth -// derives from an optional read; a default-less context throws on it); the -// wrapper below restores the loud error for parts, naming the component. +// A `null` default: a default-less context throws on the root's optional +// parent lookup; the wrapper restores the loud error for parts. export const DialogContext: Context = createContext(null) diff --git a/packages/solid/dialog/src/dialog.tsx b/packages/solid/dialog/src/dialog.tsx index be76b38..1d9fac5 100644 --- a/packages/solid/dialog/src/dialog.tsx +++ b/packages/solid/dialog/src/dialog.tsx @@ -25,16 +25,13 @@ import { mergeProps, normalize } from '@dunky.dev/solid-state-machine' import { DialogContext, useDialogContext } from './context' import { useDialog } from './use-dialog' -// A part's bindings merge INSIDE the JSX spread: the compiler wraps the -// expression in a reactive scope, so a machine transition re-translates it. -// `children` never rides that spread — a re-evaluated spread re-CREATES the -// children it carries, and a child whose lifecycle writes to the machine -// (Title's presence) would then loop machine -> spread -> remount -> machine. -// Every part strips it and renders `{props.children}` explicitly. - -// A consumer ref that crossed a component boundary is a setter function or an -// array of them (Solid 2.0 refs are functions; arrays compose); apply it -// alongside the part's own element capture. +// Bindings merge inside the JSX spread so they stay reactive. `children` must +// never ride that spread: a re-evaluated spread re-creates the children, and a +// child that writes to the machine on mount (Title) would loop forever. Every +// part omits it and renders `{props.children}` explicitly. + +// A consumer ref that crossed a component boundary is a function or an array +// of functions. function applyConsumerRef(ref: Ref | undefined, element: T): void { if (typeof ref === 'function') (ref as (element: T) => void)(element) else if (Array.isArray(ref)) for (const entry of ref) applyConsumerRef(entry as Ref, element) @@ -56,11 +53,8 @@ export const Dialog: Component & Parts = props => { const backdropRef: { current: HTMLDivElement | null } = { current: null } // closeOnBack: while open, a guard entry in the session history turns the - // host's Back into a dismissal instead of a navigation. Every decision - // (gate, veto, controlled) lives in the core's backNavigate; this effect - // only wires the web mechanics. It tracks `api.open` alone — fresh callback - // identities never churn real session-history entries. It lives on the - // root — the guard concerns the dialog's openness, not any rendered part. + // browser's Back into a dismissal. The decision (gate, veto, controlled) + // lives in the core's backNavigate; this only wires the web mechanics. createEffect( () => api.open, open => { @@ -111,8 +105,8 @@ export const Portal: Component = props => { const context = useDialogContext() if (isServer) return null return ( - // `mounted`, not `open`: an animated dialog stays in the tree through - // `closing` so its exit visual can play before everything unmounts. + // `mounted`, not `open`: an animated dialog stays mounted through + // `closing` so its exit visual can play. {/* keyed: the host portal's mount is fixed at creation, so a container swap re-creates the portal on the new target. */} @@ -140,8 +134,6 @@ export interface DialogBackdropProps extends ComponentProps<'div'> {} export const Backdrop: Component = props => { const { api, machine, backdropRef } = useDialogContext() const rest = omit(props, 'ref', 'children') - // The shared slot must not outlive the element: the context box lives on - // the root, the element only until this part's owner disposes. onSettled(() => () => (backdropRef.current = null)) const bindings = (): Record => { @@ -189,9 +181,8 @@ export const Viewport: Component = props => { } & Record return { ...attrs, - // Content presses bubble up here — only a press that started on the - // viewport itself is an outside interaction, and only the topmost dialog - // of a stack answers it. + // Only a press that started on the viewport itself is an outside + // interaction, and only the topmost dialog of a stack answers it. onClick: (event: MouseEvent) => { if (event.target !== event.currentTarget) return if (!isTopmostLayer(machine.context.id)) return @@ -210,8 +201,7 @@ export const Viewport: Component = props => { export interface DialogContentProps extends ComponentProps<'div'> { /** The element to focus when the dialog opens — an element, or an accessor - * resolved at open time (the Solid idiom for a ref variable that fills - * during render). @default the dialog window */ + * resolved at open time. @default the dialog window */ initialFocus?: HTMLElement | (() => HTMLElement | null | undefined) } @@ -223,14 +213,11 @@ export const Content: Component = props => { const rest = omit(props, 'ref', 'initialFocus', 'children') let contentEl: HTMLDivElement | undefined - // The machine's `open` state is the edge, not mount/unmount — an animated - // dialog stays mounted through `closing`, and the stack, containment, and - // focus must release the moment the exit starts, not when it finishes. - // One effect keeps the ordering right both ways: the stack joins before focus - // moves in, and on close it must release the layers beneath (un-inert them) - // before focus can move back out to one of them. Apply-phase reads go - // through untrack — `api.open` is the one edge; the options must not re-run - // the effect. + // The `open` state is the edge, not mount/unmount: an animated dialog stays + // mounted through `closing`, and the stack, containment, and focus must + // release the moment the exit starts. One effect keeps the order right both + // ways: the stack joins before focus moves in; on close it releases the + // layers beneath before focus moves back out. createEffect( () => api.open, open => { @@ -246,13 +233,12 @@ export const Content: Component = props => { backdrop: () => backdropRef.current, }) - // preventScroll everywhere: the scroll lock already froze the surface, so - // moving focus must not scroll it — otherwise opening jumps the (top-of- - // container) dialog into view and closing jumps back to the trigger. + // preventScroll everywhere: moving focus must not scroll the locked + // surface, or open/close jumps the view. const target = untrack(() => resolveInitialFocus(props.initialFocus)) ?? getInitialFocus(content) target.focus({ preventScroll: true }) - // A target that can't take focus (disabled, hidden) falls back to the panel. + // A target that can't take focus falls back to the panel. if (document.activeElement !== target) content.focus({ preventScroll: true }) return () => { @@ -262,10 +248,9 @@ export const Content: Component = props => { }, ) - // The exit window: Content live while not open only happens in `closing`. - // The layer has already released everything above, so hide the still-painting - // layer from interaction and report when its visual is done; the cleanup is - // the reopen interrupt (and final unmount) undoing both. + // The exit window: mounted while not open only happens in `closing`. Hide + // the still-painting layer and report when its visual is done; the cleanup + // is the reopen interrupt (and final unmount) undoing both. createEffect( () => api.open, open => { @@ -284,24 +269,19 @@ export const Content: Component = props => { ) // The lock spans the whole mount — through `closing` too: releasing it - // mid-exit would bring the scrollbar back and reflow the page under the - // still-painting layer. A scoped dialog locks its portal container; a page - // dialog locks the body. + // mid-exit would reflow the page under the still-painting layer. useScrollLock(() => machine.context.modal, container) useFocusTrap(() => contentEl ?? null, { - // Only a modal dialog traps, and only while topmost — a nested dialog - // owns focus while open. + // Only a modal dialog traps, and only while topmost. enabled: () => machine.context.modal && isTopmostLayer(machine.context.id), - // The Close part is the cycle's last stop wherever it renders (core - // SPEC); found by its derived id. + // Close is the cycle's last stop wherever it renders (core SPEC). last: () => document.getElementById(api.ids.close), }) - // A neutral element with the role, not : the window is the initial - // focus target, so it carries tabindex — which HTML forbids on — - // and the native element only pays off via showModal(), which this contract - // deliberately doesn't use. + // A neutral element with the role, not : the window carries + // tabindex (forbidden on ), and this contract doesn't use + // showModal() — see SPEC.md. return (
(rest, normalize(api.parts.content))} @@ -325,8 +305,7 @@ export const Title: Component = props => { const { api, machine } = useDialogContext() const rest = omit(props, 'children') - // Presence reports from the settled phase: the machine starts on the root's - // settle, which owner order puts before this one. + // onSettled: the machine starts on the root's settle, which runs first. onSettled(() => { machine.send({ type: 'part.presence', part: 'title', present: true }) return () => machine.send({ type: 'part.presence', part: 'title', present: false }) diff --git a/packages/solid/dialog/src/effects.ts b/packages/solid/dialog/src/effects.ts index 07d70c9..f7705e5 100644 --- a/packages/solid/dialog/src/effects.ts +++ b/packages/solid/dialog/src/effects.ts @@ -3,18 +3,14 @@ import type { DialogMachine, DialogOptions } from '@dunky.dev/dialog' import { dialogEffects } from '@dunky.dev/dialog' import { isTopmostLayer } from '@dunky.dev/dom-overlay' -// Substrate effects: the core's substrate-free list (the controlled-open -// echo) plus the document-level work only this host can own. type DialogEffect = ComponentEffect -// Escape is a document-level concern, not a part's — it must work wherever -// focus is. +// Escape is a document-level concern — it must work wherever focus is. const trackEscape: DialogEffect = [ (machine, props) => { const onKeyDown = (event: KeyboardEvent): void => { if (event.key !== 'Escape' || !machine.matches('open')) return - // Only the topmost dialog answers Escape — a nested stack closes one - // layer at a time. + // Only the topmost dialog answers Escape — one layer per press. if (!isTopmostLayer(machine.context.id)) return props.onEscapeKeyDown?.(event) if (!event.defaultPrevented) machine.send({ type: 'escape' }) @@ -25,4 +21,5 @@ const trackEscape: DialogEffect = [ ['onEscapeKeyDown'], ] +// The core's substrate-free effects plus the document-level work of this host. export const solidDialogEffects: DialogEffect[] = [...dialogEffects, trackEscape] diff --git a/packages/solid/dialog/src/use-dialog.ts b/packages/solid/dialog/src/use-dialog.ts index 53999fa..79d7034 100644 --- a/packages/solid/dialog/src/use-dialog.ts +++ b/packages/solid/dialog/src/use-dialog.ts @@ -7,9 +7,8 @@ import { solidDialogEffects } from './effects' export function useDialog(options: DialogOptions): { api: DialogApi; machine: DialogMachine } { const id = createUniqueId() - // `?? id` (via a live getter, not merge order): an explicit `id={undefined}` - // must not knock out the generated fallback — ids also key the dialog stack, - // so they must exist. `merge` keeps the rest of the options a reactive proxy. + // `?? id` via a live getter: an explicit `id={undefined}` must not knock out + // the generated fallback — ids also key the dialog stack. const props = merge(options, { get id() { return options.id ?? id diff --git a/packages/solid/dialog/tests/dialog.test.tsx b/packages/solid/dialog/tests/dialog.test.tsx index 12f6097..6477ad9 100644 --- a/packages/solid/dialog/tests/dialog.test.tsx +++ b/packages/solid/dialog/tests/dialog.test.tsx @@ -23,8 +23,8 @@ const DefaultDialog = (props: DialogProps) => (
) -// Solid 2.0 defers store commits + DOM updates to the microtask queue — every -// interaction flushes before the test reads the tree. +// Solid 2.0 defers store commits to the microtask queue — flush after every +// interaction before reading the tree. const press = (element: HTMLElement): void => { element.click() flush() diff --git a/packages/solid/dialog/tsdown.config.ts b/packages/solid/dialog/tsdown.config.ts index 53fe8bc..ff9c219 100644 --- a/packages/solid/dialog/tsdown.config.ts +++ b/packages/solid/dialog/tsdown.config.ts @@ -1,12 +1,10 @@ import { babel } from '@rollup/plugin-babel' import { defineConfig } from 'tsdown' -// Solid JSX needs Solid's own compiler: babel-preset-solid turns JSX into -// reactive templates + effects, which neither oxc nor rolldown's React-shaped -// JSX transform can produce. The plugin transforms .tsx before rolldown's own -// transform sees it; everything else (entry, dts, publint) inherits the root -// config. Babel applies presets last-to-first: TypeScript strips types while -// keeping the JSX (isTSX), then the Solid preset compiles it. +// Solid JSX needs Solid's own compiler (babel-preset-solid) — rolldown/oxc +// only know React-shaped JSX. Presets apply last-to-first: TypeScript strips +// types keeping the JSX, then the Solid preset compiles it. Everything else +// inherits the root config. export default defineConfig({ plugins: [ babel({ diff --git a/packages/solid/hooks/use-focus-trap/src/use-focus-trap.ts b/packages/solid/hooks/use-focus-trap/src/use-focus-trap.ts index cbec44b..9d711fe 100644 --- a/packages/solid/hooks/use-focus-trap/src/use-focus-trap.ts +++ b/packages/solid/hooks/use-focus-trap/src/use-focus-trap.ts @@ -6,19 +6,17 @@ export interface UseFocusTrapOptions extends TrapFocusOptions {} /** * Traps Tab / Shift+Tab within `target` while it holds an element — the Solid - * lifecycle around `trapFocus`. The trap follows the accessor: it arms when - * the target first yields an element, releases when it clears or the owner is - * disposed, and re-arms on a new element when the accessor is reactive. + * lifecycle around `trapFocus`. Arms when the target yields an element, + * releases on dispose, re-arms when a reactive accessor yields a new one. */ export function useFocusTrap( target: () => HTMLElement | null | undefined, options: UseFocusTrapOptions = {}, ): void { - // The compute tracks a reactive target (re-arm on a new element); the apply - // re-reads it fresh — compute runs eagerly at creation, before a plain ref - // variable fills, so binding off the computed value would arm on nothing. - // Options are read through the closure on each Tab press, so inline - // `enabled` / `last` see the latest state without re-binding the listener. + // The compute tracks a reactive target; the apply re-reads it fresh — + // compute runs eagerly at creation, before a plain ref variable fills. + // Options are read per Tab press, so inline `enabled` / `last` stay live + // without re-binding the listener. createEffect( () => target(), () => { diff --git a/packages/solid/hooks/use-focus-trap/tests/use-focus-trap.test.tsx b/packages/solid/hooks/use-focus-trap/tests/use-focus-trap.test.tsx index cb23e17..5fdd108 100644 --- a/packages/solid/hooks/use-focus-trap/tests/use-focus-trap.test.tsx +++ b/packages/solid/hooks/use-focus-trap/tests/use-focus-trap.test.tsx @@ -7,8 +7,7 @@ import { useFocusTrap } from '@dunky.dev/solid-use-focus-trap' function Trap(props: { enabled?: () => boolean }) { let target: HTMLDivElement | undefined - // The closure defers the props read to each Tab press — a direct - // `props.enabled` here would be a top-level reactive read. + // The closure defers the props read to each Tab press. useFocusTrap(() => target ?? null, { enabled: () => props.enabled?.() !== false }) return (
(target = el)} tabindex={-1} data-testid='container'> diff --git a/packages/solid/hooks/use-scroll-lock/src/use-scroll-lock.ts b/packages/solid/hooks/use-scroll-lock/src/use-scroll-lock.ts index e4fc378..2cd5bc5 100644 --- a/packages/solid/hooks/use-scroll-lock/src/use-scroll-lock.ts +++ b/packages/solid/hooks/use-scroll-lock/src/use-scroll-lock.ts @@ -1,8 +1,7 @@ import { createEffect } from 'solid-js' import { lockScroll } from '@dunky.dev/dom-scroll-lock' -/** A static value or an accessor — the Solid idiom for a parameter that may - * be reactive; resolved fresh inside the tracking scope that reads it. */ +/** A static value or an accessor — for parameters that may be reactive. */ export type MaybeAccessor = T | (() => T) function access(value: MaybeAccessor): T { @@ -11,10 +10,8 @@ function access(value: MaybeAccessor): T { /** * Locks scrolling while the owner lives and `locked` — the Solid lifecycle - * around `lockScroll`. Targets the page body unless a `target` element is - * given (e.g. a scoped/portaled surface locks its own container, not the - * page). The lock is shared per container: with several holders (e.g. nested - * modal layers), the container is restored only when the last one releases. + * around `lockScroll`. Targets the page body unless a `target` is given. The + * lock is shared per container: it restores when the last holder releases. */ export function useScrollLock( locked: MaybeAccessor = true, diff --git a/packages/solid/hooks/use-scroll-lock/tests/use-scroll-lock.test.ts b/packages/solid/hooks/use-scroll-lock/tests/use-scroll-lock.test.ts index a762047..ee8c88a 100644 --- a/packages/solid/hooks/use-scroll-lock/tests/use-scroll-lock.test.ts +++ b/packages/solid/hooks/use-scroll-lock/tests/use-scroll-lock.test.ts @@ -1,6 +1,4 @@ // @vitest-environment jsdom -// The Solid lifecycle around @dunky.dev/dom-scroll-lock — the refcount/restore -// behavior itself is covered in the util's own tests. import { renderHook } from '@solidjs/testing-library' import { describe, expect, it } from 'vitest' import { useScrollLock } from '@dunky.dev/solid-use-scroll-lock' diff --git a/packages/solid/tsconfig.json b/packages/solid/tsconfig.json index ddd9cc7..b433f53 100644 --- a/packages/solid/tsconfig.json +++ b/packages/solid/tsconfig.json @@ -1,10 +1,7 @@ { - // The Solid project: JSX is `preserve` + `@solidjs/web` as the import source - // (Solid 2.0 moved the web JSX namespace out of `solid-js`), so these - // packages and their tests see Solid's JSX namespace — not React's, which - // the root sets. The root tsconfig excludes packages/solid; `pnpm typecheck` - // runs this project separately. `paths` is inherited from the root file - // (resolved relative to its location), so no redeclaration. + // Solid 2.0's web JSX namespace lives in @solidjs/web, not React's — so + // packages/solid typechecks as its own project (the root tsconfig excludes + // it; `pnpm typecheck` runs both). `paths` is inherited from the root file. "extends": "../../tsconfig.json", "compilerOptions": { "jsx": "preserve", diff --git a/packages/solid/vitest.config.ts b/packages/solid/vitest.config.ts index 6e2d6f6..4584059 100644 --- a/packages/solid/vitest.config.ts +++ b/packages/solid/vitest.config.ts @@ -1,11 +1,6 @@ import solid from 'vite-plugin-solid' import { defineConfig } from 'vitest/config' -// Referenced as a project from the root vitest.config.ts; lives here so the -// root workspace carries no Solid dependencies. vite-plugin-solid force-injects -// `@testing-library/jest-dom/vitest` as a setup file whenever the package is -// resolvable (storybook ships it as a real dependency, so it always is here) — -// the harness devDep makes that injected bare specifier resolvable for vitest. export default defineConfig({ plugins: [solid()], resolve: { diff --git a/scripts/templates/packages/solid/__name__/src/__name__.tsx b/scripts/templates/packages/solid/__name__/src/__name__.tsx index b896cab..5f0580a 100644 --- a/scripts/templates/packages/solid/__name__/src/__name__.tsx +++ b/scripts/templates/packages/solid/__name__/src/__name__.tsx @@ -6,11 +6,9 @@ import { mergeProps, normalize } from '@dunky.dev/solid-state-machine' import { __Name__Context, use__Name__Context } from './context' import { use__Name__ } from './use-__name__' -// A part's bindings merge INSIDE the JSX spread: the compiler wraps the -// expression in a reactive scope, so a machine transition re-translates it. -// `children` never rides that spread — a re-evaluated spread re-CREATES the -// children it carries. Every part strips it and renders `{props.children}` -// explicitly. +// Bindings merge inside the JSX spread so they stay reactive. `children` must +// never ride that spread: a re-evaluated spread re-creates the children. +// Every part omits it and renders `{props.children}` explicitly. // ============================================================================= // <__Name__> — root, owns the machine and renders no DOM diff --git a/scripts/templates/packages/solid/__name__/src/context.ts b/scripts/templates/packages/solid/__name__/src/context.ts index 4beac17..667e806 100644 --- a/scripts/templates/packages/solid/__name__/src/context.ts +++ b/scripts/templates/packages/solid/__name__/src/context.ts @@ -2,14 +2,13 @@ import { createContext, useContext, type Context } from 'solid-js' import type { __Name__Api, __Name__Machine } from '@dunky.dev/__name__' export interface __Name__ContextValue { - // The connected api as a fine-grained store proxy — reading a field in JSX - // or an effect subscribes to exactly that leaf. + // Fine-grained store proxy: reading a field subscribes to exactly that leaf. api: __Name__Api machine: __Name__Machine } -// A `null` default (a default-less context throws on any un-provided read); -// the wrapper restores the loud error for parts, naming the component. +// A `null` default: a default-less context throws on any un-provided read; +// the wrapper restores the loud error for parts. export const __Name__Context: Context<__Name__ContextValue | null> = createContext< __Name__ContextValue | null >(null) diff --git a/scripts/templates/packages/solid/__name__/src/use-__name__.ts b/scripts/templates/packages/solid/__name__/src/use-__name__.ts index 66ba2d2..3e1e4b9 100644 --- a/scripts/templates/packages/solid/__name__/src/use-__name__.ts +++ b/scripts/templates/packages/solid/__name__/src/use-__name__.ts @@ -6,10 +6,8 @@ import type { __Name__ContextValue } from './context' import { __camelName__Effects } from './effects' /** - * Owns one __name__ machine for the <__Name__> root. `useMachine` creates it - * once (a Solid component body runs once), keeps options fresh through the - * reactive props proxy, runs the substrate effects, and exposes the connected - * api as a fine-grained store. + * Owns one __name__ machine for the <__Name__> root: created once, options + * stay fresh through the reactive props proxy, api is a fine-grained store. */ export function use__Name__(options: __Name__Options): __Name__ContextValue { return useMachine(__camelName__Machine, __camelName__Connect, __camelName__Effects, options) diff --git a/scripts/templates/packages/solid/__name__/tsdown.config.ts b/scripts/templates/packages/solid/__name__/tsdown.config.ts index 53fe8bc..ff9c219 100644 --- a/scripts/templates/packages/solid/__name__/tsdown.config.ts +++ b/scripts/templates/packages/solid/__name__/tsdown.config.ts @@ -1,12 +1,10 @@ import { babel } from '@rollup/plugin-babel' import { defineConfig } from 'tsdown' -// Solid JSX needs Solid's own compiler: babel-preset-solid turns JSX into -// reactive templates + effects, which neither oxc nor rolldown's React-shaped -// JSX transform can produce. The plugin transforms .tsx before rolldown's own -// transform sees it; everything else (entry, dts, publint) inherits the root -// config. Babel applies presets last-to-first: TypeScript strips types while -// keeping the JSX (isTSX), then the Solid preset compiles it. +// Solid JSX needs Solid's own compiler (babel-preset-solid) — rolldown/oxc +// only know React-shaped JSX. Presets apply last-to-first: TypeScript strips +// types keeping the JSX, then the Solid preset compiles it. Everything else +// inherits the root config. export default defineConfig({ plugins: [ babel({ diff --git a/vitest.config.ts b/vitest.config.ts index e941a1a..9f786a4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,10 +1,8 @@ import { defineConfig } from 'vitest/config' -// Two projects so the Solid tests get their JSX transform without touching the -// rest of the suite. The default project runs every package the way it always -// has (node, or jsdom via a per-file `@vitest-environment` comment) and -// EXCLUDES the Solid tests; the `solid` project lives with its substrate -// (packages/solid/vitest.config.ts) so the root carries no Solid dependencies. +// Two projects: the Solid tests need vite-plugin-solid's JSX transform, which +// must not rewrite the React `.tsx` tests. The solid project lives with its +// substrate (packages/solid/vitest.config.ts). export default defineConfig({ test: { projects: [ @@ -13,12 +11,9 @@ export default defineConfig({ name: 'default', globals: false, environment: 'node', - // scripts/templates holds __name__-tokenized scaffolding stubs — real - // files, but not runnable tests (their imports resolve only once - // scaffolded). .worktrees and .claude/worktrees hold local worktree - // checkouts; lint ignores them, vitest must too. packages/native runs - // on jest-expo (real react-native), not vitest — see - // packages/native/jest.config.cjs. + // scripts/templates holds __name__-tokenized stubs (not runnable), + // .worktrees/.claude hold local checkouts, packages/native runs on + // jest-expo — see packages/native/jest.config.cjs. exclude: [ '**/node_modules/**', '**/dist/**',