Created
May 5, 2024 15:36
-
-
Save saiashirwad/ac81ae4a3529733471f593e6f8c9344d to your computer and use it in GitHub Desktop.
cursed
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { FileSystem } from '@effect/platform'; | |
import { NodeFileSystem, NodeRuntime } from '@effect/platform-node'; | |
import * as Codegen from '@sinclair/typebox-codegen'; | |
import { Console, Context, Effect, Layer, Ref, Stream, pipe } from 'effect'; | |
import { compose } from 'effect/Function'; | |
import { kebabToSnake, snakeToPascal } from 'effect/String'; | |
import type { InternalRoute } from 'elysia'; | |
import * as fs from 'node:fs/promises'; | |
import ts from 'typescript'; | |
import { elysiaRouter } from '~/elysia.router'; | |
const path = 'src/elysia-routes'; | |
const replacements = [ | |
{ from: 'VehicleCapabilityOption', to: '{ label: string; value: string }' }, | |
]; | |
export const generateElysiaFileTypes = ( | |
sourceFile: ts.SourceFile, | |
checker: ts.TypeChecker, | |
) => { | |
return Effect.gen(function* () { | |
const validator: [string, string] = yield* Effect.fromNullable( | |
getTypeboxType(sourceFile, checker), | |
); | |
const validatorFileName = sourceFile.fileName.replace('.ts', '-type.ts'); | |
yield* Effect.tryPromise(() => Bun.write(validatorFileName, validator[1])); | |
return validator[0]; | |
}); | |
}; | |
const getTypeboxType = ( | |
sourceFile: ts.SourceFile, | |
checker: ts.TypeChecker, | |
): [string, string] | undefined => { | |
let validator: string | undefined; | |
let nodeName: string | undefined; | |
let result: [string, string] | undefined; | |
ts.forEachChild(sourceFile, (node) => { | |
if (ts.isTypeAliasDeclaration(node)) { | |
const type = checker.getTypeAtLocation(node.name); | |
let typeString = `type ${node.name.text}TypeBox = ${checker.typeToString( | |
type, | |
undefined, | |
ts.TypeFormatFlags.NoTruncation | | |
ts.TypeFormatFlags.MultilineObjectLiterals | | |
ts.TypeFormatFlags.WriteClassExpressionAsTypeLiteral | | |
ts.TypeFormatFlags.WriteTypeArgumentsOfSignature, | |
)}`; | |
for (const { from, to } of replacements) { | |
typeString = typeString.replaceAll(from, to); | |
} | |
validator = Codegen.TypeScriptToTypeBox.Generate(typeString, { | |
useExportEverything: true, | |
}); | |
result = [node.name.text, validator]; | |
} | |
}); | |
return result; | |
}; | |
export const createProgram = Effect.gen(function* () { | |
const configPath = yield* Effect.fromNullable( | |
ts.findConfigFile('.', ts.sys.fileExists, 'tsconfig.json'), | |
); | |
const configFile = ts.readConfigFile(configPath, ts.sys.readFile); | |
const parsedCommandLine = ts.parseJsonConfigFileContent( | |
configFile.config, | |
ts.sys, | |
'.', | |
); | |
return ts.createProgram( | |
parsedCommandLine.fileNames, | |
parsedCommandLine.options, | |
); | |
}); | |
export const getValidElysiaMethods = () => | |
pipe( | |
Effect.promise(() => fs.readdir(path)), | |
Effect.flatMap((files) => Effect.all(files.map(checkFileValidity))), | |
Effect.map((files) => { | |
return files | |
.filter(([_, isValid]) => isValid) | |
.map(([filename]) => filename); | |
}), | |
); | |
const checkFileValidity = (fileName: string) => | |
Effect.gen(function* () { | |
const contents = yield* Effect.promise(() => | |
Bun.file(`${path}/${fileName}`).text(), | |
); | |
return [fileName, contents.includes('ElysiaReturnType<')] as [ | |
string, | |
boolean, | |
]; | |
}); | |
const EnvLive = NodeFileSystem.layer.pipe(Layer.provide(NodeFileSystem.layer)); | |
const PATH = 'src/elysia-routes'; | |
type FileHashes = Record<string, number | bigint>; | |
class FileHashesState extends Context.Tag('FileHashes')< | |
FileHashesState, | |
Ref.Ref<Record<string, number | bigint>> | |
>() {} | |
class HashUnchangedError { | |
_tag = 'HashUnchangedError'; | |
} | |
class IsTypeFileError { | |
_tag = 'IsTypeFileError'; | |
} | |
const getFileHashes = (sem: Effect.Semaphore) => | |
sem.withPermits(1)( | |
Effect.gen(function* () { | |
return yield* FileHashesState; | |
}), | |
); | |
const generateElysiaType = (path: string) => | |
Effect.gen(function* () { | |
const program = yield* createProgram; | |
const checker = program.getTypeChecker(); | |
const source: ts.SourceFile = yield* Effect.fromNullable( | |
program.getSourceFile(path), | |
); | |
yield* generateElysiaFileTypes(source, checker); | |
const typesFilePath = path.replace('.ts', '-type.ts'); | |
yield* Effect.promise( | |
() => Bun.$`bun biome lint --apply-unsafe ${typesFilePath}`, | |
); | |
yield* Effect.promise( | |
() => Bun.$`bun biome format --write ${typesFilePath}`, | |
); | |
}); | |
const getFileHash = (filename: string, fileHashes: Ref.Ref<FileHashes>) => | |
Effect.gen(function* () { | |
const fs = yield* FileSystem.FileSystem; | |
const contents = yield* fs.readFileString(filename); | |
const hash = Bun.hash(contents); | |
const fileHashesMap: FileHashes = yield* Ref.get(fileHashes); | |
const prevHash = fileHashesMap[filename]; | |
if (prevHash && prevHash === hash) { | |
yield* Effect.fail(new HashUnchangedError()); | |
} | |
yield* Ref.update(fileHashes, (fileHashes) => ({ | |
...fileHashes, | |
[filename]: hash, | |
})); | |
return hash; | |
}); | |
const kebabToPascal = compose(kebabToSnake, snakeToPascal); | |
const generateSwaggerDocs = () => | |
Effect.gen(function* () { | |
const validFileNames = yield* getValidElysiaMethods(); | |
const fileNamesMap = new Map<string, string>(); | |
for (const validFileName of validFileNames) { | |
const name = validFileName.split('.ts')[0]!; | |
fileNamesMap.set(kebabToPascal(name), name); | |
} | |
// ugh | |
for (let i = 0; i < elysiaRouter.routes.length; i++) { | |
const route = elysiaRouter.routes[i]; | |
const tags = route?.hooks.detail.tags; | |
if (!tags) continue; | |
// @ts-ignore | |
const pascalName: string = tags[0]; | |
// @ts-ignore | |
const kebabName = fileNamesMap.get(route?.hooks.detail.tags[0]); | |
if (kebabName && pascalName) { | |
const filePath = `./src/elysia-routes/${kebabName}-type.ts`; | |
const types = require(filePath); | |
const schema = types[`${pascalName}TypeBox`]; | |
Object.assign(elysiaRouter.routes[i]!.hooks, { | |
response: schema, | |
}); | |
} | |
} | |
const swaggerJsonRoute: InternalRoute = yield* Effect.fromNullable( | |
elysiaRouter.routes.find((route) => route.path === '/swagger/json'), | |
); | |
// @ts-ignore | |
const openApiSchema: string = yield* Effect.fromNullable( | |
// @ts-ignore | |
swaggerJsonRoute?.handler(), | |
); | |
yield* Effect.promise(() => | |
Bun.write( | |
'driver_app/swagger.json', | |
JSON.stringify(openApiSchema, null, 2), | |
), | |
); | |
yield* Console.log('Swagger docs generated!'); | |
yield* Effect.promise(() => Bun.$`bun swagger`); | |
}); | |
const main = () => | |
Effect.gen(function* ($) { | |
const fs = yield* FileSystem.FileSystem; | |
const mutex = yield* Effect.makeSemaphore(1); | |
yield* $( | |
fs.watch(PATH), | |
Stream.runForEach((event) => | |
Effect.gen(function* () { | |
const fileHashes = yield* getFileHashes(mutex); | |
const path = yield* fs | |
.realPath(`${PATH}/${event.path}`) | |
.pipe( | |
Effect.flatMap((path) => | |
path.endsWith('type.ts') | |
? Effect.fail(new IsTypeFileError()) | |
: Effect.succeed(path), | |
), | |
); | |
yield* getFileHash(path, fileHashes); | |
yield* generateElysiaType(path); | |
yield* generateSwaggerDocs(); | |
}).pipe( | |
Effect.catchTag('HashUnchangedError', () => Console.log('')), | |
Effect.catchAll(() => Effect.void), | |
), | |
), | |
); | |
}).pipe( | |
Effect.provide(EnvLive), | |
Effect.provideServiceEffect(FileHashesState, Ref.make({})), | |
NodeRuntime.runMain, | |
); | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment