UNPKG

@live-demo/core

Version:

Core components for @live-demo plugins.

253 lines (236 loc) 7.35 kB
import path from "node:path"; import fs from "node:fs"; import { parseSync } from "@oxidation-compiler/napi"; 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; const parsed = parseSync(code, { sourceType: "module", sourceFilename: fileName }); const ast = JSON.parse(parsed.program); return { files, ast }; }; //#endregion //#region src/node/helpers/resolveFileInfo.ts function resolveFileInfo({ dirname, importPath }) { const absolutePath = path.join(dirname, importPath); const pathsToCheck = getPossiblePaths(absolutePath); for (const absolutePath$1 of pathsToCheck) if (fs.existsSync(absolutePath$1)) { const fileName = path.basename(absolutePath$1); return { absolutePath: absolutePath$1, fileName }; } 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 { files, ast } = getFilesAndAst({ absolutePath, fileName }); const allFiles = { ...files }; for (const statement of ast.body) { if (statement.type !== "ImportDeclaration") continue; const importPath = statement.source.value; if (isRelativeImport(importPath)) { const dirname = path.dirname(absolutePath); const fileInfo = resolveFileInfo({ importPath, dirname }); const nested = getFilesAndImports({ uniqueImports, ...fileInfo }); Object.assign(allFiles, nested.files); } else uniqueImports.add(importPath); } 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"); const mdxAst = processor.parse(mdxSource); return mdxAst; }; //#endregion //#region src/node/helpers/getMdxJsxAttribute.ts const getMdxJsxAttribute = (node, attrName) => { const attribute = node.attributes.find((attr) => { return attr.type === "mdxJsxAttribute" && attr.name === attrName; }); return attribute?.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) => { const moduleCodeString = Array.from(allImports).reduce((acc, moduleName, index) => { const name = `'${moduleName}'`; const value = `i_${index}`; const importStatement = `import * as ${value} from ${name};`; const addToImportsMap = `${IMPORTS_MAP}.set(${name}, ${value});`; return `${acc}\n\n${importStatement}\n${addToImportsMap}`; }, getImportFnString); return moduleCodeString; }; //#endregion //#region src/node/htmlTags.ts const htmlTags = [{ tag: "script", head: true, attrs: { src: "https://cdn.jsdelivr.net/npm/@babel/standalone@7.26.4/babel.min.js", integrity: "sha256-oShy6o2j0psqKWxRv6x8SC6BQZx1XyIHpJrZt3IA9Oo=", crossorigin: "anonymous" } }, { tag: "script", head: true, attrs: { src: "https://cdn.jsdelivr.net/npm/@rollup/browser@4.31.0/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; const isLive = node.meta?.includes("live"); if (!(isLive && node.lang in LiveDemoLanguage)) return; const entryFileName = `App.${node.lang}`; const baseProps = { entryFileName, files: { [entryFileName]: node.value } }; const props = getPropsWithOptions(baseProps, 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 { const mdxAst = getMdxAst(filePath); visit(mdxAst, "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) }); const demo = getFilesAndImports({ uniqueImports, ...entryFile }); demoDataByPath[importPath] = { files: demo.files, entryFileName: entryFile.fileName }; }); } catch (e) { console.error(e); throw e; } } }; //#endregion export { getFilesAndImports, getMdxAst, getMdxJsxAttribute, getVirtualModulesCode, htmlTags, remarkPlugin, resolveFileInfo, visitFilePaths };