@live-demo/core
Version:
Core components for @live-demo plugins.
253 lines (236 loc) • 7.35 kB
JavaScript
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 };