Skip to content

Migrate the playground front-end to js-toolkit v4 - #79

Merged
titouanmathis merged 8 commits into
mainfrom
feat/js-toolkit-v4
Aug 28, 2026
Merged

Migrate the playground front-end to js-toolkit v4#79
titouanmathis merged 8 commits into
mainfrom
feat/js-toolkit-v4

Conversation

@titouanmathis

Copy link
Copy Markdown
Contributor

The failure this fixes

@studiometa/playground shipped a front-end shell built on js-toolkit v3. In dist/front/js/create-playground.js:

import { createApp } from "@studiometa/js-toolkit";

return createApp(Playground);

v4 removed createApp. @studiometa/ui is migrating to v4 and its documentation site embeds this playground, so under v4 the shell throws TypeError: (0, Z.createApp) is not a function before it renders anything and no example on that site executes.

Measured, not assumed. Importing every emitted browser module of main's build in a real Chromium against js-toolkit 4.0.0-alpha.1 gives 22 of 44; the 22 failures are all SyntaxError: The requested module '@studiometa/js-toolkit/utils' does not provide an export named 'domScheduler' and its siblings. After this branch: 44 of 46 (see below).

This moves packages/playground to @studiometa/js-toolkit@^4.0.0-alpha.1 (the next dist-tag) and ports all 17 front-end components, the stores and the page template.

What changed and why

v3 API → v4

v3 v4 Note
createApp(Playground) registerComponent(Playground) pages/index.twig now declares data-component="Playground" on <body> — the element already carried the component's options and refs, it just had no declaration. createPlayground() returns nothing: v4 has no application object.
$children.X $watchChildren(X) Not a rename — see below.
getClosestParent(instance, Class) $closest(name) Resizable finds the ResizableSync above a cursor.
getInstanceFromElement(el, Class) getInstance(el, name) Resolving Resizable by name also removes it from LayoutReactive's import graph, so the lazy chunk is lazy again — the eager import Resizable had been defeating the import() in Playground.config.components.
config.emits: ['switch'] $emits in the props type A payload is one object now, so $emit('switch', value)$emit('switch', { value }).
service hook auto-emitted explicit $emit v3 emitted every hook call as an event for free. ResizableCursor.dragged() now republishes the drag props itself.
{ event, index, args, target } { event, target, payload } Delegated child handler payload.
destroyed() the cleanup mounted() returns Editors dispose their Monaco instance and drop their theme subscription; v3 leaked both for the life of the page.
domScheduler this.$read / this.$write; defaultScheduler in the stores Component tasks are cancelled on unmount.
nextTick(), isArray(), nextFrame wait(), Array.isArray(), nextFrame from the package root
importMap: Object { type: Object, default: () => ({}) } v4 requires a factory for Object/Array defaults.

HtmlEditor, ScriptEditor and StyleEditor now declare their own config.name. They inherited Editor's (name: 'Editor'), which was harmless in v3 — the config.components key decided resolution — but in v4 the name is the registry key, so all three editors would have registered under Editor and never mounted. Config merges along the prototype chain now, so nothing else had to be respelled.

No decorators were introduced anywhere. Every component is a plain class with static config and explicit registration, and the emitted dist/ is checked for stray syntax below.

The ordering-assumption rewrites

v4 guarantees no mount ordering between a parent and its children, so two places could not be translated and had to be re-thought.

PlaygroundEditorVisibility / Editors / Iframe. v3's mounted() read this.$children.EditorVisibility, picked the three instances apart by data-lang and called toggle() on each. That only worked because a v3 parent mounted after its children. In v4 the coordinator owns the state — a Map of data-lang → visible — and $watchChildren(EditorVisibility, { added }) pushes it onto every instance as it arrives. An editor that mounts after the coordinator gets the same treatment as one that was already there, and mounted() re-applies the state once the stored values resolve. Editors and Iframe are watched the same way and every use of them is now guarded, because "there is no Iframe yet" is a real state in v4 and was not in v3.

