- Write Changelog & Migration Guide
- replace imports of
package.jsonfor better support - Figure out how to deal with
__dirname - Re-implement beautify filter
- Figure out browsersync + react or maybe replace with vite?
- Migrate Packages to ESM
- core
- fractal
- handlebars
- mandelbrot
- nunjucks
- react
- twig
- web
- Update all dependencies within their existing semver ranges (2026-08-14)
- Dependency-declaration + monorepo-setup audit (2026-08-14) — every bare import in all 8 packages was checked against that package's published dependency closure by packing each tarball and resolving its specifiers. Fixed:
@fractality/mandelbrotshipped two undeclared runtime imports (same failure class as thelink:bug below, and hidden the same way):src/theme.jsimportsfs-extra(declared nowhere) and@fractality/web(declared only as a devDependency), while the published closure was{lodash}alone. npm's flat hoisting masked it; pnpm/Yarn-PnP consumers would getERR_MODULE_NOT_FOUND.@fractality/webwas made a peerDependency, not a plain dependency —packages/web/src/web.jsdoesinstanceof Themeagainst theThememandelbrot constructs atsrc/theme.js:126, so a duplicate copy would throw "Fractality themes must inherit from the base Theme class"- The private root declared 5 unused runtime
dependencies(anymatch,fs-extra,js-yaml,lodash,vinyl) that no root-level code imported. Their only effect was populating rootnode_modulesand masking phantom deps like the one above — removing them makes that class of bug fail locally instead of only for published consumers filesallowlists added to all 8 packages — tarballs previously shippedtest/, and mandelbrot additionally shippedassets/(build-time SCSS/JS),vite.config.jsand.npmignore. The old mandelbrot tarball contained 10 unresolvable bare imports (2 real runtime, 8 from those build-time files); it is now 0. mandelbrot 220 KB/150 files → 190 KB/84 files, core 24 KB/41 files → 16 KB/28 files. mandelbrot's.npmignorewas deleted (superseded byfiles)exportsmaps added to all 8 packages (onlycorehad one). This also fixed a latent bug:@fractality/react/components— used byexamples/react/components/path/path.jsx— failed under real ESM withERR_UNSUPPORTED_DIR_IMPORTand only worked because the react adapter routes JSX through@babel/register's CJS resolution. Note this is the one semver-relevant change in the batch: it closes off deep-importing package internals- pnpm pinned via
packageManager: pnpm@11.21.0and the hardcodedversion:inputs dropped from both workflows so the manifest is the single source of truth. CI was on pnpm^9while local dev was on a12.0.0-rc;pnpm-workspace.yaml'sallowBuilds/minimumReleaseAgeExcludeare pnpm 10+/11+ keys that pnpm 9 silently ignored, so the configured supply-chain policy was not actually being enforced in CI release.ymlran Node 20 despiteengines.node >= 22and execa 10 requiring it — raised to 22, added pnpm caching, and removed a duplicate barepnpm installstep that ran after (and defeated) the--frozen-lockfileone- Dead
bootstrapscript removed (lerna bootstrapwas removed in lerna 7 and errors out); unusedmarked-highlightdependency dropped from@fractality/core; 4 stray near-empty npmpackage-lock.jsonfiles removed (2 of them underpackages/*/src/, so they were being published); examples switched fromlink:toworkspace:*to match the packages; mandelbrot's misleading"test": "pnpm run build"removed (rootvitest runis the single test entrypoint, andtest/dist.spec.jsbuilds its own dist inbeforeAll)
-
pnpm testbroke permanently after running an example's own build script (found and fixed 2026-08-14; pre-existing onmain). Vitest 4'sdefaultExcludeis only["**/node_modules/**", "**/.git/**"]— unlike Vitest 3 it no longer excludesdist, andvite.config.jsset noexcludeof its own.fractality buildexports raw copies of every component, including their*.spec.jsfiles, into the example'sdist, so running the documentedpnpm buildin any example leftpnpm testcollecting those copies from then on; they fail withCannot find module '../../fractality.config.js'because the relative import cannot resolve from the export location. Surfaced by running a real end-to-endfractality buildwhile reviewing the branch. Fixed by settingexclude: [...defaultExclude, "**/dist/**"]. CI never hit it because it does not build the examples - Follow-ups from that audit, deliberately not done:
engines.nodeis now declared per package and checked against what the dependency tree actually accepts (2026-08-14). Two were wrong:@fractality/fractalityclaimed>=22.0.0whilecommander15 needs>=22.12.0and@inquirer/promptsneeds^22.13.0 || >=23.5.0(pre-existing);@fractality/reactclaimed>=22.0.0while Babel 8 needs^22.18.0 || >=24.11.0(introduced by the 2026-08-14 major bumps). Both now declare their real ranges. Worth re-checking whenever a dependency major moves — CI only exercises the newest 22.x and 24.x, so it cannot catch a floor being raised underneath these valuesauthoris missing on@fractality/core,@fractality/reactand@fractality/web(the other 5 credit Mark Perkins) — left alone because attribution is a call for the maintainers, not a mechanical fill-in. No package haskeywordsnunjucks@3.2.4wantschokidar@^3.3.0but the workspace is onchokidar@^5.0.0(pnpm peers checkreports it; pnpm currently installs both copies). Pre-existing, not introduced by the audit, but chokidar 4 dropped glob support so nunjucks' watch mode is worth a look- Local dev now needs pnpm 11: with
packageManagerpinned, a system pnpm that disagrees will fail any script that shells out topnpm(e.g. mandelbrot'sprepack) withERR_PNPM_BAD_PM_VERSION.corepack enableis the fix
- Update dependencies across major-version bumps (not done automatically — each needs its own review/testing pass, likely breaking changes):
-
eslint9 → 10 +@eslint/js9 → 10 (2026-08-14) — flat config ineslint.config.jsneeded no changes;pnpm run eslintclean -
stylelint16 → 17 +stylelint-config-standard39 → 40 (2026-08-14) — no rule changes needed;pnpm run stylelintclean -
@babel/core/@babel/preset-env/@babel/preset-react/@babel/register7 → 8 (2026-08-14) — only@fractality/reactstill uses Babel (mandelbrot dropped it in the vite migration, seedocs/adr/0003); the@babel/register+presets: ['@babel/preset-react', '@babel/preset-env']setup insrc/adapter.jsworks unchanged, covered by theexamples/reactspecs -
lerna9 → 10 (2026-08-14) — only used forversion/publishinrelease.yml; no config changes needed inlerna.json -
chalk5 → 6 (2026-08-14) — the CLI only useschalk.red/green/yellow/cyan/magenta/dim, all unchanged in v6; verified by runningfractality infoend-to-end -
execa9 → 10 (2026-08-14) — no code changes needed: the only call site isexeca('npm', ['install'])inpackages/fractality/src/cli/commands/new.js, and none of v10's breaking changes apply to it (removedexecaCommand/execaCommandSync, subprocess no longer aChildProcess— Node APIs moved tosubprocess.nodeChildProcess,stdio: [..., 'ipc']replaced byipc: true,input/inputFilenow takes priority over inherited stdin). v10 requires Node >= 22, which@fractality/fractalityalready declares and CI already matches (22, 24) -
js-yaml4 → 5 (2026-08-14) — the only bump in this batch that needed a code change. v5 dropped the default export and is named-exports-only under ESM, soimport yaml from 'js-yaml'silently yieldedundefinedand every YAML data file failed to parse with "Cannot read properties of undefined (reading 'load')" (42 of 63 test files). Fixed by switching toimport * as yamlinpackages/core/src/data.jsandpackages/web/src/engine/filters/format.js, the only two call sites -
sassin@fractality/mandelbrot1.79.6 → 1.102.0 (2026-08-14) — the~pin was holding back the@importdeprecation. Migrated the wholeassets/scsstree off@importonto the module system (@use), which was the only deprecation the build emitted (86 warnings). Ran the officialsass-migrator module --migrate-deps, which namespaced every cross-file reference correctly (theme.$color-link,mixins.link-default) but produced a module loop:core/_all.scsswas doing double duty as both an aggregator (loadingmixins/foundation) and a config module (defining$width-sidebar,$handle-size,$mq_*), soall→mixins→all. Split those five variables intocore/_config.scss; components andcore/_mixins.scssnow load that rather than the aggregator, which is also more honest about what they actually need. Skins keep the variable-override pattern via@use "../theme" with (...). Verified behaviour-preserving: all 18 compiled stylesheets are byte-identical to the pre-migration output. Also confirmed clean against all 26 currently-active Sass deprecations (not just the ones that happened to fire) by recompiling every entrypoint withfatalDeprecationsset — there are nofuture-status deprecations outstanding. The range is^1.102.0: it was pinned with~for years to hold back the@importdeprecation, and with that migration done the pin no longer earns its keep. Note the tradeoff — Sass ships new deprecations in minor releases, so a future minor can reintroduce warnings without a manifest change. ThefatalDeprecationscheck above is the guard: worth re-running whenever the resolved Sass minor moves.
-
- Migrate Examples
- adapter-tests
- handlebars
- nunjucks
- react
- twig
- Make tests pass
- packages
- core
- fractal
- handlebars
- mandelbrot
- nunjucks
- react
- twig
- web
- examples
- handlebars
- nunjucks
- react
- twig
- packages
- migrate test runner to vitest
- migrate
@fractality/mandelbrot's bundler from webpack to vite (2026-08-14) — seedocs/adr/0002(explicit jQuery global shim, since Vite doesn't auto-expose it any more than webpack did without expose-loader),docs/adr/0003(dropped Babel/corejs, verified as a no-op for this codebase's actual browserslist targets), anddocs/adr/0004(several skins compile to byte-identical CSS, e.g.default/blue; Rollup silently deduped them onto one file with no filename hashing to fall back on — fixed by injecting a per-skin-unique custom property)- follow-up: remove jQuery/
jquery-pjax/jquery-resizable-domfrom the theme entirely (deliberately deferred out of the bundler migration — no behavioral test coverage exists for pjax navigation, resizable panels, or search highlighting today; needs its own characterization-test-first plan before touching it)
- follow-up: remove jQuery/
- fix
link:../xinter-package dependencies silently breaking published packages —link:is never rewritten onpnpm pack/publish(verified via a pack dry-run), so every published@fractality/*package exceptwebwould have failed to resolve its@fractality/*deps for external consumers. Switched all inter-package deps toworkspace:*, which pnpm correctly rewrites to the real version at pack/publish time (2026-08-14)- Note: this surfaced a pre-existing cyclic workspace dependency between
@fractality/fractalityand@fractality/handlebars(fractality depends on handlebars as its default engine; handlebars depends on fractality as a dev/peer dependency for its example-based tests). pnpm tolerates it but warns on install — worth a deliberate look at whether this is the intended shape or should be broken up (e.g. moving the shared example-testing scaffolding out of the peer relationship)
- Note: this surfaced a pre-existing cyclic workspace dependency between
-
@fractality/react'sreact/react-domare listed as directdependencies(bundling a copy) AND aspeerDependencieswith a stale range (>= 16.8.0 < 18, fixed to>= 18.0.0 < 20on 2026-08-14) — worth deciding deliberately whether an adapter package like this should depend on react directly at all, or purely as a peer (letting the consuming project supply its own copy) -
@fractality/fractality's CLI read the wrongpackage.json(found and fixed 2026-08-14; pre-existing, reproduced onmain, unrelated to the dependency work that surfaced it).packages/fractality/bin/fractality.jsdidreadJsonSync('./package.json')— a relative path resolved againstprocess.cwd()rather than the CLI's own location — so it read whateverpackage.jsonthe user happened to be standing in. That value feeds four things: theupdate-notifierpkg, the legacy version-mismatch log, thesemver.gtmismatch check, andcli.init, which surfaces it as "CLI helper version" infractality info. Symptoms: from a directory whosepackage.jsonlacksname/version(the repo root) the CLI died withError: pkg.name and pkg.version required; fromexamples/handlebarsit reported the example's version (0.5.1) as the CLI version instead of 1.6.3, and would have polled npm for updates to the user's package. Now resolved against__dirname, matching the repo's existing idiom, and verified from the repo root, an example dir, a directory with nopackage.json, and the extracted tarball layout. The project's own manifest is still read separately and deliberately fromprocess.cwd()a few lines below — the two were always meant to be distinct. Same class as the completed "replace imports ofpackage.jsonfor better support" item above, which missed this call site - Architecture: the mixin system in
packages/core/src/mixins/(built onmixwith,mix(A).with(B, C)) is needlessly indirect for this codebase's size and will make the planned TypeScript conversion significantly harder (mixin class-factories don't type well without heavy generics). Consider migrating to plain composition/classes before or during a TS conversion, rather than extending the mixin pattern further. - Test coverage:
packages/handlebars,packages/nunjucks,packages/react,packages/twigstill have zero package-level unit tests (only the shared example-fixture specs underexamples/*exercise them indirectly).packages/core/src/resolver.jsand the watch/rebuild pipeline inpackages/core/src/mixins/source.jsgained their first unit tests on 2026-08-14 (seepackages/core/test/resolver.spec.js,packages/core/test/mixins/source-idle.spec.js) but are still thin relative to how load-bearing they are.packages/mandelbrotgained its first package-level tests on 2026-08-14 (test/theme.spec.jsfor config/routing,test/dist.spec.jsas a build-output smoke test) as a safety net for its webpack→vite migration — still no behavioral/DOM-interaction coverage (pjax navigation, resizable panels, search highlighting).
- Convert to Typescript
- Not realistic as a single pass. Recommended path: enable
allowJs/checkJswith JSDoc types first (catches real bugs without a syntax rewrite — this is how several bugs fixed on 2026-08-14 would have been caught statically, e.g. an undefined variable inside acatchblock, andmodule.exportsused inside an ESM-only file). Convert leaf modules with no mixin dependency first (utils.js,resolver.js,data.js). Convert or hand-write.d.tsfor thepackages/core/src/mixins/layer last — it's the highest-effort, highest-risk part (see the mixin-system note above).
- Not realistic as a single pass. Recommended path: enable
- Provide Types for Component-Data
import { defineComponent } from '@fractality/fractal'; export default defineComponent({ name: 'button', });
- Multiple Adapters at once - eg. using react with handlebars