react-repl-plus
Version:
React component for editing React components, it is similar to vue repl
151 lines (133 loc) • 7.13 kB
text/typescript
import { File, ReplStore } from "../component/repl/storeContext";
import { transform } from "@babel/standalone";
import { type NodePath } from "@babel/traverse";
import type { ExportDefaultDeclaration, Identifier, ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration } from "@babel/types";
import MagicString from "magic-string";
const modulesKey = '__modules__'
const moduleKey = '__module__'
const exportKey = '__export__'
export function compileModulesForPreview(store: ReplStore) {
const processed: string[] = []
const seen = new Set<File>()
processFile(store, store.files[store.mainFile], processed, seen)
return processed
}
function processFile(store: ReplStore, file: File, processed: string[], seen: Set<File>) {
if (seen.has(file)) {
return
}
seen.add(file)
const { code: js, importedFiles } = processModule(store, file.filename)
processChildFiles(store, importedFiles, processed, seen)
processed.push(js)
}
function processChildFiles(store: ReplStore, importedFiles: Set<string>, processed: string[], seen: Set<File>) {
for (const filename of importedFiles) {
processFile(store, store.files[filename], processed, seen)
}
}
/**
* todo: 模块解析这里是否能优化,解析了两次
*/
function processModule(store: ReplStore, filename: string) {
function defineExport(name: string, local = name) {
s.append(`\n${exportKey}(${moduleKey}, "${name}", () => ${local})`)
}
const rawCode = store.files[filename].code
const s = new MagicString(rawCode)
const importedFiles = new Set<string>()
let code = transform(rawCode, {
presets: ['react'],
plugins: [() => {
return {
visitor: {
ExportAllDeclaration(path: NodePath<ExportAllDeclaration>) {
const { node } = path
const modulePath = node.source.value.replace(/\.\//, '')
importedFiles.add(modulePath)
s.overwrite(node.start!, node.end!, `for (const key in ${modulesKey}["${modulePath}"]) {
if (key !== 'default') {
${moduleKey}[key] = ${modulesKey}["${modulePath}"][key]
}
}`)
},
ExportDefaultDeclaration(path: NodePath<ExportDefaultDeclaration>) {
const { node } = path
s.append(`${moduleKey}.default = ${(node.declaration as Identifier).name}\n`)
s.remove(node.start!, node.end!)
},
ExportNamedDeclaration(path: NodePath<ExportNamedDeclaration>) {
const { node } = path
if (node.declaration) {
// export const age = 18, name = 'xxx'
if (node.declaration.type === 'VariableDeclaration') {
for (const decl of node.declaration.declarations) {
const varName = decl.id.name
s.append(`\n ${moduleKey}.${varName} = ${varName}`)
}
}
// export function get(){}, export class Person{}
else if (node.declaration.type === 'ClassDeclaration' || node.declaration.type === 'FunctionDeclaration') {
const varName = node.declaration.id?.name
s.append(`\n ${moduleKey}.${varName} = ${varName}`)
}
s.remove(node.start!, node.declaration.start!)
}
// export { foo, bar } from './foo'
else if (node.source) {
const modulePath = node.source.value.replace(/\.\//, '')
const expandNames = node.specifiers?.map(specifier => specifier.local.name)
if (expandNames && expandNames.length !== 0) s.overwrite(node.start!, node.end!, `const { ${expandNames.join(',')} } = ${modulesKey}["${modulePath}"]`)
for (const specifier of node.specifiers) {
const varName = specifier.local.name
s.appendRight(node.end!, `\n ${moduleKey}.${varName} = ${varName}`)
}
s.remove(node.start!, node.end!)
}
// export { name, age }
else {
for (const specifier of node.specifiers) {
const varName = specifier.local.name
s.append(`\n ${moduleKey}.${varName} = ${varName}`)
}
s.remove(node.start!, node.end!)
}
},
ImportDeclaration(path: NodePath<ImportDeclaration>) {
const { node } = path
const importDefault = node.specifiers.find(specifier => specifier.type === 'ImportDefaultSpecifier')
if (node.source.value.startsWith('./')) {
const modulePath = node.source.value.replace(/^\.\//, '')
importedFiles.add(modulePath)
if (importDefault) {
// import foo from 'xx'
const moduleName = importDefault.local.name
s.overwrite(node.start!, node.end!, `const ${moduleName} = ${modulesKey}["${modulePath}"].default\n`)
}else {
const nameSpaceObj = node.specifiers.find(specifier => specifier.type === 'ImportNamespaceSpecifier')
if (nameSpaceObj) {
// import * as xx from 'xx'
s.overwrite(node.start!, node.end!, `const ${nameSpaceObj.local.name} = ${modulesKey}["${modulePath}"]`)
} else {
// import { foo } from 'xx'
const expandNames = node.specifiers?.map(specifier => specifier.local.name)
if (expandNames && expandNames.length !== 0) s.overwrite(node.start!, node.end!, `const { ${expandNames.join(',')} } = ${modulesKey}["${modulePath}"]`)
}
}
}
}
}
}
}]
}).code!
code = transform(s.toString(), {
presets: ['react'],
}).code!
code = `const ${moduleKey} = ${modulesKey}[${JSON.stringify(
filename,
)}] = { [Symbol.toStringTag]: "Module" }\n\n` + code
return {
code,
importedFiles
}
}