Skip to content

Commit 04184b9

Browse files
authored
feat(hooks): add "portable" generation mode (#1850)
1 parent 716091e commit 04184b9

File tree

17 files changed

+266
-37
lines changed

17 files changed

+266
-37
lines changed

packages/plugins/swr/src/generator.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ function generateModelHooks(
6060
const fileName = paramCase(model.name);
6161
const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true });
6262

63-
sf.addStatements('/* eslint-disable */');
64-
6563
const prismaImport = getPrismaClientImportSpec(outDir, options);
6664
sf.addImportDeclaration({
6765
namedImports: ['Prisma'],
@@ -261,6 +259,7 @@ function generateIndex(project: Project, outDir: string, models: DataModel[]) {
261259
const sf = project.createSourceFile(path.join(outDir, 'index.ts'), undefined, { overwrite: true });
262260
sf.addStatements(models.map((d) => `export * from './${paramCase(d.name)}';`));
263261
sf.addStatements(`export { Provider } from '@zenstackhq/swr/runtime';`);
262+
sf.addStatements(`export { default as metadata } from './__model_meta';`);
264263
}
265264

266265
function generateQueryHook(

packages/plugins/tanstack-query/src/generator.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { DataModel, DataModelFieldType, Model, isEnum, isTypeDef } from '@zenstackhq/sdk/ast';
1515
import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma';
1616
import { paramCase } from 'change-case';
17+
import fs from 'fs';
1718
import { lowerCaseFirst } from 'lower-case-first';
1819
import path from 'path';
1920
import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph';
@@ -45,6 +46,14 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
4546
outDir = resolvePath(outDir, options);
4647
ensureEmptyDir(outDir);
4748

49+
if (options.portable && typeof options.portable !== 'boolean') {
50+
throw new PluginError(
51+
name,
52+
`Invalid value for "portable" option: ${options.portable}, a boolean value is expected`
53+
);
54+
}
55+
const portable = options.portable ?? false;
56+
4857
await generateModelMeta(project, models, typeDefs, {
4958
output: path.join(outDir, '__model_meta.ts'),
5059
generateAttributes: false,
@@ -61,6 +70,10 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
6170
generateModelHooks(target, version, project, outDir, dataModel, mapping, options);
6271
});
6372

73+
if (portable) {
74+
generateBundledTypes(project, outDir, options);
75+
}
76+
6477
await saveProject(project);
6578
return { warnings };
6679
}
@@ -333,9 +346,7 @@ function generateModelHooks(
333346
const fileName = paramCase(model.name);
334347
const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true });
335348

336-
sf.addStatements('/* eslint-disable */');
337-
338-
const prismaImport = getPrismaClientImportSpec(outDir, options);
349+
const prismaImport = options.portable ? './__types' : getPrismaClientImportSpec(outDir, options);
339350
sf.addImportDeclaration({
340351
namedImports: ['Prisma', model.name],
341352
isTypeOnly: true,
@@ -584,6 +595,7 @@ function generateIndex(
584595
sf.addStatements(`export { SvelteQueryContextKey, setHooksContext } from '${runtimeImportBase}/svelte';`);
585596
break;
586597
}
598+
sf.addStatements(`export { default as metadata } from './__model_meta';`);
587599
}
588600

589601
function makeGetContext(target: TargetFramework) {
@@ -724,3 +736,21 @@ function makeMutationOptions(target: string, returnType: string, argsType: strin
724736
function makeRuntimeImportBase(version: TanStackVersion) {
725737
return `@zenstackhq/tanstack-query/runtime${version === 'v5' ? '-v5' : ''}`;
726738
}
739+
740+
function generateBundledTypes(project: Project, outDir: string, options: PluginOptions) {
741+
if (!options.prismaClientDtsPath) {
742+
throw new PluginError(name, `Unable to determine the location of PrismaClient types`);
743+
}
744+
745+
// copy PrismaClient index.d.ts
746+
const content = fs.readFileSync(options.prismaClientDtsPath, 'utf-8');
747+
project.createSourceFile(path.join(outDir, '__types.d.ts'), content, { overwrite: true });
748+
749+
// "runtime/library.d.ts" is referenced by Prisma's DTS, and it's generated into Prisma's output
750+
// folder if a custom output is specified; if not, it's referenced from '@prisma/client'
751+
const libraryDts = path.join(path.dirname(options.prismaClientDtsPath), 'runtime', 'library.d.ts');
752+
if (fs.existsSync(libraryDts)) {
753+
const content = fs.readFileSync(libraryDts, 'utf-8');
754+
project.createSourceFile(path.join(outDir, 'runtime', 'library.d.ts'), content, { overwrite: true });
755+
}
756+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/// <reference types="@types/jest" />
2+
3+
import { loadSchema, normalizePath } from '@zenstackhq/testtools';
4+
import path from 'path';
5+
import tmp from 'tmp';
6+
7+
describe('Tanstack Query Plugin Portable Tests', () => {
8+
it('supports portable for standard prisma client', async () => {
9+
await loadSchema(
10+
`
11+
plugin tanstack {
12+
provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
13+
output = '$projectRoot/hooks'
14+
target = 'react'
15+
portable = true
16+
}
17+
18+
model User {
19+
id Int @id @default(autoincrement())
20+
email String
21+
posts Post[]
22+
}
23+
24+
model Post {
25+
id Int @id @default(autoincrement())
26+
title String
27+
author User @relation(fields: [authorId], references: [id])
28+
authorId Int
29+
}
30+
`,
31+
{
32+
provider: 'postgresql',
33+
pushDb: false,
34+
extraDependencies: ['[email protected]', '@types/[email protected]', '@tanstack/[email protected]'],
35+
copyDependencies: [path.resolve(__dirname, '../dist')],
36+
compile: true,
37+
extraSourceFiles: [
38+
{
39+
name: 'main.ts',
40+
content: `
41+
import { useFindUniqueUser } from './hooks';
42+
const { data } = useFindUniqueUser({ where: { id: 1 }, include: { posts: true } });
43+
console.log(data?.email);
44+
console.log(data?.posts[0].title);
45+
`,
46+
},
47+
],
48+
}
49+
);
50+
});
51+
52+
it('supports portable for custom prisma client output', async () => {
53+
const t = tmp.dirSync({ unsafeCleanup: true });
54+
const projectDir = t.name;
55+
56+
await loadSchema(
57+
`
58+
datasource db {
59+
provider = 'postgresql'
60+
url = env('DATABASE_URL')
61+
}
62+
63+
generator client {
64+
provider = 'prisma-client-js'
65+
output = '$projectRoot/myprisma'
66+
}
67+
68+
plugin tanstack {
69+
provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
70+
output = '$projectRoot/hooks'
71+
target = 'react'
72+
portable = true
73+
}
74+
75+
model User {
76+
id Int @id @default(autoincrement())
77+
email String
78+
posts Post[]
79+
}
80+
81+
model Post {
82+
id Int @id @default(autoincrement())
83+
title String
84+
author User @relation(fields: [authorId], references: [id])
85+
authorId Int
86+
}
87+
`,
88+
{
89+
provider: 'postgresql',
90+
pushDb: false,
91+
extraDependencies: ['[email protected]', '@types/[email protected]', '@tanstack/[email protected]'],
92+
copyDependencies: [path.resolve(__dirname, '../dist')],
93+
compile: true,
94+
addPrelude: false,
95+
projectDir,
96+
prismaLoadPath: `${projectDir}/myprisma`,
97+
extraSourceFiles: [
98+
{
99+
name: 'main.ts',
100+
content: `
101+
import { useFindUniqueUser } from './hooks';
102+
const { data } = useFindUniqueUser({ where: { id: 1 }, include: { posts: true } });
103+
console.log(data?.email);
104+
console.log(data?.posts[0].title);
105+
`,
106+
},
107+
],
108+
}
109+
);
110+
});
111+
112+
it('supports portable for logical client', async () => {
113+
await loadSchema(
114+
`
115+
plugin tanstack {
116+
provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
117+
output = '$projectRoot/hooks'
118+
target = 'react'
119+
portable = true
120+
}
121+
122+
model Base {
123+
id Int @id @default(autoincrement())
124+
createdAt DateTime @default(now())
125+
type String
126+
@@delegate(type)
127+
}
128+
129+
model User extends Base {
130+
email String
131+
}
132+
`,
133+
{
134+
provider: 'postgresql',
135+
pushDb: false,
136+
extraDependencies: ['[email protected]', '@types/[email protected]', '@tanstack/[email protected]'],
137+
copyDependencies: [path.resolve(__dirname, '../dist')],
138+
compile: true,
139+
extraSourceFiles: [
140+
{
141+
name: 'main.ts',
142+
content: `
143+
import { useFindUniqueUser } from './hooks';
144+
const { data } = useFindUniqueUser({ where: { id: 1 } });
145+
console.log(data?.email);
146+
console.log(data?.createdAt);
147+
`,
148+
},
149+
],
150+
}
151+
);
152+
});
153+
});

packages/plugins/trpc/src/client-helper/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ export function generateClientTypingForModel(
4141
}
4242
);
4343

44-
sf.addStatements([`/* eslint-disable */`]);
45-
4644
generateImports(clientType, sf, options, version);
4745

4846
// generate a `ClientType` interface that contains typing for query/mutation operations

packages/plugins/trpc/src/generator.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,6 @@ function createAppRouter(
122122
overwrite: true,
123123
});
124124

125-
appRouter.addStatements('/* eslint-disable */');
126-
127125
const prismaImport = getPrismaClientImportSpec(path.dirname(indexFile), options);
128126

129127
if (version === 'v10') {
@@ -274,8 +272,6 @@ function generateModelCreateRouter(
274272
overwrite: true,
275273
});
276274

277-
modelRouter.addStatements('/* eslint-disable */');
278-
279275
if (version === 'v10') {
280276
modelRouter.addImportDeclarations([
281277
{
@@ -386,7 +382,6 @@ function createHelper(outDir: string) {
386382
overwrite: true,
387383
});
388384

389-
sf.addStatements('/* eslint-disable */');
390385
sf.addStatements(`import { TRPCError } from '@trpc/server';`);
391386
sf.addStatements(`import { isPrismaClientKnownRequestError } from '${RUNTIME_PACKAGE}';`);
392387

packages/schema/src/cli/plugin-runner.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ export class PluginRunner {
136136
let dmmf: DMMF.Document | undefined = undefined;
137137
let shortNameMap: Map<string, string> | undefined;
138138
let prismaClientPath = '@prisma/client';
139+
let prismaClientDtsPath: string | undefined = undefined;
140+
139141
const project = createProject();
140142
for (const { name, description, run, options: pluginOptions } of corePlugins) {
141143
const options = { ...pluginOptions, prismaClientPath };
@@ -165,6 +167,7 @@ export class PluginRunner {
165167
if (r.prismaClientPath) {
166168
// use the prisma client path returned by the plugin
167169
prismaClientPath = r.prismaClientPath;
170+
prismaClientDtsPath = r.prismaClientDtsPath;
168171
}
169172
}
170173

@@ -173,13 +176,13 @@ export class PluginRunner {
173176

174177
// run user plugins
175178
for (const { name, description, run, options: pluginOptions } of userPlugins) {
176-
const options = { ...pluginOptions, prismaClientPath };
179+
const options = { ...pluginOptions, prismaClientPath, prismaClientDtsPath };
177180
const r = await this.runPlugin(
178181
name,
179182
description,
180183
run,
181184
runnerOptions,
182-
options,
185+
options as PluginOptions,
183186
dmmf,
184187
shortNameMap,
185188
project,

packages/schema/src/plugins/enhancer/enhance/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class EnhancerGenerator {
6262
private readonly outDir: string
6363
) {}
6464

65-
async generate(): Promise<{ dmmf: DMMF.Document | undefined }> {
65+
async generate(): Promise<{ dmmf: DMMF.Document | undefined; newPrismaClientDtsPath: string | undefined }> {
6666
let dmmf: DMMF.Document | undefined;
6767

6868
const prismaImport = getPrismaClientImportSpec(this.outDir, this.options);
@@ -128,7 +128,12 @@ ${
128128
await this.saveSourceFile(enhanceTs);
129129
}
130130

131-
return { dmmf };
131+
return {
132+
dmmf,
133+
newPrismaClientDtsPath: prismaTypesFixed
134+
? path.resolve(this.outDir, LOGICAL_CLIENT_GENERATION_PATH, 'index-fixed.d.ts')
135+
: undefined,
136+
};
132137
}
133138

134139
private getZodImport() {

packages/schema/src/plugins/enhancer/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const run: PluginFunction = async (model, options, _dmmf, globalOptions) => {
2626

2727
await generateModelMeta(model, options, project, outDir);
2828
await generatePolicy(model, options, project, outDir);
29-
const { dmmf } = await new EnhancerGenerator(model, options, project, outDir).generate();
29+
const { dmmf, newPrismaClientDtsPath } = await new EnhancerGenerator(model, options, project, outDir).generate();
3030

3131
let prismaClientPath: string | undefined;
3232
if (dmmf) {
@@ -44,7 +44,7 @@ const run: PluginFunction = async (model, options, _dmmf, globalOptions) => {
4444
}
4545
}
4646

47-
return { dmmf, warnings: [], prismaClientPath };
47+
return { dmmf, warnings: [], prismaClientPath, prismaClientDtsPath: newPrismaClientDtsPath };
4848
};
4949

5050
export default run;

packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ export class PolicyGenerator {
5959

6060
async generate(project: Project, model: Model, output: string) {
6161
const sf = project.createSourceFile(path.join(output, 'policy.ts'), undefined, { overwrite: true });
62-
sf.addStatements('/* eslint-disable */');
6362

6463
this.writeImports(model, output, sf);
6564

0 commit comments

Comments
 (0)