pnpm installs a package once per peer set, so when an app and @docspace/shared declare different optional peers of a common dependency, the two resolve to different installed copies. Both end up in the bundle, and module state (React contexts, stores) stops being shared. ui-kit's ScrollbarContext split emptied the info panel Share tab, and its ApiContext split crashed selectors.
- scripts/split-copies.cjs: finds dependencies that an app and shared resolve to different copies
- client: resolve.test.ts checks that Vite's resolve.dedupe collapses every split to the client's copy
- sdk: next.config.test.ts checks that the webpack NormalModuleReplacementPlugin does the same, and vitest.config.ts now includes it
- common tests: SplitDependencyCopiesTest fails for any other app that has a split copy
ui-kit and ai-chat now come from npm, so update-ui-kit and update-ai-chat no longer manage a committed tarball. They now switch the apps from the npm release to a locally packed build, so a build can be tried before it is published. update-ui-kit now repoints every app manifest at file:../../onlyoffice-apps-ui-kit.tgz itself, using a shared repointManifests helper that update-ai-chat uses as well. It no longer fails when the lockfile has no tarball entry yet, which is the normal state when coming from the registry. Its rollback also restores the manifests. Both scripts print how to go back: pnpm -r update <pkg>@^<version>, then delete the tarball.
lockDrift in vendored-tarball.mjs now also finds lockfile entries written as explicit YAML keys (? '<key>'). pnpm writes these for snapshot keys longer than 1024 characters, which a long peer suffix produces.
Comments, docs, rules, skills, the workspace tooltips and the Dockerfiles no longer describe ui-kit as a prebuilt tarball. CLAUDE.md, README and the pnpm rule now describe the npm install, the update-ui-kit/update-ai-chat local-pack flow and how to switch back.
Rewriting the integrity makes pnpm read the new tarball, but it keeps the
package's recorded peerDependencies and snapshot as they were. That is how
the lockfile kept katex ^0.16.47 after ui-kit moved to ^0.17.0, and a new
runtime dependency would be missed the same way and never linked, with
--frozen-lockfile staying green.
After the install, compare the lockfile entry with the packed manifest:
every peer range (skipping names under overrides, which rewrite the range)
and every runtime dependency. On drift, fail and point at
`pnpm update -r @onlyoffice/apps-ui-kit`. It is not run automatically,
because it also re-resolves unrelated parts of the tree such as codemirror.
update-ui-kit.mjs and start-ui-kit-src.mjs were the only new scripts without
it; the license-header test does not scan .mjs or scripts/, so nothing caught it.
From a colleague's Windows run of this branch.
`start-ui-kit-src.mjs` only checked that the checkout has a package.json.
The common case is a fresh clone with no `pnpm install`, which passes
that check: Nx then starts all five apps, the four Next ones come up, and
the client alone dies inside vite.config.ts. It reads as "the portal will
not open" rather than "the kit is not installed". The check now covers
node_modules and says what to run, before Nx is handed anything.
Nothing anywhere said the checkout needs its own install -- not the
README, not the button tooltip, not the launcher's own error. The rule,
the tooltip and the launcher message all say it now.
README pointed `update-ui-kit` at `../docspace-ui-kit-react`; every other
place, code included, says `../../docspace-ui-kit-react`. Following the
README put the checkout inside DocSpace/ next to the client, where the
default never resolves. It also documented only the tarball flow, so
source mode existed solely in CLAUDE.md and the rule -- it is in the
README now, with the install requirement and the refusal to build.
Main spread its whole props object onto <main>, so `mainBarVisible` and
`isFrame` from the store and `isDesktop` from Shell all landed on the DOM
node and React warned about each on every page. The report guessed the
third came from NavMenu; it is the same component. They are destructured
out, and a test asserts the node carries none of them and that React logs
nothing -- it fails without the fix with exactly three warnings.
FilesStore explains an erased cast by saying ui-kit is "a separate
submodule"; it is a separate repository, consumed as a prebuilt package.
generate-biome-i18n-constants-plugin.js carried a note about writing a
second copy of the generated plugin for ui-kit's standalone CI. That copy
left OUT_FILES when ui-kit moved out, so the comment described an entry
that is no longer there. The generator still writes the one copy it
should, and the regenerated file is byte-identical.
`pnpm run start:ui-kit-src` started all five apps but only the client
served ui-kit from the checkout; login, doceditor, management and sdk
kept using the installed tarball, so editing ui-kit still meant build,
pack, install and a restart for four of the five.
They now honour DOCSPACE_UI_KIT_SRC through scripts/ui-kit-dev.cjs, which
each next.config.js calls twice: once outside the webpack hook to refuse
a production build while the variable is set, and once inside it to
- alias @onlyoffice/apps-ui-kit to the checkout,
- alias every peer the checkout declares to the app's own copy, so a
package both trees carry does not end up loaded twice (two Reacts, two
ThemeContexts), and
- add the checkout to the next-swc-loader rules, which is what makes its
TypeScript compile: Next runs its loaders over the app's own tree, and
a checkout outside it is not part of that.
The CSS needed nothing. Next's rules key off the extension, so the
checkout's *.module.scss go through the app's CSS Modules pipeline and
come out named by it -- `.AsideHeader_container__gUShG` where the package
would ship `dsui-AsideHeader__container--<hash>`. That is the same trade
the client already makes in this mode, and it is why a build from a
checkout is refused rather than merely discouraged.
Verified by running it: all five dev servers up, each of the four Next
apps compiling ui-kit from the checkout (40 to 54 files of their dev
output reference it), `next build` refusing with the variable set, and a
normal `next build` with it unset producing output that references the
checkout nowhere. tsc, Biome and the common checks pass.
The escaping-import guard the client has (config/plugins/ui-kit-boundary)
has no Next equivalent yet; such an import fails later, in ui-kit's own
build. Noted in the rule.
update-ui-kit read pnpm-lock.yaml and bailed on a missing @onlyoffice/apps-ui-kit entry only after the tarballs and app manifests had already been overwritten, and the rollback that undoes those writes is wired up later still -- so the one precondition failure the script guards against left behind exactly the half-updated tree it exists to avoid. Read the lockfile and match the entry up front, while there is nothing to undo.
Point the PluginStore initSettings mocks at @onlyoffice/apps-ui-kit instead of the old @docspace/ui-kit paths, and prefix ChangeWebPlugin with "s:" to match the event the store subscribes to.
The sdk resolved its own copy of @onlyoffice/apps-ui-kit, and with it
its own copy of every React context inside -- shared components
rendered in the sdk read the theme from a different ThemeContext than
the app around them. The two installed variants differed in exactly one
thing, several levels down: openai lists zod as an optional peer with a
range spanning two majors (^3.23.8 || ^4), and pnpm resolved it to 4.5.4
under client and 3.25.76 under sdk. Two zod resolutions made two openai
variants, two ai-chat variants, two ui-kit copies. Declaring the two
peers the sdk lacked (document-editor-react, component-emitter), the fix
the review proposed, changes nothing -- they were never the difference.
zod is now a catalog entry declared by client and sdk; the lockfile
carries one ai-chat variant and one ui-kit variant, and every app
resolves the same directory. UnusedDependenciesTest accepts the entry
because openai declares zod as a peer. Verified: sdk build and its 357
unit tests; DifferentDependencyVersionsTest and UnusedDependenciesTest.
Make the boundary plugin testable and cover it with unit tests:
- `uiKitBoundaryPlugin` takes the checkout root as a parameter
instead of reading it from `ui-kit-dev.ts`, so the tests run
against a fake root with no checkout on disk. `isInside(root,
target)` is the pure helper; `isInsideUiKit` wraps it.
- Add `config/plugins/ui-kit-boundary.test.ts` covering relative and
aliased escapes, in-checkout imports, bare specifiers, Vite virtual
ids, importers outside the checkout or in node_modules, entry ids,
and query/hash stripping.
- Extend the client vitest `include` to `config/**` so plugin tests
next to the plugins are picked up.
Source-mode cleanups found along the way:
- Drop the `styles.css` alias and `uiKitStylesEntry`: nothing imports
the bundle any more, components carry their own stylesheet and
ThemeProvider imports its SCSS directly. One prefix alias suffices.
- Log the ui-kit mode from `vite.config.ts` once the command is known
rather than at module import time, so a build that is about to
refuse `DOCSPACE_UI_KIT_SRC` does not print it first.
`update-ui-kit.mjs`: when ai-chat moves, snapshot the previous
tarballs and the app manifests and restore them alongside the ui-kit
tarball and lockfile if `pnpm install` fails, so a failed run never
leaves manifests naming a file the lockfile does not know.
Docs: state that SCSS boundary checking only covers bare
`@docspace/shared` loads, not relative `@use` paths that climb out;
record `config/**` as a unit-test location; fix the env var name in
the update-pnpm skill (`DOCSPACE_UI_KIT_SRC`).
@onlyoffice/apps-ui-kit is back under AGPL-3.0-only, which the audit rejected
for all six workspace packages.
The check exists to keep third-party copyleft out of the product: an AGPL
dependency from another author would impose its terms on everything shipped
alongside it. A first-party one carries no such risk -- same copyright holder,
and this product is itself AGPL-3.0-only, so it adds no obligation that is not
already there.
The exemption is deliberately narrow: only the @onlyoffice and @docspace
scopes, and only the exact identifier AGPL-3.0-only. A third-party AGPL
dependency, or any other copyleft license from our own scope, still fails.
ui-kit requires ai-chat and this repo vendors it, so the two have to move
together. They had not: ui-kit's peer asked for 0.5.113 while this repo
vendored 0.5.110 and both apps named it.
Take 0.5.113 -- the newest, and the only one ui-kit still ships -- and drop the
0.5.110 tarball.
The drift was possible because, unlike the ui-kit tarball, ai-chat carries its
version in the filename: a bump is not a file swap but an edit in every app
manifest that names it, done by hand. `pnpm run update-ui-kit` now does it,
taking whatever ai-chat sits beside the ui-kit checkout as the truth: it copies
the newest, removes the stale one, repoints every manifest, and no longer
short-circuits on an unchanged ui-kit when only ai-chat moved. Exercised
against a planted 0.5.110 drift -- it repaired the tarball and both manifests.
Three defects in the script whose whole purpose is catching a silent no-op.
It verified by comparing dist/styles.css alone. A ui-kit change that touches
only TypeScript leaves the stylesheet byte-identical -- the globalThis brand
lookup did exactly that -- so the check printed "installed and verified" for a
build whose JavaScript had not landed. Hash the whole packed tree instead.
Confirmed against a tampered install: the old check reports a match, the new
one does not.
It verified one copy. pnpm installs one physical copy per distinct
peer-resolution set, and packages/sdk currently resolves a different copy than
the other five, so the stale one would never have been looked at. Resolve from
every workspace package and check each distinct copy.
It could not run on Windows at all: Node will not spawn pnpm's .CMD shim
without a shell, and there is no node_modules/.bin/pnpm to fall back on. The
install ran after the tarball and the lockfile had already been rewritten, so
the ENOENT left the tree claiming a version it did not have. Pass shell on
win32 and roll both files back when the install fails.
The tarball keeps one filename across versions, so the `file:` specifier never
changes. Copy a new build over it, run `pnpm install`, and pnpm matches the
integrity already in the lockfile and keeps the cached copy: green install,
untouched lockfile, previous build still in node_modules. `--force` behaves the
same way, because the recorded integrity still matches what it has. That is how
the ui-kit cascade fix failed to reach the client on the first attempt today --
the hash of the installed dist/styles.css was the one from before the fix.
The script copies the tarball, rewrites the recorded integrity, drops the
extracted copy under node_modules/.pnpm, reinstalls with --force (a plain
install short-circuits on "Already up to date" and never re-links), and then
compares the installed dist/styles.css against the one inside the tarball --
it refuses to report success on the evidence of pnpm's exit code alone.
Verified both ways: on an up-to-date tree it reports nothing to do, and from a
deliberately stale tarball it reproduces the committed lockfile byte for byte.
Declare no-dynamic-i18n-key and no-constants-via-i18n once in
packages/shared/biome.json instead of per-package, so shared
itself is also linted.
Fix the violations this surfaces in shared: drop t() calls on
already-translated room-type strings and add justified
biome-ignore comments where dynamic keys are intentional.
Also emit the generated no-constants-via-i18n.grit plugin into
libs/ui-kit, since the submodule's standalone CI cannot reach
packages/shared.
Filter downloaded @font-face blocks to the unicode subsets
actually used by DocSpace UI languages (latin, cyrillic,
greek, hebrew, vietnamese and their extensions), excluding
the CDN's math and symbols subsets covered by system fonts.
Add a --subsets CLI option (comma-separated list or "all")
and fail if a requested subset is missing from the CDN
response.
Run the emitted CSS through Prettier (when available) using the
resolved project config with the css parser and LF line endings,
so re-running the font update script produces diff-minimal output
matching the committed Prettier-formatted fonts.css. Falls back to
unformatted CSS with a warning if prettier is not installed.
Generated from public/locales/.constants/*.json, the plugin flags
`t("ProductName")`, `t("Common:SSO")`, culture keys, and similar
constants that must instead be accessed via getBrandName(),
getConstName(), or getCultureLabel(). Also extend the locales test to
catch bare (non-namespaced) forms of these keys.