ResizableResizableSync / ResizableCursor. $children.ResizableSync became $watchChildren(ResizableSync), a live DOM-ordered collection, and getClosestParent(cursor, ResizableSync) became cursor.$closest('ResizableSync') — DOM ancestry rather than a parent-owned graph. The drag props are flat, so props.distance.x is props.distanceX; v3 mutated that distance object in place to invert it for the right/bottom layouts, which v4's readonly props make impossible, so the inversion moved to a local scalar.

Bugs the v3 code was hiding

  • Playground.onResizableDragged(props) never ran either branch. v3 delivered { event, index, args, target } to a delegated child handler, so props.mode was always undefinedselect-none and pointer-events-none were never applied during a resize. Typed against v4's DelegatedEvent, it works.
  • is-resizing could stick forever. It was raised on pointerdown and cleared on pointerup, both bound to the handle. A mouse released away from the handle — any drag long enough to matter — never delivered the pointerup, so the class stayed on <html> and is-resizing:pointer-events-none left the preview iframe unclickable. Now driven from the drag service's start/drop/stop. Reproduced in the browser before the fix.
  • ResizableSync.set() wrote to this.$el[prop], not this.$el.style[prop] — assigning width/height properties that do not exist on a <div>.
  • Editing the script did nothing under v4. v4 registers a class under its name once and ignores a second registration, so re-running an edited script in the same iframe realm kept the author's first version of every class. Driven in a browser: editing the script to render SECOND VERSION left the preview on the first version, and so did pressing reload. updateScript() now navigates the frame, which drops the realm and its module map. v3 had no registry, so re-running createApp() was enough and this problem could not exist.
  • The preview registered a second import map on every rebuild, which the engine reports once per overlapping specifier. Four warnings under v3; ninety-five under v4.

Other packages

packages/playground-preview has no js-toolkit dependency — only a createApp example in its README, updated. packages/demo is the dev/test consumer: its default script now uses registerComponent, its default HTML declares data-component="App", and its js-toolkit dependency is pinned to 4.0.0-alpha.1 because v4 is on the next dist-tag and an unversioned esm.sh URL would still serve v3 into the preview iframe.

Verification

Lint, test, build

$ npm run lint
Found 0 warnings and 0 errors.   (oxlint, 86 files)
Checking formatting...
All matched files use Prettier code style!
(stylelint: no output)

$ npm test
Test Files  7 passed (7)
Tests  153 passed (153)
Duration  4.06s

$ npm run build
Done building in 1440.50ms!   (playground: esbuild)
Done in 71ms                  (tailwind)
Done building in 858.97ms!    (playground-preview: tsdown)

$ npm run demo:build
Compiled successfully in 2.66s

None of those 153 tests touch a js-toolkit component. They cover src/lib (the webpack plugin and dependency resolution), patch-iframe-url, resolve-import-map-urls and playground-preview. The suite is not evidence about this migration; the browser run below is. Also worth correcting: CLAUDE.md says the repo tests DOM code with happy-dom, but PR #66 already moved playground-preview to a real Chromium via @vitest/browser-playwright, and the playground project runs in node. happy-dom is still a root devDependency that nothing uses.

The built artifact actually imports

Three mechanical checks over every emitted module, not a sample.

1. Parse. node --check on every .js under both dist/ directories, which honours the package "type": "module" and therefore parses them as ES modules:

parsed 58 emitted modules
ALL PARSE OK

The check is not a no-op: run against a canary directory containing export const ok = 1, class A { @dec foo() {} } and export const x = ;, it accepts the first and rejects the other two with SyntaxError. That is the exact shape of the failure @studiometa/ui shipped.

2. Import in Node, one child process per module so nothing hides behind anything else:

imported 11/12 modules from packages/playground/dist   (front/ excluded — browser only)
FAIL dist/index.js
  ERR_MODULE_NOT_FOUND: Cannot find module '.../packages/playground/html-loader.js'
    imported from .../dist/front/js/store/config.js

