diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index 967be9727..5ecfe4d84 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -60,8 +60,6 @@ function generateModelHooks( const fileName = paramCase(model.name); const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true }); - sf.addStatements('/* eslint-disable */'); - const prismaImport = getPrismaClientImportSpec(outDir, options); sf.addImportDeclaration({ namedImports: ['Prisma'], @@ -261,6 +259,7 @@ function generateIndex(project: Project, outDir: string, models: DataModel[]) { const sf = project.createSourceFile(path.join(outDir, 'index.ts'), undefined, { overwrite: true }); sf.addStatements(models.map((d) => `export * from './${paramCase(d.name)}';`)); sf.addStatements(`export { Provider } from '@zenstackhq/swr/runtime';`); + sf.addStatements(`export { default as metadata } from './__model_meta';`); } function generateQueryHook( diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 8833f77f9..c45a32517 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -14,6 +14,7 @@ import { import { DataModel, DataModelFieldType, Model, isEnum, isTypeDef } from '@zenstackhq/sdk/ast'; import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma'; import { paramCase } from 'change-case'; +import fs from 'fs'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; @@ -45,6 +46,14 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. outDir = resolvePath(outDir, options); ensureEmptyDir(outDir); + if (options.portable && typeof options.portable !== 'boolean') { + throw new PluginError( + name, + `Invalid value for "portable" option: ${options.portable}, a boolean value is expected` + ); + } + const portable = options.portable ?? false; + await generateModelMeta(project, models, typeDefs, { output: path.join(outDir, '__model_meta.ts'), generateAttributes: false, @@ -61,6 +70,10 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. generateModelHooks(target, version, project, outDir, dataModel, mapping, options); }); + if (portable) { + generateBundledTypes(project, outDir, options); + } + await saveProject(project); return { warnings }; } @@ -333,9 +346,7 @@ function generateModelHooks( const fileName = paramCase(model.name); const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true }); - sf.addStatements('/* eslint-disable */'); - - const prismaImport = getPrismaClientImportSpec(outDir, options); + const prismaImport = options.portable ? './__types' : getPrismaClientImportSpec(outDir, options); sf.addImportDeclaration({ namedImports: ['Prisma', model.name], isTypeOnly: true, @@ -584,6 +595,7 @@ function generateIndex( sf.addStatements(`export { SvelteQueryContextKey, setHooksContext } from '${runtimeImportBase}/svelte';`); break; } + sf.addStatements(`export { default as metadata } from './__model_meta';`); } function makeGetContext(target: TargetFramework) { @@ -724,3 +736,21 @@ function makeMutationOptions(target: string, returnType: string, argsType: strin function makeRuntimeImportBase(version: TanStackVersion) { return `@zenstackhq/tanstack-query/runtime${version === 'v5' ? '-v5' : ''}`; } + +function generateBundledTypes(project: Project, outDir: string, options: PluginOptions) { + if (!options.prismaClientDtsPath) { + throw new PluginError(name, `Unable to determine the location of PrismaClient types`); + } + + // copy PrismaClient index.d.ts + const content = fs.readFileSync(options.prismaClientDtsPath, 'utf-8'); + project.createSourceFile(path.join(outDir, '__types.d.ts'), content, { overwrite: true }); + + // "runtime/library.d.ts" is referenced by Prisma's DTS, and it's generated into Prisma's output + // folder if a custom output is specified; if not, it's referenced from '@prisma/client' + const libraryDts = path.join(path.dirname(options.prismaClientDtsPath), 'runtime', 'library.d.ts'); + if (fs.existsSync(libraryDts)) { + const content = fs.readFileSync(libraryDts, 'utf-8'); + project.createSourceFile(path.join(outDir, 'runtime', 'library.d.ts'), content, { overwrite: true }); + } +} diff --git a/packages/plugins/tanstack-query/tests/portable.test.ts b/packages/plugins/tanstack-query/tests/portable.test.ts new file mode 100644 index 000000000..6ddbb1cc3 --- /dev/null +++ b/packages/plugins/tanstack-query/tests/portable.test.ts @@ -0,0 +1,153 @@ +/// + +import { loadSchema, normalizePath } from '@zenstackhq/testtools'; +import path from 'path'; +import tmp from 'tmp'; + +describe('Tanstack Query Plugin Portable Tests', () => { + it('supports portable for standard prisma client', async () => { + await loadSchema( + ` + plugin tanstack { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/hooks' + target = 'react' + portable = true + } + + model User { + id Int @id @default(autoincrement()) + email String + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int + } + `, + { + provider: 'postgresql', + pushDb: false, + extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@5.56.x'], + copyDependencies: [path.resolve(__dirname, '../dist')], + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { useFindUniqueUser } from './hooks'; +const { data } = useFindUniqueUser({ where: { id: 1 }, include: { posts: true } }); +console.log(data?.email); +console.log(data?.posts[0].title); +`, + }, + ], + } + ); + }); + + it('supports portable for custom prisma client output', async () => { + const t = tmp.dirSync({ unsafeCleanup: true }); + const projectDir = t.name; + + await loadSchema( + ` + datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + generator client { + provider = 'prisma-client-js' + output = '$projectRoot/myprisma' + } + + plugin tanstack { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/hooks' + target = 'react' + portable = true + } + + model User { + id Int @id @default(autoincrement()) + email String + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int + } + `, + { + provider: 'postgresql', + pushDb: false, + extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@5.56.x'], + copyDependencies: [path.resolve(__dirname, '../dist')], + compile: true, + addPrelude: false, + projectDir, + prismaLoadPath: `${projectDir}/myprisma`, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { useFindUniqueUser } from './hooks'; +const { data } = useFindUniqueUser({ where: { id: 1 }, include: { posts: true } }); +console.log(data?.email); +console.log(data?.posts[0].title); +`, + }, + ], + } + ); + }); + + it('supports portable for logical client', async () => { + await loadSchema( + ` + plugin tanstack { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + output = '$projectRoot/hooks' + target = 'react' + portable = true + } + + model Base { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + type String + @@delegate(type) + } + + model User extends Base { + email String + } + `, + { + provider: 'postgresql', + pushDb: false, + extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@5.56.x'], + copyDependencies: [path.resolve(__dirname, '../dist')], + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { useFindUniqueUser } from './hooks'; +const { data } = useFindUniqueUser({ where: { id: 1 } }); +console.log(data?.email); +console.log(data?.createdAt); +`, + }, + ], + } + ); + }); +}); diff --git a/packages/plugins/trpc/src/client-helper/index.ts b/packages/plugins/trpc/src/client-helper/index.ts index f296ca1ab..99679c716 100644 --- a/packages/plugins/trpc/src/client-helper/index.ts +++ b/packages/plugins/trpc/src/client-helper/index.ts @@ -41,8 +41,6 @@ export function generateClientTypingForModel( } ); - sf.addStatements([`/* eslint-disable */`]); - generateImports(clientType, sf, options, version); // generate a `ClientType` interface that contains typing for query/mutation operations diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 6574069f8..5f7d8601b 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -122,8 +122,6 @@ function createAppRouter( overwrite: true, }); - appRouter.addStatements('/* eslint-disable */'); - const prismaImport = getPrismaClientImportSpec(path.dirname(indexFile), options); if (version === 'v10') { @@ -274,8 +272,6 @@ function generateModelCreateRouter( overwrite: true, }); - modelRouter.addStatements('/* eslint-disable */'); - if (version === 'v10') { modelRouter.addImportDeclarations([ { @@ -386,7 +382,6 @@ function createHelper(outDir: string) { overwrite: true, }); - sf.addStatements('/* eslint-disable */'); sf.addStatements(`import { TRPCError } from '@trpc/server';`); sf.addStatements(`import { isPrismaClientKnownRequestError } from '${RUNTIME_PACKAGE}';`); diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 4158bd256..a13ca2afc 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -136,6 +136,8 @@ export class PluginRunner { let dmmf: DMMF.Document | undefined = undefined; let shortNameMap: Map | undefined; let prismaClientPath = '@prisma/client'; + let prismaClientDtsPath: string | undefined = undefined; + const project = createProject(); for (const { name, description, run, options: pluginOptions } of corePlugins) { const options = { ...pluginOptions, prismaClientPath }; @@ -165,6 +167,7 @@ export class PluginRunner { if (r.prismaClientPath) { // use the prisma client path returned by the plugin prismaClientPath = r.prismaClientPath; + prismaClientDtsPath = r.prismaClientDtsPath; } } @@ -173,13 +176,13 @@ export class PluginRunner { // run user plugins for (const { name, description, run, options: pluginOptions } of userPlugins) { - const options = { ...pluginOptions, prismaClientPath }; + const options = { ...pluginOptions, prismaClientPath, prismaClientDtsPath }; const r = await this.runPlugin( name, description, run, runnerOptions, - options, + options as PluginOptions, dmmf, shortNameMap, project, diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 3f9d371a2..7236cac85 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -62,7 +62,7 @@ export class EnhancerGenerator { private readonly outDir: string ) {} - async generate(): Promise<{ dmmf: DMMF.Document | undefined }> { + async generate(): Promise<{ dmmf: DMMF.Document | undefined; newPrismaClientDtsPath: string | undefined }> { let dmmf: DMMF.Document | undefined; const prismaImport = getPrismaClientImportSpec(this.outDir, this.options); @@ -128,7 +128,12 @@ ${ await this.saveSourceFile(enhanceTs); } - return { dmmf }; + return { + dmmf, + newPrismaClientDtsPath: prismaTypesFixed + ? path.resolve(this.outDir, LOGICAL_CLIENT_GENERATION_PATH, 'index-fixed.d.ts') + : undefined, + }; } private getZodImport() { diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts index c0cd7e13d..c0bd93564 100644 --- a/packages/schema/src/plugins/enhancer/index.ts +++ b/packages/schema/src/plugins/enhancer/index.ts @@ -26,7 +26,7 @@ const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { await generateModelMeta(model, options, project, outDir); await generatePolicy(model, options, project, outDir); - const { dmmf } = await new EnhancerGenerator(model, options, project, outDir).generate(); + const { dmmf, newPrismaClientDtsPath } = await new EnhancerGenerator(model, options, project, outDir).generate(); let prismaClientPath: string | undefined; if (dmmf) { @@ -44,7 +44,7 @@ const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { } } - return { dmmf, warnings: [], prismaClientPath }; + return { dmmf, warnings: [], prismaClientPath, prismaClientDtsPath: newPrismaClientDtsPath }; }; export default run; diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index 62469f744..8622d13d4 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -59,7 +59,6 @@ export class PolicyGenerator { async generate(project: Project, model: Model, output: string) { const sf = project.createSourceFile(path.join(output, 'policy.ts'), undefined, { overwrite: true }); - sf.addStatements('/* eslint-disable */'); this.writeImports(model, output, sf); diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index 7fa94cd92..9849cc938 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -28,8 +28,16 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => { const mergedOptions = { ...options, output } as unknown as PluginOptions; const { warnings, shortNameMap } = await new PrismaSchemaGenerator(model).generate(mergedOptions); + + // the path to import the prisma client from let prismaClientPath = '@prisma/client'; + // the real path where the prisma client was generated + let clientOutputDir = '.prisma/client'; + + // the path to the prisma client dts file + let prismaClientDtsPath: string | undefined = undefined; + if (options.generateClient !== false) { let generateCmd = `prisma generate --schema "${output}"`; if (typeof options.generateArgs === 'string') { @@ -68,6 +76,23 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => { // then make it relative to the zmodel schema location prismaClientPath = normalizedRelative(path.dirname(options.schemaPath), absPath); } + + // record custom location where the prisma client was generated + clientOutputDir = prismaClientPath; + } + + // get PrismaClient dts path + try { + const prismaClientResolvedPath = require.resolve(clientOutputDir, { + paths: [path.dirname(options.schemaPath)], + }); + prismaClientDtsPath = path.join(path.dirname(prismaClientResolvedPath), 'index.d.ts'); + } catch (err) { + console.warn( + colors.yellow( + `Could not resolve PrismaClient type declaration path. This may break plugins that depend on it.` + ) + ); } } else { console.warn( @@ -82,7 +107,7 @@ const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => { datamodel: fs.readFileSync(output, 'utf-8'), }); - return { warnings, dmmf, prismaClientPath, shortNameMap }; + return { warnings, dmmf, prismaClientPath, prismaClientDtsPath, shortNameMap }; }; function getDefaultPrismaOutputFile(schemaPath: string) { diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 969cef3c7..0cdc8d44a 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -13,6 +13,7 @@ import { isIdField, parseOptionAsStrings, resolvePath, + saveSourceFile, } from '@zenstackhq/sdk'; import { DataModel, EnumField, Model, TypeDef, isDataModel, isEnum, isTypeDef } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers'; @@ -143,12 +144,7 @@ export class ZodSchemaGenerator { if (this.options.preserveTsFiles === true || this.options.output) { // if preserveTsFiles is true or the user provided a custom output directory, // save the generated files - await Promise.all( - this.sourceFiles.map(async (sf) => { - await sf.formatText(); - await sf.save(); - }) - ); + this.sourceFiles.forEach(saveSourceFile); } } @@ -325,8 +321,6 @@ export class ZodSchemaGenerator { } private addPreludeAndImports(decl: DataModel | TypeDef, writer: CodeBlockWriter, output: string) { - writer.writeLine('/* eslint-disable */'); - writer.writeLine('// @ts-nocheck'); writer.writeLine(`import { z } from 'zod';`); // import user-defined enums from Prisma as they might be referenced in the expressions diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index 081dcaf4a..a61eec3a2 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -59,7 +59,7 @@ export default class Transformer { for (const enumType of this.enumTypes) { const name = upperCaseFirst(enumType.name); const filePath = path.join(Transformer.outputPath, `enums/${name}.schema.ts`); - const content = `/* eslint-disable */\n${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( + const content = `${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( `${name}`, `z.enum(${JSON.stringify(enumType.values)})` )}`; @@ -72,7 +72,7 @@ export default class Transformer { for (const enumDecl of extraEnums) { const name = upperCaseFirst(enumDecl.name); const filePath = path.join(Transformer.outputPath, `enums/${name}.schema.ts`); - const content = `/* eslint-disable */\n${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( + const content = `${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( `${name}`, `z.enum(${JSON.stringify(enumDecl.fields.map((f) => f.name))})` )}`; @@ -107,7 +107,7 @@ export default class Transformer { const objectSchema = this.prepareObjectSchema(schemaFields, options); const filePath = path.join(Transformer.outputPath, `objects/${this.name}.schema.ts`); - const content = '/* eslint-disable */\n' + extraImports.join('\n\n') + objectSchema; + const content = extraImports.join('\n\n') + objectSchema; this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true })); return `${this.name}.schema`; } @@ -773,7 +773,6 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; const filePath = path.join(Transformer.outputPath, `input/${modelName}Input.schema.ts`); const content = ` - /* eslint-disable */ ${imports.join(';\n')} type ${modelName}InputSchemaType = { @@ -794,7 +793,6 @@ ${operations const indexFilePath = path.join(Transformer.outputPath, 'input/index.ts'); const indexContent = ` -/* eslint-disable */ ${globalExports.join(';\n')} `; this.sourceFiles.push(this.project.createSourceFile(indexFilePath, indexContent, { overwrite: true })); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index b2be27d13..a66a32eaf 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -48,6 +48,7 @@ "./dmmf-helpers": { "types": "./dmmf-helpers/index.d.ts", "default": "./dmmf-helpers/index.js" - } + }, + "./package.json": "./package.json" } } diff --git a/packages/sdk/src/code-gen.ts b/packages/sdk/src/code-gen.ts index 3f80e7e4d..7ed528aa7 100644 --- a/packages/sdk/src/code-gen.ts +++ b/packages/sdk/src/code-gen.ts @@ -1,4 +1,5 @@ -import { CompilerOptions, DiagnosticCategory, ModuleKind, Project, ScriptTarget } from 'ts-morph'; +import { CompilerOptions, DiagnosticCategory, ModuleKind, Project, ScriptTarget, SourceFile } from 'ts-morph'; +import pkgJson from './package.json'; import { PluginError } from './types'; /** @@ -19,11 +20,27 @@ export function createProject(options?: CompilerOptions) { }); } +export function saveSourceFile(sourceFile: SourceFile) { + sourceFile.replaceWithText( + `/****************************************************************************** +* This file was generated by ZenStack CLI ${pkgJson.version}. +******************************************************************************/ + +/* eslint-disable */ +// @ts-nocheck + + ${sourceFile.getText()} + ` + ); + sourceFile.formatText(); + sourceFile.saveSync(); +} + /** * Persists a TS project to disk. */ export async function saveProject(project: Project) { - project.getSourceFiles().forEach((sf) => sf.formatText()); + project.getSourceFiles().forEach(saveSourceFile); await project.save(); } diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index ae76f2fe5..4140786cb 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -73,7 +73,6 @@ export async function generate( options: ModelMetaGeneratorOptions ) { const sf = project.createSourceFile(options.output, undefined, { overwrite: true }); - sf.addStatements('/* eslint-disable */'); sf.addVariableStatement({ declarationKind: VariableDeclarationKind.Const, declarations: [ diff --git a/packages/sdk/src/package.json b/packages/sdk/src/package.json new file mode 120000 index 000000000..4e26811d4 --- /dev/null +++ b/packages/sdk/src/package.json @@ -0,0 +1 @@ +../package.json \ No newline at end of file diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 92c099717..29160c283 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -31,6 +31,12 @@ export type PluginOptions = { */ prismaClientPath?: string; + /** + * PrismaClient's TypeScript declaration file's path + * @private + */ + prismaClientDtsPath?: string; + /** * An optional map of full names to shortened names * @private @@ -74,6 +80,12 @@ export type PluginResult = { */ prismaClientPath?: string; + /** + * PrismaClient's TypeScript declaration file's path + * @private + */ + prismaClientDtsPath?: string; + /** * An optional Prisma DMMF document that a plugin can generate * @private