From 7887d5dbb8cc3027d87d19b95ba1564af655054f Mon Sep 17 00:00:00 2001 From: Darius Jankauskas Date: Fri, 14 Jul 2023 18:54:35 -0400 Subject: [PATCH] feature: generate prettified code, ignore generated code in watch mode --- demo/stl-api-gen/api/posts/create.ts | 15 +++++- demo/stl-api-gen/api/posts/models.ts | 23 ++++++++- demo/stl-api-gen/api/posts/retrieve.ts | 25 ++++++++-- demo/stl-api-gen/index.ts | 7 ++- packages/cli/package.json | 2 + packages/cli/src/format.ts | 23 +++++++++ packages/cli/src/index.ts | 49 ++++++++++--------- packages/cli/src/watch.ts | 14 +++--- .../src/__tests__/multiFileTestCase.ts | 5 +- packages/ts-to-zod/src/filePathConfig.ts | 4 +- packages/ts-to-zod/src/generateFiles.ts | 10 ++-- pnpm-lock.yaml | 15 +++++- 12 files changed, 142 insertions(+), 50 deletions(-) create mode 100644 packages/cli/src/format.ts diff --git a/demo/stl-api-gen/api/posts/create.ts b/demo/stl-api-gen/api/posts/create.ts index 1ba51e8f..2c242bbf 100644 --- a/demo/stl-api-gen/api/posts/create.ts +++ b/demo/stl-api-gen/api/posts/create.ts @@ -1,6 +1,17 @@ import { z } from "stainless"; import { PostType as __symbol_PostType } from "./create"; -export const Query: z.ZodTypeAny = z.object({ include: z.includes(z.lazy(() => __symbol_PostType), 3).optional() }); +export const Query: z.ZodTypeAny = z.object({ + include: z + .includes( + z.lazy(() => __symbol_PostType), + 3 + ) + .optional(), +}); export const Body: z.ZodTypeAny = z.object({ body: z.string() }); export const PostType: z.ZodTypeAny = z.any(); -export const post__api_posts: any = { query: z.lazy(() => Query), body: z.lazy(() => Body), response: z.lazy(() => __symbol_PostType) }; +export const post__api_posts: any = { + query: z.lazy(() => Query), + body: z.lazy(() => Body), + response: z.lazy(() => __symbol_PostType), +}; diff --git a/demo/stl-api-gen/api/posts/models.ts b/demo/stl-api-gen/api/posts/models.ts index 656b5bd0..aa88cef4 100644 --- a/demo/stl-api-gen/api/posts/models.ts +++ b/demo/stl-api-gen/api/posts/models.ts @@ -1,4 +1,23 @@ import { z } from "stainless"; -import { IncludableUserSchema, SelectableUserSchema, IncludableCommentsSchema, IncludableCommentsFieldSchema } from "../../../api/posts/models"; +import { + IncludableUserSchema, + SelectableUserSchema, + IncludableCommentsSchema, + IncludableCommentsFieldSchema, +} from "../../../api/posts/models"; import { prisma } from "../../../libs/prismadb"; -export const PostType: z.ZodTypeAny = z.object({ id: z.string().uuid(), body: z.string(), createdAt: z.date(), updatedAt: z.date(), userId: z.string().uuid(), likedIds: z.array(z.string().uuid()), image: z.string().nullable().optional(), user: z.lazy(() => IncludableUserSchema), user_fields: z.lazy(() => SelectableUserSchema), comments: z.lazy(() => IncludableCommentsSchema), comments_field: z.lazy(() => IncludableCommentsFieldSchema) }).prismaModel(prisma.post); +export const PostType: z.ZodTypeAny = z + .object({ + id: z.string().uuid(), + body: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + userId: z.string().uuid(), + likedIds: z.array(z.string().uuid()), + image: z.string().nullable().optional(), + user: z.lazy(() => IncludableUserSchema), + user_fields: z.lazy(() => SelectableUserSchema), + comments: z.lazy(() => IncludableCommentsSchema), + comments_field: z.lazy(() => IncludableCommentsFieldSchema), + }) + .prismaModel(prisma.post); diff --git a/demo/stl-api-gen/api/posts/retrieve.ts b/demo/stl-api-gen/api/posts/retrieve.ts index 3c2a8cef..6989d0cf 100644 --- a/demo/stl-api-gen/api/posts/retrieve.ts +++ b/demo/stl-api-gen/api/posts/retrieve.ts @@ -1,7 +1,26 @@ import { z } from "stainless"; import { PostType as __symbol_PostType } from "./retrieve"; import { prisma } from "../../../libs/prismadb"; -export const Query: z.ZodTypeAny = z.object({ include: z.includes(z.lazy(() => __symbol_PostType), 3).optional(), select: z.selects(z.lazy(() => __symbol_PostType), 3).optional() }); -export const Path: z.ZodTypeAny = z.object({ post: z.string().prismaModelLoader(prisma.post) }); +export const Query: z.ZodTypeAny = z.object({ + include: z + .includes( + z.lazy(() => __symbol_PostType), + 3 + ) + .optional(), + select: z + .selects( + z.lazy(() => __symbol_PostType), + 3 + ) + .optional(), +}); +export const Path: z.ZodTypeAny = z.object({ + post: z.string().prismaModelLoader(prisma.post), +}); export const PostType: z.ZodTypeAny = z.any(); -export const get__api_posts_$post$: any = { query: z.lazy(() => Query), path: z.lazy(() => Path), response: z.lazy(() => __symbol_PostType) }; +export const get__api_posts_$post$: any = { + query: z.lazy(() => Query), + path: z.lazy(() => Path), + response: z.lazy(() => __symbol_PostType), +}; diff --git a/demo/stl-api-gen/index.ts b/demo/stl-api-gen/index.ts index 99c5a758..a838c216 100644 --- a/demo/stl-api-gen/index.ts +++ b/demo/stl-api-gen/index.ts @@ -1 +1,6 @@ -export const typeSchemas = { "post /api/posts": () => import("./api/posts/create").then(mod => mod.post__api_posts), "get /api/posts/{post}": () => import("./api/posts/retrieve").then(mod => mod.get__api_posts_$post$) }; +export const typeSchemas = { + "post /api/posts": () => + import("./api/posts/create").then((mod) => mod.post__api_posts), + "get /api/posts/{post}": () => + import("./api/posts/retrieve").then((mod) => mod.get__api_posts_$post$), +}; diff --git a/packages/cli/package.json b/packages/cli/package.json index c79fbc84..0395842f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -30,6 +30,7 @@ "commander": "^11.0.0", "lodash": "^4.17.21", "pkg-up": "3.1", + "resolve": "^1.22.2", "ts-morph": "^19.0.0", "ts-to-zod": "workspace:*" }, @@ -37,6 +38,7 @@ "@swc/core": "^1.3.66", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/resolve": "^1.20.2", "jest": "^29.5.0", "jest-watch-typeahead": "^2.2.2", "nodemon": "^2.0.22", diff --git a/packages/cli/src/format.ts b/packages/cli/src/format.ts new file mode 100644 index 00000000..0bd6bcb4 --- /dev/null +++ b/packages/cli/src/format.ts @@ -0,0 +1,23 @@ +import { promisify } from "util"; +import path from "path"; +import resolve from "resolve"; +import defaultPrettier from "prettier"; + +export async function format( + source: string, + filepath: string +): Promise { + let prettier = defaultPrettier; + + try { + const prettierPath = await promisify((cb) => + resolve("prettier", { basedir: path.dirname(filepath) }, cb) + )(); + if (prettierPath) { + prettier = await import(prettierPath); + } + } catch (error) {} + + const config = await prettier.resolveConfig(filepath); + return prettier.format(source, config || { filepath }); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ca0ca1a5..4b55133d 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -31,6 +31,7 @@ import { import { GenOptions, + GenerationConfig, createGenerationConfig, } from "ts-to-zod/dist/filePathConfig"; @@ -48,6 +49,7 @@ import { mangleRouteToIdentifier, statOrExit, } from "./utils"; +import { format } from "./format"; // TODO: add dry run functionality? argParser.option("-w, --watch", "enables watch mode"); @@ -106,9 +108,19 @@ async function main() { console.error(`Error: '${tsConfigFilePath}' is not a file.`); } + const generationOptions = { + genLocation: { + type: "folder", + genPath: FOLDER_GEN_PATH, + }, + rootPath, + zPackage: "stainless", + } as const; + const generationConfig = createGenerationConfig(generationOptions); + let watcher: Watcher | undefined; if (options.watch) { - watcher = new Watcher(rootPath); + watcher = new Watcher(rootPath, generationConfig.basePath); } const project = @@ -120,21 +132,12 @@ async function main() { const baseCtx = new SchemaGenContext(project); - const generationOptions = { - genLocation: { - type: "folder", - genPath: FOLDER_GEN_PATH, - }, - rootPath, - zPackage: "stainless", - } as const; - const printer = tm.ts.createPrinter(); const succeeded = await evaluate( project, baseCtx, - generationOptions, + generationConfig, rootPath, printer ); @@ -147,7 +150,7 @@ async function main() { const succeeded = await evaluate( watcher.project, watcher.baseCtx, - generationOptions, + generationConfig, rootPath, printer ); @@ -182,12 +185,10 @@ function generateIncidentLocation( async function evaluate( project: tm.Project, baseCtx: SchemaGenContext, - generationOptions: GenOptions, + generationConfig: GenerationConfig, rootPath: string, printer: ts.Printer ): Promise { - const generationConfig = createGenerationConfig(generationOptions); - // accumulated diagnostics to emit const callDiagnostics: CallDiagnostics[] = []; // every stl.types.endpoint call found per file @@ -365,8 +366,7 @@ async function evaluate( imports.set(name, { ...importInfo, sourceFile: Path.join( - rootPath, - FOLDER_GEN_PATH, + generationConfig.basePath, Path.relative(rootPath, importInfo.sourceFile) ), }); @@ -379,7 +379,6 @@ async function evaluate( let importDeclarations = generateImportStatements( generationConfig, file.getFilePath(), - "stainless", imports, namespacedImports ); @@ -426,7 +425,7 @@ async function evaluate( // Commit all operations potentially destructive to AST visiting. fileOperations.forEach((op) => op()); - const generatedFileContents = generateFiles(baseCtx, generationOptions); + const generatedFileContents = generateFiles(baseCtx, generationConfig); if (callDiagnostics.length) { const output = []; @@ -511,8 +510,7 @@ async function evaluate( if (endpointCalls.size) { const mapEntries = []; - const genPath = Path.join(rootPath, FOLDER_GEN_PATH); - const endpointMapGenPath = Path.join(genPath, "index.ts"); + const endpointMapGenPath = Path.join(generationConfig.basePath, "index.ts"); for (const [file, calls] of endpointCalls) { for (const call of calls) { @@ -578,11 +576,11 @@ async function evaluate( 0 ); - await fs.promises.mkdir(genPath, { recursive: true }); + await fs.promises.mkdir(generationConfig.basePath, { recursive: true }); await fs.promises.writeFile( endpointMapGenPath, - printer.printFile(mapSourceFile) + await format(printer.printFile(mapSourceFile), endpointMapGenPath) ); } @@ -600,7 +598,10 @@ async function evaluate( ); // write sourceFile to file - await fs.promises.writeFile(file, printer.printFile(sourceFile)); + await fs.promises.writeFile( + file, + await format(printer.printFile(sourceFile), file) + ); } project.save(); diff --git a/packages/cli/src/watch.ts b/packages/cli/src/watch.ts index e59b28af..34fe28ab 100644 --- a/packages/cli/src/watch.ts +++ b/packages/cli/src/watch.ts @@ -1,4 +1,4 @@ -import chokidar, {FSWatcher} from "chokidar"; +import chokidar, { FSWatcher } from "chokidar"; import * as tm from "ts-morph"; import path from "path"; import { SchemaGenContext } from "ts-to-zod/dist/convertType"; @@ -46,7 +46,7 @@ export class Watcher { } } - constructor(rootPath: string) { + constructor(rootPath: string, genFolderPath: string) { this.tsConfigFilePath = path.join(rootPath, "tsconfig.json"); this.project = new tm.Project({ tsConfigFilePath: this.tsConfigFilePath, @@ -54,7 +54,9 @@ export class Watcher { this.baseCtx = new SchemaGenContext(this.project); - this.watcher = chokidar.watch(path.join(rootPath, "**")); + this.watcher = chokidar.watch(path.join(rootPath, "**"), { + ignored: path.join(genFolderPath, "**") + }); this.watcher.on("ready", () => { console.log("Watching for file changes..."); this.ready = true; @@ -96,7 +98,7 @@ export class Watcher { if (sourceFile) { this.project.removeSourceFile(sourceFile); } - this.pushEvent({path, type: "unlink"}); + this.pushEvent({ path, type: "unlink" }); }); // handle an error occuring during the file watching process @@ -107,12 +109,12 @@ export class Watcher { } async *getEvents(): AsyncGenerator { while (true) { - const event = this.eventQueue.shift() + const event = this.eventQueue.shift(); if (event) { yield event; } else { yield new Promise((resolve) => { - this.resolvers.push(resolve) + this.resolvers.push(resolve); }); } } diff --git a/packages/ts-to-zod/src/__tests__/multiFileTestCase.ts b/packages/ts-to-zod/src/__tests__/multiFileTestCase.ts index cde61653..5420516e 100644 --- a/packages/ts-to-zod/src/__tests__/multiFileTestCase.ts +++ b/packages/ts-to-zod/src/__tests__/multiFileTestCase.ts @@ -4,7 +4,7 @@ const factory = ts.factory; import { SchemaGenContext, convertSymbol } from "../convertType"; import { testProject } from "./testProject"; import { generateFiles } from "../generateFiles"; -import { GenOptions } from "../filePathConfig"; +import { GenOptions, createGenerationConfig } from "../filePathConfig"; import * as path from "path"; import pkgUp from "pkg-up"; @@ -49,7 +49,8 @@ export const multiFileTestCase = async (options: { }, rootPath, }; - for (const [file, statements] of generateFiles(ctx, genOptions)) { + const generationConfig = createGenerationConfig(genOptions); + for (const [file, statements] of generateFiles(ctx, generationConfig)) { const relativeFile = path.relative(rootPath, file); const sourceFile = factory.createSourceFile( statements, diff --git a/packages/ts-to-zod/src/filePathConfig.ts b/packages/ts-to-zod/src/filePathConfig.ts index eee985b5..0e070d66 100644 --- a/packages/ts-to-zod/src/filePathConfig.ts +++ b/packages/ts-to-zod/src/filePathConfig.ts @@ -24,6 +24,7 @@ export interface GenerationConfig { baseDependenciesPath: string; rootPath: string; suffix?: string; + zPackage?: string; } export function createGenerationConfig(options: GenOptions): GenerationConfig { @@ -58,6 +59,7 @@ export function createGenerationConfig(options: GenOptions): GenerationConfig { basePath, baseDependenciesPath, suffix, - rootPath: options.rootPath + rootPath: options.rootPath, + zPackage: options.zPackage } } diff --git a/packages/ts-to-zod/src/generateFiles.ts b/packages/ts-to-zod/src/generateFiles.ts index 0bfc123c..7675a3e9 100644 --- a/packages/ts-to-zod/src/generateFiles.ts +++ b/packages/ts-to-zod/src/generateFiles.ts @@ -16,10 +16,9 @@ import { export function generateFiles( ctx: SchemaGenContext, - options: GenOptions + generationConfig: GenerationConfig ): Map { const outputMap = new Map(); - const generationConfig = createGenerationConfig(options); for (const [path, info] of ctx.files.entries()) { const generatedPath = generatePath(path, generationConfig); @@ -28,7 +27,7 @@ export function generateFiles( // TODO: clean up by removing duplicate functions, rename function outputMap.set( tsPath, - generateStatements(info, generationConfig, generatedPath, options) + generateStatements(info, generationConfig, generatedPath) ); } return outputMap; @@ -38,12 +37,10 @@ function generateStatements( info: FileInfo, generationConfig: GenerationConfig, generatedPath: string, - options: GenOptions ): ts.Statement[] { const statements: ts.Statement[] = generateImportStatements( generationConfig, generatedPath, - options.zPackage, info.imports, info.namespaceImports ); @@ -140,7 +137,6 @@ function normalizeImport(relativePath: string): string { export function generateImportStatements( config: GenerationConfig, filePath: string, - zPackage: string | undefined, imports: Map, namespaceImports: Map ): ts.ImportDeclaration[] { @@ -158,7 +154,7 @@ export function generateImportStatements( const zImportDeclaration = factory.createImportDeclaration( [], zImportClause, - factory.createStringLiteral(zPackage || "zod") + factory.createStringLiteral(config.zPackage || "zod") ); const importDeclarations = [zImportDeclaration]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1a21759..c0cd4a16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,6 +244,9 @@ importers: pkg-up: specifier: '3.1' version: 3.1.0 + resolve: + specifier: ^1.22.2 + version: 1.22.2 ts-morph: specifier: ^19.0.0 version: 19.0.0 @@ -260,6 +263,9 @@ importers: '@types/node': specifier: ^20.3.1 version: 20.3.1 + '@types/resolve': + specifier: ^1.20.2 + version: 1.20.2 jest: specifier: ^29.5.0 version: 29.5.0(@types/node@20.3.1) @@ -4132,6 +4138,10 @@ packages: '@types/scheduler': 0.16.3 csstype: 3.1.2 + /@types/resolve@1.20.2: + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + dev: true + /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: @@ -7683,7 +7693,7 @@ packages: dependencies: ansi-escapes: 6.2.0 chalk: 5.2.0 - jest: 29.5.0(@types/node@20.2.3) + jest: 29.5.0(@types/node@20.3.1) jest-regex-util: 29.4.3 jest-watcher: 29.5.0 slash: 5.1.0 @@ -10005,6 +10015,7 @@ packages: /resolve@1.22.2: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} + hasBin: true dependencies: is-core-module: 2.12.1 path-parse: 1.0.7 @@ -10839,7 +10850,7 @@ packages: '@babel/core': 7.21.8 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.5.0(@types/node@20.2.3) + jest: 29.5.0(@types/node@20.3.1) jest-util: 29.5.0 json5: 2.2.3 lodash.memoize: 4.1.2