Pre-existing and unchanged. dist/index.js re-exports createPlayground, whose graph reaches the three @studiometa/playground/{html,style,script}-loader.js aliases that PlaygroundDependenciesPlugin writes as webpack virtual modules; they have no on-disk target. Building main in a separate worktree and running the same check gives the identical single failure.

3. Import in a real Chromium. A static server mounts packages/playground/dist, packages/playground-preview/dist, and the on-disk @studiometa/js-toolkit, morphdom and fflate; a page carries an import map for the five bare specifiers the emitted front modules use; each module is then await import()ed on its own.

imported 44/46 browser modules
FAIL /dist/front/js/utils/js/index.js
FAIL /dist/front/js/utils/twig/index.js
  Failed to load module script: Expected a JavaScript-or-Wasm module script
  but the server responded with a MIME type of "application/json".

Those two are import snippets from './snippets.json' with no with { type: 'json' } — webpack-only modules that no engine can load natively. Pre-existing: the same two fail identically on main. Every other emitted module, including all the migrated components, imports and evaluates.

On main the same run gives 22/44, all 22 failing on missing v3 named exports. That difference is the fix.

What was driven in a browser

npm run demo:build then packages/demo/preview.js on http://localhost:8099/, loaded in headless Chromium at 1440×900 with the console, page errors and failed requests captured.

The shell boots, all three Monaco editors mount, and the preview iframe executes the user's script under js-toolkit v4registerComponent(App) mounts App on the <p data-component="App"> and mounted() rewrites it to HELLO, WORLD!.

Driven Observed
load <body> declares Playground; 3 editors, 3 with a .monaco-editor; iframe renders HELLO, WORLD!
uncheck CSS text/cssdisplay: none, its panel width 475 → 0
uncheck HTML + JS all three hidden, Editors container gets hidden, width 1440 → 0
recheck all three all shown, container back to 1440
layout → right <html> is-topis-right, editors container 1440 → 576
layout → top back to is-top, 1440
theme → dark <html> gains dark; the iframe document gains dark too (syncColorScheme watcher)
theme → light both cleared
drag the HTML/CSS splitter 120 px panel widths [480, 480, 480][360, 584, 480]; is-resizing cleared afterwards
edit the HTML editor iframe body changes and App mounts on the new element
click reload preview rebuilt, no error
replace the whole script with one rendering SECOND VERSION preview shows SECOND VERSION (before the realm fix it stayed on the first version)

Console, whole session:

[log] getDefault html  (x2)
[log] updating style...  (x2)
[log] getDefault style  (x3)
[log] style updated!  (x2)
[log] updating script...  (x3)
[log] getDefault script  (x4)
[log] script updated!  (x3)
[requestfailed] http://localhost:8099/static/deps/demo-lib/index.js net::ERR_ABORTED
[requestfailed] https://esm.sh/@studiometa/js-toolkit@4.0.0-alpha.1/utils.d.ts?bundle=false net::ERR_ABORTED
[log] updating html...
[log] html updated!
[warning] [js-toolkit:registry.conflict] "App" is already registered; the incoming declaration was ignored.

No page errors and no uncaught exceptions. The [log] lines are pre-existing console.log calls in Iframe.ts and store/config.ts, left alone. The two aborted requests are in-flight fetches cancelled by the iframe navigation on reload. The single registry.conflict warning comes from the HTML-edit path, which still re-runs the script in the existing realm — deliberately, so an author's imperative script keeps the re-run it had in v3; the registration already there mounts whatever new elements the edit introduced, which is what the table above shows.

Judgement calls a reviewer should challenge

  1. Navigating the iframe on every script edit. It is the only way to drop v4's registry, but it costs a navigation and a module-map rebuild per debounced edit (500 ms). An alternative would be a resetRegistry export in js-toolkit that the preview's script could call.
  2. The HTML-edit path deliberately keeps re-running the script in the same realm — v3's behaviour, one registry warning. Making it a realm reset too would be more consistent but would throw away iframe state on every HTML keystroke batch.
  3. config.components still carries wait(100).then(() => import(…)) importers rather than moving to a mountStrategy. v4 registers the importer without calling it, so the deferral is preserved as-is; mountStrategy: 'idle' may be the better expression of the intent.
  4. Editors now dispose their Monaco instance on unmount. Correct and it fixes a leak, but a DOM move is an unmount plus a mount in v4, so anything that relocates an editor element will rebuild it.
  5. The demo keeps subpaths: true for js-toolkit. v4 has 95 export subpaths against v3's one, so the demo's index.html went from 18 KB to 73 KB. It exercises the feature, which is the demo's job, but it is a lot of import map.
  6. Playground mounts on <body> because that is where its options and refs already lived. It works — data-ref lookups from <body> cross no component boundary — but a dedicated wrapper element would be less surprising.
  7. The is-resizing, ResizableSync.set() and import-map fixes are not strictly part of the migration. They are in separate commits and can be dropped.

Unfinished

  • No automated test covers any migrated component. The playground vitest project runs in node with no DOM, so a component test would need a new project with a browser environment — deliberately out of scope here. The browser evidence above is the only coverage this change has.
  • dist/index.js still cannot be imported from Node (pre-existing; the loader aliases are webpack virtual modules), and utils/js/index.js / utils/twig/index.js still cannot be loaded by any engine (pre-existing; JSON import without an import attribute). Neither is touched here.
  • Pre-existing console.log debug output in Iframe.ts and store/config.ts is left as it was.
  • CLAUDE.md's testing section is stale (happy-dom) and is not corrected here.
  • The postversion hook npm version -ws $npm_package_version fails under npm 12, which no longer accepts -ws. Workspace versions were synced with npm version --ws; the hook itself is not fixed.
  • @studiometa/ui consuming this package is what will actually confirm the fix. This PR does not prove that end to end: it proves the artifact imports and that the demo consumer works.

🤖 Generated with Claude Code

https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R

titouanmathis and others added 8 commits August 28, 2026 13:41
The shipped shell called `createApp()`, which v4 removed. Any consumer
already on v4 — @studiometa/ui's documentation site, which embeds this
playground — got `TypeError: createApp is not a function` from
dist/front/js/create-playground.js before a single example rendered.

What each v3 API became:

- `createApp(Playground)` -> `registerComponent(Playground)`, and
  `pages/index.twig` declares `data-component="Playground"` on `<body>`.
  An instance exists because its element is in the document and its class
  is registered; there is no application object, so `createPlayground()`
  returns nothing.
- `$children.X` -> `$watchChildren(X)`. v4 guarantees no mount ordering,
  so this is not a rename. `Playground` used to read the editor
  visibility out of `$children` in `mounted()`, which only worked because
  a v3 parent mounted after its children; it now owns that state and
  pushes it onto every `EditorVisibility` through the `added` callback,
  so an editor that mounts later gets the same treatment as one that was
  already there. `Resizable` watches its `ResizableSync` children the
  same way.
- `getClosestParent(instance, Class)` -> `$closest(name)`, and
  `getInstanceFromElement(el, Class)` -> `getInstance(el, name)`.
  Resolving `Resizable` by name also keeps its class out of
  `LayoutReactive`'s import graph, so the lazy chunk stays lazy.
- `config.emits` -> `$emits` in the props type. A payload is now one
  object, so `$emit('switch', value)` is `$emit('switch', { value })`.
- v3 emitted every hook call as an event for free. v4 does not, so
  `ResizableCursor.dragged()` republishes the drag props itself — which
  is what `Resizable.onResizableCursorDragged()` waits on.
- Delegated child handlers receive `{ event, target, payload }`, not
  `{ event, index, args, target }`. `Playground.onResizableDragged()` was
  typed as if it received the drag props directly, so `props.mode` was
  always undefined and neither of its branches ever ran.
- `destroyed()` -> the cleanup `mounted()` returns. Editors now dispose
  their Monaco instance and drop their theme subscription; v3 leaked both.
- `domScheduler` -> `this.$read` / `this.$write` in components, cancelled
  on unmount, and `defaultScheduler` in the stores, which have no
  instance. `nextTick()` -> `wait()`, `isArray()` -> `Array.isArray()`,
  `nextFrame` moved to the package root.
