Skip to content

Instantly share code, notes, and snippets.

@bigmistqke
Last active February 12, 2024 14:05
Show Gist options
  • Save bigmistqke/d42395f8b889cc8e586a26a2468492b9 to your computer and use it in GitHub Desktop.
Save bigmistqke/d42395f8b889cc8e586a26a2468492b9 to your computer and use it in GitHub Desktop.
dynamic esm module tag template literal with typescript-support
import ts from 'typescript'
type Accessor<T> = () => T
const tsModules: Record<string, { content: string; version: number }> = {}
function modifyImportPaths(code: string) {
return code.replace(/import ([^"']+) from ["']([^"']+)["']/g, (match, varName, path) => {
if (path.startsWith('blob:') || path.startsWith('http:') || path.startsWith('https:') || path.startsWith('.')) {
return `import ${varName} from "${path}"`
} else {
return `import ${varName} from "https://esm.sh/${path}"`
}
})
}
const load = (url: Accessor<string>) => import(url())
class TsNode {
id: string
path: Accessor<string>
code: {
js: string
ts: string
}
private _module: any
constructor({
id,
path,
code,
module,
}: {
id: string
path: Accessor<string>
code: {
js: Accessor<string>
ts: Accessor<string>
}
module: any
}) {
this.id = id
this.path = path
this.code = {
get js() {
return code.js()
},
get ts() {
return code.ts()
},
}
this._module = module
}
get module() {
return this._module
}
getType(symbolName: string) {
return getType(this.id, symbolName)
}
}
let _id = 0
export function typescript(code: string): Promise<TsNode>
export function typescript(strings: TemplateStringsArray, ...holes: (TsNode | Accessor<any>)[]): Promise<TsNode>
export async function typescript(
strings: TemplateStringsArray | string,
...holes: (TsNode | Accessor<any>)[]
): Promise<TsNode> {
const id = _id + '.ts'
_id++
const jsCode = () =>
transpileToJS(
modifyImportPaths(
typeof strings === 'string'
? strings
: strings.reduce((acc, str, idx) => {
const hole = holes[idx]
const result = hole instanceof TsNode ? hole.path() : hole?.()
return acc + str + (result || '')
}, '')
)
)
const tsCode = () =>
typeof strings === 'string'
? strings
: strings.reduce((acc, str, idx) => {
const hole = holes[idx]
const result = hole instanceof TsNode ? hole.id : hole?.()
return acc + str + (result || '')
}, '')
// Transpile TypeScript to JavaScript
const transpileToJS = (tsCode: string) => {
const result = ts.transpileModule(tsCode, {
compilerOptions: {
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
},
})
return result.outputText
}
const path = () => {
updateModule(id, tsCode())
const url = URL.createObjectURL(
new Blob([jsCode()], {
type: 'application/javascript',
})
)
/* onCleanup(() => URL.revokeObjectURL(url)) */
return url
}
const module = await load(path)
return new TsNode({ id, path, code: { js: jsCode, ts: tsCode }, module })
}
function updateModule(fileName: string, newContent: string) {
const file = tsModules[fileName]
if (!file) {
tsModules[fileName] = { content: newContent, version: 0 }
} else {
file.content = newContent
file.version++
}
}
const servicesHost: ts.LanguageServiceHost = {
getScriptFileNames: () => Object.keys(tsModules),
getScriptVersion: (fileName) => tsModules[fileName].version.toString(),
getScriptSnapshot: (fileName) => {
return ts.ScriptSnapshot.fromString(tsModules[fileName].content)
},
getCurrentDirectory: () => '/',
getCompilationSettings: () => ({
noLib: true,
allowJs: true,
target: ts.ScriptTarget.Latest,
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
}),
getDefaultLibFileName: () => 'lib.d.ts',
fileExists: (fileName) => !!tsModules[fileName],
readFile: (fileName: string) => tsModules[fileName].content || '',
resolveModuleNames(moduleNames) {
return moduleNames.map((name) => {
// If we're dealing with relative paths, normalize them
if (name.startsWith('./')) {
name = name.substring(2)
}
if (name.startsWith('./')) {
name = name.substring(2)
}
if (tsModules[name]) {
return {
resolvedFileName: name,
extension: '.ts',
}
}
return undefined // The module couldn't be resolved
})
},
}
const languageService = ts.createLanguageService(servicesHost, ts.createDocumentRegistry())
function getType(fileName: string, symbolName: string) {
const program = languageService.getProgram()!
const sourceFile = program.getSourceFile(fileName)!
const checker = program.getTypeChecker()
function findNodeByName(sourceFile: ts.SourceFile, name: string) {
let foundNode = null
function visit(node: ts.SourceFile | ts.Node) {
if (ts.isIdentifier(node) && node.getText() === name) {
foundNode = node
return
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return foundNode
}
const node = findNodeByName(sourceFile, symbolName)!
const type = checker.getTypeAtLocation(node)
return checker.typeToString(type)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment