@live-demo/core
Version:
Core components for @live-demo plugins.
240 lines (223 loc) • 7.34 kB
JavaScript
import path from "node:path";
import fs from "node:fs";
import { parseSync } from "oxc-parser";
import { createProcessor } from "@mdx-js/mdx";
import remarkGFM from "remark-gfm";
import { visit } from "unist-util-visit";
//#region src/shared/constants.ts
let LiveDemoLanguage = /* @__PURE__ */ function(LiveDemoLanguage$1) {
LiveDemoLanguage$1["ts"] = "ts";
LiveDemoLanguage$1["tsx"] = "tsx";
LiveDemoLanguage$1["js"] = "js";
LiveDemoLanguage$1["jsx"] = "jsx";
return LiveDemoLanguage$1;
}({});
//#endregion
//#region src/shared/pathHelpers.ts
/** starting with ./ or ../ */
const relativeImportRegex = /^\.{1,2}\//;
const isRelativeImport = (importPath) => relativeImportRegex.test(importPath);
const getFileExt = (filename) => filename.split(".")[1];
const getPossiblePaths = (filePath) => {
const fileExt = getFileExt(filePath);
if (fileExt in LiveDemoLanguage) return [filePath];
if (!fileExt) return Object.keys(LiveDemoLanguage).map((ext) => `${filePath}.${ext}`);
throw new Error(`Couldn't resolve \`${filePath}\`.\nOnly .jsx and .tsx files are supported`);
};
//#endregion
//#region src/node/helpers/getFilesAndAst.ts
const getFilesAndAst = (params) => {
const { absolutePath, fileName } = params;
const files = {};
const code = fs.readFileSync(absolutePath, { encoding: "utf8" });
files[fileName] = code;
return {
files,
ast: parseSync(fileName, code, { sourceType: "module" }).program
};
};
//#endregion
//#region src/node/helpers/resolveFileInfo.ts
function resolveFileInfo({ dirname, importPath }) {
const pathsToCheck = getPossiblePaths(path.join(dirname, importPath));
for (const absolutePath of pathsToCheck) if (fs.existsSync(absolutePath)) return {
absolutePath,
fileName: path.basename(absolutePath)
};
throw new Error(`[LiveDemo]: Couldn't resolve \`${importPath}\`.\nOnly .js(x) and .ts(x) files are supported`);
}
//#endregion
//#region src/node/helpers/getFilesAndImports.ts
const getFilesAndImports = (params) => {
const { absolutePath, fileName, uniqueImports } = params;
const visited = params.visited || /* @__PURE__ */ new Set();
if (visited.has(absolutePath)) {
const chain = Array.from(visited).map((p) => path.basename(p)).join(" → ");
throw new Error(`[LiveDemo] Circular import detected: ${fileName}\nImport chain: ${chain} → ${fileName}`);
}
visited.add(absolutePath);
const { files, ast } = getFilesAndAst({
absolutePath,
fileName
});
const allFiles = { ...files };
for (const statement of ast.body) {
let sourcePath;
if (statement.type === "ImportDeclaration") sourcePath = statement.source.value;
else if (statement.type === "ExportNamedDeclaration" && statement.source) sourcePath = statement.source.value;
else if (statement.type === "ExportAllDeclaration") sourcePath = statement.source.value;
if (!sourcePath) continue;
if (isRelativeImport(sourcePath)) {
const dirname = path.dirname(absolutePath);
const nested = getFilesAndImports({
uniqueImports,
visited,
...resolveFileInfo({
importPath: sourcePath,
dirname
})
});
Object.assign(allFiles, nested.files);
} else uniqueImports.add(sourcePath);
}
return { files: allFiles };
};
//#endregion
//#region src/node/helpers/getMdxAst.ts
const processor = createProcessor({
format: "mdx",
remarkPlugins: [remarkGFM]
});
const getMdxAst = (filepath) => {
const mdxSource = fs.readFileSync(filepath, "utf-8");
return processor.parse(mdxSource);
};
//#endregion
//#region src/node/helpers/getMdxJsxAttribute.ts
const getMdxJsxAttribute = (node, attrName) => {
return node.attributes.find((attr) => {
return attr.type === "mdxJsxAttribute" && attr.name === attrName;
})?.value;
};
//#endregion
//#region src/node/helpers/getVirtualModulesCode.ts
const IMPORTS_MAP = "importsMap";
const getImportFnString = `const ${IMPORTS_MAP} = new Map()
function getImport(importName, getDefault) {
const result = ${IMPORTS_MAP}.get(importName)
if (!result) {
throw new Error(\`Can't resolve \${importName}.\`)
}
if (getDefault && typeof result === "object") {
return result.default || result
}
return result
}
export default getImport`;
/**
* Prepares a string template to be injected into
* node_modules with RspackVirtualModulePlugin.
* It will be used to resolve external modules
* when compiling code in the browser
*
* Usage:
* import getImport from '_live_demo_virtual_modules'
*
* getImport('react')
*/
const getVirtualModulesCode = (allImports) => {
return Array.from(allImports).reduce((acc, moduleName, index) => {
const name = `'${moduleName}'`;
const value = `i_${index}`;
return `${acc}\n\n${`import * as ${value} from ${name};`}\n${`${IMPORTS_MAP}.set(${name}, ${value});`}`;
}, getImportFnString);
};
//#endregion
//#region src/node/htmlTags.ts
const htmlTags = [{
tag: "script",
head: true,
attrs: { src: "https://cdn.jsdelivr.net/npm/@babel/standalone@7.28.3/babel.min.js" }
}, {
tag: "script",
head: true,
attrs: { src: "https://cdn.jsdelivr.net/npm/@rollup/browser@4.46.3/dist/rollup.browser.min.js" }
}];
//#endregion
//#region src/node/remarkPlugin.ts
/**
* Inject <LiveDemo /> into MDX
*/
const remarkPlugin = ({ options, getDemoDataByPath }) => {
const demoDataByPath = getDemoDataByPath();
return (tree, _vfile) => {
visit(tree, "mdxJsxFlowElement", (node) => {
if (node.name !== "code") return;
const importPath = getMdxJsxAttribute(node, "src");
if (typeof importPath !== "string" || !demoDataByPath[importPath]) return;
const props = getPropsWithOptions(demoDataByPath[importPath], options);
Object.assign(node, {
type: "mdxJsxFlowElement",
name: "LiveDemo",
attributes: getJsxAttributesFromProps(props)
});
});
visit(tree, "code", (node) => {
if (!node?.lang) return;
if (!(node.meta?.includes("live") && node.lang in LiveDemoLanguage)) return;
const entryFileName = `App.${node.lang}`;
const props = getPropsWithOptions({
entryFileName,
files: { [entryFileName]: node.value }
}, options);
Object.assign(node, {
type: "mdxJsxFlowElement",
name: "LiveDemo",
attributes: getJsxAttributesFromProps(props)
});
});
};
};
function getPropsWithOptions(props, options) {
return options ? {
...props,
options
} : props;
}
function getJsxAttributesFromProps(props) {
return Object.entries(props).map(([name, value]) => ({
name,
value: JSON.stringify(value),
type: "mdxJsxAttribute"
}));
}
//#endregion
//#region src/node/visitFilePaths.ts
const visitFilePaths = ({ filePaths, uniqueImports, demoDataByPath }) => {
for (const filePath of filePaths) {
if (!filePath.endsWith(".mdx")) continue;
try {
visit(getMdxAst(filePath), "mdxJsxFlowElement", (node) => {
if (node.name !== "code") return;
const importPath = getMdxJsxAttribute(node, "src");
if (typeof importPath !== "string") return;
const entryFile = resolveFileInfo({
importPath,
dirname: path.dirname(filePath)
});
demoDataByPath[importPath] = {
files: getFilesAndImports({
uniqueImports,
...entryFile
}).files,
entryFileName: entryFile.fileName
};
});
} catch (e) {
console.error(e);
throw e;
}
}
};
//#endregion
export { getFilesAndImports, getMdxAst, getMdxJsxAttribute, getVirtualModulesCode, htmlTags, remarkPlugin, resolveFileInfo, visitFilePaths };