eslint-mdx
Version:
ESLint Parser for MDX
516 lines • 22.8 kB
JavaScript
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
if (typeof path === "string" && /^\.\.?\//.test(path)) {
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
});
}
return path;
};
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { promisify } from 'node:util';
import * as acorn from 'acorn';
import acornJsx from 'acorn-jsx';
import { visit as visitEstree } from 'estree-util-visit';
import remarkMdx from 'remark-mdx';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
import { extractProperties, runAsWorker } from 'synckit';
import { unified } from 'unified';
import { Configuration } from 'unified-engine';
import { visit } from 'unist-util-visit';
import { ok as assert } from 'uvu/assert';
import { VFile } from 'vfile';
import { cjsRequire, nextCharOffsetFactory, normalizePosition, prevCharOffsetFactory, } from "./helpers.js";
import { restoreTokens } from "./tokens.js";
let acornParser;
let tokTypes;
let jsxTokTypes;
let tt;
let TokenTranslator;
export const processorCache = new Map();
const configLoadCache = new Map();
let Ignore;
const ignoreCheckCache = new Map();
const getRemarkConfig = async (filePath, cwd, remarkConfigPath) => {
const cacheKey = remarkConfigPath ? `${cwd}\0${remarkConfigPath}` : cwd;
let configLoad = configLoadCache.get(cacheKey);
if (!configLoad) {
const config = new Configuration({
cwd,
packageField: 'remarkConfig',
pluginPrefix: 'remark',
rcName: '.remarkrc',
rcPath: remarkConfigPath,
detectConfig: true,
});
configLoad = promisify(config.load.bind(config));
configLoadCache.set(cacheKey, configLoad);
}
if (!Ignore) {
;
({ Ignore } = (await import(__rewriteRelativeImportExtension(pathToFileURL(path.resolve(cjsRequire.resolve('unified-engine'), '../lib/ignore.js')).href, true))));
}
let ignoreCheck = ignoreCheckCache.get(cacheKey);
if (!ignoreCheck) {
const ignore = new Ignore({
cwd,
ignoreName: '.remarkignore',
detectIgnore: true,
});
ignoreCheck = promisify(ignore.check.bind(ignore));
ignoreCheckCache.set(cacheKey, ignoreCheck);
}
return configLoad(filePath);
};
const getRemarkMdxOptions = (tokens) => ({
acorn: acornParser,
acornOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true,
ranges: true,
onToken: tokens,
},
});
const sharedTokens = [];
export const getRemarkProcessor = async (filePath, isMdx, ignoreRemarkConfig, cwd = process.cwd(), remarkConfigPath) => {
const initCacheKey = `${String(isMdx)}-${cwd}\0${filePath}`;
let cachedProcessor = processorCache.get(initCacheKey);
if (cachedProcessor) {
return cachedProcessor;
}
const result = ignoreRemarkConfig
? null
: await getRemarkConfig(filePath, cwd, remarkConfigPath);
const cacheKey = result?.filePath
? `${String(isMdx)}-${result.filePath}`
: String(isMdx);
cachedProcessor = processorCache.get(cacheKey);
if (cachedProcessor) {
return cachedProcessor;
}
const remarkProcessor = unified().use(remarkParse).freeze();
if (result?.filePath) {
const { plugins, settings } = result;
if (plugins.length > 0) {
try {
plugins.push([
(await import('remark-lint-file-extension')).default,
false,
]);
}
catch {
}
}
const initProcessor = remarkProcessor()
.use({ settings })
.use(remarkStringify);
if (isMdx) {
initProcessor.use(remarkMdx, getRemarkMdxOptions(sharedTokens));
}
cachedProcessor = plugins
.reduce((processor, plugin) => processor.use(...plugin), initProcessor)
.freeze();
}
else {
const initProcessor = remarkProcessor().use(remarkStringify);
if (isMdx) {
initProcessor.use(remarkMdx, getRemarkMdxOptions(sharedTokens));
}
cachedProcessor = initProcessor.freeze();
}
processorCache
.set(initCacheKey, cachedProcessor)
.set(cacheKey, cachedProcessor);
return cachedProcessor;
};
function isExpressionStatement(statement) {
assert(!statement || statement.type === 'ExpressionStatement');
}
runAsWorker(async ({ filePath, code, cwd, isMdx, process, ignoreRemarkConfig, remarkConfigPath, }) => {
sharedTokens.length = 0;
if (!acornParser) {
acornParser = acorn.Parser.extend(acornJsx());
}
const processor = await getRemarkProcessor(filePath, isMdx, ignoreRemarkConfig, cwd, remarkConfigPath);
const fileOptions = {
path: filePath,
value: code,
cwd,
};
if (process) {
const cacheKey = remarkConfigPath ? `${cwd}\0${remarkConfigPath}` : cwd;
if (await ignoreCheckCache.get(cacheKey)(filePath)) {
return {
messages: [],
};
}
const file = new VFile(fileOptions);
try {
await processor.process(file);
}
catch (err) {
const error = err;
if (!file.messages.includes(error)) {
file.message(error).fatal = true;
}
}
return {
messages: file.messages.map(message => extractProperties(message)),
content: file.toString(),
};
}
if (!tokTypes) {
tokTypes = acorn.tokTypes;
}
if (!jsxTokTypes) {
jsxTokTypes = acornJsx({
allowNamespacedObjects: true,
})(acorn.Parser).acornJsx.tokTypes;
}
if (!TokenTranslator) {
TokenTranslator = (await import(__rewriteRelativeImportExtension(pathToFileURL(path.resolve(cjsRequire.resolve('espree/package.json'), '../lib/token-translator.js')).href, true))).default;
}
if (!tt) {
tt = {
...tokTypes,
...jsxTokTypes,
};
}
const tokenTranslator = new TokenTranslator(tt, code);
const root = processor.parse(fileOptions);
const body = [];
const comments = [];
const tokens = [];
const processed = new WeakSet();
if (isMdx) {
const prevCharOffset = prevCharOffsetFactory(code);
const nextCharOffset = nextCharOffsetFactory(code);
const normalizeNode = (start, end) => ({
...normalizePosition({
start: { offset: start },
end: { offset: end },
code,
}),
raw: code.slice(start, end),
});
const handleJsxName = (nodeName, start) => {
const name = nodeName.trim();
const nameIndex = nodeName.indexOf(name);
const colonIndex = nodeName.indexOf(':');
if (colonIndex !== -1) {
const [fullNamespace, fullName] = nodeName.split(':');
return {
...normalizeNode(start + nameIndex, start + nameIndex + name.length),
type: 'JSXNamespacedName',
namespace: handleJsxName(fullNamespace, start),
name: handleJsxName(fullName, start + colonIndex + 1),
};
}
const lastPointIndex = nodeName.lastIndexOf('.');
if (lastPointIndex === -1) {
return {
...normalizeNode(start + nameIndex, start + nameIndex + name.length),
type: 'JSXIdentifier',
name,
};
}
const objectName = nodeName.slice(0, lastPointIndex);
const propertyName = nodeName.slice(lastPointIndex + 1);
return {
...normalizeNode(start + nameIndex, start + nameIndex + name.length),
type: 'JSXMemberExpression',
object: handleJsxName(objectName, start),
property: handleJsxName(propertyName, start + lastPointIndex + 1),
};
};
visit(root, node => {
if (processed.has(node) ||
(node.type !== 'mdxFlowExpression' &&
node.type !== 'mdxJsxFlowElement' &&
node.type !== 'mdxJsxTextElement' &&
node.type !== 'mdxTextExpression' &&
node.type !== 'mdxjsEsm')) {
return;
}
processed.add(node);
function handleChildren(node) {
return 'children' in node
? node.children.reduce((acc, child) => {
processed.add(child);
if (child.data && 'estree' in child.data && child.data.estree) {
const { estree } = child.data;
assert(estree.body.length <= 1);
const statement = estree.body[0];
isExpressionStatement(statement);
const expression = statement?.expression;
if (child.type === 'mdxTextExpression') {
const { start: { offset: start }, end: { offset: end }, } = node.position;
const expressionContainer = {
...normalizeNode(start, end),
type: 'JSXExpressionContainer',
expression: expression || {
...normalizeNode(start + 1, end - 1),
type: 'JSXEmptyExpression',
},
};
acc.push(expressionContainer);
}
else if (expression) {
acc.push(expression);
}
comments.push(...estree.comments);
}
else {
const expression = handleNode(child);
if (Array.isArray(expression)) {
acc.push(...expression);
}
else if (expression) {
acc.push(expression);
}
}
return acc;
}, [])
: [];
}
function handleNode(node) {
if (node.type === 'paragraph') {
return handleChildren(node);
}
const { start: { offset: start }, end: { offset: end }, } = node.position;
if (node.type === 'code') {
const { lang, meta, value } = node;
const mdxJsxCode = {
...normalizeNode(start, end),
type: 'MDXCode',
lang,
meta,
value,
};
return mdxJsxCode;
}
if (node.type === 'heading') {
const { depth } = node;
const mdxJsxHeading = {
...normalizeNode(start, end),
type: 'MDXHeading',
depth,
children: handleChildren(node),
};
return mdxJsxHeading;
}
if (node.type === 'text') {
const jsxText = {
...normalizeNode(start, end),
type: 'JSXText',
value: node.value,
};
return jsxText;
}
if (node.type !== 'mdxJsxTextElement' &&
node.type !== 'mdxJsxFlowElement') {
return;
}
const children = handleChildren(node);
const nodePos = node.position;
const nodeStart = nodePos.start.offset;
const nodeEnd = nodePos.end.offset;
const lastCharOffset = prevCharOffset(nodeEnd - 2);
let expression;
if ('name' in node && node.name) {
const nodeNameLength = node.name.length;
const nodeNameStart = nextCharOffset(nodeStart + 1);
const selfClosing = code[lastCharOffset] === '/';
let lastAttrOffset = nodeNameStart + nodeNameLength - 1;
let closingElement = null;
if (!selfClosing) {
const prevOffset = prevCharOffset(lastCharOffset);
const slashOffset = prevCharOffset(prevOffset - nodeNameLength);
assert(code[slashOffset] === '/', `expect \`${code[slashOffset]}\` to be \`/\`, the node is ${node.name}`);
const tagStartOffset = prevCharOffset(slashOffset - 1);
assert(code[tagStartOffset] === '<');
closingElement = {
...normalizeNode(tagStartOffset, nodeEnd),
type: 'JSXClosingElement',
name: handleJsxName(node.name, prevOffset + 1 - nodeNameLength),
};
}
const jsxEl = {
...normalizeNode(nodeStart, nodeEnd),
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: handleJsxName(node.name, nodeNameStart),
attributes: node.attributes.map(attr => {
if (attr.type === 'mdxJsxExpressionAttribute') {
assert(attr.data);
assert(attr.data.estree);
assert(attr.data.estree.range);
let [attrValStart, attrValEnd] = attr.data.estree.range;
attrValStart = prevCharOffset(attrValStart - 1);
attrValEnd = nextCharOffset(attrValEnd);
assert(code[attrValStart] === '{');
assert(code[attrValEnd] === '}');
lastAttrOffset = attrValEnd;
return {
...normalizeNode(attrValStart, attrValEnd + 1),
type: 'JSXSpreadAttribute',
argument: attr.data.estree.body[0]
.expression.properties[0].argument,
};
}
const attrStart = nextCharOffset(lastAttrOffset + 1);
assert(attrStart != null);
const attrName = attr.name;
const attrNameLength = attrName.length;
const attrValue = attr.value;
lastAttrOffset = attrStart + attrNameLength;
const attrNamePos = normalizeNode(attrStart, lastAttrOffset);
if (attrValue == null) {
return {
...normalizeNode(attrStart, lastAttrOffset),
type: 'JSXAttribute',
name: {
...attrNamePos,
type: 'JSXIdentifier',
name: attrName,
},
value: null,
};
}
const attrEqualOffset = nextCharOffset(attrStart + attrNameLength);
assert(code[attrEqualOffset] === '=');
let attrValuePos;
if (typeof attrValue === 'string') {
const attrQuoteOffset = nextCharOffset(attrEqualOffset + 1);
const attrQuote = code[attrQuoteOffset];
assert(attrQuote === '"' || attrQuote === "'");
lastAttrOffset = nextCharOffset(attrQuoteOffset + attrValue.length + 1);
assert(code[lastAttrOffset] === attrQuote);
attrValuePos = normalizeNode(attrQuoteOffset, lastAttrOffset + 1);
}
else {
const data = attrValue.data;
let [attrValStart, attrValEnd] = data.estree.range;
attrValStart = prevCharOffset(attrValStart - 1);
attrValEnd = nextCharOffset(attrValEnd);
assert(code[attrValStart] === '{');
assert(code[attrValEnd] === '}');
lastAttrOffset = attrValEnd;
attrValuePos = normalizeNode(attrValStart, attrValEnd + 1);
}
return {
...normalizeNode(attrStart, lastAttrOffset + 1),
type: 'JSXAttribute',
name: {
...attrNamePos,
type: 'JSXIdentifier',
name: attrName,
},
value: typeof attr.value === 'string'
? {
...attrValuePos,
type: 'Literal',
value: attr.value,
}
: {
...attrValuePos,
type: 'JSXExpressionContainer',
expression: attr.value.data.estree
.body[0].expression,
},
};
}),
selfClosing,
},
closingElement,
children,
};
let nextOffset = nextCharOffset(lastAttrOffset + 1);
let nextChar = code[nextOffset];
const expectedNextChar = selfClosing ? '/' : '>';
if (nextChar !== expectedNextChar) {
nextOffset = nextCharOffset(lastAttrOffset);
nextChar = code[nextOffset];
}
assert(nextChar === expectedNextChar, `\`nextChar\` must be '${expectedNextChar}' but actually is '${nextChar}'`);
Object.assign(jsxEl.openingElement, normalizeNode(nodeStart, selfClosing ? nodeEnd : nextOffset + 1));
expression = jsxEl;
}
else {
const openEndOffset = nextCharOffset(nodeStart + 1);
const openPos = normalizeNode(nodeStart, openEndOffset);
const closeStartOffset = prevCharOffset(lastCharOffset - 1);
const jsxFrg = {
...openPos,
type: 'JSXFragment',
openingFragment: {
...openPos,
type: 'JSXOpeningFragment',
},
closingFragment: {
...normalizeNode(closeStartOffset, nodeEnd),
type: 'JSXClosingFragment',
},
children,
};
expression = jsxFrg;
}
return expression;
}
const expression = handleNode(node);
if (expression) {
body.push({
...normalizePosition(node.position),
type: 'ExpressionStatement',
expression: expression,
});
}
const estree = ((node.data &&
'estree' in node.data &&
node.data.estree) || {
body: [],
comments: [],
});
body.push(...estree.body);
comments.push(...estree.comments);
});
}
visitEstree({
type: 'Program',
sourceType: 'module',
body,
}, node => {
if (node.type !== 'TemplateElement') {
return;
}
const templateElement = node;
const startOffset = -1;
const endOffset = templateElement.tail ? 1 : 2;
templateElement.start += startOffset;
templateElement.end += endOffset;
if (templateElement.range) {
templateElement.range[0] += startOffset;
templateElement.range[1] += endOffset;
}
if (templateElement.loc) {
templateElement.loc.start.column += startOffset;
templateElement.loc.end.column += endOffset;
}
});
for (const token of restoreTokens(code, root, sharedTokens, tt, visit)) {
tokenTranslator.onToken(token, {
ecmaVersion: 'latest',
tokens: tokens,
});
}
return {
root,
body,
comments,
tokens,
};
});
//# sourceMappingURL=worker.js.map