@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 +228 -2
- package/dist/LexicalCompiler.dev.js +1725 -0
- package/dist/LexicalCompiler.dev.mjs +1704 -0
- package/dist/LexicalCompiler.js +11 -0
- package/dist/LexicalCompiler.js.flow +32 -0
- package/dist/LexicalCompiler.mjs +17 -0
- package/dist/LexicalCompiler.node.mjs +15 -0
- package/dist/LexicalCompiler.prod.js +9 -0
- package/dist/LexicalCompiler.prod.mjs +9 -0
- package/dist/LexicalCompilerPureAnnotations.dev.js +1725 -0
- package/dist/LexicalCompilerPureAnnotations.dev.mjs +1704 -0
- package/dist/LexicalCompilerPureAnnotations.js +11 -0
- package/dist/LexicalCompilerPureAnnotations.js.flow +78 -0
- package/dist/LexicalCompilerPureAnnotations.mjs +17 -0
- package/dist/LexicalCompilerPureAnnotations.node.mjs +15 -0
- package/dist/LexicalCompilerPureAnnotations.prod.js +9 -0
- package/dist/LexicalCompilerPureAnnotations.prod.mjs +9 -0
- package/dist/PureAnnotations.d.ts +172 -0
- package/dist/index.d.ts +8 -0
- package/dist/passes/pureAnnotations.d.mts +246 -0
- package/dist/typescript-too-old.d.ts +18 -0
- package/package.json +111 -6
- package/src/PureAnnotations.ts +206 -0
- package/src/index.ts +12 -0
- package/src/passes/pureAnnotations.mjs +1618 -0
package/README.md
CHANGED
|
@@ -1,3 +1,229 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@lexical/compiler`
|
|
2
2
|
|
|
3
|
-
|
|
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.
|