[*] Refactor: Publish packages from their root directory#8554
Merged
Conversation
Make each public package directory itself the publishable npm package so `pnpm link` and `file:` protocol consumers can point at packages/<name> directly. Build artifacts now live entirely under packages/<name>/dist and package.json `exports`/`main`/`module`/`types` reference that path, removing the separate `npm/` tree the release used to construct. Phase 1 (layout): - updateVersion.mjs rewrites public packages with `./dist/...` paths, adds a `files` whitelist, and copies the monorepo LICENSE into each package so a checkout is publishable without extra prep. - prepare-release.mjs becomes a publish-time guard: it verifies every path the exports map references actually exists on disk (catching the "publish a dev build" footgun) and that README/LICENSE are present. - release.mjs publishes from packages/<dir> directly; pnpm rewrites `workspace:*` automatically. - Integration test setup packs from the package root. - viteModuleResolution stops re-prefixing `dist/` since the exports map already includes it. Phase 2 (build outputs): - Dev/prod/release builds all emit the same on-disk shape: `.dev` or `.prod` variants for each entry point plus matching fork modules (`Foo.mjs`, `Foo.js`, `Foo.node.mjs`). The fork unconditionally re-exports the available variant in dev-only or prod-only builds and uses the runtime NODE_ENV check only when both were built. No fake variant is ever emitted, so the publish guard catches dev-only builds that lack the prod files their exports map promises. - TypeScript declarations and Flow stubs are copied into dist on every npm build, not just release, so linked consumers get types out of the box. - tsconfig excludes the internal www `<Name>.js` shims at the package root so tsc doesn't follow their require() into the compiled dist.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…heckout fixture
Phase 3 of the package-root-publish reorg: expose each package's
TypeScript source under a `source` export condition so a bundler
configured with `resolve.conditions: ['source', ...]` can consume
Lexical without building dist artifacts first. The typical use case is
`pnpm link` from a local Lexical checkout, where edits to the source
files are picked up by the downstream bundler on the next request.
- updateVersion.mjs:
- exportEntry emits a `source` entry (sibling to import/require)
pointing at ./src/<entry>.{ts,tsx,js}. For multi-entry packages the
source path is derived from the src filename iterated when building
the exports map; for main-style packages it falls back to
src/index.{ts,tsx,js}.
- withBrowser keeps `source` first so it wins over `browser` when both
conditions are enabled by the consumer.
- The public `files` whitelist gains `src` plus negations for
`__tests__`, `__bench__`, and `*.{test,bench}.{ts,tsx}` so consumers
get source without 100+ KB of test ballast per package.
- updateDependencies preserves `link:`/`file:`/`portal:` dep pins so
fixtures and examples can keep pointing at the local checkout
across update-version runs.
- New integration fixture
scripts/__tests__/integration/fixtures/lexical-link-source-mode/:
consumes `lexical` via `link:` to packages/lexical, configures Vite
with `resolve.conditions: ['source', ...]` and a `shared/*` alias
back into packages/shared/src, and asserts the resulting bundle
inlines the Lexical class (proving source was compiled rather than
the prebuilt artifact).
- Test harness (scripts/__tests__/integration/utils.mjs) gains
describeLinkedFixture, used by prepare-release.test.mjs whenever a
fixture's package.json has a `link:` dep. It wipes
node_modules/lockfile and runs `pnpm install --ignore-workspace`
before building so each run hits the linked package freshly.
- Docs: new maintainers-guide-link.md walks through both the built-
artifact and source-mode workflows, including the `shared/` aliasing
requirement and bundler caveats. The existing prepare-release section
is rewritten to reflect the new publish-from-package-root flow.
…rnal so source mode needs no consumer config
Source-mode consumption (the `source` export condition over a linked
checkout) previously required the consumer to alias the private `shared/*`
module space, define `__DEV__`, and tolerate `invariant`/version helpers
that only worked after the build transform. This removes the `shared`
caveat at its root.
shared is dismantled and its modules move to where they belong:
- Cross-package runtime utilities (invariant, devInvariant, canUseDOM,
environment, warnOnlyOnce, simpleDiffWithCursor, caretFromPoint, the four
format* helpers, plus a new version module) become `@lexical/internal` —
a real, published-but-internal leaf package. It resolves through normal
package resolution (so source mode needs no alias) but the monorepo build
keeps inlining it into every other package via a rollup alias, so npm and
www bundle shapes are byte-identical to before and no new runtime
dependency is actually executed. update-version adds it as a dependency
wherever source imports it.
- React-only helpers (useLayoutEffect, reactPatches) move into
@lexical/react's existing src/shared/ as relative internal modules.
- React test helpers (react-test-utils) move to a new private
@lexical/test-utils package; the invariant/devInvariant/warnOnlyOnce
vitest auto-mocks move with their modules.
- The monorepo vite tooling (lexicalMonorepoPlugin, viteModuleResolution)
moves to scripts/vite/ where build tooling lives; all importers updated.
invariant/devInvariant now have real in-situ implementations (interpolate
`%s` and throw/warn) instead of throwing "meant to be replaced at compile
time". The transformErrorMessages plugin still rewrites call sites in built
output (now importing `@lexical/internal/format*`), so the runtime impl only
executes in untransformed/source mode.
LEXICAL_VERSION becomes a generated `@lexical/internal/version` module:
`process.env.LEXICAL_VERSION ?? '<ver>+source'`. The build replaces the env
ref (minification drops the fallback) so artifacts are unchanged; source
mode gets the literal, regenerated by update-version alongside the bump.
The lexical-link-source-mode integration fixture drops its `shared` alias
and realpathSync walk entirely — its vite config is now just the `source`
condition plus `define: {__DEV__}` — and still verifies the bundle is
compiled from source. tsconfig/flow/viteModuleResolution updated; docs in
maintainers-guide-link.md rewritten to show the zero-alias setup.
… so source mode needs zero config
`__DEV__` is a bare global that only exists after the build's `replace`
step, so a source-mode consumer had to define it or hit a runtime
ReferenceError. That was the last remaining consumer-side requirement for
the `source` export condition. Since Meta consumes Lexical's built www
artifacts (where the flag is already baked) rather than the TS source, we
can drop the `__DEV__` convention.
- Source: every dev guard (`if (__DEV__)`, `__DEV__ && …`, `__DEV__ ? …`)
becomes `process.env.NODE_ENV !== 'production'` — 34 occurrences across
14 files. No `!__DEV__` existed, so precedence is unaffected. This is
also what the fork modules already branch on, so it's consistent rather
than introducing a second flag.
- build.mjs: the `replace` plugin now bakes `process.env.NODE_ENV`
(`"production"`/`"development"`) per dev/prod variant instead of
`__DEV__`. terser folds `"production" !== 'production'` so prod dev-code
is still DCE'd (verified: dev bundle 570K w/ assertions, prod 146K
stripped). Fork modules are generated post-rollup and keep their runtime
NODE_ENV check.
- Dropped the `__DEV__` declarations from libdefs (globals.d.ts,
environment.js) so tsc flags any straggler, and the now-unused
`__DEV__: true` vitest defines. The playground validation server sets
`process.env.NODE_ENV = 'development'` instead of `global.__DEV__`.
- The source-mode fixture and maintainers-guide-link.md drop the
`define: {__DEV__}` — source mode is now just the `source` resolve
condition, nothing else (every mainstream bundler substitutes
process.env.NODE_ENV by default).
With `shared` replaced by the public `@lexical/internal` package, the only remaining private package vite-built code could reference is `@lexical/test-utils`, which is test-only and resolved by vitest via tsconfig paths — never imported by the playground/examples/devtools that use this resolver. Remove the now-dead `getPrivateModuleEntries` branches so both source and dist resolution only alias public packages. Verified: example builds in production (dist) mode and the dev server resolves `@lexical/*` sources (source mode) with no errors.
…variant mocks Two follow-ups to the @lexical/internal extraction: - The auto-generated `flow/*.js.flow` stubs were empty boilerplate; fill each with the real type declarations matching the TypeScript source (CAN_USE_DOM, environment booleans, invariant/devInvariant, the format* helpers, caretFromPoint, simpleDiffWithCursor, warnOnlyOnce, LEXICAL_VERSION). - Drop the vitest auto-mocks for `invariant` and `devInvariant` (and their vi.mock() calls). They existed only because the old runtime impls threw "meant to be replaced at compile time"; now that the shipped impls interpolate `%s` and throw on their own, tests exercise the real code. The `warnOnlyOnce` mock stays — it deliberately warns every call so module-level `warnOnlyOnce` closures don't leak dedupe state across tests (LexicalNestedComposer.test.tsx asserts per-test warnings).
range-selection.ts imported TableSelection with value syntax but only uses it in a type position (the RangeSelection | TableSelection parameter of $getSelectionStyleValueForProperty). babel already elided it from the build, so this is a no-op at runtime, but `import type` states the intent correctly and keeps it out of the value/dependency graph.
…lection The function only needs anchor/focus to trim boundary text nodes, which is RangeSelection-specific; move that into the existing $isRangeSelection guard and widen the parameter to BaseSelection. This drops the lone @lexical/table type import from the package (it was the only undeclared cross-package import in any published package) and is a backwards-compatible widening for callers. Other selection types now style every node they contain rather than running the range-only edge trim. Flow stub updated to match.
The playground imported @lexical/code-core, extension, headless, history, html, markdown, text and yjs without declaring them — it resolved only via the monorepo's tsconfig path aliases. Add them to dependencies so the package is self-consistent and resolves through normal package resolution.
… scope path aliases to test + website The root tsconfig.json carried a ~290-line generated `paths` block aliasing every package to its src. Now that each package declares its real deps and exposes a `source` export condition, the root and build configs resolve via `customConditions: ["source"]` instead, shrinking tsconfig.json to ~30 hand-maintained lines and making `pnpm tsc` validate the actual dependency graph (it would now flag an under-declared dep). The package aliases are still needed by two consumers and are scoped to them: - tsconfig.test.json: unit tests import sibling packages without declaring them (to avoid circular package.json deps) and use deep `*/src/__tests__/utils` paths; a new `tsc-test` script type-checks it and runs in ci-check. Root tsc now excludes __tests__/__bench__. - packages/lexical-website/tsconfig.json: type-checks the out-of-workspace @examples sources, which only resolve consistently through the alias. update-tsconfig now generates those two configs (+ devtools) instead of the root configs. vitest builds its resolve.alias from tsconfig.test.json's paths (Vite's native resolve.tsconfigPaths only reads the root, which is now lean). The validation script points ts-node at tsconfig.test.json so it keeps resolving to src.
The playground validation HTTP server (src/server/validation.ts, the `validation` npm script, and the tsconfig `ts-node`/tsconfig-paths block) was dead: nothing imports it, no script or CI invokes it, and `tsconfig-paths` — required by its ts-node register hook — was never even in the lockfile, so it couldn't run. Remove the server, the script, the ts-node tsconfig block, and the now-unused ts-node devDependency rather than carry a broken path-alias consumer into the new resolution setup.
Track upstream main (10 commits) on this branch. The merge was textually clean; the only semantic fixup was main's new `shared/environment` import in LexicalUtils.test.ts, converted to import IS_APPLE from `lexical` (this branch replaced the `shared` alias with @lexical/internal + lexical-core re-exports). All other shared/* usages from main auto-merged to the migrated specifiers.
…browser bundles Consuming the version module via the `source` export condition in a browser bundle that doesn't define/replace process.env would throw `ReferenceError: process is not defined`. Read it inside a try/catch so the access can't throw, falling back to the generated literal. The exact `process.env.LEXICAL_VERSION` member expression is preserved, so our Rollup build (and a consumer's bundler `define`) still statically replace it — verified the built dist bakes a clean literal with no process reference. Optional chaining wouldn't work: it neither prevents the ReferenceError (undeclared global) nor matches the replacement.
…ude/kind-johnson-RexEG Track upstream main. Reconciled the new invariant surface from facebook#8519 with this branch's shared -> @lexical/internal migration: - LexicalElementNode.ts: resolved the invariant import conflict in favor of @lexical/internal/invariant, and dropped the now-unused environment import (IS_APPLE_WEBKIT/IS_IOS/IS_SAFARI) — facebook#8519 moved that logic into the new LexicalDOMSlot.ts. - LexicalDOMSlot.ts (new): converted shared/invariant -> @lexical/internal/invariant and shared/environment -> ./environment. Verified: tsc, tsc-test, tsc-scripts, flow, lint, prettier clean; test-unit 3280 pass (incl. facebook#8519's new tests).
zurfyx
approved these changes
May 27, 2026
…acebook#8569) into claude/kind-johnson-RexEG Resolved conflicts and reconciled new upstream surface with this branch's shared -> @lexical/internal migration and per-module __DEV__ convention: - clipboard.ts: kept @lexical/internal/invariant; dropped $generateNodesFromDOM (no longer used after facebook#8528 introduced the DOMImportExtension pipeline). - code-core / rich-text package.json + pnpm-lock: took main's dependency set (adds @lexical/html) and regenerated via update-version + pnpm install, which re-applies the @lexical/internal deps and files allowlist. - Converted new shared/invariant imports (facebook#8528 parseCss.ts/sel.ts, facebook#8569 test) to @lexical/internal/invariant. - Added per-module `const __DEV__` to facebook#8528's compileImportRules.ts / runImport.ts (this branch removed the global __DEV__). Verified: tsc, tsc-test, tsc-scripts, flow, lint, prettier clean; test-unit 3373 pass.
The dom-import (facebook#8528) and node-state-style dev-example vite configs imported lexicalMonorepoPlugin from the old ../../packages/shared/ path, which this branch moved to scripts/vite/ (and removed packages/shared). This broke the integration test's dev-example build. Repointed both to ../../scripts/vite/lexicalMonorepoPlugin, matching every examples/* config. Verified both dev-examples build against a fresh prod dist.
This was referenced May 27, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Make each public package directory itself the publishable npm package so
pnpm linkandfile:protocol consumers can point at packages/ directly. Build artifacts now live entirely under packages//dist and package.jsonexports/main/module/typesreference that path, removing the separatenpm/tree the release used to construct.updateVersion.mjs rewrites public packages with
./dist/...paths, adds afileswhitelist, and copies the monorepo LICENSE into each package so a checkout is publishable without extra prep.prepare-release.mjs becomes a publish-time guard: it verifies every path the exports map references actually exists on disk (catching the "publish a dev build" footgun) and that README/LICENSE are present.
release.mjs publishes from packages/
directly; pnpm rewritesworkspace:*automatically.Integration test setup packs from the package root.
viteModuleResolution stops re-prefixing
dist/since the exports map already includes it.Dev/prod/release builds all emit the same on-disk shape:
.devor.prodvariants for each entry point plus matching fork modules (Foo.mjs,Foo.js,Foo.node.mjs). The fork unconditionally re-exports the available variant in dev-only or prod-only builds and uses the runtime NODE_ENV check only when both were built. No fake variant is ever emitted, so the publish guard catches dev-only builds that lack the prod files their exports map promises.TypeScript declarations and Flow stubs are copied into dist on every npm build, not just release, so linked consumers get types out of the box.
tsconfig excludes the internal www
<Name>.jsshims at the package root so tsc doesn't follow their require() into the compiled dist.Test plan
New integration tests to ensure that pnpm link works
Here's a demo repo to show that lexical can be used as a vendored typescript source directly with minimal configuration required https://github.com/etrepum/vendored-lexical-demo (via subtree of this PR's branch)