WIP: create ts-patch tsc transformers for mapping interface -> zodified schemata

This commit is contained in:
2022-01-14 01:03:50 -06:00
parent bffa4c5b27
commit 87daee7c06
6 changed files with 429 additions and 5 deletions

View File

@@ -15,9 +15,11 @@ export class CompilePhase extends Phase {
}
public run(): Promise<void> {
const dir = this.config.compileDir || 'exbuild'
Logger.verb('tsc', 'transpile sources')
Logger.verb('++', `tsc -p ${path.resolve(this.tsconfigPath)}`)
const tsc = childProcess.spawn('tsc', ['-p', path.resolve(this.tsconfigPath)])
Logger.verb('++', `tsc -p ${path.resolve(dir, this.tsconfigPath)}`)
const tsc = childProcess.spawn('./node_modules/.bin/tsc', ['-p', path.resolve(dir, this.tsconfigPath)])
tsc.stdout.on('data', (output: Buffer) => {
Logger.info('tsc', output.toString('utf-8'))

View File

@@ -29,12 +29,37 @@ export class PreparePhase extends Phase {
await fse.copy(src, path.join(dir, src))
}
const transformerPath = path.resolve(__dirname, '..', 'transformer.js')
Logger.verb('prepare', `copy ${transformerPath}`)
await fse.copy(transformerPath, path.join(dir, 'transformer.js'))
const tsconfig = rfdc()(this.tsconfig)
const outDir = tsconfig?.compilerOptions?.outDir || './lib'
if ( !tsconfig.compilerOptions ) {
tsconfig.compilerOptions = {}
}
if ( !tsconfig.compilerOptions.plugins ) {
tsconfig.compilerOptions.plugins = []
}
tsconfig.compilerOptions.plugins.push({
transform: './transformer.js',
import: 'identifierProgram',
})
tsconfig.compilerOptions.plugins.push({
transform: './transformer.js',
import: 'transformProgram',
transformProgram: true,
})
/* tsconfig.compilerOptions.plugins.push({
transform: './transformer.js',
import: 'transformerProgram',
after: true,
})*/
tsconfig.compilerOptions.outDir = path.join('..', outDir)
fse.writeFileSync(path.join(dir, 'tsconfig.json'), JSON.stringify(tsconfig, undefined, 4))

View File

@@ -17,6 +17,8 @@ async function* walk(dir: string): any {
}
export class ZodifyPhase extends Phase {
private static zodId = 1
constructor(
config: ExtolloCompileConfig,
tsconfig: any,
@@ -68,11 +70,21 @@ export class ZodifyPhase extends Phase {
lines.forEach((lineVal, idx) => {
if ( idx === line ) {
newlines.push('import { z } from "zod";')
newlines.push('import { registerZodifiedSchema } from "@extollo/lib";')
}
newlines.push(lineVal)
})
return [...newlines, ...zodSource.split('\n').slice(2)].join('\n')
const id = ZodifyPhase.zodId++
const zodLines = [
`/** @ex-zod-id ${id}@ */`,
...zodSource.split('\n')
.filter(x => x.trim())
.slice(2),
`registerZodifiedSchema(${id}, exZodifiedSchema);`,
]
return [...newlines, ...zodLines].join('\n')
}
}

274
src/transformer.ts Normal file
View File

@@ -0,0 +1,274 @@
import * as ts from 'typescript'
import {} from 'ts-expose-internals'
const interfaceDeclarations = [] as ts.InterfaceDeclaration[]
const zodifiedIds = [] as string[]
const identifierProgram = (program: ts.Program, options: any) => {
return (context: ts.TransformationContext) => {
const factory = context.factory
return (sourceFile: ts.SourceFile) => {
console.log(`TT: ${sourceFile.fileName} : ${interfaceDeclarations.length}`) // eslint-disable-line no-console
const visitor = (node: ts.Node): ts.Node => {
if ( ts.isNewExpression(node) ) {
const exp = node as ts.NewExpression
console.log(`New Expression: ${exp.expression.getText()}`) // eslint-disable-line no-console
const schema = [] as number[]
for ( const type of exp.typeArguments ?? [] ) {
console.log(`Type symbol: ${type.localSymbol?.name} : ${type.id} : ${type.getText()}`) // eslint-disable-line no-console
for ( const decl of interfaceDeclarations ) {
if ( decl.name.symbol === (type as any).typeName.symbol ) {
console.log(`Match to interface: ${decl.name.text}`) // eslint-disable-line no-console
console.log(decl) // eslint-disable-line no-console
const id = parseInt(zodifiedIds[interfaceDeclarations.indexOf(decl)], 10)
if ( !isNaN(id) ) {
schema.push(id)
}
}
}
}
console.log(schema) // eslint-disable-line no-console
const call = factory.createCallExpression(
factory.createParenthesizedExpression(factory.createArrowFunction(
undefined,
undefined,
[],
undefined,
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
factory.createBlock(
[
factory.createVariableStatement(
undefined,
factory.createVariableDeclarationList(
[factory.createVariableDeclaration(
factory.createIdentifier('vI'),
undefined,
undefined,
node,
)],
ts.NodeFlags.Const,
),
),
factory.createExpressionStatement(factory.createBinaryExpression(
factory.createPropertyAccessExpression(
factory.createParenthesizedExpression(factory.createAsExpression(
factory.createIdentifier('vI'),
factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
)),
factory.createIdentifier('__exZodifiedSchemata'),
),
factory.createToken(ts.SyntaxKind.EqualsToken),
factory.createArrayLiteralExpression(
schema.map(x => factory.createNumericLiteral(x)),
false,
),
)),
factory.createReturnStatement(factory.createIdentifier('vI')),
],
true,
),
)),
undefined,
[],
)
return call
}
return ts.visitEachChild(node, visitor, context)
}
return ts.visitEachChild(sourceFile, visitor, context)
}
}
}
function transformAst(this: typeof ts, context: ts.TransformationContext) {
// eslint-disable-next-line @typescript-eslint/no-this-alias,no-invalid-this
const tsInstance = this
return (sourceFile: ts.SourceFile) => {
if ( sourceFile.fileName.endsWith('.d.ts') ) {
return sourceFile
}
console.log(`ID: ${sourceFile.fileName}`) // eslint-disable-line no-console
const visit = (node: ts.Node): ts.Node => {
if ( ts.isInterfaceDeclaration(node) ) {
console.log(`Interface Declaration: ${(node as ts.InterfaceDeclaration).name}`) // eslint-disable-line no-console
if ( sourceFile.identifiers.has('exZodifiedSchema') ) {
const id = [...(sourceFile as any).statements].reverse()[0].expression.arguments[0].text
console.log('Zod-ID: ' + id) // eslint-disable-line no-console
interfaceDeclarations.push(node)
zodifiedIds.push(id)
}
}
return tsInstance.visitEachChild(node, visit, context)
}
return tsInstance.visitEachChild(sourceFile, visit, context)
}
}
/**
* Patches existing Compiler Host (or creates new one) to allow feeding updated file content from cache
*/
function getPatchedHost(
maybeHost: ts.CompilerHost | undefined,
tsInstance: typeof ts,
compilerOptions: ts.CompilerOptions,
): ts.CompilerHost & { fileCache: Map<string, ts.SourceFile> } {
const fileCache = new Map()
const compilerHost = maybeHost ?? tsInstance.createCompilerHost(compilerOptions, true)
const originalGetSourceFile = compilerHost.getSourceFile
return Object.assign(compilerHost, {
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget) {
fileName = tsInstance.normalizePath(fileName)
if (fileCache.has(fileName)) {
return fileCache.get(fileName)
}
// eslint-disable-next-line prefer-spread,prefer-rest-params
const sourceFile = originalGetSourceFile.apply(void 0, Array.from(arguments) as any)
fileCache.set(fileName, sourceFile)
return sourceFile
},
fileCache,
})
}
const transformProgram = (program: ts.Program, host: ts.CompilerHost | undefined, options: any, { ts: tsInstance }: any) => {
const compilerOptions = program.getCompilerOptions()
const compilerHost = getPatchedHost(host, tsInstance, compilerOptions)
const rootFileNames = program.getRootFileNames().map(tsInstance.normalizePath)
// transform ast
const transformedSource = tsInstance.transform(
[...program.getSourceFiles()],
[transformAst.bind(tsInstance)],
compilerOptions,
).transformed
return program
/* /!* Render modified files and create new SourceFiles for them to use in host's cache *!/
const { printFile } = tsInstance.createPrinter()
for (const sourceFile of transformedSource) {
const { fileName, languageVersion } = sourceFile
const updatedSourceFile = tsInstance.createSourceFile(fileName, printFile(sourceFile), languageVersion)
compilerHost.fileCache.set(fileName, updatedSourceFile)
}
/!* Re-create Program instance *!/
return tsInstance.createProgram(rootFileNames, compilerOptions, compilerHost)*/
}
const identifierProgramOld = (program: ts.Program) => {
const transformerFactory: ts.TransformerFactory<ts.SourceFile> = context => {
const factory = context.factory
return sourceFile => {
console.log(`ID Source: ${sourceFile.fileName}`) // eslint-disable-line no-console
const visitor = (node: ts.Node): ts.Node => {
if (ts.isInterfaceDeclaration(node)) {
console.log('Found Interface: ' + (node as ts.InterfaceDeclaration).name.text) // eslint-disable-line no-console
interfaceDeclarations.push(node)
}
return ts.visitEachChild(node, visitor, context)
}
return ts.visitNode(sourceFile, visitor)
}
}
return transformerFactory
}
/* const transformerProgram = (program: ts.Program) => {
// const typeChecker = program.getTypeChecker()
// Create array of found symbols
// const foundSymbols = new Array<ts.Symbol>()
const typeReferences = [] as ts.TypeReference[]
const newExpressions = [] as ts.NewExpression[]
const transformerFactory: ts.TransformerFactory<ts.SourceFile> = context => {
const factory = context.factory
return sourceFile => {
console.log(`TT Source: ${sourceFile.fileName} : ${interfaceDeclarations.length}`) // eslint-disable-line no-console
const visitor = (node: ts.Node): ts.Node => {
if ( ts.isNewExpression(node) ) {
const exp = node as ts.NewExpression
console.log('New Expression: ' + exp.expression.getText() + ' w/ type args: ' + exp.typeArguments?.[0].getText()) // eslint-disable-line no-console
// newExpressions.push(node)
const call = factory.createCallExpression(
factory.createParenthesizedExpression(factory.createArrowFunction(
undefined,
undefined,
[],
undefined,
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
factory.createBlock(
[
factory.createVariableStatement(
undefined,
factory.createVariableDeclarationList(
[factory.createVariableDeclaration(
factory.createIdentifier('vI'),
undefined,
undefined,
exp,
)],
ts.NodeFlags.Const,
),
),
factory.createExpressionStatement(factory.createBinaryExpression(
factory.createPropertyAccessExpression(
factory.createParenthesizedExpression(factory.createAsExpression(
factory.createIdentifier('vI'),
factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
)),
factory.createIdentifier('__exSchema'),
),
factory.createToken(ts.SyntaxKind.EqualsToken),
factory.createObjectLiteralExpression(
[],
false,
),
)),
factory.createReturnStatement(factory.createIdentifier('vI')),
],
true,
),
)),
undefined,
[],
)
console.log('returned call expression') // eslint-disable-line no-console
return call
}
return ts.visitEachChild(node, visitor, context)
}
return ts.visitNode(sourceFile, visitor)
}
}
return transformerFactory
}*/
export {identifierProgram, transformProgram}