@mintlify/common 1.0.1204 → 1.0.1205

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.
@@ -4,6 +4,7 @@ import { DocsConfig } from '@mintlify/validation';
4
4
  import type { Root } from 'mdast';
5
5
  import type { PluggableList } from 'unified';
6
6
  import { MdxExtracts } from '../types/mdx/MdxExtracts.js';
7
+ import { type MdxJsDetection } from './plugins/remark/remarkMdxRemoveJs.js';
7
8
  type MDXOptionsData = {
8
9
  snippetTreeMap: Record<string, Root>;
9
10
  pageMetadata: PageMetaTags;
@@ -12,7 +13,7 @@ type MDXOptionsData = {
12
13
  tailwindSelectors?: string[];
13
14
  path?: string;
14
15
  };
15
- export declare const getMDXOptions: ({ data, remarkPlugins, earlyRemarkPlugins, rehypePlugins, mdxExtracts, allowedComponents, fragmentPlugins, }: {
16
+ export declare const getMDXOptions: ({ data, remarkPlugins, earlyRemarkPlugins, rehypePlugins, mdxExtracts, allowedComponents, fragmentPlugins, detectJs, }: {
16
17
  data: MDXOptionsData;
17
18
  remarkPlugins?: PluggableList;
18
19
  earlyRemarkPlugins?: PluggableList;
@@ -24,5 +25,6 @@ export declare const getMDXOptions: ({ data, remarkPlugins, earlyRemarkPlugins,
24
25
  remarkPlugins: PluggableList;
25
26
  rehypePlugins: PluggableList;
26
27
  };
28
+ detectJs?: MdxJsDetection;
27
29
  }) => SerializeOptions["mdxOptions"];
28
30
  export {};
@@ -1,5 +1,6 @@
1
1
  import { rehypeCodeBlocks, rehypeDynamicTailwindCss, rehypeKeyboardSymbols, rehypeListItemText, rehypeMdxExtractEndpoint, rehypeMdxExtractExamples, rehypeParamFieldIds, rehypeRawComponents, rehypeTable, rehypeUnicodeIds, rehypeZoomImages, remarkExtractChangelogFilters, remarkExtractTableOfContents, remarkFileTree, remarkFrames, remarkComponentIds, remarkMdxInjectSnippets, remarkMdxStyleStringToObject, remarkMdxRemoveUnusedVariables, remarkRemoveImports, remarkMdxExtractPanel, remarkResolveRelativeLinks, remarkVideo, remarkMdxClientComponentBoundaries, remarkExtractMultiView, remarkPrompt, remarkUnwrapInlineJsx, remarkUnwrapJsxHeadings, remarkMapExplicitAnchors, } from './plugins/index.js';
2
2
  import { remarkMdxExpandExpressions, } from './plugins/remark/remarkMdxExpandExpressions.js';
3
+ import { remarkMdxDetectJs } from './plugins/remark/remarkMdxRemoveJs.js';
3
4
  import { remarkMdxRemoveUnknownJsx } from './plugins/remark/remarkMdxRemoveUnknownJsx/index.js';
4
5
  import { remarkMermaid } from './plugins/remark/remarkMermaid.js';
5
6
  import { remarkNoCopyLanguage } from './plugins/remark/remarkNoCopyLanguage.js';
@@ -13,7 +14,7 @@ const rehypeExtractors = (mdxExtracts, data) => {
13
14
  [rehypeMdxExtractEndpoint, data.pageMetadata, data.config, mdxExtracts],
14
15
  ];
15
16
  };
16
- export const getMDXOptions = ({ data, remarkPlugins = [], earlyRemarkPlugins = [], rehypePlugins = [], mdxExtracts, allowedComponents = [], fragmentPlugins, }) => {
17
+ export const getMDXOptions = ({ data, remarkPlugins = [], earlyRemarkPlugins = [], rehypePlugins = [], mdxExtracts, allowedComponents = [], fragmentPlugins, detectJs, }) => {
17
18
  // `fragment`: the list is compiling the body of an `<MDX>` inside an expression. Page-level
18
19
  // extractors that hoist an element out of the tree (`<Panel>`, `<RequestExample>`,
19
20
  // `<ResponseExample>`) are skipped there so the element stays in its branch and renders inline
@@ -55,6 +56,7 @@ export const getMDXOptions = ({ data, remarkPlugins = [], earlyRemarkPlugins = [
55
56
  remarkMermaid,
56
57
  remarkNoCopyLanguage,
57
58
  remarkVideo,
59
+ ...(fragment || !detectJs ? [] : [[remarkMdxDetectJs, detectJs]]),
58
60
  ...remarkPlugins,
59
61
  remarkRenderTitleMarkdown,
60
62
  remarkMapExplicitAnchors,
@@ -8,3 +8,7 @@ export declare function isPureDataExpression(value: unknown, depth?: number): bo
8
8
  export declare function isPureDataProgram(estree: Program): boolean;
9
9
  export declare function isUnsafeDataAttributeName(name: string): boolean;
10
10
  export declare function remarkMdxRemoveJs(): (tree: Root) => void;
11
+ export type MdxJsDetection = {
12
+ hasJs: boolean;
13
+ };
14
+ export declare function remarkMdxDetectJs(detection: MdxJsDetection): (tree: Root) => void;
@@ -570,7 +570,106 @@ function tryResolveAsString(estree, constants) {
570
570
  const val = constants.get(expr.name);
571
571
  return val !== undefined ? val : null;
572
572
  }
573
+ const isMdxExpression = (node) => node.type === 'mdxTextExpression' || node.type === 'mdxFlowExpression';
574
+ // mdx comments ({/* … */}) parse to an expression with an empty estree body.
575
+ // the raw source must also be comment/whitespace-only: empty-body alone is an
576
+ // undocumented parser detail a toolchain bump could silently widen
577
+ // linear scan: a regex over repeated block comments backtracks exponentially
578
+ function isBlockCommentsAndWhitespace(value) {
579
+ let index = 0;
580
+ while (index < value.length) {
581
+ if (/\s/.test(value.charAt(index))) {
582
+ index++;
583
+ }
584
+ else if (value.startsWith('/*', index)) {
585
+ const end = value.indexOf('*/', index + 2);
586
+ if (end === -1)
587
+ return false;
588
+ index = end + 2;
589
+ }
590
+ else {
591
+ return false;
592
+ }
593
+ }
594
+ return true;
595
+ }
596
+ function isCommentOnlyExpression(node) {
597
+ var _a, _b, _c;
598
+ const estree = (_a = node.data) === null || _a === void 0 ? void 0 : _a.estree;
599
+ return (estree != null &&
600
+ estree.body.length === 0 &&
601
+ ((_b = estree.comments) !== null && _b !== void 0 ? _b : []).every((comment) => comment.type === 'Block' && !comment.value.includes('*/')) &&
602
+ isBlockCommentsAndWhitespace((_c = node.value) !== null && _c !== void 0 ? _c : ''));
603
+ }
604
+ // prettier emits {" "} to preserve spaces between inline elements. whitespace-only
605
+ // string literals are inert in every render position (unlike arbitrary string
606
+ // literals, which would become executable text inside kept <script> children),
607
+ // so both passes keep them and rendering stays byte-identical by construction
608
+ function isWhitespaceStringExpression(node) {
609
+ var _a;
610
+ const estree = (_a = node.data) === null || _a === void 0 ? void 0 : _a.estree;
611
+ if (estree == null || estree.body.length !== 1)
612
+ return false;
613
+ const stmt = estree.body[0];
614
+ if ((stmt === null || stmt === void 0 ? void 0 : stmt.type) !== 'ExpressionStatement')
615
+ return false;
616
+ const expr = stmt.expression;
617
+ return (expr.type === 'Literal' &&
618
+ typeof expr.value === 'string' &&
619
+ expr.value.trim() === '' &&
620
+ typeof expr.raw === 'string' &&
621
+ /^(["'])(?:\s|\\[nrt])*\1$/.test(expr.raw));
622
+ }
623
+ function isInertExpression(node) {
624
+ return isCommentOnlyExpression(node) || isWhitespaceStringExpression(node);
625
+ }
626
+ const applyEdits = {
627
+ replaceChild: (parent, index, node) => {
628
+ parent.children[index] = node;
629
+ },
630
+ remove: (tree, test) => {
631
+ remove(tree, test);
632
+ },
633
+ setAttributes: (node, attributes) => {
634
+ node.attributes = attributes;
635
+ },
636
+ rewriteAttribute: (value, raw, estree) => {
637
+ value.value = raw;
638
+ value.data = Object.assign(Object.assign({}, value.data), { estree });
639
+ },
640
+ prepend: (tree, node) => {
641
+ tree.children.unshift(node);
642
+ },
643
+ };
644
+ const WOULD_CHANGE = new Error('remarkMdxRemoveJs would change the tree');
645
+ const detectEdits = {
646
+ replaceChild: () => {
647
+ throw WOULD_CHANGE;
648
+ },
649
+ remove: (tree, test) => {
650
+ visit(tree, (node) => {
651
+ if (test(node))
652
+ throw WOULD_CHANGE;
653
+ });
654
+ },
655
+ setAttributes: (node, attributes) => {
656
+ if (attributes.length !== node.attributes.length ||
657
+ attributes.some((attr, i) => attr !== node.attributes[i]))
658
+ throw WOULD_CHANGE;
659
+ },
660
+ rewriteAttribute: () => {
661
+ throw WOULD_CHANGE;
662
+ },
663
+ prepend: () => {
664
+ throw WOULD_CHANGE;
665
+ },
666
+ };
573
667
  export function remarkMdxRemoveJs() {
668
+ return sanitizer(applyEdits);
669
+ }
670
+ // change the tree only through edit: remarkMdxDetectJs only sees edits, so a direct mutation
671
+ // would skip the no-js compile and serve unsanitized output
672
+ function sanitizer(edit) {
574
673
  return (tree) => {
575
674
  const constantStrings = collectStringConstants(tree);
576
675
  if (constantStrings.size > 0) {
@@ -587,10 +686,10 @@ export function remarkMdxRemoveJs() {
587
686
  const replacement = node.type === 'mdxTextExpression'
588
687
  ? { type: 'text', value: substitution }
589
688
  : { type: 'paragraph', children: [{ type: 'text', value: substitution }] };
590
- parent.children[index] = replacement;
689
+ edit.replaceChild(parent, index, replacement);
591
690
  });
592
691
  }
593
- remove(tree, ['mdxTextExpression', 'mdxFlowExpression']);
692
+ edit.remove(tree, (node) => isMdxExpression(node) && !isInertExpression(node));
594
693
  // Collect exported names before stripping ESM so we can inject stubs.
595
694
  // Map keyed by name deduplicates naturally.
596
695
  const stubbedNamesMap = new Map();
@@ -603,7 +702,7 @@ export function remarkMdxRemoveJs() {
603
702
  }
604
703
  }
605
704
  });
606
- remove(tree, (node) => isMdxJsEsm(node));
705
+ edit.remove(tree, (node) => isMdxJsEsm(node));
607
706
  // Sanitize JSX attributes. Any bare identifier that survives is added to stubbedNamesMap
608
707
  // so it gets a stub even if its import was already stripped by a prior plugin.
609
708
  visit(tree, (node) => {
@@ -634,28 +733,42 @@ export function remarkMdxRemoveJs() {
634
733
  const stmt = filteredEstree.body[0];
635
734
  if ((stmt === null || stmt === void 0 ? void 0 : stmt.type) === 'ExpressionStatement' &&
636
735
  stmt.expression.type === 'ObjectExpression') {
637
- attr.value.value = rebuildStyleValue(stmt.expression.properties);
638
- attr.value.data.estree = filteredEstree;
736
+ edit.rewriteAttribute(attr.value, rebuildStyleValue(stmt.expression.properties), filteredEstree);
639
737
  return attr;
640
738
  }
641
739
  }
642
740
  return undefined;
643
741
  }
644
742
  if (Object.keys(DEFAULT_PROP_EXPRESSIONS).includes(attr.name)) {
645
- attr.value.value = (_f = (_e = DEFAULT_PROP_EXPRESSIONS[attr.name]) === null || _e === void 0 ? void 0 : _e.value) !== null && _f !== void 0 ? _f : '{}';
646
- attr.value.data = {
647
- estree: structuredClone((_h = (_g = DEFAULT_PROP_EXPRESSIONS[attr.name]) === null || _g === void 0 ? void 0 : _g.estree) !== null && _h !== void 0 ? _h : objectEstree),
648
- };
743
+ edit.rewriteAttribute(attr.value, (_f = (_e = DEFAULT_PROP_EXPRESSIONS[attr.name]) === null || _e === void 0 ? void 0 : _e.value) !== null && _f !== void 0 ? _f : '{}', structuredClone((_h = (_g = DEFAULT_PROP_EXPRESSIONS[attr.name]) === null || _g === void 0 ? void 0 : _g.estree) !== null && _h !== void 0 ? _h : objectEstree));
649
744
  return attr;
650
745
  }
651
746
  }
652
747
  return undefined;
653
748
  });
654
- node.attributes = newAttributes.filter((attr) => attr !== undefined);
749
+ edit.setAttributes(node, newAttributes.filter((attr) => attr !== undefined));
655
750
  });
656
751
  // Inject stubs after the attribute walk so identifiers collected from attributes are included
657
752
  if (stubbedNamesMap.size > 0) {
658
- tree.children.unshift(buildStubEsmNode([...stubbedNamesMap.values()]));
753
+ edit.prepend(tree, buildStubEsmNode([...stubbedNamesMap.values()]));
754
+ }
755
+ };
756
+ }
757
+ // dry-run twin of remarkMdxRemoveJs for the with-js compile pass: reports
758
+ // whether the sanitizer would change this tree, without touching it. a false
759
+ // detection proves the no-js compile would be byte-identical and can be skipped
760
+ export function remarkMdxDetectJs(detection) {
761
+ const detect = sanitizer(detectEdits);
762
+ return (tree) => {
763
+ let hasJs = false;
764
+ try {
765
+ detect(tree);
766
+ }
767
+ catch (error) {
768
+ if (error !== WOULD_CHANGE)
769
+ throw error;
770
+ hasJs = true;
659
771
  }
772
+ detection.hasJs = hasJs;
660
773
  };
661
774
  }
@@ -13,7 +13,7 @@ export type GetMdxType = {
13
13
  panelMdxSourceWithNoJs?: SerializeSuccess;
14
14
  parseFailed?: boolean;
15
15
  };
16
- export declare function getMdx({ path, content, metadata, snippets, subdomain, codeStyling, config, tailwindSelectors, pageType, trace, customLanguages, variables, rehypePlugins, remarkPlugins, earlyRemarkPlugins, allowedComponents, }: {
16
+ export declare function getMdx({ path, content, metadata, snippets, subdomain, codeStyling, config, tailwindSelectors, pageType, trace, customLanguages, variables, rehypePlugins, remarkPlugins, earlyRemarkPlugins, allowedComponents, singlePass, }: {
17
17
  path: string;
18
18
  content: string;
19
19
  metadata: PageMetaTags;
@@ -30,5 +30,6 @@ export declare function getMdx({ path, content, metadata, snippets, subdomain, c
30
30
  remarkPlugins?: PluggableList;
31
31
  earlyRemarkPlugins?: PluggableList;
32
32
  allowedComponents?: string[];
33
+ singlePass?: boolean;
33
34
  }): Promise<GetMdxType>;
34
35
  export { createSnippetTreeMap, type Snippet };
@@ -17,7 +17,7 @@ import { replaceVariables } from '../replaceVariables.js';
17
17
  import { createSnippetTreeMap } from './getMdx/snippets.js';
18
18
  import { getSerializeExtras } from './getSerializeExtras.js';
19
19
  export function getMdx(_a) {
20
- return __awaiter(this, arguments, void 0, function* ({ path, content, metadata, snippets, subdomain, codeStyling, config, tailwindSelectors = undefined, pageType = 'default', trace = undefined, customLanguages = [], variables = undefined, rehypePlugins = [], remarkPlugins = [], earlyRemarkPlugins = [], allowedComponents = [], }) {
20
+ return __awaiter(this, arguments, void 0, function* ({ path, content, metadata, snippets, subdomain, codeStyling, config, tailwindSelectors = undefined, pageType = 'default', trace = undefined, customLanguages = [], variables = undefined, rehypePlugins = [], remarkPlugins = [], earlyRemarkPlugins = [], allowedComponents = [], singlePass = true, }) {
21
21
  const traceFn = trace !== null && trace !== void 0 ? trace : ((_name, fn) => fn());
22
22
  const processedContent = preprocessCustomHeadingIds(preprocessMdxFragmentCode(replaceVariables(content, variables)));
23
23
  const processedSnippets = snippets.map((snippet) => (Object.assign(Object.assign({}, snippet), { content: preprocessCustomHeadingIds(preprocessMdxFragmentCode(replaceVariables(snippet.content, variables))) })));
@@ -46,12 +46,16 @@ export function getMdx(_a) {
46
46
  plugins = [...plugins, remarkExpandContent, remarkSplitCodeGroup, remarkSplitTabs];
47
47
  }
48
48
  const syntaxHighlightingOptions = Object.assign(Object.assign({}, codeStylingToThemeOrThemes(codeStyling)), { customLanguages });
49
+ // detector sits exactly where remarkMdxRemoveJs sits in the no-js pass, so it
50
+ // sees the identical tree state and applies the identical decisions
51
+ const jsDetection = { hasJs: true };
49
52
  const mdxOptions = getMDXOptions({
50
53
  data: mdxOptionsData,
51
54
  remarkPlugins: plugins,
52
55
  earlyRemarkPlugins,
53
56
  rehypePlugins,
54
57
  mdxExtracts,
58
+ detectJs: jsDetection,
55
59
  allowedComponents,
56
60
  fragmentPlugins: getSerializeExtras(syntaxHighlightingOptions),
57
61
  });
@@ -78,6 +82,7 @@ export function getMdx(_a) {
78
82
  path,
79
83
  traceFn,
80
84
  customLanguages,
85
+ skipNoJs: singlePass ? { detection: jsDetection, mdxExtracts } : undefined,
81
86
  });
82
87
  }));
83
88
  let parseFailed = mainParseFailed;
@@ -117,7 +122,7 @@ export function getMdx(_a) {
117
122
  });
118
123
  }
119
124
  function compileMdx(_a) {
120
- return __awaiter(this, arguments, void 0, function* ({ content, scope, mdxOptions, mdxOptionsNoJs, codeStyling, path, traceFn, customLanguages, }) {
125
+ return __awaiter(this, arguments, void 0, function* ({ content, scope, mdxOptions, mdxOptionsNoJs, codeStyling, path, traceFn, customLanguages, skipNoJs, }) {
121
126
  const withJs = yield traceFn('compileMdx.withJs', () => __awaiter(this, void 0, void 0, function* () {
122
127
  return getCompiledMdxWithFallback({
123
128
  source: content,
@@ -129,6 +134,22 @@ function compileMdx(_a) {
129
134
  customLanguages,
130
135
  });
131
136
  }));
137
+ // the two passes differ only in remarkMdxRemoveJs (detector reported a no-op)
138
+ // and rehypeMdxExtractExamples splicing (nothing was collected), so the no-js
139
+ // compile would be byte-identical: reuse the with-js result
140
+ const examples = skipNoJs === null || skipNoJs === void 0 ? void 0 : skipNoJs.mdxExtracts.codeExamples;
141
+ const canSkip = skipNoJs !== undefined &&
142
+ !withJs.parseFailed &&
143
+ !skipNoJs.detection.hasJs &&
144
+ !(examples === null || examples === void 0 ? void 0 : examples.request) &&
145
+ !(examples === null || examples === void 0 ? void 0 : examples.response);
146
+ if (canSkip) {
147
+ return {
148
+ mdxSource: withJs.result,
149
+ mdxSourceWithNoJs: withJs.result,
150
+ parseFailed: withJs.parseFailed,
151
+ };
152
+ }
132
153
  const withoutJs = yield traceFn('compileMdx.withoutJs', () => __awaiter(this, void 0, void 0, function* () {
133
154
  return getCompiledMdxWithFallback({
134
155
  source: content,