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