@lexical/compiler 0.0.0-bootstrap.0 → 0.49.1-nightly.20260901.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,229 @@
1
- # @lexical/compiler
1
+ # `@lexical/compiler`
2
2
 
3
- This 0.0.0-bootstrap.0 placeholder was published only to claim the npm package name so trusted publishing can be configured on npmjs.com. It contains no code; a future release will replace it.
3
+ Lexical's build-time compiler: source-to-source passes over Lexical code,
4
+ delivered as a Vite/Rollup plugin and as plain transform functions for
5
+ builds with no plugin API of their own.
6
+
7
+ Today it does one thing — **tree-shaking**. It inserts `/* @__PURE__ */`
8
+ annotations before module-scope calls to the side-effect-free factories
9
+ (`defineExtension`, `createCommand`, `createState`, `safeCast`,
10
+ `defineImportRule`, …) so bundlers can drop the extension, command, and rule
11
+ definitions an application never uses; it replaces calls to the trivial ones
12
+ with the value they would have returned; and with `strict` it refuses to let
13
+ an unannotated call hide inside a definition, where it would pin the whole
14
+ thing into the bundle.
15
+
16
+ Lexical's published `dist` bundles already carry the annotations — this
17
+ package is for builds that compile Lexical from its TypeScript source:
18
+
19
+ - consumers that opt into the `source` export condition
20
+ (`resolve.conditions: ['source', ...]`),
21
+ - a vendored copy or a git checkout of the monorepo,
22
+ - and your own application code, whose `defineExtension`/`createCommand`
23
+ definitions get annotated the same way.
24
+
25
+ ## Why the annotations are needed
26
+
27
+ The factories are annotated with `@__NO_SIDE_EFFECTS__` where they are
28
+ defined, but esbuild only honors that for calls in the same file as the
29
+ definition, and webpack/terser do not honor it at all. Bundlers do honor a
30
+ `/* @__PURE__ */` annotation at the call site, and that is what this
31
+ transform adds.
32
+
33
+ Argument-position calls matter too: a pure call is only removable when its
34
+ arguments are also side-effect-free, so an unannotated nested `safeCast(...)`
35
+ inside a `defineExtension({...})` config pins the whole definition. The
36
+ transform annotates every module-scope call, nested ones included.
37
+
38
+ Calls inside function bodies, class fields, and static blocks are left alone —
39
+ they are not evaluated when the module is initialized, so an annotation there
40
+ has no effect on tree-shaking.
41
+
42
+ ## Guaranteeing a definition can be dropped
43
+
44
+ An annotated definition is only droppable when everything it is built from is
45
+ too, so a single unannotated call in its arguments quietly pins it into every
46
+ bundle that imports the module. With `strict: true` that is a build error
47
+ naming the call rather than something you find later in a bundle analyzer:
48
+
49
+ ```
50
+ @lexical/compiler: 1 call(s) evaluated inside a definition in
51
+ src/MdastFootnoteExtension.ts are not known to be side-effect free, so the
52
+ definition cannot be tree-shaken:
53
+ 1145:25 gfmFootnoteFromMarkdown(...) inside defineExtension(...)
54
+ Make the call lazy, or declare the function side-effect free
55
+ (@__NO_SIDE_EFFECTS__ plus the transform's `functions`/`namespaces` options)
56
+ so that its calls are annotated too.
57
+ ```
58
+
59
+ Calls that only run later (a `nodes: () => [...]` callback, a `register`
60
+ method body) are not counted, and neither are the built-ins every bundler
61
+ already knows are pure (`new Map(...)`, `new Set(...)`, `Object`, `Symbol`,
62
+ …). Modules under `node_modules` are annotated as usual but never
63
+ checked: a definition a dependency shipped is not yours to fix. Lexical's own
64
+ build runs with `strict` on.
65
+
66
+ ## Usage
67
+
68
+ Each pass is its own entry point, so a build that wants one of them does not
69
+ load the rest: `@lexical/compiler/PureAnnotations` is the tree-shaking pass,
70
+ and `@lexical/compiler` re-exports every pass for convenience.
71
+
72
+ ### Vite
73
+
74
+ ```js
75
+ // vite.config.js
76
+ import {pureAnnotations} from '@lexical/compiler/PureAnnotations';
77
+ import {defineConfig} from 'vite';
78
+
79
+ export default defineConfig({
80
+ plugins: [pureAnnotations()],
81
+ resolve: {conditions: ['source']},
82
+ });
83
+ ```
84
+
85
+ The plugin defaults to `enforce: 'post'` so it runs after Vite has compiled
86
+ TypeScript and JSX away.
87
+
88
+ ### Rollup
89
+
90
+ ```js
91
+ // rollup.config.js
92
+ import {pureAnnotations} from '@lexical/compiler/PureAnnotations';
93
+
94
+ export default {
95
+ plugins: [
96
+ // ... any TypeScript/JSX plugin first
97
+ pureAnnotations(),
98
+ ],
99
+ };
100
+ ```
101
+
102
+ ### Any other bundler
103
+
104
+ `transformPureAnnotations` is the transform itself, with no bundler
105
+ integration, so it can be wrapped in a webpack loader or run as a codemod:
106
+
107
+ ```js
108
+ import {transformPureAnnotations} from '@lexical/compiler/PureAnnotations';
109
+
110
+ const result = transformPureAnnotations(code, {filename});
111
+ // `null` when the module needs no annotations
112
+ const annotated = result === null ? code : result.code;
113
+ ```
114
+
115
+ ## Which calls get annotated
116
+
117
+ A call is annotated only when the transform can establish that the function
118
+ being called really is side-effect free. It resolves the callee to its binding
119
+ in the module and annotates the call when that binding is:
120
+
121
+ - **imported from a Lexical package** — a specifier matching `lexical` or
122
+ `@lexical/*` (configurable with `sources`). Every factory in
123
+ `PURE_FACTORY_FUNCTIONS` is declared `@__NO_SIDE_EFFECTS__` in those
124
+ packages, so the import is evidence enough. Aliased imports
125
+ (`import {defineExtension as define}`) are resolved too;
126
+ - **declared in the same module with `@__NO_SIDE_EFFECTS__`**, or **imported
127
+ from a relative module that declares it that way** — the imported file is
128
+ read and parsed to check (turn this off with `relativeImports: false`).
129
+ Such a declaration is evidence on its own, so a factory of your own does
130
+ not have to be named in `functions`: mark it and its module-scope calls are
131
+ annotated; or
132
+ - **a method of a pure namespace** — an object whose methods build values and
133
+ touch nothing else, like `@lexical/html`'s `sel`, so that
134
+ `sel.tag('span').attr('data-x', true)` is annotated the way a factory call
135
+ is. The names are `PURE_NAMESPACES` (configurable with `namespaces`) when
136
+ imported from a Lexical package, and any object marked
137
+ `@lexical-pure-namespace` where it is declared, including through a
138
+ relative import or a local alias. Only the outermost call of a chain is
139
+ annotated — rollup, terser and esbuild all drop the whole chain from that
140
+ one.
141
+
142
+ Anything else is left alone. Your own `safeCast` from `./utils`, or a
143
+ `createCommand` from some other library, will not be annotated just because it
144
+ shares a name with a Lexical factory — annotating a call that does have side
145
+ effects would let a bundler drop it. Mark your own factories
146
+ `@__NO_SIDE_EFFECTS__` (and add them to `functions`) to opt them in.
147
+
148
+ ## Eliding the trivial factories
149
+
150
+ Some of the factories are pure type-level helpers whose implementation is a
151
+ trivial expression over their own arguments:
152
+
153
+ | Factory | Returns | Form |
154
+ | --- | --- | --- |
155
+ | `safeCast(value)` | `value` | `identity` |
156
+ | `defineExtension(extension)` | `extension` | `identity` |
157
+ | `defineImportRule(rule)` | `rule` | `identity` |
158
+ | `configExtension(...args)` | `args` | `args` |
159
+ | `declarePeerDependency(...args)` | `args` | `args` |
160
+
161
+ With `inline: true` the call is replaced by that expression instead of being
162
+ annotated:
163
+
164
+ ```js
165
+ export const MarkExtension = defineExtension({name: '@lexical/mark', ...});
166
+ // becomes
167
+ export const MarkExtension = {name: '@lexical/mark', ...};
168
+ ```
169
+
170
+ which is better than an annotation in every way that matters: a literal is
171
+ inert for every bundler with nothing to preserve through minification, there
172
+ is no nested call left to pin the definition, and the factory itself can be
173
+ dropped from the bundle once nothing calls it (the import it came from is
174
+ removed along with the last call to it).
175
+
176
+ A call that does not fit its form is annotated as usual: a spread argument
177
+ where a single parameter is expected, or a call whose result is discarded.
178
+ The replacement is parenthesized wherever bare syntax would not bind the way
179
+ the call did — `safeCast(1 + 2) * 3` is not `1 + 2 * 3`, an object literal at
180
+ the start of an expression statement would read as a block, and an `args`
181
+ array is always wrapped, since a leading `[` is a member access on whatever
182
+ the previous line ended with. Minifiers drop the parentheses again.
183
+
184
+ `createCommand` is deliberately not inlined: `{type: x}` is more bytes than a
185
+ call to a minified one-character name.
186
+
187
+ Each inlined factory is marked `@lexical-inline <form>` where it is defined.
188
+ For an import from a Lexical package the marker is documentation — the table
189
+ is trusted, because this package ships alongside that Lexical. Everywhere
190
+ else (a factory of your own, declared in the module or imported relatively)
191
+ the marker is what makes inlining possible at all: `@__NO_SIDE_EFFECTS__`
192
+ says a call is safe to *drop*, but only the marker says what the call can be
193
+ *replaced with*, so a same-named look-alike is annotated instead. Add your
194
+ factory's name to `functions`, mark it with the form its body takes, and its
195
+ calls are inlined like Lexical's own.
196
+
197
+ A marker that names a form the transform does not implement (a typo, or a
198
+ form from an older version) throws, naming the file and the forms it accepts.
199
+ Failing is the point: a marker that silently did nothing would leave the
200
+ factory un-inlined with no sign of it.
201
+
202
+ This is **off by default** because it reproduces those function bodies, so it
203
+ assumes the Lexical you are building matches this package's version — they
204
+ are released together with the same version number. Lexical's own build turns
205
+ it on for the bundles it publishes. The package's tests check that the
206
+ markers in the tree and the table agree, and that the real functions still
207
+ return what the table claims.
208
+
209
+ ## Options
210
+
211
+ | Option | Default | Description |
212
+ | --- | --- | --- |
213
+ | `inline` | `false` | Replace calls to the trivial factories with the literal they would have returned (see above). |
214
+ | `functions` | `PURE_FACTORY_FUNCTIONS` | Names of the factories whose module-scope calls are annotated. Pass your own list (or `[...PURE_FACTORY_FUNCTIONS, 'myFactory']`) to cover factories of your own. |
215
+ | `namespaces` | `PURE_NAMESPACES` | Names of objects whose method calls are annotated (see above). |
216
+ | `sources` | `[/^lexical$/, /^@lexical\//]` | `RegExp` (or array) of module specifiers whose exports are trusted to be the factories without reading them. |
217
+ | `strict` | `false` | Throw when a call evaluated inside one of the definitions is not known to be side-effect free (see above). |
218
+ | `relativeImports` | `true` | Whether to read relatively imported modules to look for a `@__NO_SIDE_EFFECTS__` declaration. |
219
+ | `include` | every `.js`/`.jsx`/`.ts`/`.tsx`/`.mjs`/`.cjs`/`.mts`/`.cts` module | `RegExp` (or array) matched against the module id with any query string removed. |
220
+ | `exclude` | none | `RegExp` (or array) of module ids to skip. |
221
+ | `enforce` | `'post'` | Vite plugin ordering. |
222
+ | `parserPlugins` | none | Extra [`@babel/parser`](https://babeljs.io/docs/babel-parser#plugins) plugins, for syntax the defaults do not cover. |
223
+ | `sourceMap` | `true` | Set to `false` to skip source map generation. |
224
+
225
+ Modules that fail to parse are passed through unchanged with a warning rather
226
+ than failing the build.
227
+
228
+ The transform is idempotent: a call that already has a `/* @__PURE__ */` (or
229
+ terser's `/* #__PURE__ */`) annotation immediately before it is left alone.