Skip to content

feat(plugins): pluggable createT/Entity — plugin() + contributed methods + typing#67

Merged
bigmistqke merged 10 commits into
solidjs-community:next-cleanupfrom
bigmistqke:plugins
Jun 5, 2026
Merged

feat(plugins): pluggable createT/Entity — plugin() + contributed methods + typing#67
bigmistqke merged 10 commits into
solidjs-community:next-cleanupfrom
bigmistqke:plugins

Conversation

@bigmistqke

Copy link
Copy Markdown
Contributor

Act 2 of 3 (follows #66, now merged). Adds the plugin seam that Act 3 (XR as a plugin) builds on. Squash-merges to one commit on next-cleanup.

Summary

Adds a typed plugin system so createT/<Entity> can be extended with contributed props and methods, with no overhead when unused:

  • plugin() — global / class-filter / type-guard forms, plus the type machinery.
  • resolvePluginMethods + applyProp routing — contributed methods run instead of instance assignment; createT resolves plugins per element, gated by plugins.length (a no-plugin namespace never touches the path).
  • Props<T, TPlugins> surfaces contributed methods as inferred-typed props; enforced <Entity plugins> via UnionToIntersection over a readonly tuple.
  • Context.initializePlugin + meta.ctx give plugin code its mount-site context.
  • exports plugin() from the package entry.

Commits (10)

minimal plugin seam foundation · plugin() forms + type machinery · resolvePluginMethods · applyProp routing + per-element resolve (gated) · Props<T, TPlugins> · typed+enforced <Entity plugins> · PropsWithPlugins helper · rename Props->BaseProps · Context.initializePlugin + meta.ctx · export plugin()

Test plan

  • full suite green at this commit
  • benchmarked perf-neutral vs no-plugin (vsync-uncapped scenes, up to ~3,900 nodes): plugin-enabled matches the baseline within noise

bigmistqke added 10 commits June 5, 2026 18:49
Adds the input-agnostic plugin scaffolding on top of solidjs-community#64's Pointer engine:
- Context.owner + initializedPlugins
- Plugin type; createT(catalogue, plugins) + <Entity plugins> composition
- creation-gated plugin setup (initPlugins, src/plugin.ts) — off the per-attach
  scene-graph hot path (applySceneGraph stays byte-identical to solidjs-community#64)
- public Pointer.dispatch(name) + Pointer/Plugin exports

Perf-validated against three-bench (fractal-5x5 at parity). This is an interim
seam; the solidjs-community#37 plugin API (inferred types, class-filter, meta.ctx) supersedes
the Plugin shape next.
…erred-typed props

createT captures the plugin tuple type and threads it into Props<T, TPlugins> via
PluginPropsOf, so a contributed method's first-param type becomes the element prop
type. Generics ported from solidjs-community#37 with readonly-tuple constraints; the type-param
default is the constraint (readonly Plugin[]) not [] so inference isn't pre-empted.
…n + readonly tuple

Per-element typed contributed props now infer from the JSX plugins prop AND enforce
(reject bogus props). Root cause of the prior failure (investigated empirically):
- a const-inferred JSX array is a readonly tuple -> mutable Plugin[] constraint rejected it
  (=> readonly Plugin[] constraints throughout);
- the recursive PluginMerge is unevaluable during inference (=> UnionToIntersection);
- PluginPropsOf buried in Props's Overwrite defeats inference (=> Props stays plugin-free;
  PluginPropsOf is intersected directly at createT's proxy and <Entity>).
createT namespace path keeps working via function-arg inference.
…ibuted props)

DRYs the 'Props<T> & Partial<PluginPropsOf<InstanceOf<T>, TPlugins>>' intersection
used by createT's element proxy and <Entity>. A plain top-level intersection alias —
keeps PluginPropsOf un-buried so TPlugins stays inferable; verified inference +
enforcement unchanged (lint:types clean, @ts-expect-error bogus-prop still holds).
BaseProps<T> = raw three-instance props; Props<T, TPlugins> = base + plugin-contributed
props. lint:types clean, full suite + plugin inference/enforcement tests green.
…t for plugin code)