- `Object` option defaults must be factories, so `importMap: Object`
  is `{ type: Object, default: () => ({}) }`.
- `HtmlEditor`, `ScriptEditor` and `StyleEditor` declare their own
  `config.name`. They inherited `Editor`'s, which v4 would have
  registered under the wrong `data-component` token; config now merges
  along the prototype chain, so nothing else is respelled.

`ResizableSync.set()` also writes to `style.width`/`style.height` rather
than to element properties that do not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
`is-resizing` was raised on `pointerdown` and cleared on `pointerup`,
both bound to the handle element. A mouse released anywhere else — which
is what happens on any drag long enough to matter — never delivered the
`pointerup`, so the class stayed on `<html>` and
`is-resizing:pointer-events-none` left the preview iframe unclickable
until the next drag happened to end on a handle.

The drag service reports the end of the gesture wherever the pointer is,
so `dragged()` owns both halves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
`initIframe()` rewrites `documentElement.innerHTML`, which removes the
`<script type="importmap">` element but not the map the browser has
already registered for that document. Appending a second one makes the
engine report every overlapping specifier.

That was four warnings under js-toolkit v3. Its v4 import map, with
`subpaths: true`, has ninety-five entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
js-toolkit v4 registers a component class under its name in a
module-scoped registry, and ignores a second registration of the same
name — it warns `"App" is already registered`. The preview re-ran the
edited script inside the same iframe realm, so the registry still held
the author's first version of every class and nothing the author typed
after that took effect. Driving it in a browser: editing the script to
render "SECOND VERSION" left the preview showing the first version, and
so did pressing reload.

v3 had no registry, so re-running `createApp()` was enough and this
problem did not exist.

Navigating the frame is what drops the registry, because it drops the
realm and its module map with it. `updateScript()` does that when it is
asked to reset, and the reload button now goes through the same path
rather than rebuilding the document in place.

An HTML edit still re-runs the script in the existing realm, as it did in
v3: the registration already there mounts whatever new `data-component`
elements the edit introduced, and an author's imperative script keeps the
re-run it used to get. That path still reports one registry warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
Both showed `createApp()`, which no longer exists. The demo's default
script registers its component and its default HTML declares it, so the
example the demo boots with is a working v4 example.

The demo also pins `@studiometa/js-toolkit` to `4.0.0-alpha.1`: v4 is
published on the `next` dist-tag, so an unversioned esm.sh URL would
still serve v3 into the preview iframe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
A breaking change: consumers must be on js-toolkit v4, so the minor bump
this project uses for breaking changes before 1.0.

The `postversion` hook (`npm version -ws $npm_package_version`) fails
under npm 12, which no longer accepts `-ws` as a single-hyphen flag, so
the workspace versions were synced with `npm version --ws`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
@github-actions

Copy link
Copy Markdown

Size Change: +1.74 kB (+3.21%)

Total Size: 55.9 kB

