@readme/markdown 14.10.0 → 14.10.2

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,49 @@
1
+ /**
2
+ * Structural HTML diff for MDX-vs-MDXish render comparison.
3
+ *
4
+ * Algorithm overview:
5
+ * parseDocument(html, { xmlMode: true }) → walk → canonical {tag, attrs, children} tree
6
+ * apply canonicalization pipeline (sort/normalize/collapse)
7
+ * compute bottom-up content hash
8
+ * fast-path match if hashes equal
9
+ * otherwise, recursive walk emitting Change records
10
+ *
11
+ * Tree-walk targets the htmlparser2 / domhandler node shape. The canonicalization
12
+ * rules and severity model are transplanted verbatim from the reference differ.ts
13
+ * (PR #18479 in the readme repo). The tree-walk is re-targeted to htmlparser2/domhandler.
14
+ *
15
+ * The lint disables below are intentional and scoped to this file:
16
+ * - `no-restricted-syntax` + `no-continue`: this is a tree-walker with greedy
17
+ * alignment and lookahead; for-of with continue is the natural shape and
18
+ * array iteration helpers would obscure the control flow.
19
+ * - `@typescript-eslint/no-use-before-define`: helpers are ordered bottom-up
20
+ * (primitives first, public `diff()` last) so the public API anchors the
21
+ * end of the file.
22
+ */
23
+ import type { DiffOptions, DiffResult } from './types';
24
+ /**
25
+ * Diff two rendered HTML strings.
26
+ *
27
+ * The diff tool is engine-agnostic — `leftHtml` and `rightHtml` are simply
28
+ * the two HTML strings to compare in that order. Common use cases:
29
+ * - MDX vs MDXish (Suite B in this repo)
30
+ * - Before vs after a `@readme/markdown` version bump on the same source
31
+ * - Any two HTML strings a consumer wants to compare
32
+ *
33
+ * Both strings are canonicalized (whitespace collapse, class sort, attribute
34
+ * normalization, noise-attr drop, heading-counter strip, void-tag handling,
35
+ * text-equivalent merging) before comparison.
36
+ *
37
+ * A bottom-up content hash provides a fast-path: if both trees hash identically,
38
+ * `{ status: 'match' }` is returned without a full walk.
39
+ *
40
+ * The returned `changes[]` is ordered by document position and is identical
41
+ * across repeated calls on the same input. Each change carries `left` and
42
+ * `right` fields corresponding to the two inputs.
43
+ *
44
+ * @param leftHtml - First HTML string to compare.
45
+ * @param rightHtml - Second HTML string to compare.
46
+ * @param opts - Optional canonicalization / diff configuration.
47
+ * @returns `{ status: 'match' }` or `{ status: 'differ', severity, changes }`.
48
+ */
49
+ export declare function diff(leftHtml: string, rightHtml: string, opts?: DiffOptions): DiffResult;
@@ -0,0 +1,2 @@
1
+ export { diff } from './differ';
2
+ export type { Change, DiffOptions, DiffResult, Preset, Severity } from './types';
@@ -0,0 +1,119 @@
1
+ /**
2
+ * TypeScript types for the render-diff HTML diffing tool.
3
+ *
4
+ * These types form the public contract for `lib/render-diff/differ.ts`.
5
+ */
6
+ /**
7
+ * Severity level of a single diff change or aggregate page diff result.
8
+ *
9
+ * - `cosmetic` — minor presentation differences (e.g. `data-*` / `aria-*` attrs)
10
+ * - `structural` — tag, element, or layout differences
11
+ * - `content` — visible text or meaningful attribute value differences
12
+ */
13
+ export type Severity = 'content' | 'cosmetic' | 'structural';
14
+ /**
15
+ * A single diffed change between two rendered HTML inputs.
16
+ *
17
+ * The diff tool is engine-agnostic — `left` and `right` are simply the two
18
+ * HTML strings passed to `diff()` in that order. Suite B uses them as
19
+ * MDX vs MDXish; other consumers (e.g. before/after a markdown version bump)
20
+ * use them however they like.
21
+ *
22
+ * Each change is emitted at a specific document-position path.
23
+ */
24
+ export interface Change {
25
+ /**
26
+ * Attribute name — populated only when `kind` is `'attr'`.
27
+ */
28
+ attrName?: string;
29
+ /**
30
+ * The type of difference detected.
31
+ *
32
+ * - `tag` — element tag mismatch between `left` and `right`
33
+ * - `attr` — attribute present, missing, or different value
34
+ * - `text` — text node content differs
35
+ * - `missing` — node present in `left` but absent in `right`
36
+ * - `extra` — node present in `right` but absent in `left`
37
+ */
38
+ kind: 'attr' | 'extra' | 'missing' | 'tag' | 'text';
39
+ /**
40
+ * String representation from the `left` input, or `null` when the node is absent.
41
+ */
42
+ left: string | null;
43
+ /**
44
+ * Document-position path of the diffed node (top-to-bottom tree-walk order).
45
+ */
46
+ path: string;
47
+ /**
48
+ * String representation from the `right` input, or `null` when the node is absent.
49
+ */
50
+ right: string | null;
51
+ /**
52
+ * Severity of this individual change.
53
+ */
54
+ severity: Severity;
55
+ }
56
+ /**
57
+ * Result returned by `diff(leftHtml, rightHtml)`.
58
+ *
59
+ * Discriminated union on `status`:
60
+ * - `'match'` arm collapses to `{ status: 'match' }` — no `severity` or `changes` keys.
61
+ * - `'differ'` arm carries the full `{ status, severity, changes }` shape.
62
+ *
63
+ * TypeScript narrowing on `result.status === 'differ'` exposes `severity` and `changes`.
64
+ */
65
+ export type DiffResult = {
66
+ changes: Change[];
67
+ severity: Severity;
68
+ status: 'differ';
69
+ } | {
70
+ status: 'match';
71
+ };
72
+ /**
73
+ * Canonicalization preset for `diff()`.
74
+ *
75
+ * - `'cross-engine'` — full normalization including span-flatten and adjacent-text
76
+ * merge. Designed for MDX↔MDXish comparison (Suite B).
77
+ * - `'minimal'` — lighter normalization; span-flatten and adjacent-text merge are
78
+ * skipped. Suitable for same-engine before/after comparison where those transforms
79
+ * would mask real structural changes.
80
+ */
81
+ export type Preset = 'cross-engine' | 'minimal';
82
+ /**
83
+ * Options accepted by `diff()`.
84
+ *
85
+ * All fields are optional. Default behaviour: hash fast-path enabled, all
86
+ * heading counter suffixes stripped, no additional attributes ignored.
87
+ */
88
+ export interface DiffOptions {
89
+ /**
90
+ * Additional attribute names to drop during canonicalization (lowercased).
91
+ * Added to the built-in noise-attr set (`data-reactroot`, `data-testid`,
92
+ * `suppresshydrationwarning`).
93
+ */
94
+ attrIgnore?: Set<string>;
95
+ /**
96
+ * When `true`, bypass the content-hash fast-path and always perform the full
97
+ * tree walk. Intended for testing only.
98
+ */
99
+ noHashFastPath?: boolean;
100
+ /**
101
+ * When `true`, skip stripping `-N` counter suffixes from heading `id` attrs.
102
+ * Default: `false` (suffixes are stripped so repeated headings compare equal).
103
+ */
104
+ preserveHeadingCounters?: boolean;
105
+ /**
106
+ * Canonicalization preset. Controls which structural-normalization transforms
107
+ * are applied before comparison.
108
+ *
109
+ * - `'cross-engine'` (default) — full normalization including span-flatten
110
+ * and adjacent-text merge. Designed for MDX↔MDXish comparison (Suite B).
111
+ * - `'minimal'` — lighter normalization; span-flatten and adjacent-text merge
112
+ * are skipped. Suitable for same-engine before/after comparison where those
113
+ * transforms would mask real structural changes.
114
+ *
115
+ * Every normalization `'minimal'` performs is also performed by `'cross-engine'`.
116
+ * Default: omitting `preset` is equivalent to `'cross-engine'` (backward compatible).
117
+ */
118
+ preset?: Preset;
119
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Public barrel for `@readme/markdown/render-fixture`.
3
+ *
4
+ * Pairs with the engine-free `@readme/markdown/render-diff` subpath: this
5
+ * bundle DOES include the engine (it has to — its job is to render). A
6
+ * typical consumer that wants regression coverage across @readme/markdown
7
+ * version bumps pairs the two:
8
+ *
9
+ * import { renderFixture } from '@readme/markdown/render-fixture';
10
+ * import { diff } from '@readme/markdown/render-diff';
11
+ *
12
+ * const oldHtml = readFileSync('snapshot.html', 'utf8');
13
+ * const { html: newHtml } = renderFixture(body, ctx, 'mdxish');
14
+ * const result = diff(oldHtml, newHtml, { preset: 'minimal' });
15
+ */
16
+ export { loadFixture } from './loadFixture';
17
+ export type { RenderContext } from './loadFixture';
18
+ export { renderFixture } from './renderFixture';
19
+ export type { Engine, FixtureRenderResult } from './renderFixture';
@@ -0,0 +1,21 @@
1
+ export interface RenderContext {
2
+ components: {
3
+ source: string;
4
+ tag: string;
5
+ }[];
6
+ glossary: {
7
+ definition: string;
8
+ term: string;
9
+ }[];
10
+ variables: {
11
+ defaults: {
12
+ default: string;
13
+ name: string;
14
+ }[];
15
+ user: Record<string, string>;
16
+ };
17
+ }
18
+ export declare function loadFixture(dir: string): {
19
+ body: string;
20
+ ctx: RenderContext;
21
+ };
@@ -0,0 +1,17 @@
1
+ import type { RenderContext } from './loadFixture';
2
+ export type Engine = 'mdx' | 'mdxish';
3
+ export interface FixtureRenderResult {
4
+ error: string | null;
5
+ html: string;
6
+ }
7
+ /**
8
+ * Renders a fixture body string through the specified engine and returns the
9
+ * serialized HTML. Renders are deterministic — Date.now and Math.random are
10
+ * frozen for the duration of each render call.
11
+ *
12
+ * For MDXish, the components map is passed to BOTH mdxish() and renderMdxish()
13
+ * — both call sites need it for custom-block resolution.
14
+ *
15
+ * The loader's `ctx.glossary` is mapped onto the engine's `terms` parameter.
16
+ */
17
+ export declare function renderFixture(body: string, ctx: RenderContext, engine: Engine): FixtureRenderResult;
package/dist/main.js CHANGED
@@ -76783,6 +76783,7 @@ const readmeComponents = (opts) => () => tree => {
76783
76783
 
76784
76784
 
76785
76785
 
76786
+
76786
76787
  const imageAttrs = ['align', 'alt', 'caption', 'border', 'height', 'src', 'title', 'width', 'lazy', 'className'];
76787
76788
  const readmeToMdx = () => tree => {
76788
76789
  // Unwrap pinned nodes, replace rdme-pin with its child node
@@ -76873,7 +76874,11 @@ const readmeToMdx = () => tree => {
76873
76874
  }
76874
76875
  });
76875
76876
  visit(tree, NodeTypes.imageBlock, (image, index, parent) => {
76876
- const attributes = toAttributes({ ...image, ...image.data.hProperties }, imageAttrs);
76877
+ // Special case for caption: Captions are parsed as children of the image node, so we need to extract them explicitly
76878
+ const captionChildren = Array.isArray(image.children) ? image.children : [];
76879
+ // We want to add it back to the attributes, so re-serialize it to string
76880
+ const caption = captionChildren.length ? toMarkdown({ type: 'root', children: captionChildren }).trim() : '';
76881
+ const attributes = toAttributes({ ...image, ...image.data.hProperties, ...(caption && { caption }) }, imageAttrs);
76877
76882
  if (hasExtra(attributes)) {
76878
76883
  parent.children.splice(index, 1, {
76879
76884
  type: 'mdxJsxFlowElement',
@@ -98611,6 +98616,11 @@ function smartCamelCase(str) {
98611
98616
  if (str.includes('-')) {
98612
98617
  return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
98613
98618
  }
98619
+ // Keys that already carry casing (e.g. `cardWidth`) are correct as-is; running
98620
+ // boundary matching over them mangles inner matches (e.g. `id` inside `Width`).
98621
+ if (/[A-Z]/.test(str)) {
98622
+ return str;
98623
+ }
98614
98624
  const allBoundaries = [...REACT_HTML_PROP_BOUNDARIES, ...CSS_STYLE_PROP_BOUNDARIES, ...CUSTOM_PROP_BOUNDARIES];
98615
98625
  // Return as-is if already a boundary word to avoid incorrect splitting
98616
98626
  if (allBoundaries.includes(str.toLowerCase())) {
@@ -101719,6 +101729,43 @@ const parseSibling = (stack, parent, index, sibling, safeMode) => {
101719
101729
  stack.push(parent);
101720
101730
  }
101721
101731
  };
101732
+ /**
101733
+ * Advance a point by the substring of source consumed from it.
101734
+ */
101735
+ const pointAfter = (start, consumed) => {
101736
+ const newlineIndex = consumed.lastIndexOf('\n');
101737
+ const newlineCount = newlineIndex === -1 ? 0 : consumed.split('\n').length - 1;
101738
+ return {
101739
+ line: start.line + newlineCount,
101740
+ column: newlineCount === 0 ? start.column + consumed.length : consumed.length - newlineIndex,
101741
+ offset: start.offset + consumed.length,
101742
+ };
101743
+ };
101744
+ /**
101745
+ * Build a position ending at `consumedLength` into the html node's value, so the
101746
+ * component doesn't claim trailing content the tokenizer swallowed into one node.
101747
+ */
101748
+ const positionEndingAtConsumed = (nodePosition, value, consumedLength) => {
101749
+ if (!nodePosition?.start)
101750
+ return nodePosition;
101751
+ return { start: nodePosition.start, end: pointAfter(nodePosition.start, value.slice(0, consumedLength)) };
101752
+ };
101753
+ /**
101754
+ * Build a position ending right after the last occurrence of `closingTag` within
101755
+ * this node's span in the original source. Used in the trailing-content path so
101756
+ * the offset is computed against the real source bytes (including blockquote/list
101757
+ * prefixes that were stripped from the html node's value).
101758
+ */
101759
+ const positionEndingAtClosingTagInSource = (nodePosition, closingTag, source) => {
101760
+ if (!nodePosition?.start || !nodePosition.end)
101761
+ return nodePosition;
101762
+ const nodeSource = source.slice(nodePosition.start.offset, nodePosition.end.offset);
101763
+ const closingTagOffset = nodeSource.lastIndexOf(closingTag);
101764
+ if (closingTagOffset === -1)
101765
+ return nodePosition;
101766
+ const consumed = nodeSource.slice(0, closingTagOffset + closingTag.length);
101767
+ return { start: nodePosition.start, end: pointAfter(nodePosition.start, consumed) };
101768
+ };
101722
101769
  /**
101723
101770
  * Create an MdxJsxFlowElement node from component data.
101724
101771
  */
@@ -101762,9 +101809,10 @@ const substituteNodeWithMdxNode = (parent, index, mdxNode) => {
101762
101809
  * The opening tag, content, and closing tag are all captured in one HTML node
101763
101810
  * (guaranteed by the mdx-component tokenizer).
101764
101811
  */
101765
- const mdxishMdxComponentBlocks = (opts = {}) => tree => {
101812
+ const mdxishMdxComponentBlocks = (opts = {}) => (tree, file) => {
101766
101813
  const stack = [tree];
101767
101814
  const safeMode = !!opts.safeMode;
101815
+ const source = file?.value ? String(file.value) : null;
101768
101816
  const parseOpts = { preserveExpressionsAsText: safeMode };
101769
101817
  const processChildNode = (parent, index) => {
101770
101818
  const node = parent.children[index];
@@ -101778,10 +101826,16 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
101778
101826
  const value = node.value;
101779
101827
  if (node.type !== 'html' || typeof value !== 'string')
101780
101828
  return;
101781
- const parsed = parseTag(value.trim(), parseOpts);
101829
+ const trimmed = value.trim();
101830
+ const parsed = parseTag(trimmed, parseOpts);
101782
101831
  if (!parsed)
101783
101832
  return;
101784
101833
  const { tag, attributes, selfClosing, contentAfterTag = '' } = parsed;
101834
+ // Offset of `trimmed` within the (possibly whitespace-padded) html node value,
101835
+ // so consumed-length math maps back onto the node's real source offsets.
101836
+ const leadingWhitespace = value.length - value.trimStart().length;
101837
+ // Index right after the opening tag's `>` within `trimmed`.
101838
+ const openingTagEnd = trimmed.length - contentAfterTag.length;
101785
101839
  // Skip tags that have dedicated transformers
101786
101840
  if (GENERIC_MDX_COMPONENT_EXCLUDED_TAGS.has(tag))
101787
101841
  return;
@@ -101806,6 +101860,8 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
101806
101860
  attributes,
101807
101861
  children: [],
101808
101862
  startPosition: node.position,
101863
+ // End at the self-closing tag, not at any trailing content.
101864
+ endPosition: positionEndingAtConsumed(node.position, value, leadingWhitespace + openingTagEnd),
101809
101865
  });
101810
101866
  substituteNodeWithMdxNode(parent, index, componentNode);
101811
101867
  // Check and parse if there's relevant content after the current closing tag
@@ -101837,6 +101893,17 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
101837
101893
  attributes,
101838
101894
  children: parsedChildren,
101839
101895
  startPosition: node.position,
101896
+ // When trailing content follows the closing tag, compute the end position precisely
101897
+ // within the html node's value so the component doesn't claim that content.
101898
+ // Prefer source-based positioning when the original source is available: the html
101899
+ // node's value has '> '/space prefixes stripped for blockquotes/list items, so
101900
+ // positionEndingAtConsumed would undercount source offsets. When the entire node
101901
+ // is consumed, use the original node position directly.
101902
+ endPosition: contentAfterClose
101903
+ ? source
101904
+ ? positionEndingAtClosingTagInSource(node.position, closingTagStr, source)
101905
+ : positionEndingAtConsumed(node.position, value, leadingWhitespace + openingTagEnd + closingTagIndex + closingTagStr.length)
101906
+ : node.position,
101840
101907
  });
101841
101908
  substituteNodeWithMdxNode(parent, index, componentNode);
101842
101909
  // After the closing tag, there might be more content to be processed
@@ -104107,6 +104174,24 @@ function restoreHTMLElements(content, htmlElements) {
104107
104174
  return content;
104108
104175
  return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
104109
104176
  }
104177
+ const ESM_DECLARATION_START = /^(?:export|import)\b/;
104178
+ /**
104179
+ * Whether a line begins a top-level `export`/`import` declaration. Its braces
104180
+ * are valid JS owned by the mdxjsEsm tokenizer (which tolerates blank lines),
104181
+ * so escaping them would corrupt the source and break acorn parsing.
104182
+ */
104183
+ function startsEsmDeclaration(chars, lineStart) {
104184
+ return ESM_DECLARATION_START.test(chars.slice(lineStart, lineStart + 7).join(''));
104185
+ }
104186
+ function isBlankLine(chars, lineStart) {
104187
+ for (let i = lineStart; i < chars.length; i += 1) {
104188
+ if (chars[i] === '\n')
104189
+ return true;
104190
+ if (chars[i] !== ' ' && chars[i] !== '\t')
104191
+ return false;
104192
+ }
104193
+ return true;
104194
+ }
104110
104195
  /**
104111
104196
  * Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
104112
104197
  */
@@ -104122,8 +104207,18 @@ function escapeProblematicBraces(content) {
104122
104207
  // Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
104123
104208
  const chars = Array.from(protectedContent);
104124
104209
  const openStack = [];
104210
+ // Whether the current top-level statement is an `export`/`import` declaration.
104211
+ let insideEsmDeclaration = false;
104125
104212
  for (let i = 0; i < chars.length; i += 1) {
104126
104213
  const ch = chars[i];
104214
+ // At a top-level (brace depth 0) line start, decide whether we're inside an
104215
+ // ESM declaration. The flag persists across the declaration's own lines.
104216
+ if (openStack.length === 0 && (i === 0 || chars[i - 1] === '\n')) {
104217
+ if (startsEsmDeclaration(chars, i))
104218
+ insideEsmDeclaration = true;
104219
+ else if (isBlankLine(chars, i))
104220
+ insideEsmDeclaration = false;
104221
+ }
104127
104222
  // Track string delimiters inside expressions to ignore braces within them
104128
104223
  if (openStack.length > 0) {
104129
104224
  if (strDelim) {
@@ -104181,19 +104276,22 @@ function escapeProblematicBraces(content) {
104181
104276
  if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
104182
104277
  isAttrExpr = true;
104183
104278
  }
104184
- openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
104279
+ openStack.push({ pos: i, hasBlankLine: false, isAttrExpr, isEsmDeclaration: insideEsmDeclaration });
104185
104280
  lastNewlinePos = -2;
104186
104281
  }
104187
104282
  else if (ch === '}') {
104188
104283
  if (openStack.length > 0) {
104189
104284
  const entry = openStack.pop();
104285
+ // The declaration's braces are closed; later top-level braces are not ESM.
104286
+ if (openStack.length === 0)
104287
+ insideEsmDeclaration = false;
104190
104288
  // Pure `{/* ... */}` comments are handled downstream by the jsxComment
104191
104289
  // tokenizer — escaping their braces would prevent it from running.
104192
104290
  const isPureJsxComment = chars[entry.pos + 1] === '/' &&
104193
104291
  chars[entry.pos + 2] === '*' &&
104194
104292
  chars[i - 1] === '/' &&
104195
104293
  chars[i - 2] === '*';
104196
- if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr) {
104294
+ if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr && !entry.isEsmDeclaration) {
104197
104295
  toEscape.add(entry.pos);
104198
104296
  toEscape.add(i);
104199
104297
  }
@@ -104202,9 +104300,16 @@ function escapeProblematicBraces(content) {
104202
104300
  toEscape.add(i);
104203
104301
  }
104204
104302
  }
104303
+ else if (ch === ';' && openStack.length === 0) {
104304
+ // A top-level `;` ends the current statement, ESM or otherwise.
104305
+ insideEsmDeclaration = false;
104306
+ }
104205
104307
  }
104206
104308
  // Anything still open is unbalanced.
104207
- openStack.forEach(entry => toEscape.add(entry.pos));
104309
+ openStack.forEach(entry => {
104310
+ if (!entry.isEsmDeclaration)
104311
+ toEscape.add(entry.pos);
104312
+ });
104208
104313
  // Reconstruct the content with the escaped braces.
104209
104314
  const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
104210
104315
  return restoreHTMLElements(escapedContent, htmlElements);
@@ -105703,11 +105808,18 @@ function exportComponentsForRehype(components) {
105703
105808
  * semantics). That breaks any component prop that is genuinely an array. With
105704
105809
  * `passNode: true` we get the original hast node in `props.node` for components
105705
105810
  * and can read the raw properties directly.
105811
+ *
105812
+ * Only arrays win over `rest`, overwriting other shapes would undo correct
105813
+ * React conversions like `style="color:red"` → `{ color: 'red' }`.
105706
105814
  */
105707
105815
  function createElementPreservingHastProps(type, props, ...children) {
105708
105816
  if (props?.node?.properties) {
105709
105817
  const { node, ...rest } = props;
105710
- const mergedProps = { ...rest, ...node.properties };
105818
+ const mergedProps = { ...rest };
105819
+ Object.entries(node.properties).forEach(([key, value]) => {
105820
+ if (Array.isArray(value))
105821
+ mergedProps[key] = value;
105822
+ });
105711
105823
  // Strip undefined so positional args don't shadow node.properties.children
105712
105824
  const definedChildren = children.filter(c => c !== undefined);
105713
105825
  return external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement(type, mergedProps, ...definedChildren);