Skip to content

Instantly share code, notes, and snippets.

@saiashirwad
Created May 5, 2024 15:36
Show Gist options
  • Save saiashirwad/ac81ae4a3529733471f593e6f8c9344d to your computer and use it in GitHub Desktop.
Save saiashirwad/ac81ae4a3529733471f593e6f8c9344d to your computer and use it in GitHub Desktop.
cursed
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