📦 View Changed
Filename Size Change
packages/playground/dist/front/css/app.css 4.25 kB -10 B (-0.23%)
packages/playground/dist/front/js/components/Editor.js 954 B +25 B (+2.69%)
packages/playground/dist/front/js/components/Editors.js 242 B -23 B (-8.68%)
packages/playground/dist/front/js/components/EditorVisibility.js 442 B +99 B (+28.86%) 🚨
packages/playground/dist/front/js/components/HeaderSwitcher.js 419 B -17 B (-3.9%)
packages/playground/dist/front/js/components/HtmlEditor.js 883 B +135 B (+18.05%) ⚠️
packages/playground/dist/front/js/components/Iframe.js 2.35 kB +566 B (+31.66%) 🚨
packages/playground/dist/front/js/components/LayoutReactive.js 576 B -40 B (-6.49%)
packages/playground/dist/front/js/components/LayoutSwitcher.js 299 B -42 B (-12.32%) 👏
packages/playground/dist/front/js/components/Playground.js 1.65 kB +519 B (+45.73%) 🚨
packages/playground/dist/front/js/components/Resizable.js 1.16 kB +166 B (+16.73%) ⚠️
packages/playground/dist/front/js/components/ResizableCursor.js 477 B +179 B (+60.07%) 🆘
packages/playground/dist/front/js/components/ResizableSync.js 500 B -12 B (-2.34%)
packages/playground/dist/front/js/components/ScriptEditor.js 593 B +73 B (+14.04%) ⚠️
packages/playground/dist/front/js/components/StyleEditor.js 389 B +142 B (+57.49%) 🆘
packages/playground/dist/front/js/components/Switcher.js 368 B +34 B (+10.18%) ⚠️
packages/playground/dist/front/js/components/ThemeSwitcher.js 402 B -54 B (-11.84%) 👏
packages/playground/dist/front/js/store/header.js 572 B -1 B (-0.17%)
packages/playground/dist/front/js/store/theme.js 517 B +1 B (+0.19%)
ℹ️ View Unchanged
Filename Size
packages/playground-preview/dist/element.js 10 kB
packages/playground-preview/dist/index.js 10 kB
packages/playground/dist/front/js/app.js 139 B
packages/playground/dist/front/js/components/IframeReloader.js 182 B
packages/playground/dist/front/js/create-playground.js 196 B
packages/playground/dist/front/js/loaders/html.js 124 B
packages/playground/dist/front/js/loaders/script.js 124 B
packages/playground/dist/front/js/loaders/style.js 124 B
packages/playground/dist/front/js/store/config.js 468 B
packages/playground/dist/front/js/store/content.js 400 B
packages/playground/dist/front/js/store/index.js 109 B
packages/playground/dist/front/js/store/layout.js 606 B
packages/playground/dist/front/js/utils/js/index.js 436 B
packages/playground/dist/front/js/utils/monaco.js 844 B
packages/playground/dist/front/js/utils/patch-iframe-url.js 362 B
packages/playground/dist/front/js/utils/resolve-import-map-urls.js 246 B
packages/playground/dist/front/js/utils/storage/AbstractStorageProvider.js 138 B
packages/playground/dist/front/js/utils/storage/FallbackStorageProvider.js 266 B
packages/playground/dist/front/js/utils/storage/index.js 479 B
packages/playground/dist/front/js/utils/storage/LocalStorageProvider.js 254 B
packages/playground/dist/front/js/utils/storage/MemoryStorageProvider.js 206 B
packages/playground/dist/front/js/utils/storage/MultiStorageProvider.js 307 B
packages/playground/dist/front/js/utils/storage/StorageProviderInterface.js 73 B
packages/playground/dist/front/js/utils/storage/SyncedStorageProvider.js 292 B
packages/playground/dist/front/js/utils/storage/URLStorageProvider.js 365 B
packages/playground/dist/front/js/utils/storage/WatchableStore.js 307 B
packages/playground/dist/front/js/utils/storage/ZipStorageProvider.js 271 B
packages/playground/dist/front/js/utils/twig/index.js 435 B
packages/playground/dist/index.js 154 B
packages/playground/dist/lib/plugins/PlaygroundDependenciesPlugin.js 4.36 kB
packages/playground/dist/lib/plugins/PlaygroundLoadersPlugin.js 511 B
packages/playground/dist/lib/presets/html-webpack-script-type-module.js 288 B
packages/playground/dist/lib/presets/playground.js 1.07 kB
packages/playground/dist/lib/presets/production-build.js 261 B
packages/playground/dist/lib/tailwind-config.js 509 B
packages/playground/dist/lib/utils/resolve-dependencies.js 3.04 kB
packages/playground/dist/lib/utils/resolve-public-path.js 283 B
packages/playground/dist/lib/utils/zip.js 310 B
packages/playground/dist/preset.js 190 B
packages/playground/dist/tailwind.js 113 B

compressed-size-action

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying studiometa-playground with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1b868b5
Status: ✅  Deploy successful!
Preview URL: https://8d521cd5.studiometa-playground.pages.dev
Branch Preview URL: https://feat-js-toolkit-v4.studiometa-playground.pages.dev

View logs

@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 149 lines in your changes missing coverage. Please review.
✅ Project coverage is 33.07%. Comparing base (7298ca5) to head (1b868b5).

Files with missing lines Patch % Lines
...s/playground/src/front/js/components/Playground.ts 0.00% 44 Missing ⚠️
...kages/playground/src/front/js/components/Iframe.ts 0.00% 24 Missing ⚠️
...es/playground/src/front/js/components/Resizable.ts 0.00% 21 Missing ⚠️
...kages/playground/src/front/js/components/Editor.ts 0.00% 7 Missing ⚠️
...layground/src/front/js/components/ResizableSync.ts 0.00% 7 Missing ⚠️
...ayground/src/front/js/components/LayoutReactive.ts 0.00% 6 Missing ⚠️
...ayground/src/front/js/components/LayoutSwitcher.ts 0.00% 5 Missing ⚠️
...yground/src/front/js/components/ResizableCursor.ts 0.00% 5 Missing ⚠️
...ges/playground/src/front/js/components/Switcher.ts 0.00% 5 Missing ⚠️
...layground/src/front/js/components/ThemeSwitcher.ts 0.00% 5 Missing ⚠️
... and 10 more

❌ Your patch check has failed because the patch coverage (0.00%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #79      +/-   ##
==========================================
- Coverage   34.00%   33.07%   -0.94%     
==========================================
  Files          55       55              
  Lines        1135     1167      +32     
  Branches      232      236       +4     
==========================================
  Hits          386      386              
- Misses        721      753      +32     
  Partials       28       28              
Flag Coverage Δ
playground 33.07% <0.00%> (-0.94%) ⬇️
playground-preview 33.07% <0.00%> (-0.94%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

titouanmathis added a commit to studiometa/ui that referenced this pull request Aug 28, 2026
The default snippet the editor loads still imported and called
`createApp()`, which v4 removed, so every visitor's first playground
session started on a dead API.

`registerComponent()` alone would have been a worse trap. In v4 an
instance exists because its element is in the document *and* its class is
registered, so registering `App` against markup that never declares it
produces a class that mounts nothing, with no error — which is exactly
how the three playground editor subclasses and the 21 self-registering
`@studiometa/ui` components were nearly lost. The default markup now
declares `data-component="App"`.

Verified by loading the default session at /play/ against a locally built
`@studiometa/playground` 0.4.0: the preview iframe completes, the element
carries `data-component="App"`, and the console reports `getDefault
script` with no errors.

The comment about js-toolkit's registry is reworded for the same reason;
it named `createApp` as the thing holding the mutable state.

Note that the site's playground shell stays broken until
`@studiometa/playground` 0.4.0 is released — studiometa/playground#79.
That is a version bump here, not a change to this file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
@titouanmathis
titouanmathis merged commit 821f60d into main Aug 28, 2026
7 of 8 checks passed
titouanmathis added a commit to studiometa/ui that referenced this pull request Aug 28, 2026
`@studiometa/playground` 0.3.13 was a js-toolkit v3 application: its shell
called `createApp()`, which v4 removed, so the shell threw before it
rendered anything and no example on this site executed. 0.4.0 is the v4
migration of that package — studiometa/playground#79.

This is the whole of the fix on our side. The version conflict resolves
itself: 0.3.13 declared `^3.4.3` against a workspace pinned to
4.0.0-alpha.1, which npm reported as an invalid tree, and 0.4.0 declares
`^4.0.0-alpha.1`. `npm ls @studiometa/js-toolkit` is clean.

Verified in a browser through ddev against the published package, not a
local build: the Disclosure examples page mounts three `Disclosure`
instances, clicking the second moves the group from
`["true","false","false"]` to `["false","true","false"]`, and the console
carries no errors where it used to report `createApp is not a function`
plus three follow-on failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011izFBQT4AsFcD4tVZz1f7R
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