UNPKG

@mintlify/common

Version:

Commonly shared code within Mintlify

543 lines (542 loc) 22 kB
import { remove } from 'unist-util-remove'; import { CONTINUE, visit } from 'unist-util-visit'; import { isMdxJsEsm } from '../../utils.js'; const objectEstree = { type: 'Program', body: [ { type: 'ExpressionStatement', expression: { type: 'ObjectExpression', properties: [], }, }, ], sourceType: 'module', comments: [], }; const arrayEstree = { type: 'Program', body: [ { type: 'ExpressionStatement', expression: { type: 'ArrayExpression', elements: [], }, }, ], sourceType: 'module', comments: [], }; const falseEstree = { type: 'Program', body: [ { type: 'ExpressionStatement', expression: { type: 'Literal', value: false, raw: 'false', }, }, ], sourceType: 'module', comments: [], }; const trueEstree = { type: 'Program', body: [ { type: 'ExpressionStatement', expression: { type: 'Literal', value: true, raw: 'true', }, }, ], sourceType: 'module', comments: [], }; function createStringEstree(str) { return { type: 'Program', body: [ { type: 'ExpressionStatement', expression: { type: 'Literal', value: str, raw: `'${str}'`, }, }, ], sourceType: 'module', comments: [], }; } const numberEstree = { type: 'Program', body: [ { type: 'ExpressionStatement', expression: { type: 'Literal', value: 1, raw: '1', }, }, ], sourceType: 'module', comments: [], }; const DEFAULT_PROP_EXPRESSIONS = { placement: { value: '"bottom"', estree: createStringEstree('bottom') }, deflectionEmail: { value: '""', estree: createStringEstree('') }, searchSites: { value: '[]', estree: arrayEstree }, spendLimit: { value: '1', estree: numberEstree }, sampleQuestions: { value: '[]', estree: arrayEstree }, slack: { value: 'false', estree: falseEstree }, subdomain: { value: '""', estree: createStringEstree('') }, context: { value: '{}', estree: objectEstree }, title: { value: '""', estree: createStringEstree('') }, description: { value: '""', estree: createStringEstree('') }, icon: { value: '""', estree: createStringEstree('') }, href: { value: '""', estree: createStringEstree('') }, iconType: { value: '"regular"', estree: createStringEstree('regular') }, enabled: { value: '"false"', estree: createStringEstree('false') }, name: { value: '""', estree: createStringEstree('') }, mcp: { value: '{}', estree: objectEstree }, openapi: { value: '""', estree: createStringEstree('') }, servers: { value: '[]', estree: arrayEstree }, securitySchemes: { value: '{}', estree: objectEstree }, security: { value: '{}', estree: objectEstree }, authMethod: { value: '"bearer"', estree: createStringEstree('bearer') }, playground: { value: '"interactive"', estree: createStringEstree('interactive'), }, server: { value: '""', estree: createStringEstree('') }, metadata: { value: '{}', estree: objectEstree }, content: { value: '""', estree: createStringEstree('') }, codeSamples: { value: '[]', estree: arrayEstree }, lang: { value: '""', estree: createStringEstree('') }, label: { value: '""', estree: createStringEstree('') }, source: { value: '""', estree: createStringEstree('') }, examples: { value: '{}', estree: objectEstree }, summary: { value: '""', estree: createStringEstree('') }, value: { value: '{}', estree: objectEstree }, contextual: { value: '{}', estree: objectEstree }, api: { value: '{}', estree: objectEstree }, navigation: { value: '{}', estree: objectEstree }, asyncapi: { value: '""', estree: createStringEstree('') }, directory: { value: '""', estree: createStringEstree('') }, 'x-hidden': { value: '"true"', estree: trueEstree }, 'x-excluded': { value: '"true"', estree: trueEstree }, }; export function isStringSafe(value) { switch (true) { case value === 'true': case value === 'false': // purely alphanumeric string case /^[_a-zA-Z0-9]+$/.test(value): // string literal with quotes case /^'[_a-zA-Z0-9\s`".\-\\${}?\(\)&*^%#@!~=+\[\]|:;,\/]+'$/.test(value): case /^"[_a-zA-Z0-9\s`'.\-\\${}?\(\)&*^%#@!~=+\[\]|:;,\/]+"$/.test(value): case /^`[_a-zA-Z0-9\s'".\-\\{}?\(\)&*^%#@!~=+\[\]|:;,\/]+`$/.test(value): // purely alphanumeric string prefixed with `!!` case /^!![_a-zA-Z0-9]+$/.test(value): // purely alphanumeric string wrapped in parens case /^\([_a-zA-Z0-9]+\)$/.test(value): // purely alphanumeric string prefixed with `!!` wrapped in parens case /^\(!![_a-zA-Z0-9]+\)$/.test(value): return true; // could contain arbitrary javascript default: return false; } } function isStylePropertyValueSafe(property) { if (property.computed) return false; const value = property.value; if (value.type === 'Literal') return true; if (value.type === 'UnaryExpression' && value.operator === '-' && value.argument.type === 'Literal' && typeof value.argument.value === 'number') { return true; } return false; } export function filterStyleProperties(estree) { if (!estree.body.length) return null; const stmt = estree.body[0]; if ((stmt === null || stmt === void 0 ? void 0 : stmt.type) !== 'ExpressionStatement') return null; const expr = stmt.expression; if (expr.type !== 'ObjectExpression') return null; const safeProperties = expr.properties.filter((prop) => prop.type === 'Property' && isStylePropertyValueSafe(prop)); if (safeProperties.length === 0) return null; const newExpr = Object.assign(Object.assign({}, expr), { properties: safeProperties }); return Object.assign(Object.assign({}, estree), { body: [ Object.assign(Object.assign({}, stmt), { expression: newExpr }), ] }); } export function rebuildStyleValue(properties) { const parts = properties.map((prop) => { const key = prop.key.type === 'Identifier' ? prop.key.name : prop.key.type === 'Literal' ? `'${String(prop.key.value).replace(/'/g, "\\'")}'` : ''; let val = ''; if (prop.value.type === 'Literal') { val = typeof prop.value.value === 'string' ? `'${String(prop.value.value).replace(/'/g, "\\'")}'` : String(prop.value.value); } else if (prop.value.type === 'UnaryExpression' && prop.value.operator === '-' && prop.value.argument.type === 'Literal') { val = `-${prop.value.argument.value}`; } return `${key}: ${val}`; }); return `{ ${parts.join(', ')} }`; } function isPrimitiveLiteralExpression(node) { if (node.type === 'Literal') return true; return (node.type === 'UnaryExpression' && node.operator === '-' && typeof node.argument === 'object' && node.argument != null && node.argument.type === 'Literal'); } function isSafeJsxAttributeValue(value) { if (value == null) return true; if (typeof value !== 'object') return false; const node = value; if (node.type === 'Literal') return true; if (node.type !== 'JSXExpressionContainer') return false; if (typeof node.expression !== 'object' || node.expression == null) return false; return isPrimitiveLiteralExpression(node.expression); } const DANGEROUS_ELEMENT_NAMES = new Set(['script', 'style', 'iframe', 'meta']); function isSafeJsxElementName(name) { if (typeof name !== 'object' || name == null) return false; const node = name; if (node.type !== 'JSXIdentifier') return false; const elementName = node.name; if (typeof elementName !== 'string') return false; return !DANGEROUS_ELEMENT_NAMES.has(elementName.toLowerCase()); } function isEventHandlerAttribute(attributeNode) { const name = attributeNode.name; if (typeof name !== 'object' || name == null) return false; const nameNode = name; if (typeof nameNode.name !== 'string') return false; return /^on[A-Z]/.test(nameNode.name); } function isSafeStaticJsxNode(node) { if (node.type === 'JSXText') return true; if (node.type === 'JSXEmptyExpression') return true; if (node.type === 'JSXExpressionContainer') { if (typeof node.expression !== 'object' || node.expression == null) return false; const expression = node.expression; if (expression.type === 'JSXEmptyExpression') return true; return isPrimitiveLiteralExpression(expression); } if (node.type === 'JSXFragment') { if (!Array.isArray(node.children)) return false; return node.children.every((child) => typeof child === 'object' && child != null && isSafeStaticJsxNode(child)); } if (node.type !== 'JSXElement') return false; if (typeof node.openingElement !== 'object' || node.openingElement == null) return false; const openingElement = node.openingElement; if (!isSafeJsxElementName(openingElement.name)) return false; if (!Array.isArray(openingElement.attributes)) return false; const attributesAreSafe = openingElement.attributes.every((attribute) => { if (typeof attribute !== 'object' || attribute == null) return false; const attributeNode = attribute; if (attributeNode.type !== 'JSXAttribute') return false; if (isEventHandlerAttribute(attributeNode)) return false; return isSafeJsxAttributeValue(attributeNode.value); }); if (!attributesAreSafe) return false; if (!Array.isArray(node.children)) return false; return node.children.every((child) => typeof child === 'object' && child != null && isSafeStaticJsxNode(child)); } function isSafeStaticJsx(estree) { const stmt = estree === null || estree === void 0 ? void 0 : estree.body[0]; if ((stmt === null || stmt === void 0 ? void 0 : stmt.type) !== 'ExpressionStatement') return false; return isSafeStaticJsxNode(stmt.expression); } function collectIdentifiersFromPattern(pattern, out) { if (pattern.type === 'Identifier') { out.push({ name: pattern.name, isFunction: false }); } else if (pattern.type === 'ObjectPattern') { for (const prop of pattern.properties) { if (prop.type === 'Property') { collectIdentifiersFromPattern(prop.value, out); } else { collectIdentifiersFromPattern(prop.argument, out); } } } else if (pattern.type === 'ArrayPattern') { for (const el of pattern.elements) { if (el) collectIdentifiersFromPattern(el, out); } } else if (pattern.type === 'AssignmentPattern') { collectIdentifiersFromPattern(pattern.left, out); } else if (pattern.type === 'RestElement') { collectIdentifiersFromPattern(pattern.argument, out); } } // Returns the identifier name if the estree expression is a bare reference that will be kept // by isStringSafe but needs a stub to avoid a ReferenceError at render time: // {foo} → Identifier // {!!foo} → UnaryExpression(! UnaryExpression(! Identifier)) // Parenthesised forms are transparent in ESTree so (foo) produces the same AST as foo. function extractIdentifierRef(estree) { const stmt = estree === null || estree === void 0 ? void 0 : estree.body[0]; if ((stmt === null || stmt === void 0 ? void 0 : stmt.type) !== 'ExpressionStatement') return null; const expr = stmt.expression; if (expr.type === 'Identifier') return expr.name; if (expr.type === 'UnaryExpression' && expr.operator === '!' && expr.argument.type === 'UnaryExpression' && expr.argument.operator === '!' && expr.argument.argument.type === 'Identifier') return expr.argument.argument.name; return null; } function collectExportedNames(estree) { var _a, _b; const names = []; for (const node of estree.body) { // Named exports: export const x, export function x, export { x } if (node.type === 'ExportNamedDeclaration') { const exportNode = node; const decl = exportNode.declaration; if (!decl) { // Re-export specifiers: export { X } from 'module' or export { X } for (const spec of exportNode.specifiers) { // ES2022 allows Literal export names (e.g. export { foo as "bar" }), // which have no .name — skip to avoid emitting `export const undefined = undefined` // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (spec.exported.type !== 'Identifier') continue; // 'default' is a reserved word and cannot be used as a variable name in a stub if (spec.exported.name === 'default') continue; names.push({ name: spec.exported.name, isFunction: false }); } continue; } if (decl.type === 'FunctionDeclaration' && decl.id) { names.push({ name: decl.id.name, isFunction: true }); } else if (decl.type === 'VariableDeclaration') { for (const declarator of decl.declarations) { if (declarator.id.type === 'Identifier') { const isFn = ((_a = declarator.init) === null || _a === void 0 ? void 0 : _a.type) === 'ArrowFunctionExpression' || ((_b = declarator.init) === null || _b === void 0 ? void 0 : _b.type) === 'FunctionExpression'; names.push({ name: declarator.id.name, isFunction: isFn }); } else { collectIdentifiersFromPattern(declarator.id, names); } } } } // Default export: export default function Name() {} creates a named binding in scope if (node.type === 'ExportDefaultDeclaration') { const decl = node.declaration; if ((decl.type === 'FunctionDeclaration' || decl.type === 'FunctionExpression') && decl.id) { names.push({ name: decl.id.name, isFunction: true }); } } } return names; } function buildStubEsmNode(names) { const sourceLines = []; const body = []; for (const { name, isFunction } of names) { if (isFunction) { sourceLines.push(`export const ${name} = () => null;`); body.push({ type: 'ExportNamedDeclaration', declaration: { type: 'VariableDeclaration', declarations: [ { type: 'VariableDeclarator', id: { type: 'Identifier', name }, init: { type: 'ArrowFunctionExpression', params: [], body: { type: 'Literal', value: null, raw: 'null' }, expression: true, async: false, generator: false, }, }, ], kind: 'const', }, specifiers: [], source: null, }); } else { sourceLines.push(`export const ${name} = undefined;`); body.push({ type: 'ExportNamedDeclaration', declaration: { type: 'VariableDeclaration', declarations: [ { type: 'VariableDeclarator', id: { type: 'Identifier', name }, init: { type: 'Identifier', name: 'undefined' }, }, ], kind: 'const', }, specifiers: [], source: null, }); } } return { type: 'mdxjsEsm', value: sourceLines.join('\n'), data: { estree: { type: 'Program', body, sourceType: 'module', comments: [], }, }, }; } function isArrayOfStringLiterals(estree) { const stmt = estree.body[0]; if (estree.body.length !== 1 || (stmt === null || stmt === void 0 ? void 0 : stmt.type) !== 'ExpressionStatement') return false; if (stmt.expression.type !== 'ArrayExpression') return false; return stmt.expression.elements.every((el) => (el === null || el === void 0 ? void 0 : el.type) === 'Literal' && typeof el.value === 'string'); } export function remarkMdxRemoveJs() { return (tree) => { remove(tree, ['mdxTextExpression', 'mdxFlowExpression']); // Collect exported names before stripping ESM so we can inject stubs. // Map keyed by name deduplicates naturally. const stubbedNamesMap = new Map(); visit(tree, (node) => { var _a; if (isMdxJsEsm(node) && ((_a = node.data) === null || _a === void 0 ? void 0 : _a.estree)) { for (const entry of collectExportedNames(node.data.estree)) { if (!stubbedNamesMap.has(entry.name)) stubbedNamesMap.set(entry.name, entry); } } }); remove(tree, (node) => isMdxJsEsm(node)); // Sanitize JSX attributes. Any bare identifier that survives is added to stubbedNamesMap // so it gets a stub even if its import was already stripped by a prior plugin. visit(tree, (node) => { if (!('attributes' in node)) return CONTINUE; const newAttributes = node.attributes.map((attr) => { var _a, _b, _c, _d, _e, _f, _g, _h; if (attr.type === 'mdxJsxAttribute' && !(attr.value instanceof Object)) return attr; if (typeof attr.value === 'string' && isStringSafe(attr.value)) return attr; if (attr.type === 'mdxJsxAttribute' && attr.value instanceof Object) { if (isStringSafe(attr.value.value)) { const ident = extractIdentifierRef((_a = attr.value.data) === null || _a === void 0 ? void 0 : _a.estree); if (ident && !stubbedNamesMap.has(ident)) stubbedNamesMap.set(ident, { name: ident, isFunction: false }); return attr; } if (((_b = attr.value.data) === null || _b === void 0 ? void 0 : _b.estree) && isArrayOfStringLiterals(attr.value.data.estree)) return attr; if (isSafeStaticJsx((_c = attr.value.data) === null || _c === void 0 ? void 0 : _c.estree)) return attr; if (attr.name === 'style' && ((_d = attr.value.data) === null || _d === void 0 ? void 0 : _d.estree)) { const filteredEstree = filterStyleProperties(attr.value.data.estree); if (filteredEstree) { const stmt = filteredEstree.body[0]; if ((stmt === null || stmt === void 0 ? void 0 : stmt.type) === 'ExpressionStatement' && stmt.expression.type === 'ObjectExpression') { attr.value.value = rebuildStyleValue(stmt.expression.properties); attr.value.data.estree = filteredEstree; return attr; } } return undefined; } if (Object.keys(DEFAULT_PROP_EXPRESSIONS).includes(attr.name)) { attr.value.value = (_f = (_e = DEFAULT_PROP_EXPRESSIONS[attr.name]) === null || _e === void 0 ? void 0 : _e.value) !== null && _f !== void 0 ? _f : '{}'; attr.value.data = { estree: structuredClone((_h = (_g = DEFAULT_PROP_EXPRESSIONS[attr.name]) === null || _g === void 0 ? void 0 : _g.estree) !== null && _h !== void 0 ? _h : objectEstree), }; return attr; } } return undefined; }); node.attributes = newAttributes.filter((attr) => attr !== undefined); }); // Inject stubs after the attribute walk so identifiers collected from attributes are included if (stubbedNamesMap.size > 0) { tree.children.unshift(buildStubEsmNode([...stubbedNamesMap.values()])); } }; }