UNPKG

@live-demo/core

Version:

Core components for @live-demo plugins.

240 lines (223 loc) 7.34 kB
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 };