@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.
@@ -0,0 +1,1704 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import { parse } from '@babel/parser';
10
+ import MagicString from 'magic-string';
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+
14
+ /**
15
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
16
+ *
17
+ * This source code is licensed under the MIT license found in the
18
+ * LICENSE file in the root directory of this source tree.
19
+ *
20
+ */
21
+
22
+
23
+ /**
24
+ * The Lexical factories that are annotated with `__NO_SIDE_EFFECTS__` at
25
+ * their definition. That annotation is only honored by esbuild for calls in
26
+ * the same file as the definition (and not at all by webpack/terser), so
27
+ * every module-scope call site needs its own `__PURE__` annotation for
28
+ * bundlers to tree-shake unused extension/command/rule definitions out of
29
+ * application bundles.
30
+ *
31
+ * Argument-position calls (e.g. `configExtension` / `safeCast` inside a
32
+ * `defineExtension` config) matter too: a pure call is only removable when
33
+ * its arguments are also side-effect-free, so one unannotated nested call
34
+ * pins the entire enclosing definition.
35
+ *
36
+ * @type {ReadonlyArray<string>}
37
+ */
38
+ const PURE_FACTORY_FUNCTIONS$1 = [
39
+ 'configExtension',
40
+ 'createCommand',
41
+ 'createContextState',
42
+ 'createImportState',
43
+ 'createRenderState',
44
+ 'createState',
45
+ 'declarePeerDependency',
46
+ 'defineExtension',
47
+ 'defineImportRule',
48
+ 'defineOverlayRules',
49
+ 'domOverride',
50
+ 'safeCast',
51
+ ];
52
+
53
+ /**
54
+ * The factories whose implementation is a trivial expression over their own
55
+ * arguments, and the shape a call to them can be replaced with. Eliding the
56
+ * call is better than annotating it: a literal is inert for every bundler,
57
+ * with no annotation to preserve and nothing left to pin the definition it
58
+ * appears in.
59
+ *
60
+ * Each of these is marked `@lexical-inline <form>` where it is defined, and
61
+ * the unit tests check both that the marker and this table agree and that
62
+ * the real function still returns what the form claims. `createCommand` is
63
+ * deliberately not here: `{type: x}` is more bytes than a call to a minified
64
+ * one-character name.
65
+ *
66
+ * - `identity` returns its single argument
67
+ * - `args` returns all of its arguments as an array
68
+ *
69
+ * @type {ReadonlyMap<string, {form: 'args' | 'identity'}>}
70
+ */
71
+ const INLINE_FACTORIES = new Map([
72
+ ['configExtension', {form: 'args'}],
73
+ ['declarePeerDependency', {form: 'args'}],
74
+ ['defineExtension', {form: 'identity'}],
75
+ ['defineImportRule', {form: 'identity'}],
76
+ ['safeCast', {form: 'identity'}],
77
+ ]);
78
+
79
+ /** The marker a definition uses to declare itself inlinable. */
80
+ // A JSDoc tag, so it has to start its line (after the comment's own `*`).
81
+ // Prose that merely mentions the tag — including this file's own
82
+ // documentation of it — is not a marker.
83
+ const INLINE_MARKER_RE =
84
+ /^[^\S\n]*\*?[^\S\n]*@lexical-inline(?:[^\S\n]+(\S+))?[^\S\n]*$/m;
85
+
86
+ /**
87
+ * An error the author has to act on — a malformed marker, or a call this
88
+ * transform cannot prove is safe — as opposed to source it cannot parse.
89
+ * The plugin passes an unparseable module through with a warning; these it
90
+ * rethrows, because ignoring them would silently produce the bundle the
91
+ * author was trying to avoid.
92
+ */
93
+ class PureAnnotationsError extends Error {
94
+ /** @param {string} message */
95
+ constructor(message) {
96
+ super(message);
97
+ this.name = 'PureAnnotationsError';
98
+ }
99
+ }
100
+
101
+ // The tag that declares an object's methods side-effect free, so that a
102
+ // namespace reached through a relative import or a local alias — as
103
+ // @lexical/html's own modules reach `selBase` — is recognized without
104
+ // having to be listed in the table above.
105
+ const NAMESPACE_MARKER_RE =
106
+ /^[^\S\n]*\*?[^\S\n]*@lexical-pure-namespace[^\S\n]*$/m;
107
+
108
+ /** The forms a `@lexical-inline` marker may name. */
109
+ const INLINE_FORMS = new Set(['args', 'identity']);
110
+
111
+ /**
112
+ * Imported objects whose method calls are side-effect free, so that a
113
+ * module-scope call like `sel.tag('p').attr('data-x', true)` is annotated
114
+ * the same way a call to a factory function is. Only the outermost call of
115
+ * a chain is annotated: rollup, terser and esbuild all drop the whole chain
116
+ * from that one annotation.
117
+ *
118
+ * As with the factories, the name is only trusted when it is imported from
119
+ * a Lexical package.
120
+ */
121
+ const PURE_NAMESPACES$1 = ['sel'];
122
+
123
+ /**
124
+ * The form each inlinable factory is replaced with, for the tests that keep
125
+ * this table honest and for anyone who wants to know what `inline` does.
126
+ *
127
+ * @type {ReadonlyMap<string, string>}
128
+ */
129
+ const INLINE_FACTORY_FORMS$1 = new Map(
130
+ Array.from(INLINE_FACTORIES, ([name, spec]) => [name, spec.form]),
131
+ );
132
+
133
+ /**
134
+ * The annotation inserted before a module-scope factory call. No trailing
135
+ * space: a block comment is a token separator on its own, so the space would
136
+ * only be there to be stripped again by the minifier.
137
+ */
138
+ const ANNOTATION = '/* @__PURE__ */';
139
+
140
+ /** Matches the contents of a comment that is already a pure annotation. */
141
+ /**
142
+ * `overwrite` clears the intro and outro of every chunk it spans unless it is
143
+ * told to touch the content only — and an annotation this transform appended
144
+ * to a nested call lives in the outro of the chunk that ends where that call
145
+ * begins. Replacing a call's `factory(` with `([` would take the annotation
146
+ * on its first argument with it.
147
+ *
148
+ * @type {{contentOnly: true}}
149
+ */
150
+ const CONTENT_ONLY = {contentOnly: true};
151
+
152
+ const PURE_ANNOTATION_RE = /[#@]__PURE__/;
153
+
154
+ /** The file extensions the bundler plugin transforms by default. */
155
+ const DEFAULT_INCLUDE = /\.(?:[cm]?[jt]sx?)$/;
156
+
157
+ /**
158
+ * Module specifiers whose exports are trusted to be the Lexical factories.
159
+ * Every factory in PURE_FACTORY_FUNCTIONS is declared side-effect free in
160
+ * these packages, so an import from one of them is evidence enough. A call
161
+ * to a same-named function from anywhere else is only annotated when the
162
+ * declaration it resolves to is itself marked side-effect free.
163
+ */
164
+ const DEFAULT_SOURCES = [/^lexical$/, /^@lexical\//];
165
+
166
+ /** Matches the annotation a definition uses to declare itself pure. */
167
+ const NO_SIDE_EFFECTS_RE = /[#@]__NO_SIDE_EFFECTS__/;
168
+
169
+ /** An import of a module next to this one, which may declare a namespace. */
170
+ const RELATIVE_IMPORT_RE = /\bfrom\s*['"]\./;
171
+
172
+ /**
173
+ * Expression types that read the same with or without the parentheses the
174
+ * call they replace was written with: they are a single delimited token, so
175
+ * no surrounding operator can bind into them. Anything else keeps the
176
+ * parentheses, which costs two characters a minifier removes anyway.
177
+ */
178
+ const SELF_DELIMITING_TYPES = new Set([
179
+ 'ArrayExpression',
180
+ 'BigIntLiteral',
181
+ 'BooleanLiteral',
182
+ 'DecimalLiteral',
183
+ 'Identifier',
184
+ 'JSXElement',
185
+ 'JSXFragment',
186
+ 'NullLiteral',
187
+ 'NumericLiteral',
188
+ 'ObjectExpression',
189
+ 'RegExpLiteral',
190
+ 'StringLiteral',
191
+ 'TemplateLiteral',
192
+ 'ThisExpression',
193
+ ]);
194
+
195
+ /** Extensions tried when resolving an extensionless relative import. */
196
+ const RESOLVE_EXTENSIONS = [
197
+ '.ts',
198
+ '.tsx',
199
+ '.mts',
200
+ '.cts',
201
+ '.js',
202
+ '.jsx',
203
+ '.mjs',
204
+ '.cjs',
205
+ ];
206
+
207
+ /**
208
+ * Node types whose contents are not evaluated when the module is
209
+ * initialized. A `__PURE__` annotation only affects tree-shaking of
210
+ * module-scope evaluation, so the walk never descends into these.
211
+ */
212
+ /**
213
+ * Built-ins whose construction or call has no side effects. Bundlers know
214
+ * this for themselves (rollup's known globals, esbuild's primitive
215
+ * handling), so requiring an annotation on `new Map([...])` inside a
216
+ * definition would be noise.
217
+ */
218
+ const PURE_GLOBALS = new Set([
219
+ 'Array',
220
+ 'BigInt',
221
+ 'Boolean',
222
+ 'Map',
223
+ 'Number',
224
+ 'Object',
225
+ 'RegExp',
226
+ 'Set',
227
+ 'String',
228
+ 'Symbol',
229
+ 'WeakMap',
230
+ 'WeakSet',
231
+ ]);
232
+
233
+ /**
234
+ * A dependency's own code. It is still annotated — a Lexical consumed
235
+ * through its `source` export condition is resolved from here — but `strict`
236
+ * does not apply to it: a definition somebody else shipped is not the
237
+ * building project's to fix, and neither is a marker they got wrong.
238
+ */
239
+ const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
240
+
241
+ const DEFERRED_TYPES = new Set([
242
+ 'ArrowFunctionExpression',
243
+ 'ClassAccessorProperty',
244
+ 'ClassMethod',
245
+ 'ClassPrivateMethod',
246
+ 'ClassPrivateProperty',
247
+ 'ClassProperty',
248
+ 'FunctionDeclaration',
249
+ 'FunctionExpression',
250
+ 'ObjectMethod',
251
+ 'PropertyDefinition',
252
+ 'StaticBlock',
253
+ 'TSDeclareMethod',
254
+ ]);
255
+
256
+ /** AST properties that never contain child nodes worth visiting. */
257
+ /** @type {ReadonlySet<string>} */
258
+ const EMPTY_NAMES = new Set();
259
+
260
+ const SKIPPED_KEYS = new Set([
261
+ 'comments',
262
+ 'extra',
263
+ 'innerComments',
264
+ 'leadingComments',
265
+ 'loc',
266
+ 'trailingComments',
267
+ ]);
268
+
269
+ /**
270
+ * @param {undefined | string} filename
271
+ * @param {undefined | ReadonlyArray<any>} extraPlugins
272
+ * @returns {Array<any>} the Babel parser plugins to parse this file with
273
+ */
274
+ function parserPluginsFor(filename, extraPlugins) {
275
+ const name = typeof filename === 'string' ? filename : '';
276
+ const isTypeScript = /\.[cm]?tsx?$/i.test(name);
277
+ /** @type {Array<any>} */
278
+ const plugins = ['explicitResourceManagement'];
279
+ if (isTypeScript) {
280
+ plugins.push('typescript');
281
+ }
282
+ // `<T>(value: T) => value` in a .ts file is a generic arrow function, not
283
+ // an opening JSX element, so the jsx plugin must stay off there. Any other
284
+ // extension (including an unknown one) is parsed with jsx enabled.
285
+ if (!isTypeScript || /\.[cm]?tsx$/i.test(name)) {
286
+ plugins.push('jsx');
287
+ }
288
+ return extraPlugins ? plugins.concat(extraPlugins) : plugins;
289
+ }
290
+
291
+ /**
292
+ * Find the block comment that ends immediately before `offset` (ignoring
293
+ * whitespace), if any.
294
+ *
295
+ * This reads the source text rather than the parsed `leadingComments` so
296
+ * that a comment attached to an enclosing statement (one that precedes
297
+ * `export const x = f()` rather than the call itself, where it does nothing)
298
+ * is not mistaken for an annotation on the call.
299
+ *
300
+ * @param {string} code
301
+ * @param {number} offset
302
+ * @returns {null | {end: number, start: number, value: string}}
303
+ */
304
+ function blockCommentBefore(code, offset) {
305
+ let end = offset;
306
+ while (end > 0 && /\s/.test(code[end - 1])) {
307
+ end--;
308
+ }
309
+ if (end < 4 || code[end - 1] !== '/' || code[end - 2] !== '*') {
310
+ return null;
311
+ }
312
+ const start = code.lastIndexOf('/*', end - 2);
313
+ if (start === -1) {
314
+ return null;
315
+ }
316
+ return {end, start, value: code.slice(start + 2, end - 2)};
317
+ }
318
+
319
+ /**
320
+ * @param {string} code
321
+ * @param {number} offset the start of a call expression
322
+ * @returns {boolean} true when the call already carries a pure annotation
323
+ */
324
+ function hasPureAnnotation(code, offset) {
325
+ const comment = blockCommentBefore(code, offset);
326
+ return comment !== null && PURE_ANNOTATION_RE.test(comment.value);
327
+ }
328
+
329
+ /**
330
+ * The identifier a member-expression callee is rooted at, so that every call
331
+ * in `sel.tag('p').attr('x')` resolves to `sel`.
332
+ *
333
+ * @param {any} callee
334
+ * @returns {null | string} the name, or null when the callee is not rooted
335
+ * at a plain identifier
336
+ */
337
+ function rootObjectName(callee) {
338
+ for (let node = callee; node; ) {
339
+ if (node.type === 'Identifier') {
340
+ return node.name;
341
+ } else if (node.type === 'MemberExpression') {
342
+ node = node.object;
343
+ } else if (node.type === 'CallExpression') {
344
+ node = node.callee;
345
+ } else {
346
+ return null;
347
+ }
348
+ }
349
+ return null;
350
+ }
351
+
352
+ /**
353
+ * The names a nested block binds for itself with `let`, `const`, `class` or
354
+ * a function declaration. Only these shadow an outer binding: `var` is
355
+ * scoped to the module, so a `var` of an imported name is a redeclaration
356
+ * error rather than a shadow.
357
+ *
358
+ * @param {any} node
359
+ * @returns {null | Set<string>} the names, or null when this node binds none
360
+ */
361
+ function blockScopedNames(node) {
362
+ // Only a nested block: the module's own declarations are the bindings the
363
+ // transform resolved in the first place, not shadows of them.
364
+ const body =
365
+ node.type === 'BlockStatement'
366
+ ? node.body
367
+ : node.type === 'SwitchCase'
368
+ ? node.consequent
369
+ : null;
370
+ if (body === null) {
371
+ return null;
372
+ }
373
+ /** @type {null | Set<string>} */
374
+ let names = null;
375
+ /** @param {string} name */
376
+ const add = name => {
377
+ names = names || new Set();
378
+ names.add(name);
379
+ };
380
+ for (const statement of body) {
381
+ if (statement.type === 'VariableDeclaration' && statement.kind !== 'var') {
382
+ for (const declarator of statement.declarations) {
383
+ if (declarator.id.type === 'Identifier') {
384
+ add(declarator.id.name);
385
+ }
386
+ }
387
+ } else if (
388
+ (statement.type === 'FunctionDeclaration' ||
389
+ statement.type === 'ClassDeclaration') &&
390
+ statement.id
391
+ ) {
392
+ add(statement.id.name);
393
+ }
394
+ }
395
+ return names;
396
+ }
397
+
398
+ /**
399
+ * Walk every node that is evaluated when the module is initialized, calling
400
+ * `visitor` with each module-scope call to one of the factories, or to a
401
+ * method of one of the namespaces, and where it sits: `discarded` when the
402
+ * call's value is thrown away (it is the whole of an expression statement),
403
+ * and `statementStart` when it begins one, both of which constrain what it
404
+ * can be replaced with. `namespace` marks a method call, which is annotated
405
+ * but never replaced.
406
+ *
407
+ * @param {any} node
408
+ * @param {{functions: ReadonlyMap<string, any> | ReadonlySet<string>, namespaces?: ReadonlySet<string>}} names
409
+ * @param {(node: any, position: {discarded: boolean, namespace?: boolean, statementStart: boolean}) => void} visitor
410
+ * @param {{discarded: boolean, shadowed: ReadonlySet<string>, statementStarts: Set<number>}} [state]
411
+ */
412
+ function visitModuleScopeCalls(node, names, visitor, state) {
413
+ const {functions, namespaces} = names;
414
+ const walkState = state || {
415
+ discarded: false,
416
+ shadowed: EMPTY_NAMES,
417
+ statementStarts: new Set(),
418
+ };
419
+ if (Array.isArray(node)) {
420
+ for (const child of node) {
421
+ visitModuleScopeCalls(child, names, visitor, {
422
+ discarded: false,
423
+ shadowed: walkState.shadowed,
424
+ statementStarts: walkState.statementStarts,
425
+ });
426
+ }
427
+ return;
428
+ }
429
+ if (!node || typeof node !== 'object' || typeof node.type !== 'string') {
430
+ return;
431
+ }
432
+ if (DEFERRED_TYPES.has(node.type)) {
433
+ return;
434
+ }
435
+ if (node.type === 'ExpressionStatement') {
436
+ // Every node that begins an expression statement starts at the same
437
+ // offset, so recording that one offset identifies the whole leftmost
438
+ // chain — the positions where an object literal would be read as a
439
+ // block, whatever it is nested in.
440
+ walkState.statementStarts.add(node.expression.start);
441
+ }
442
+ // Only a plain CallExpression: `factory?.(x)` parses as an
443
+ // OptionalCallExpression, and it evaluates to undefined when the factory
444
+ // is missing rather than to what the factory returns.
445
+ if (node.type === 'CallExpression' && node.callee) {
446
+ const position = {
447
+ discarded: walkState.discarded,
448
+ statementStart: walkState.statementStarts.has(node.start),
449
+ };
450
+ if (
451
+ node.callee.type === 'Identifier' &&
452
+ functions.has(node.callee.name) &&
453
+ !walkState.shadowed.has(node.callee.name)
454
+ ) {
455
+ visitor(node, position);
456
+ } else if (namespaces !== undefined && namespaces.size > 0) {
457
+ const root = rootObjectName(node.callee);
458
+ if (
459
+ root !== null &&
460
+ namespaces.has(root) &&
461
+ !walkState.shadowed.has(root)
462
+ ) {
463
+ visitor(node, {...position, namespace: true});
464
+ }
465
+ }
466
+ }
467
+ // A block that rebinds one of the names is talking about something else
468
+ // inside itself, and that something else may well have side effects.
469
+ const bound = blockScopedNames(node);
470
+ const shadowed =
471
+ bound === null
472
+ ? walkState.shadowed
473
+ : new Set([...walkState.shadowed, ...bound]);
474
+ for (const key of Object.keys(node)) {
475
+ if (!SKIPPED_KEYS.has(key)) {
476
+ const value = node[key];
477
+ if (value && typeof value === 'object') {
478
+ visitModuleScopeCalls(value, names, visitor, {
479
+ discarded:
480
+ node.type === 'ExpressionStatement' && key === 'expression',
481
+ shadowed,
482
+ statementStarts: walkState.statementStarts,
483
+ });
484
+ }
485
+ }
486
+ }
487
+ }
488
+
489
+ /**
490
+ * True when a call to a factory of this form can be replaced by the literal
491
+ * it would have returned. Anything the form cannot account for — a spread
492
+ * argument, the wrong number of arguments, a comma expression that would
493
+ * lose its parentheses — is annotated instead.
494
+ *
495
+ * @param {any} node the call expression
496
+ * @param {{form: string}} spec
497
+ * @returns {boolean}
498
+ */
499
+ function canInlineCall(node, spec) {
500
+ const args = node.arguments;
501
+ if (spec.form === 'identity') {
502
+ return (
503
+ args.length === 1 &&
504
+ args[0].type !== 'SpreadElement' &&
505
+ args[0].type !== 'SequenceExpression'
506
+ );
507
+ }
508
+ return true;
509
+ }
510
+
511
+ /**
512
+ * Walk every node in the tree, calling `visit` with each node and the node
513
+ * and key that hold it. Unlike visitModuleScopeCalls this does not stop at
514
+ * function boundaries: it is used to prove that a name is *not* referenced
515
+ * anywhere else in the module.
516
+ *
517
+ * @param {any} node
518
+ * @param {(node: any, parent: any, key: string) => void} visit
519
+ * @param {any} [parent]
520
+ * @param {string} [key]
521
+ */
522
+ function walkAllNodes(node, visit, parent, key) {
523
+ if (Array.isArray(node)) {
524
+ for (const child of node) {
525
+ walkAllNodes(child, visit, parent, key);
526
+ }
527
+ return;
528
+ }
529
+ if (!node || typeof node !== 'object' || typeof node.type !== 'string') {
530
+ return;
531
+ }
532
+ visit(node, parent, /** @type {string} */ (key));
533
+ for (const childKey of Object.keys(node)) {
534
+ if (!SKIPPED_KEYS.has(childKey)) {
535
+ const value = node[childKey];
536
+ if (value && typeof value === 'object') {
537
+ walkAllNodes(value, visit, node, childKey);
538
+ }
539
+ }
540
+ }
541
+ }
542
+
543
+ /**
544
+ * True when an identifier node is a reference to a binding rather than a
545
+ * name in some other position (a property name, an import specifier, ...).
546
+ * Anything unrecognized counts as a reference, so an unexpected position
547
+ * keeps the import rather than removing one that is still needed.
548
+ *
549
+ * @param {any} node
550
+ * @param {any} parent
551
+ * @param {string} key
552
+ * @returns {boolean}
553
+ */
554
+ function isValueReference(node, parent, key) {
555
+ if (node.type !== 'Identifier' && node.type !== 'JSXIdentifier') {
556
+ return false;
557
+ }
558
+ if (!parent) {
559
+ return true;
560
+ }
561
+ switch (parent.type) {
562
+ case 'ImportDefaultSpecifier':
563
+ case 'ImportNamespaceSpecifier':
564
+ case 'ImportSpecifier':
565
+ return false;
566
+ case 'ExportSpecifier':
567
+ // `export {imported as somethingElse}` still refers to the binding.
568
+ return key === 'local';
569
+ case 'OptionalMemberExpression':
570
+ case 'MemberExpression':
571
+ return key !== 'property' || parent.computed === true;
572
+ case 'ClassMethod':
573
+ case 'ClassProperty':
574
+ case 'ObjectMethod':
575
+ case 'ObjectProperty':
576
+ return key !== 'key' || parent.computed === true;
577
+ default:
578
+ return true;
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Remove the import specifiers whose only uses were calls that have just
584
+ * been replaced by a literal, so the transform does not leave behind an
585
+ * import that nothing references (which bundlers then warn about).
586
+ *
587
+ * @param {MagicString} magicString
588
+ * @param {string} code
589
+ * @param {any} program
590
+ * @param {ReadonlyArray<any>} inlinedCalls the call nodes that were replaced
591
+ */
592
+ function removeUnusedImports(magicString, code, program, inlinedCalls) {
593
+ const callees = new Set(inlinedCalls.map(node => node.callee));
594
+ const names = new Set(inlinedCalls.map(node => node.callee.name));
595
+ if (names.size === 0) {
596
+ return;
597
+ }
598
+ // A name is still needed if anything other than an inlined callee refers
599
+ // to it, anywhere in the module.
600
+ walkAllNodes(program, (node, parent, key) => {
601
+ if (
602
+ names.has(node.name) &&
603
+ !callees.has(node) &&
604
+ isValueReference(node, parent, key)
605
+ ) {
606
+ names.delete(node.name);
607
+ }
608
+ });
609
+ for (const statement of program.body) {
610
+ if (
611
+ statement.type !== 'ImportDeclaration' ||
612
+ statement.importKind === 'type'
613
+ ) {
614
+ continue;
615
+ }
616
+ const {specifiers} = statement;
617
+ const named = specifiers.filter(
618
+ /** @param {any} specifier */ specifier =>
619
+ specifier.type === 'ImportSpecifier',
620
+ );
621
+ const removable = named.filter(
622
+ /** @param {any} specifier */ specifier =>
623
+ specifier.importKind !== 'type' && names.has(specifier.local.name),
624
+ );
625
+ if (removable.length === 0) {
626
+ continue;
627
+ }
628
+ if (removable.length < named.length) {
629
+ // Some names are still used: take out each unused one along with the
630
+ // comma that separates it from the named import next to it. Adjacent
631
+ // unused ones go as a single range — two overlapping removals would
632
+ // leave the comma of the first behind (`import {a, } from 'x'`).
633
+ const removableSet = new Set(removable);
634
+ for (let start = 0; start < named.length; start++) {
635
+ if (!removableSet.has(named[start])) {
636
+ continue;
637
+ }
638
+ let end = start;
639
+ while (end + 1 < named.length && removableSet.has(named[end + 1])) {
640
+ end++;
641
+ }
642
+ if (end + 1 < named.length) {
643
+ magicString.remove(named[start].start, named[end + 1].start);
644
+ } else {
645
+ magicString.remove(named[start - 1].end, named[end].end);
646
+ }
647
+ start = end;
648
+ }
649
+ continue;
650
+ }
651
+ const close = code.indexOf('}', named[named.length - 1].end);
652
+ if (named.length < specifiers.length) {
653
+ // A default or namespace import is still there, so only the braced
654
+ // clause and the comma before it go.
655
+ const previous = specifiers[specifiers.indexOf(named[0]) - 1];
656
+ magicString.remove(code.indexOf(',', previous.end), close + 1);
657
+ continue;
658
+ }
659
+ // Nothing is imported from the module any more. Dropping the import
660
+ // drops the module, which is only safe because it has no side effects to
661
+ // run: the factories come either from a Lexical package (all of which
662
+ // are `sideEffects: false`) or from a module that declares functions
663
+ // side-effect free and inlinable. A module that exports such a factory
664
+ // and also does something at import time is out of scope by
665
+ // construction — Lexical has no modules with top-level side effects, and
666
+ // neither should a module that a build is told to inline from.
667
+ magicString.remove(statement.start, statement.end);
668
+ }
669
+ }
670
+
671
+ /**
672
+ * The offset just past a call's opening parenthesis, which is where its
673
+ * argument list begins — including any parentheses the first argument was
674
+ * written with, and any type arguments before it (`safeCast<T>(x)`).
675
+ *
676
+ * @param {string} code
677
+ * @param {any} node a call expression
678
+ * @returns {number}
679
+ */
680
+ function argumentsStart(code, node) {
681
+ const typeArguments = node.typeArguments || node.typeParameters;
682
+ const from = Math.max(
683
+ node.callee.end,
684
+ typeArguments ? typeArguments.end : node.callee.end,
685
+ );
686
+ return code.indexOf('(', from) + 1;
687
+ }
688
+
689
+ /**
690
+ * Replace a call with the literal the factory would have returned, keeping
691
+ * the argument source text (and any nested edits within it) intact.
692
+ *
693
+ * @param {MagicString} magicString
694
+ * @param {string} code
695
+ * @param {any} node the call expression
696
+ * @param {{form: string}} spec
697
+ * @param {boolean} [statementStart] whether the call begins an expression
698
+ * statement, where an object literal would be read as a block
699
+ */
700
+ function inlineCall(magicString, code, node, spec, statementStart) {
701
+ const comment = blockCommentBefore(code, node.start);
702
+ if (comment !== null && PURE_ANNOTATION_RE.test(comment.value)) {
703
+ // Left in front of a literal the annotation does nothing, and bundlers
704
+ // report it as misplaced.
705
+ magicString.remove(comment.start, node.start);
706
+ }
707
+ const args = node.arguments;
708
+ if (spec.form !== 'identity') {
709
+ // The argument list becomes the array literal, so the edit is on the
710
+ // call's own parentheses — `factory(` and its `)` — rather than on where
711
+ // the first and last arguments start and end. Those exclude parentheses
712
+ // an argument was written with (`configExtension(Ext, (cfg))`), which
713
+ // would be left behind unbalanced.
714
+ //
715
+ // Parenthesized so that the replacement binds exactly like the call it
716
+ // replaces, wherever that call was: a bare `[` at the start of an
717
+ // expression statement is a member access on whatever the previous line
718
+ // ended with, which is how ASI reads `f()\n[a, b].length`. Minifiers
719
+ // drop the parentheses again.
720
+ magicString.overwrite(
721
+ node.start,
722
+ argumentsStart(code, node),
723
+ '([',
724
+ CONTENT_ONLY,
725
+ );
726
+ magicString.overwrite(node.end - 1, node.end, '])', CONTENT_ONLY);
727
+ return;
728
+ }
729
+ // The call's own parentheses were doing two jobs: grouping the argument
730
+ // (`safeCast(a || b).c` must not become `a || b.c`) and, at the start of an
731
+ // expression statement, keeping an object literal from being read as a
732
+ // block. Keep them unless the argument reads the same without them.
733
+ const keepParens =
734
+ statementStart === true || !SELF_DELIMITING_TYPES.has(args[0].type);
735
+ if (keepParens) {
736
+ magicString.overwrite(node.start, args[0].start, '(', CONTENT_ONLY);
737
+ magicString.overwrite(args[0].end, node.end, ')', CONTENT_ONLY);
738
+ } else {
739
+ magicString.remove(node.start, args[0].start);
740
+ magicString.remove(args[0].end, node.end);
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Caches for the files read while resolving relative imports. They live for
746
+ * the lifetime of the process, which is what a build wants; a watch-mode
747
+ * process that changes whether a declaration is marked side-effect free has
748
+ * to be restarted for that to take effect (the annotations do not change
749
+ * behavior, only what a bundler is allowed to drop).
750
+ */
751
+ const resolvedFileCache = new Map();
752
+ const declarationCache = new Map();
753
+
754
+ /**
755
+ * The form named by a declaration's `@lexical-inline` marker, if it has one.
756
+ *
757
+ * A marker naming a form this transform does not implement is an error
758
+ * rather than a marker that silently does nothing: the whole point of the
759
+ * marker is to say what a call may be replaced with, and a typo would leave
760
+ * the factory quietly un-inlined.
761
+ *
762
+ * @param {undefined | ReadonlyArray<any>} comments
763
+ * @param {undefined | string} filename the module the comments came from
764
+ * @returns {null | string}
765
+ */
766
+ function inlineMarkerForm(comments, filename) {
767
+ if (comments) {
768
+ for (const comment of comments) {
769
+ const match = INLINE_MARKER_RE.exec(comment.value);
770
+ if (match) {
771
+ const form = match[1] || '';
772
+ if (!INLINE_FORMS.has(form)) {
773
+ const where = filename ? ` in ${filename}` : '';
774
+ throw new PureAnnotationsError(
775
+ `@lexical/compiler: unknown @lexical-inline form ` +
776
+ `${form ? `"${form}"` : '(none given)'}${where}. Write ` +
777
+ `\`@lexical-inline <form>\` with one of: ` +
778
+ `${Array.from(INLINE_FORMS).sort().join(', ')}.`,
779
+ );
780
+ }
781
+ return form;
782
+ }
783
+ }
784
+ }
785
+ return null;
786
+ }
787
+
788
+ /**
789
+ * @param {undefined | ReadonlyArray<any>} comments
790
+ * @returns {boolean} true when one of the comments is a NO_SIDE_EFFECTS annotation
791
+ */
792
+ function hasNoSideEffectsComment(comments) {
793
+ return (
794
+ comments !== undefined &&
795
+ comments !== null &&
796
+ comments.some(comment => NO_SIDE_EFFECTS_RE.test(comment.value))
797
+ );
798
+ }
799
+
800
+ /**
801
+ * The names this module declares (at module scope) with a NO_SIDE_EFFECTS
802
+ * annotation. Calls to these are safe to annotate wherever they appear,
803
+ * because the definition itself says so. The value is the form from the
804
+ * declaration's `@lexical-inline` marker, when it has one: purity alone is
805
+ * evidence enough to annotate a call, but replacing one requires knowing the
806
+ * shape of what the function returns.
807
+ *
808
+ * @param {any} program
809
+ * @param {undefined | string} filename the module being read, for errors
810
+ * @returns {Map<string, null | string>} declared name to its inline form
811
+ */
812
+ function pureDeclaredNames(program, filename) {
813
+ /** @type {Map<string, null | string>} */
814
+ const names = new Map();
815
+ for (const statement of program.body) {
816
+ const declaration =
817
+ statement.type === 'ExportNamedDeclaration' && statement.declaration
818
+ ? statement.declaration
819
+ : statement;
820
+ const comments = (statement.leadingComments || []).concat(
821
+ declaration === statement ? [] : declaration.leadingComments || [],
822
+ );
823
+ const declared = hasNoSideEffectsComment(comments);
824
+ const inlineForm = inlineMarkerForm(comments, filename);
825
+ if (
826
+ // An overloaded function carries its documentation — and so its
827
+ // annotation — on the first signature, which is a TSDeclareFunction;
828
+ // the implementation that follows is a bare FunctionDeclaration.
829
+ (declaration.type === 'FunctionDeclaration' ||
830
+ declaration.type === 'TSDeclareFunction') &&
831
+ declaration.id
832
+ ) {
833
+ if (declared) {
834
+ names.set(declaration.id.name, inlineForm);
835
+ }
836
+ } else if (declaration.type === 'VariableDeclaration') {
837
+ for (const declarator of declaration.declarations) {
838
+ const declaratorComments = (declarator.leadingComments || []).concat(
839
+ (declarator.init && declarator.init.leadingComments) || [],
840
+ );
841
+ if (
842
+ declarator.id.type === 'Identifier' &&
843
+ (declared || hasNoSideEffectsComment(declaratorComments))
844
+ ) {
845
+ names.set(
846
+ declarator.id.name,
847
+ inlineForm || inlineMarkerForm(declaratorComments, filename),
848
+ );
849
+ }
850
+ }
851
+ }
852
+ }
853
+ return names;
854
+ }
855
+
856
+ /**
857
+ * Resolve a relative import specifier to a file on disk, trying the
858
+ * extensions a bundler would (including the `.js` specifier TypeScript uses
859
+ * for a `.ts` file).
860
+ *
861
+ * @param {string} filename the importing module
862
+ * @param {string} specifier a relative specifier
863
+ * @returns {null | string} the resolved path, or null when there is no such file
864
+ */
865
+ function resolveRelativeSpecifier(filename, specifier) {
866
+ const base = path.resolve(path.dirname(filename), specifier);
867
+ const cached = resolvedFileCache.get(base);
868
+ if (cached !== undefined) {
869
+ return cached;
870
+ }
871
+ /** @type {Array<string>} */
872
+ const candidates = [];
873
+ const extension = path.extname(base);
874
+ if (extension !== '') {
875
+ candidates.push(base);
876
+ if (/^\.[cm]?js$/.test(extension)) {
877
+ const withoutExtension = base.slice(0, -extension.length);
878
+ candidates.push(
879
+ withoutExtension + extension.replace('js', 'ts'),
880
+ withoutExtension + '.tsx',
881
+ );
882
+ }
883
+ }
884
+ for (const candidate of RESOLVE_EXTENSIONS) {
885
+ candidates.push(base + candidate);
886
+ }
887
+ for (const candidate of RESOLVE_EXTENSIONS) {
888
+ candidates.push(path.join(base, `index${candidate}`));
889
+ }
890
+ let resolved = null;
891
+ for (const candidate of candidates) {
892
+ try {
893
+ if (fs.statSync(candidate).isFile()) {
894
+ resolved = candidate;
895
+ break;
896
+ }
897
+ } catch (_err) {
898
+ // Not this candidate; try the next one.
899
+ }
900
+ }
901
+ resolvedFileCache.set(base, resolved);
902
+ return resolved;
903
+ }
904
+
905
+ /**
906
+ * The side-effect-free declarations of another file, parsed and cached.
907
+ *
908
+ * @param {string} filename
909
+ * @param {undefined | ReadonlyArray<any>} parserPlugins
910
+ * @returns {ReadonlyMap<string, null | string>}
911
+ */
912
+ function pureDeclaredNamesOfFile(filename, parserPlugins) {
913
+ return declarationsOfFile(filename, parserPlugins).functions;
914
+ }
915
+
916
+ /**
917
+ * What a module next to this one declares: the functions it says are
918
+ * side-effect free, and the objects it marks as pure namespaces. Parsed
919
+ * once per file for the life of the process — see the cache note above.
920
+ *
921
+ * @param {string} filename
922
+ * @param {undefined | ReadonlyArray<any>} parserPlugins
923
+ * @returns {{functions: ReadonlyMap<string, null | string>, namespaces: ReadonlySet<string>}}
924
+ */
925
+ function declarationsOfFile(filename, parserPlugins) {
926
+ const cached = declarationCache.get(filename);
927
+ if (cached !== undefined) {
928
+ return cached;
929
+ }
930
+ /** @type {null | any} */
931
+ let program = null;
932
+ try {
933
+ program = parse(fs.readFileSync(filename, 'utf8'), {
934
+ plugins: parserPluginsFor(filename, parserPlugins),
935
+ sourceType: 'module',
936
+ }).program;
937
+ } catch (_err) {
938
+ // A file this cannot read or parse simply provides no evidence. A
939
+ // malformed marker inside a file it *can* read is a different matter:
940
+ // reading the declarations happens outside this catch so that its error
941
+ // reaches the build.
942
+ }
943
+ const declarations =
944
+ program === null
945
+ ? {functions: new Map(), namespaces: new Set()}
946
+ : {
947
+ functions: pureDeclaredNames(program, filename),
948
+ namespaces: namespaceDeclaredNames(program),
949
+ };
950
+ declarationCache.set(filename, declarations);
951
+ return declarations;
952
+ }
953
+
954
+ /**
955
+ * The local names in this module that refer to a side-effect-free factory,
956
+ * and may therefore have their module-scope calls annotated.
957
+ *
958
+ * A name qualifies when it is imported from a trusted Lexical package, when
959
+ * it is declared in this module with a NO_SIDE_EFFECTS annotation, or when
960
+ * it is imported from a relative module that declares it that way. A local
961
+ * helper that merely shares a name with one of the factories does not
962
+ * qualify, which is what keeps the transform from claiming that somebody
963
+ * else's `safeCast` or `createState` has no side effects.
964
+ *
965
+ * @param {any} program
966
+ * @param {ReadonlySet<string>} functions
967
+ * @param {TransformPureAnnotationsOptions} opts
968
+ * @returns {Map<string, {inline: undefined | any, name: string}>} local name
969
+ * to the factory it refers to, and the form it may be inlined with
970
+ */
971
+ function collectFactoryNames(program, functions, opts) {
972
+ const sources = toRegExpArray(opts.sources, DEFAULT_SOURCES);
973
+ /** @type {Map<string, {inline: undefined | any, name: string}>} */
974
+ const names = new Map();
975
+ /**
976
+ * A call is only replaced by a literal when the shape of what the factory
977
+ * returns is known. An import from a Lexical package is taken on trust
978
+ * (this package ships with that Lexical); anything else has to say so
979
+ * itself with a `@lexical-inline` marker whose form matches the table.
980
+ *
981
+ * @param {string} local
982
+ * @param {string} name
983
+ * @param {null | string | undefined} markerForm
984
+ * @param {boolean} trusted
985
+ */
986
+ function add(local, name, markerForm, trusted) {
987
+ const spec = INLINE_FACTORIES.get(name);
988
+ const inline =
989
+ spec !== undefined
990
+ ? // One of Lexical's own: an import from a Lexical package is taken
991
+ // on trust, anything else has to agree with the table.
992
+ trusted || markerForm === spec.form
993
+ ? spec
994
+ : undefined
995
+ : // A factory of your own: its marker is the only thing that says
996
+ // what its calls may be replaced with, so it is the authority.
997
+ markerForm
998
+ ? {form: markerForm}
999
+ : undefined;
1000
+ names.set(local, {inline, name});
1001
+ }
1002
+ for (const [name, markerForm] of pureDeclaredNames(program, opts.filename)) {
1003
+ // A declaration in this module carries its own evidence, so it does not
1004
+ // have to be one of the known factory names: marking your own factory
1005
+ // `__NO_SIDE_EFFECTS__` is enough for the build to annotate its calls.
1006
+ // (A relatively imported one still has to be listed in `functions`, so
1007
+ // that a build does not parse every module it imports looking for one.)
1008
+ add(name, name, markerForm, false);
1009
+ }
1010
+ for (const statement of program.body) {
1011
+ if (statement.type !== 'ImportDeclaration') {
1012
+ continue;
1013
+ }
1014
+ const source = statement.source.value;
1015
+ const trusted = matchesAny(sources, source);
1016
+ const relative =
1017
+ !trusted &&
1018
+ opts.relativeImports !== false &&
1019
+ typeof opts.filename === 'string' &&
1020
+ source.startsWith('.');
1021
+ if (!trusted && !relative) {
1022
+ continue;
1023
+ }
1024
+ for (const specifier of statement.specifiers) {
1025
+ if (
1026
+ specifier.type !== 'ImportSpecifier' ||
1027
+ specifier.imported.type !== 'Identifier'
1028
+ ) {
1029
+ continue;
1030
+ }
1031
+ if (trusted) {
1032
+ if (functions.has(specifier.imported.name)) {
1033
+ add(specifier.local.name, specifier.imported.name, null, true);
1034
+ }
1035
+ continue;
1036
+ }
1037
+ const resolved = resolveRelativeSpecifier(
1038
+ /** @type {string} */ (opts.filename),
1039
+ source,
1040
+ );
1041
+ const declared =
1042
+ resolved === null
1043
+ ? undefined
1044
+ : pureDeclaredNamesOfFile(resolved, opts.parserPlugins).get(
1045
+ specifier.imported.name,
1046
+ );
1047
+ if (declared !== undefined) {
1048
+ add(specifier.local.name, specifier.imported.name, declared, false);
1049
+ }
1050
+ }
1051
+ }
1052
+ return names;
1053
+ }
1054
+
1055
+ /**
1056
+ * @param {string} code
1057
+ * @param {ReadonlySet<string>} functions
1058
+ * @param {ReadonlySet<string>} namespaces
1059
+ * @returns {boolean} true when the source mentions any of the names at all
1060
+ */
1061
+ function mayNeedAnnotations(code, functions, namespaces) {
1062
+ if (
1063
+ code.includes('__NO_SIDE_EFFECTS__') ||
1064
+ code.includes('@lexical-pure-namespace') ||
1065
+ RELATIVE_IMPORT_RE.test(code)
1066
+ ) {
1067
+ // Modules that may annotate calls for reasons other than the names in
1068
+ // the list: a declaration of their own, or a relatively imported
1069
+ // namespace, which is only known by reading that module.
1070
+ return true;
1071
+ }
1072
+ for (const name of functions) {
1073
+ if (code.includes(name)) {
1074
+ return true;
1075
+ }
1076
+ }
1077
+ for (const name of namespaces) {
1078
+ if (code.includes(name)) {
1079
+ return true;
1080
+ }
1081
+ }
1082
+ return false;
1083
+ }
1084
+
1085
+ /**
1086
+ * @typedef {Object} TransformPureAnnotationsOptions
1087
+ * @property {string} [filename] Path of the module being transformed, used
1088
+ * to choose the TypeScript and JSX parser plugins.
1089
+ * @property {Iterable<string>} [functions] Names of the factories whose
1090
+ * module-scope calls are annotated. Defaults to PURE_FACTORY_FUNCTIONS.
1091
+ * @property {boolean} [inline] Replace calls to the factories whose result
1092
+ * is a trivial expression over their arguments with that expression,
1093
+ * rather than annotating them. Off by default: it assumes the Lexical
1094
+ * being built matches this package's version.
1095
+ * @property {ReadonlyArray<any>} [parserPlugins] Extra Babel parser
1096
+ * plugins, for syntax the defaults do not cover.
1097
+ * @property {boolean} [relativeImports] Set to false to skip reading
1098
+ * relatively imported modules to look for a NO_SIDE_EFFECTS declaration.
1099
+ * @property {Iterable<string>} [namespaces] Imported objects whose
1100
+ * method calls are side-effect free, defaults to PURE_NAMESPACES.
1101
+ * @property {boolean} [sourceMap] Set to false to skip source map generation.
1102
+ * @property {RegExp | ReadonlyArray<RegExp>} [sources] Module specifiers
1103
+ * whose exports are trusted to be the factories, defaults to `lexical` and
1104
+ * `@lexical/*`.
1105
+ * @property {boolean} [strict] Throw when a call evaluated inside one of the
1106
+ * definitions is not known to be side-effect free, since it pins the
1107
+ * definition into every bundle that imports the module.
1108
+ */
1109
+
1110
+ /**
1111
+ * @typedef {Object} PureAnnotationsResult
1112
+ * @property {number} count Number of annotations that were inserted.
1113
+ * @property {string} code
1114
+ * @property {number} inlined Number of calls that were replaced by the
1115
+ * literal they would have returned.
1116
+ * @property {any} [map]
1117
+ */
1118
+
1119
+ /**
1120
+ * The names this module declares with a `@lexical-pure-namespace` marker:
1121
+ * objects whose methods are side-effect free.
1122
+ *
1123
+ * @param {any} program
1124
+ * @returns {Set<string>}
1125
+ */
1126
+ function namespaceDeclaredNames(program) {
1127
+ /** @type {Set<string>} */
1128
+ const names = new Set();
1129
+ /** @param {undefined | ReadonlyArray<any>} comments */
1130
+ const marked = comments =>
1131
+ (comments || []).some(comment => NAMESPACE_MARKER_RE.test(comment.value));
1132
+ for (const statement of program.body) {
1133
+ const declaration =
1134
+ statement.type === 'ExportNamedDeclaration' && statement.declaration
1135
+ ? statement.declaration
1136
+ : statement;
1137
+ const outer = marked(statement.leadingComments);
1138
+ if (declaration.type !== 'VariableDeclaration') {
1139
+ continue;
1140
+ }
1141
+ for (const declarator of declaration.declarations) {
1142
+ if (
1143
+ declarator.id.type === 'Identifier' &&
1144
+ (outer ||
1145
+ marked(declaration.leadingComments) ||
1146
+ marked(declarator.leadingComments) ||
1147
+ (declarator.init && marked(declarator.init.leadingComments)))
1148
+ ) {
1149
+ names.add(declarator.id.name);
1150
+ }
1151
+ }
1152
+ }
1153
+ return names;
1154
+ }
1155
+
1156
+ /**
1157
+ * The `@lexical-pure-namespace` names a relatively imported module declares.
1158
+ *
1159
+ * @param {string} filename
1160
+ * @param {undefined | ReadonlyArray<any>} parserPlugins
1161
+ * @returns {ReadonlySet<string>}
1162
+ */
1163
+ function namespaceNamesOfFile(filename, parserPlugins) {
1164
+ return declarationsOfFile(filename, parserPlugins).namespaces;
1165
+ }
1166
+
1167
+ /**
1168
+ * The local names in this module bound to one of the side-effect-free
1169
+ * namespaces, imported from a trusted Lexical package. A relative import is
1170
+ * not enough here: unlike a factory function, whose declaration can say
1171
+ * `__NO_SIDE_EFFECTS__` for itself, an object's methods are only known to
1172
+ * be pure by the table above.
1173
+ *
1174
+ * @param {any} program
1175
+ * @param {ReadonlySet<string>} namespaces
1176
+ * @param {TransformPureAnnotationsOptions} opts
1177
+ * @returns {Set<string>} local names
1178
+ */
1179
+ function collectNamespaceNames(program, namespaces, opts) {
1180
+ /** @type {Set<string>} */
1181
+ const names = new Set();
1182
+ const sources = toRegExpArray(opts.sources, DEFAULT_SOURCES);
1183
+ for (const name of namespaceDeclaredNames(program)) {
1184
+ names.add(name);
1185
+ }
1186
+ for (const statement of program.body) {
1187
+ if (statement.type !== 'ImportDeclaration') {
1188
+ continue;
1189
+ }
1190
+ const source = statement.source.value;
1191
+ const trusted = matchesAny(sources, source);
1192
+ const relative =
1193
+ !trusted &&
1194
+ opts.relativeImports !== false &&
1195
+ typeof opts.filename === 'string' &&
1196
+ source.startsWith('.');
1197
+ if (!trusted && !relative) {
1198
+ continue;
1199
+ }
1200
+ /** @type {undefined | ReadonlySet<string>} */
1201
+ let declared;
1202
+ for (const specifier of statement.specifiers) {
1203
+ if (
1204
+ specifier.type !== 'ImportSpecifier' ||
1205
+ specifier.imported.type !== 'Identifier'
1206
+ ) {
1207
+ continue;
1208
+ }
1209
+ if (trusted) {
1210
+ if (namespaces.has(specifier.imported.name)) {
1211
+ names.add(specifier.local.name);
1212
+ }
1213
+ continue;
1214
+ }
1215
+ if (declared === undefined) {
1216
+ const resolved = resolveRelativeSpecifier(
1217
+ /** @type {string} */ (opts.filename),
1218
+ source,
1219
+ );
1220
+ declared =
1221
+ resolved === null
1222
+ ? new Set()
1223
+ : namespaceNamesOfFile(resolved, opts.parserPlugins);
1224
+ }
1225
+ if (declared.has(specifier.imported.name)) {
1226
+ names.add(specifier.local.name);
1227
+ }
1228
+ }
1229
+ }
1230
+ // `const sel = selBase;` — an alias is the same object. One pass is
1231
+ // enough for the aliases these modules actually write.
1232
+ for (const statement of program.body) {
1233
+ if (statement.type !== 'VariableDeclaration') {
1234
+ continue;
1235
+ }
1236
+ for (const declarator of statement.declarations) {
1237
+ if (
1238
+ declarator.id.type === 'Identifier' &&
1239
+ declarator.init &&
1240
+ declarator.init.type === 'Identifier' &&
1241
+ names.has(declarator.init.name)
1242
+ ) {
1243
+ names.add(declarator.id.name);
1244
+ }
1245
+ }
1246
+ }
1247
+ return names;
1248
+ }
1249
+
1250
+ /**
1251
+ * Every call evaluated inside `node`'s arguments, in the order they appear.
1252
+ * Calls in a deferred position (a callback, a method body) are not: they do
1253
+ * not run when the module is initialized, so they pin nothing.
1254
+ *
1255
+ * @param {any} node a call expression
1256
+ * @param {(call: any) => void} visit
1257
+ */
1258
+ function visitEvaluatedCalls(node, visit) {
1259
+ /** @param {any} value */
1260
+ const walk = value => {
1261
+ if (Array.isArray(value)) {
1262
+ value.forEach(walk);
1263
+ return;
1264
+ }
1265
+ if (
1266
+ !value ||
1267
+ typeof value !== 'object' ||
1268
+ typeof value.type !== 'string' ||
1269
+ DEFERRED_TYPES.has(value.type)
1270
+ ) {
1271
+ return;
1272
+ }
1273
+ if (
1274
+ value.type === 'CallExpression' ||
1275
+ value.type === 'NewExpression' ||
1276
+ value.type === 'TaggedTemplateExpression'
1277
+ ) {
1278
+ visit(value);
1279
+ }
1280
+ for (const key of Object.keys(value)) {
1281
+ if (!SKIPPED_KEYS.has(key)) {
1282
+ walk(value[key]);
1283
+ }
1284
+ }
1285
+ };
1286
+ node.arguments.forEach(walk);
1287
+ }
1288
+
1289
+ /**
1290
+ * A call in the arguments of an extension, command, or rule definition is
1291
+ * evaluated when the module is initialized, and an unannotated one pins the
1292
+ * whole definition into every bundle that imports the module — the
1293
+ * definition is only droppable when everything it is built from is too.
1294
+ *
1295
+ * With `strict`, that is an error naming the call rather than a definition
1296
+ * that quietly stops tree-shaking.
1297
+ *
1298
+ * @param {string} code
1299
+ * @param {undefined | string} filename
1300
+ * @param {ReadonlyArray<any>} factoryCalls
1301
+ * @param {ReadonlySet<number>} covered offsets this transform annotates or
1302
+ * replaces
1303
+ */
1304
+ function assertArgumentsArePure(code, filename, factoryCalls, covered) {
1305
+ /** @type {Array<string>} */
1306
+ const violations = [];
1307
+ /** @type {Set<number>} */
1308
+ const reported = new Set();
1309
+ for (const call of factoryCalls) {
1310
+ visitEvaluatedCalls(call, inner => {
1311
+ const callee = inner.callee || inner.tag;
1312
+ if (
1313
+ covered.has(inner.start) ||
1314
+ hasPureAnnotation(code, inner.start) ||
1315
+ reported.has(inner.start) ||
1316
+ (callee.type === 'Identifier' && PURE_GLOBALS.has(callee.name))
1317
+ ) {
1318
+ return;
1319
+ }
1320
+ reported.add(inner.start);
1321
+ const text = code.slice(callee.start, callee.end).replace(/\s+/g, ' ');
1322
+ const where = inner.loc
1323
+ ? `${inner.loc.start.line}:${inner.loc.start.column + 1}`
1324
+ : '?';
1325
+ violations.push(
1326
+ ` ${where} ${text}(...) inside ${code.slice(
1327
+ call.callee.start,
1328
+ call.callee.end,
1329
+ )}(...)`,
1330
+ );
1331
+ });
1332
+ }
1333
+ if (violations.length > 0) {
1334
+ throw new PureAnnotationsError(
1335
+ `@lexical/compiler: ${violations.length} call(s) evaluated ` +
1336
+ `inside a definition in ${filename || '<unknown>'} are not known to ` +
1337
+ `be side-effect free, so the definition cannot be tree-shaken:\n` +
1338
+ `${violations.join('\n')}\n` +
1339
+ `Make the call lazy, or declare the function side-effect free ` +
1340
+ `(@__NO_SIDE_EFFECTS__ plus the transform's \`functions\`/` +
1341
+ `\`namespaces\` options) so that its calls are annotated too.`,
1342
+ );
1343
+ }
1344
+ }
1345
+
1346
+ /**
1347
+ * The local names in `code` whose module-scope calls this transform would
1348
+ * annotate — the same decision {@link transformPureAnnotations} makes, for
1349
+ * tooling that needs to ask about a module without rewriting it (Lexical's
1350
+ * `no-pure-annotation` ESLint rule removes a hand-written annotation only
1351
+ * for a name this returns, so that the build is guaranteed to put it back).
1352
+ *
1353
+ * `functions` are called directly, `namespaces` are the objects whose method
1354
+ * calls (`sel.tag('p').attr('x')`) are annotated.
1355
+ *
1356
+ * @param {string} code
1357
+ * @param {TransformPureAnnotationsOptions} [options]
1358
+ * @returns {{functions: Set<string>, namespaces: Set<string>}}
1359
+ */
1360
+ function pureCallNames$1(code, options) {
1361
+ const opts = options || {};
1362
+ const functions = new Set(opts.functions || PURE_FACTORY_FUNCTIONS$1);
1363
+ const ast = parse(code, {
1364
+ errorRecovery: false,
1365
+ plugins: parserPluginsFor(opts.filename, opts.parserPlugins),
1366
+ sourceFilename: opts.filename,
1367
+ sourceType: 'module',
1368
+ });
1369
+ return {
1370
+ functions: new Set(
1371
+ collectFactoryNames(ast.program, functions, opts).keys(),
1372
+ ),
1373
+ namespaces: collectNamespaceNames(
1374
+ ast.program,
1375
+ new Set(opts.namespaces || PURE_NAMESPACES$1),
1376
+ opts,
1377
+ ),
1378
+ };
1379
+ }
1380
+
1381
+ /**
1382
+ * Insert a `__PURE__` annotation before every module-scope call to one of the
1383
+ * side-effect-free Lexical factories that does not already have one.
1384
+ *
1385
+ * Returns `null` when the module needs no annotations, so a caller can pass
1386
+ * the original source through untouched.
1387
+ *
1388
+ * @param {string} code
1389
+ * @param {TransformPureAnnotationsOptions} [options]
1390
+ * @returns {null | PureAnnotationsResult}
1391
+ */
1392
+ function transformPureAnnotations$1(code, options) {
1393
+ const opts = options || {};
1394
+ const functions = new Set(opts.functions || PURE_FACTORY_FUNCTIONS$1);
1395
+ const namespaceSet = new Set(opts.namespaces || PURE_NAMESPACES$1);
1396
+ if (!mayNeedAnnotations(code, functions, namespaceSet)) {
1397
+ return null;
1398
+ }
1399
+ const ast = parse(code, {
1400
+ errorRecovery: false,
1401
+ plugins: parserPluginsFor(opts.filename, opts.parserPlugins),
1402
+ sourceFilename: opts.filename,
1403
+ sourceType: 'module',
1404
+ });
1405
+ // Only the calls whose callee resolves to a factory this module imported
1406
+ // from Lexical (or to a declaration that says it is side-effect free) are
1407
+ // annotated; a same-named local helper is left alone.
1408
+ const factoryNames = collectFactoryNames(ast.program, functions, opts);
1409
+ const namespaceNames = collectNamespaceNames(ast.program, namespaceSet, opts);
1410
+ if (factoryNames.size === 0 && namespaceNames.size === 0) {
1411
+ return null;
1412
+ }
1413
+ /** @type {Set<number>} */
1414
+ const annotated = new Set();
1415
+ /** @type {Array<[any, {form: string}, boolean]>} */
1416
+ const inlined = [];
1417
+ /** @type {Array<any>} */
1418
+ const factoryCalls = [];
1419
+ visitModuleScopeCalls(
1420
+ ast.program,
1421
+ {functions: factoryNames, namespaces: namespaceNames},
1422
+ (node, position) => {
1423
+ if (position.namespace === true) {
1424
+ // Every call in a chain starts at the same offset, and one
1425
+ // annotation there covers the whole chain.
1426
+ if (!hasPureAnnotation(code, node.start)) {
1427
+ annotated.add(node.start);
1428
+ }
1429
+ return;
1430
+ }
1431
+ factoryCalls.push(node);
1432
+ const factory = /** @type {{inline: undefined | any, name: string}} */ (
1433
+ factoryNames.get(node.callee.name)
1434
+ );
1435
+ const spec = opts.inline === true ? factory.inline : undefined;
1436
+ if (
1437
+ spec !== undefined &&
1438
+ !position.discarded &&
1439
+ canInlineCall(node, spec)
1440
+ ) {
1441
+ inlined.push([node, spec, position.statementStart]);
1442
+ } else if (!hasPureAnnotation(code, node.start)) {
1443
+ annotated.add(node.start);
1444
+ }
1445
+ },
1446
+ );
1447
+ const offsets = Array.from(annotated);
1448
+ if (opts.strict === true) {
1449
+ const covered = new Set(annotated);
1450
+ for (const [node] of inlined) {
1451
+ covered.add(node.start);
1452
+ }
1453
+ assertArgumentsArePure(code, opts.filename, factoryCalls, covered);
1454
+ }
1455
+ if (offsets.length === 0 && inlined.length === 0) {
1456
+ return null;
1457
+ }
1458
+ const magicString = new MagicString(code);
1459
+ for (const offset of offsets) {
1460
+ magicString.appendLeft(offset, ANNOTATION);
1461
+ }
1462
+ for (const [node, spec, statementStart] of inlined) {
1463
+ inlineCall(magicString, code, node, spec, statementStart);
1464
+ }
1465
+ removeUnusedImports(
1466
+ magicString,
1467
+ code,
1468
+ ast.program,
1469
+ inlined.map(([node]) => node),
1470
+ );
1471
+ return {
1472
+ code: magicString.toString(),
1473
+ count: offsets.length,
1474
+ inlined: inlined.length,
1475
+ map:
1476
+ opts.sourceMap === false
1477
+ ? undefined
1478
+ : magicString.generateMap({hires: 'boundary', source: opts.filename}),
1479
+ };
1480
+ }
1481
+
1482
+ /**
1483
+ * @param {undefined | RegExp | ReadonlyArray<RegExp>} value
1484
+ * @param {ReadonlyArray<RegExp>} defaultValue
1485
+ * @returns {ReadonlyArray<RegExp>}
1486
+ */
1487
+ function toRegExpArray(value, defaultValue) {
1488
+ if (value === undefined) {
1489
+ return defaultValue;
1490
+ }
1491
+ return Array.isArray(value) ? value : [/** @type {RegExp} */ (value)];
1492
+ }
1493
+
1494
+ /**
1495
+ * @param {ReadonlyArray<RegExp>} patterns
1496
+ * @param {string} id
1497
+ * @returns {boolean}
1498
+ */
1499
+ function matchesAny(patterns, id) {
1500
+ return patterns.some(pattern => pattern.test(id));
1501
+ }
1502
+
1503
+ /**
1504
+ * Strip the query string and hash a bundler may append to a module id
1505
+ * (e.g. `Foo.tsx?vue&type=script`).
1506
+ *
1507
+ * @param {string} id
1508
+ * @returns {string}
1509
+ */
1510
+ function cleanModuleId(id) {
1511
+ return id.replace(/[?#][\s\S]*$/, '');
1512
+ }
1513
+
1514
+ /**
1515
+ * @typedef {Object} PureAnnotationsOptions
1516
+ * @property {'post' | 'pre'} [enforce] Vite plugin ordering, `'post'` by
1517
+ * default so the transform runs after TypeScript/JSX has been compiled away.
1518
+ * @property {RegExp | ReadonlyArray<RegExp>} [exclude] Module ids to skip.
1519
+ * @property {Iterable<string>} [functions] Names of the factories whose
1520
+ * module-scope calls are annotated. Defaults to PURE_FACTORY_FUNCTIONS.
1521
+ * @property {RegExp | ReadonlyArray<RegExp>} [include] Module ids to
1522
+ * transform, defaults to every .js/.jsx/.ts/.tsx (and .mjs/.cjs/.mts/.cts)
1523
+ * module.
1524
+ * @property {boolean} [inline] Replace calls to the factories whose result
1525
+ * is a trivial expression over their arguments with that expression,
1526
+ * rather than annotating them. Off by default: it assumes the Lexical
1527
+ * being built matches this package's version.
1528
+ * @property {ReadonlyArray<any>} [parserPlugins] Extra Babel parser
1529
+ * plugins, for syntax the defaults do not cover.
1530
+ * @property {boolean} [relativeImports] Set to false to skip reading
1531
+ * relatively imported modules to look for a NO_SIDE_EFFECTS declaration.
1532
+ * @property {Iterable<string>} [namespaces] Imported objects whose
1533
+ * method calls are side-effect free, defaults to PURE_NAMESPACES.
1534
+ * @property {boolean} [sourceMap] Set to false to skip source map generation.
1535
+ * @property {RegExp | ReadonlyArray<RegExp>} [sources] Module specifiers
1536
+ * whose exports are trusted to be the factories, defaults to `lexical` and
1537
+ * `@lexical/*`.
1538
+ * @property {boolean} [strict] Throw when a call evaluated inside one of the
1539
+ * definitions is not known to be side-effect free, since it pins the
1540
+ * definition into every bundle that imports the module.
1541
+ */
1542
+
1543
+ /**
1544
+ * A Vite/Rollup plugin that inserts the `__PURE__` annotations that let
1545
+ * bundlers tree-shake unused Lexical extension, command, and rule
1546
+ * definitions. Use it when building Lexical (or your own extensions) from
1547
+ * TypeScript source rather than from the published `dist` bundles, which
1548
+ * already carry the annotations.
1549
+ *
1550
+ * @param {PureAnnotationsOptions} [options]
1551
+ * @returns {any} the plugin object
1552
+ */
1553
+ function pureAnnotations$1(options) {
1554
+ const opts = options || {};
1555
+ const functions = new Set(opts.functions || PURE_FACTORY_FUNCTIONS$1);
1556
+ const include = toRegExpArray(opts.include, [DEFAULT_INCLUDE]);
1557
+ const exclude = toRegExpArray(opts.exclude, []);
1558
+ return {
1559
+ enforce: opts.enforce === undefined ? 'post' : opts.enforce,
1560
+ name: '@lexical/compiler',
1561
+ /**
1562
+ * @param {string} code
1563
+ * @param {string} id
1564
+ * @returns {null | {code: string, map: any}}
1565
+ */
1566
+ transform(code, id) {
1567
+ const filename = cleanModuleId(id);
1568
+ if (!matchesAny(include, filename) || matchesAny(exclude, filename)) {
1569
+ return null;
1570
+ }
1571
+ const dependency = NODE_MODULES_RE.test(filename);
1572
+ let result = null;
1573
+ try {
1574
+ result = transformPureAnnotations$1(code, {
1575
+ filename,
1576
+ functions,
1577
+ inline: opts.inline,
1578
+ namespaces: opts.namespaces,
1579
+ parserPlugins: opts.parserPlugins,
1580
+ relativeImports: opts.relativeImports,
1581
+ sourceMap: opts.sourceMap,
1582
+ sources: opts.sources,
1583
+ strict: opts.strict === true && !dependency,
1584
+ });
1585
+ } catch (err) {
1586
+ if (err instanceof PureAnnotationsError && !dependency) {
1587
+ // Not something to shrug off: the module says one thing and means
1588
+ // another, or a definition cannot be tree-shaken.
1589
+ throw err;
1590
+ }
1591
+ // A module this plugin cannot parse is a module it has nothing to
1592
+ // say about; warn rather than failing the consumer's build.
1593
+ if (this && typeof this.warn === 'function') {
1594
+ this.warn(
1595
+ `@lexical/compiler could not parse ${id}, no annotations were added: ${
1596
+ err instanceof Error ? err.message : String(err)
1597
+ }`,
1598
+ );
1599
+ }
1600
+ return null;
1601
+ }
1602
+ return result === null
1603
+ ? null
1604
+ : {
1605
+ code: result.code,
1606
+ map: result.map === undefined ? null : result.map,
1607
+ };
1608
+ },
1609
+ };
1610
+ }
1611
+
1612
+ /**
1613
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1614
+ *
1615
+ * This source code is licensed under the MIT license found in the
1616
+ * LICENSE file in the root directory of this source tree.
1617
+ *
1618
+ */
1619
+
1620
+
1621
+ // The comments here spell the annotations without their leading sigil
1622
+ // (`__PURE__`) because bundlers scan comments for annotations, and one that
1623
+ // does not precede a call expression is reported as misplaced (rollup's
1624
+ // INVALID_ANNOTATION).
1625
+
1626
+ /**
1627
+ * The subset of a source map that bundlers consume, matching what
1628
+ * `magic-string` produces. Declared structurally so this package needs no
1629
+ * dependency on any particular bundler's types.
1630
+ */
1631
+
1632
+ /**
1633
+ * The structural shape of the returned plugin. It is assignable to Vite's
1634
+ * `Plugin` and to Rollup's `Plugin`, so it can be dropped into the `plugins`
1635
+ * array of either.
1636
+ */
1637
+
1638
+ /**
1639
+ * The Lexical factories that are annotated with `__NO_SIDE_EFFECTS__` at
1640
+ * their definition and whose module-scope call sites therefore need a
1641
+ * `__PURE__` annotation for bundlers to tree-shake unused definitions.
1642
+ */
1643
+ const PURE_FACTORY_FUNCTIONS = PURE_FACTORY_FUNCTIONS$1;
1644
+
1645
+ /**
1646
+ * Objects imported from a Lexical package whose method calls are
1647
+ * side-effect free, so that a module-scope `sel.tag('p')` is annotated the
1648
+ * way a factory call is. Only the outermost call of a chain is annotated:
1649
+ * rollup, terser and esbuild all drop the whole chain from that one.
1650
+ */
1651
+ const PURE_NAMESPACES = PURE_NAMESPACES$1;
1652
+
1653
+ /**
1654
+ * The factories that `inline` replaces with a literal, and the form each one
1655
+ * takes: `identity` returns its single argument and `args` returns all of its
1656
+ * arguments as an array. Each is marked `@lexical-inline <form>` where it is
1657
+ * defined.
1658
+ */
1659
+ const INLINE_FACTORY_FORMS = INLINE_FACTORY_FORMS$1;
1660
+
1661
+ /**
1662
+ * Insert a `__PURE__` annotation before every module-scope call to one of the
1663
+ * side-effect-free Lexical factories that does not already have one. Returns
1664
+ * `null` when the module needs no annotations.
1665
+ */
1666
+ function transformPureAnnotations(code, options) {
1667
+ return transformPureAnnotations$1(code, options);
1668
+ }
1669
+
1670
+ /**
1671
+ * The local names in `code` whose module-scope calls the transform would
1672
+ * annotate: `functions` are called directly, `namespaces` are objects whose
1673
+ * method calls are annotated. A name qualifies by being imported from a
1674
+ * Lexical package, declared in the module as side-effect free, or imported
1675
+ * from a relative module that declares it that way. For tooling that needs
1676
+ * the same decision without rewriting the module — a lint rule that removes
1677
+ * a hand-written annotation only where the build puts one back. Throws on
1678
+ * source it cannot parse.
1679
+ */
1680
+ function pureCallNames(code, options) {
1681
+ return pureCallNames$1(code, options);
1682
+ }
1683
+
1684
+ /**
1685
+ * A Vite/Rollup plugin that inserts the `__PURE__` annotations that let
1686
+ * bundlers tree-shake unused Lexical extension, command, and rule
1687
+ * definitions.
1688
+ *
1689
+ * The published `dist` bundles already carry the annotations, so this is for
1690
+ * builds that compile Lexical from its TypeScript source (the `source`
1691
+ * export condition, a git checkout, or a vendored copy) — and for annotating
1692
+ * the extensions and commands your own application defines.
1693
+ *
1694
+ * ```js
1695
+ * import {pureAnnotations} from '@lexical/compiler';
1696
+ *
1697
+ * export default defineConfig({plugins: [pureAnnotations()]});
1698
+ * ```
1699
+ */
1700
+ function pureAnnotations(options) {
1701
+ return pureAnnotations$1(options);
1702
+ }
1703
+
1704
+ export { INLINE_FACTORY_FORMS, PURE_FACTORY_FUNCTIONS, PURE_NAMESPACES, pureAnnotations, pureCallNames, transformPureAnnotations };