- Data.ctx: the element's mount-site Context, set by useProps for plugged elements
  (at creation, not attach — contributed methods run during applyProp, before a
  top-level element attaches). Gated by plugins.length so no-plugin elements pay nothing.
- Context.initializePlugin(token, fn): runs fn once per context (private dedup set),
  the home for a plugin's one-time per-context setup. Replaces the Phase-1
  initializedPlugins field; removes the dead initPlugins (setup-object remnant).
Plugin code reaches the store via getMeta(element).ctx.
No Phase-1 remnants remain (initPlugins/setup-object removed in PT7). plugin() is the
public creator; Plugin type already exported; advanced Props types via the S3 namespace.
@pkg-pr-new

pkg-pr-new Bot commented Jun 5, 2026

Copy link
Copy Markdown

commit: 318abb5

@bigmistqke bigmistqke merged commit 374450d into solidjs-community:next-cleanup Jun 5, 2026
2 checks passed
@bigmistqke bigmistqke deleted the plugins branch June 5, 2026 16:54
bigmistqke added a commit that referenced this pull request Jun 6, 2026
…ride typing (#68)

## Summary

The #66 events rewrite and the #67 plugin system both shipped without
their doc updates — the site was describing an API that no longer exists
and a feature that was never documented. This clears that debt, adds an
interactive plugins tour chapter, and fixes one plugin-typing bug the
demo surfaced.

## Stale docs corrected (#66)

- **`events/overview`** — drops the `onMouse*` family (removed in #66;
only `onPointer*` / `onClick` / `onWheel` remain) from the supported,
stoppable, and non-stoppable lists and the event-applicability table.
- **`utilities/raycasters`** — the page documented
`EventRaycaster.update(event, context)`, which #66 deleted. Rewritten
around the real `cast(registry, context)` interface, with a corrected
custom-raycaster example and new sections for `ScreenRaycaster` and
`ControllerRaycaster`.

## Plugins documented (#67)

- **`utilities/plugin`** — new API page: what a plugin is, the three
`plugin()` forms (global / class-filtered / type-guard), registration
via `createT` and `<Entity plugins>`, and the typed contributed-prop
guarantee.
- **Tour chapter 09 "Plugins"** — builds a `lookAt` plugin from the
"props set properties, they don't call methods" motivation, ending in a
live demo: a field of cones that turn to face the pointer. Inserted
before the WebGPU peek (renumbered 09 → 10 so the peek stays the
finale); chapter 08's close now hands off to it.

## Library fix: contributed props override native members

Writing the `lookAt` demo surfaced a bug in the #67 plugin typing. A
plugin intercepts its prop at runtime (`applyProp` checks
`pluginMethods` first), so a contributed prop should be able to reuse a
native member's name — e.g. `lookAt` driving `Object3D.lookAt`. But
`Props` *intersected* the contributed type with the base, producing
`nativeMethod & V`, which no value satisfies, so `<T.Mesh lookAt={vec}
/>` failed to type-check even though it worked at runtime.

`Props` now drops the contributed keys from `BaseProps` before adding
them, so the contributed type *replaces* the native one — matching the
runtime. Guarded so the common no-plugins case is untouched (the loose
default `readonly Plugin[]` has `length: number`; only a real inferred
tuple contributes override keys). Regression test added. This is what
lets the tour demo use the natural `lookAt` prop name.

## Test plan

- [x] `pnpm build` + `pnpm lint` (circular + eslint + types) green.
- [x] Browser suite green, incl. a new test that a `Vector3` is
assignable to a contributed `lookAt` prop.
- [x] lookAt demo verified in-browser — cones point at the cursor and
track it.
- [x] Dev server: no more mid-session re-optimize / reload after the
`optimizeDeps.include` change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant