): void;
49 | }
50 |
51 | export const addRequestLogger = (async ({ event, resolve }) => {
52 | const request_id = randomUUID();
53 |
54 | const overwrite_methods = ['debug', 'error', 'info', 'log', 'warn'] as const;
55 | const overwrites = overwrite_methods.reduce((result, method) => {
56 | result[method] = (message, log_data = {}) => {
57 | console[method]({
58 | request_id,
59 | ts: Date.now(),
60 | message,
61 | ...log_data
62 | });
63 | };
64 | return result;
65 | }, {} as Console);
66 |
67 | const logger: RequestLogger = {
68 | ...console,
69 | ...overwrites
70 | };
71 | event.locals.logger = logger;
72 |
73 | const response = await resolve(event);
74 | return response;
75 | }) satisfies Handle;
76 |
77 | export const logRequestDetails = (async ({ event, resolve }) => {
78 | const timeStart = Date.now();
79 | event.locals.logger.log('Incoming request', {
80 | method: event.request.method,
81 | route: event.route.id,
82 | params: event.params,
83 | cookies: event.cookies.getAll()
84 | });
85 |
86 | const response = await resolve(event);
87 | event.locals.logger.log('Request processed', {
88 | duration_ms: Date.now() - timeStart
89 | });
90 |
91 | return response;
92 | }) satisfies Handle;
93 |
--------------------------------------------------------------------------------
/packages/plugin-request-logger/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 | Welcome to your library project
2 | Create your package using @sveltejs/package and preview/showcase your work with SvelteKit
3 | Visit kit.svelte.dev to read the documentation
4 |
--------------------------------------------------------------------------------
/packages/plugin-request-logger/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WebstoneHQ/webstone-plugins/5296c66e4191ea32b09b70bf728fc023f0f1ccc4/packages/plugin-request-logger/static/favicon.png
--------------------------------------------------------------------------------
/packages/plugin-request-logger/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import { vitePreprocess } from '@sveltejs/kit/vite';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
7 | // for more information about preprocessors
8 | preprocess: vitePreprocess(),
9 |
10 | kit: {
11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14 | adapter: adapter(),
15 | alias: {
16 | 'webstone-plugin-request-logger': 'src/lib/index.js'
17 | }
18 | }
19 | };
20 |
21 | export default config;
22 |
--------------------------------------------------------------------------------
/packages/plugin-request-logger/tests/test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@playwright/test';
2 |
3 | test('index page has expected h1', async ({ page }) => {
4 | await page.goto('/');
5 | await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
6 | });
7 |
--------------------------------------------------------------------------------
/packages/plugin-request-logger/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "NodeNext",
13 | "paths": {
14 | "$lib": ["./src/lib"],
15 | "$lib/*": ["./src/lib/*"],
16 | "webstone-plugin-request-logger": ["./src/lib/index.js"]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/plugin-request-logger/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig } from 'vitest/config';
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()],
6 | test: {
7 | include: ['src/**/*.{test,spec}.{js,ts}']
8 | }
9 | });
10 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100
6 | }
7 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # webstone-plugin-trpc-cli
2 |
3 | ## 0.2.0
4 |
5 | ### Minor Changes
6 |
7 | - e4954e6: bump gluegun version
8 |
9 | ## 0.1.0
10 |
11 | ### Minor Changes
12 |
13 | - 2826fcb: Release a beta version of the tRPC Webstone Plugin.
14 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/README.md:
--------------------------------------------------------------------------------
1 | # tRPC CLI - Webstone Plugin
2 |
3 | ## Installation
4 |
5 | Install this plugin with the following command:
6 |
7 | ```
8 | npm install -D webstone-plugin-trpc-cli
9 | ```
10 |
11 | That's it. Your project's `webstone` CLI is now extended with this plugin's functionality.
12 |
13 | ## Usage
14 |
15 | Run `webstone --help` in your project and look for the help output of the `trpc` command.
16 |
17 | ### Initialize tRPC for your project
18 |
19 | To set up the tRPC boilerplate code, such as the router, run the following command at the root of your project:
20 |
21 | ```
22 | webstone trpc init
23 | ```
24 |
25 | ## Learn more about Webstone Plugins
26 |
27 | This plugin is part of a wider ecosystem called [Webstone Plugins](https://github.com/WebstoneHQ/webstone).
28 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webstone-plugin-trpc-cli",
3 | "version": "0.2.0",
4 | "description": "",
5 | "private": true,
6 | "scripts": {
7 | "clean": "rimraf build",
8 | "copy-templates": "copyfiles -u 2 ./src/templates/* ./src/templates/**/* build/templates",
9 | "format": "prettier --plugin-search-dir . --write .",
10 | "build": "npm run clean && run-s build:cli copy-templates",
11 | "build:cli": "tsc -p tsconfig.json",
12 | "dev": "pnpm clean && pnpm copy-templates && run-p dev:watch-src dev:watch-templates",
13 | "dev:watch-src": "tsc -p tsconfig.json --watch",
14 | "dev:watch-templates": "npm-watch copy-templates"
15 | },
16 | "keywords": [
17 | "webstone",
18 | "plugin",
19 | "template"
20 | ],
21 | "watch": {
22 | "copy-templates": {
23 | "patterns": [
24 | "src/templates"
25 | ],
26 | "extensions": "ejs"
27 | }
28 | },
29 | "author": "Cahllagerfeld",
30 | "license": "MIT",
31 | "devDependencies": {
32 | "@webstone/gluegun": "^0.0.5",
33 | "copyfiles": "^2.4.1",
34 | "npm-run-all": "^4.1.5",
35 | "npm-watch": "^0.11.0",
36 | "prettier": "^2.8.8",
37 | "rimraf": "^3.0.2",
38 | "typescript": "^4.7.4"
39 | },
40 | "dependencies": {
41 | "@mrleebo/prisma-ast": "^0.4.3",
42 | "ts-morph": "^17.0.1"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/src/commands/trpc/generate.ts:
--------------------------------------------------------------------------------
1 | import { GluegunCommand } from '@webstone/gluegun';
2 | import { populateSubrouterFile, getIDType, prepareApprouter } from '../../lib/generate';
3 | import { Project } from 'ts-morph';
4 | import { generateCompleteModelName, generateRouterFilename } from '../../lib/naming';
5 | import { getAllModels, getModelByName } from '../../lib/parser';
6 |
7 | const command: GluegunCommand = {
8 | name: 'generate',
9 | alias: ['g'],
10 | description: 'Generate one or more tRPC model(s)',
11 | hidden: false,
12 | dashed: false,
13 | run: async (toolbox) => {
14 | const { print, parameters, prompt, template, strings, filesystem } = toolbox;
15 | try {
16 | let modelNames =
17 | parameters.string && parameters.string?.split(',').map((modelName) => modelName.trim());
18 |
19 | // check if the trpc plugin is initialized
20 | if (!filesystem.exists('src/lib/server/trpc/router.ts')) {
21 | print.error(
22 | "Please initialize the trpc plugin first, by running ' webstone trpc init'"
23 | );
24 | return;
25 | }
26 |
27 | if (!modelNames) {
28 | if (!filesystem.exists('prisma/schema.prisma')) {
29 | print.error(
30 | 'Please create a prisma/schema.prisma file. To learn more, see https://www.prisma.io/docs/concepts/components/prisma-schema.'
31 | );
32 | return;
33 | }
34 |
35 | const prismaModelNames = getAllModels()
36 | .filter((model) => model.type === 'model')
37 | .map((model) => model.type === 'model' && model.name);
38 |
39 | if (!prismaModelNames) {
40 | print.error('No models found in prisma/schema.prisma');
41 | return;
42 | }
43 |
44 | const result = await prompt.ask({
45 | type: 'multiselect',
46 | name: 'models',
47 | message: 'Please select your model(s)',
48 | choices: prismaModelNames as string[]
49 | });
50 | modelNames = result.models as unknown as string[];
51 | }
52 |
53 | const models = modelNames.map((modelName) => getModelByName(modelName));
54 |
55 | for (let index = 0; index < models.length; index++) {
56 | const model = models[index];
57 | const modelName = modelNames[index];
58 | if (!model) {
59 | print.error(`Model ${modelName} not found, skipping...`);
60 | continue;
61 | }
62 |
63 | if (model.type !== 'model') return;
64 |
65 | const spinner = print.spin(`Generating tRPC for model "${modelName}..."`);
66 |
67 | const idFieldType = getIDType(model);
68 |
69 | const subrouterFilename = generateRouterFilename(model.name);
70 | const subrouterTarget = `src/lib/server/trpc/subrouters/${subrouterFilename}.ts`;
71 | const zodModelName = generateCompleteModelName(model.name);
72 |
73 | await template.generate({
74 | template: 'subrouter.ejs',
75 | target: subrouterTarget,
76 | props: {
77 | capitalizedPlural: strings.upperFirst(strings.plural(modelName)),
78 | capitalizedSingular: strings.upperFirst(strings.singular(modelName)),
79 | lowercaseSingular: strings.lowerCase(modelName),
80 | zodModelName,
81 | idFieldType
82 | }
83 | });
84 |
85 | const project = new Project({
86 | tsConfigFilePath: 'tsconfig.json'
87 | });
88 |
89 | populateSubrouterFile(project, model);
90 |
91 | const indexRouter = project.getSourceFileOrThrow('src/lib/server/trpc/router.ts');
92 |
93 | prepareApprouter(indexRouter, `${strings.lowerCase(modelName)}Router`, subrouterFilename);
94 |
95 | indexRouter.formatText({
96 | tabSize: 1
97 | });
98 |
99 | project.saveSync();
100 |
101 | spinner.succeed(`Generated tRPC for model "${modelName}"`);
102 | }
103 | } catch (error) {
104 | print.error(error);
105 | }
106 | }
107 | };
108 |
109 | export default command;
110 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/src/commands/trpc/init.ts:
--------------------------------------------------------------------------------
1 | import { GluegunCommand } from '@webstone/gluegun';
2 |
3 | const command: GluegunCommand = {
4 | name: 'init',
5 | alias: ['i'],
6 | description: 'Initialize the tRPC Webstone plugin',
7 | hidden: false,
8 | dashed: false,
9 | run: async (toolbox) => {
10 | const { template, filesystem, print } = toolbox;
11 |
12 | const containsTrpc = filesystem.isDirectory(`${process.cwd()}/src/lib/server/trpc`);
13 | if (containsTrpc) {
14 | print.warning('tRPC seems to be already initialized, skipping...');
15 | return;
16 | }
17 |
18 | const isWebPackageInstalled = filesystem.exists(
19 | `${process.cwd()}/node_modules/webstone-plugin-trpc-web`
20 | );
21 | if (!isWebPackageInstalled) {
22 | print.error('Webstone tRPC Web package is not installed');
23 | return;
24 | }
25 |
26 | const spinner = print.spin('Initializing tRPC...');
27 | await template.generate({
28 | template: 'base/router.ts.ejs',
29 | target: 'src/lib/server/trpc/router.ts'
30 | });
31 | await template.generate({
32 | template: 'base/trpc.ts.ejs',
33 | target: 'src/lib/server/trpc/trpc.ts'
34 | });
35 |
36 | spinner.succeed('tRPC initialized');
37 | }
38 | };
39 |
40 | export default command;
41 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/src/extensions/trpc/hello.ts:
--------------------------------------------------------------------------------
1 | import { GluegunToolbox } from '@webstone/gluegun';
2 |
3 | const extension = (toolbox: GluegunToolbox) => {
4 | const { print } = toolbox;
5 |
6 | toolbox.sayhello = () => {
7 | print.info('Hello from an extension!');
8 | };
9 | };
10 |
11 | export default extension;
12 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/src/lib/generate.ts:
--------------------------------------------------------------------------------
1 | import { Field, Model, Enum } from '@mrleebo/prisma-ast';
2 | import { Project, SourceFile, SyntaxKind, VariableDeclarationKind } from 'ts-morph';
3 | import {
4 | generateCompleteModelName,
5 | generateEnumFilename,
6 | generateEnumName,
7 | generateModelFilename,
8 | generateRouterFilename,
9 | generateZodEnumName,
10 | generateZodModelName
11 | } from './naming';
12 | import { getAllEnums, getModelByName } from './parser';
13 |
14 | const scalarTypes = ['Int', 'String', 'BigInt', 'DateTime', 'Float', 'Decimal', 'Boolean'];
15 | const nullishAttributes = ['default'];
16 |
17 | export const generateModelSchema = (sourceFile: SourceFile, model: Model) => {
18 | sourceFile.addImportDeclaration({
19 | moduleSpecifier: 'webstone-plugin-trpc-web',
20 | namedImports: ['z']
21 | });
22 |
23 | const nonScalarFileds = getNonScalarFields(model);
24 |
25 | sourceFile.addVariableStatement({
26 | declarationKind: VariableDeclarationKind.Const,
27 | isExported: true,
28 | leadingTrivia: (writer) => writer.blankLineIfLastNot(),
29 | trailingTrivia: (writer) => writer.blankLineIfLastNot(),
30 | declarations: [
31 | {
32 | name:
33 | nonScalarFileds.length > 0
34 | ? generateZodModelName(model.name)
35 | : generateCompleteModelName(model.name),
36 | initializer: (writer) => {
37 | writer
38 | .write('z.object(')
39 | .inlineBlock(() => {
40 | model.properties
41 | .filter((prop) => prop.type === 'field')
42 | .filter(
43 | (prop) => prop.type === 'field' && scalarTypes.includes(prop.fieldType as string)
44 | )
45 | .forEach((prop) => {
46 | if (prop.type !== 'field') return;
47 | writer
48 | .write(`${prop.name}: ${mapZodType(prop)}`)
49 | .write(',')
50 | .newLine();
51 | });
52 | })
53 | .write(')');
54 | }
55 | }
56 | ]
57 | });
58 |
59 | if (nonScalarFileds.length > 0) {
60 | nonScalarFileds.forEach((field) => {
61 | if (field.type !== 'field') return;
62 | const isEnum = determineEnum(field);
63 | if (isEnum && isEnum.type === 'enum') {
64 | sourceFile.addImportDeclaration({
65 | moduleSpecifier: `../enums/${generateEnumFilename(isEnum.name)}`,
66 | namedImports: [generateZodEnumName(isEnum.name), generateEnumName(isEnum.name)]
67 | });
68 | }
69 | if (!isEnum) {
70 | sourceFile.addImportDeclarations([
71 | {
72 | moduleSpecifier: `../models/${generateModelFilename(field.fieldType as string)}`,
73 | namedImports: [`Complete${field.fieldType}Model`]
74 | },
75 | {
76 | moduleSpecifier: `../models/${generateModelFilename(field.fieldType as string)}`,
77 | namedImports: [`${field.fieldType as string}WithRelations`],
78 | isTypeOnly: true
79 | }
80 | ]);
81 | }
82 | });
83 | sourceFile.addInterface({
84 | isExported: true,
85 | name: `${model.name}WithRelations`,
86 | leadingTrivia: (writer) => writer.blankLineIfLastNot(),
87 | trailingTrivia: (writer) => writer.blankLineIfLastNot(),
88 | extends: [`z.infer`],
89 | properties: nonScalarFileds.map((field) => {
90 | let type = '';
91 | if (field.type !== 'field') return { name: '', type: 'any' };
92 | const isEnum = determineEnum(field);
93 | if (isEnum && isEnum.type === 'enum') {
94 | type = `${generateEnumName(isEnum.name)}${field.array ? '[]' : ''}${
95 | field.optional ? ' | null' : ''
96 | }`;
97 | } else {
98 | type = `${field.fieldType as string}WithRelations${field.array ? '[]' : ''}${
99 | field.optional ? ' | null' : ''
100 | }`;
101 | }
102 | return {
103 | hasQuestionToken: field.optional,
104 | name: field.name,
105 | type
106 | };
107 | })
108 | });
109 |
110 | sourceFile.addVariableStatement({
111 | declarationKind: VariableDeclarationKind.Const,
112 | isExported: true,
113 | leadingTrivia: (writer) => writer.blankLineIfLastNot(),
114 | trailingTrivia: (writer) => writer.blankLineIfLastNot(),
115 | declarations: [
116 | {
117 | name: `Complete${model.name}Model`,
118 | type: `z.ZodSchema<${model.name}WithRelations>`,
119 | initializer: (writer) => {
120 | writer
121 | .write(`z.lazy(() => ${generateZodModelName(model.name)}.extend(`)
122 | .inlineBlock(() => {
123 | nonScalarFileds.forEach((prop) => {
124 | if (prop.type !== 'field') return;
125 | writer
126 | .write(`${prop.name}: ${mapZodType(prop)}`)
127 | .write(',')
128 | .newLine();
129 | });
130 | })
131 | .write('))');
132 | }
133 | }
134 | ]
135 | });
136 | }
137 | };
138 |
139 | export const generateEnumSchema = (sourceFile: SourceFile, enumModel: Enum) => {
140 | sourceFile.addImportDeclaration({
141 | moduleSpecifier: 'webstone-plugin-trpc-web',
142 | namedImports: ['z']
143 | });
144 |
145 | const enumValues = enumModel.enumerators.map(
146 | (enumerator) => enumerator.type === 'enumerator' && enumerator.name
147 | );
148 |
149 | sourceFile.addEnum({
150 | isExported: true,
151 | name: generateEnumName(enumModel.name),
152 | leadingTrivia: (writer) => writer.blankLineIfLastNot(),
153 | trailingTrivia: (writer) => writer.blankLineIfLastNot(),
154 | members: enumValues.map((value) => ({
155 | name: value ? value : ''
156 | }))
157 | });
158 |
159 | sourceFile.addVariableStatement({
160 | declarationKind: VariableDeclarationKind.Const,
161 | isExported: true,
162 | leadingTrivia: (writer) => writer.blankLineIfLastNot(),
163 | trailingTrivia: (writer) => writer.blankLineIfLastNot(),
164 | declarations: [
165 | {
166 | name: `${generateZodEnumName(enumModel.name)}`,
167 | initializer: (writer) => {
168 | writer.write(`z.nativeEnum(${generateEnumName(enumModel.name)})`);
169 | }
170 | }
171 | ]
172 | });
173 | };
174 |
175 | export const mapZodType = (prop: Field) => {
176 | let zodType = '';
177 | let modifiers: string[] = [''];
178 | switch (prop.fieldType) {
179 | case 'Int':
180 | zodType = 'z.number()';
181 | modifiers = [...modifiers, 'int()'];
182 | break;
183 | case 'String':
184 | zodType = 'z.string()';
185 | break;
186 | case 'BigInt':
187 | zodType = 'z.bigint()';
188 | break;
189 | case 'DateTime':
190 | zodType = 'z.date()';
191 | break;
192 | case 'Float':
193 | zodType = 'z.number()';
194 | break;
195 | case 'Decimal':
196 | zodType = 'z.number()';
197 | break;
198 | case 'Boolean':
199 | zodType = 'z.boolean()';
200 | break;
201 | }
202 |
203 | if (!zodType) {
204 | const isEnum = determineEnum(prop);
205 | if (isEnum && isEnum.type === 'enum') {
206 | zodType = `${generateZodEnumName(isEnum.name)}`;
207 | }
208 | if (!isEnum) {
209 | zodType = `Complete${prop.fieldType}Model`;
210 | }
211 | }
212 |
213 | if (prop.array) modifiers = [...modifiers, 'array()'];
214 | if (prop.optional) modifiers = [...modifiers, 'nullish()'];
215 | if (prop.attributes && prop.attributes.find((attr) => nullishAttributes.includes(attr.name)))
216 | modifiers = [...modifiers, 'nullish()'];
217 | return `${zodType}${modifiers.join('.')}`;
218 | };
219 |
220 | export const getIDType = (model: Model) => {
221 | const idField = model.properties.find(
222 | (prop) => prop.type === 'field' && prop.attributes?.find((attr) => attr.name === 'id')
223 | );
224 | if (idField?.type !== 'field') throw new Error('ID field not found');
225 | return idField ? mapZodType(idField) : 'z.unknown()';
226 | };
227 |
228 | export const populateSubrouterFile = (project: Project, model: Model) => {
229 | const subrouterFilename = generateRouterFilename(model.name);
230 | const subrouterTarget = `src/lib/server/trpc/subrouters/${subrouterFilename}.ts`;
231 |
232 | generateSchemaForModel(project, model, new Set());
233 |
234 | const subRouter = project.getSourceFileOrThrow(subrouterTarget);
235 |
236 | subRouter.addImportDeclaration({
237 | moduleSpecifier: 'webstone-plugin-trpc-web',
238 | namedImports: ['z']
239 | });
240 |
241 | subRouter.formatText({
242 | tabSize: 1
243 | });
244 |
245 | subRouter.addImportDeclaration({
246 | moduleSpecifier: `../models/${generateModelFilename(model.name)}`,
247 | namedImports: [generateCompleteModelName(model.name)]
248 | });
249 | };
250 |
251 | export const getNonScalarFields = (model: Model) => {
252 | return model.properties.filter(
253 | (prop) => prop.type === 'field' && !scalarTypes.includes(prop.fieldType as string)
254 | );
255 | };
256 |
257 | export function determineEnum(field: Field) {
258 | const fieldName = field.fieldType;
259 | const allEnums = getAllEnums();
260 | const enumType = allEnums.find(
261 | (enumItem) => enumItem.type === 'enum' && enumItem.name === fieldName
262 | );
263 | return enumType;
264 | }
265 |
266 | const generateSchemaForModel = (project: Project, model: Model, generatedEntities: Set) => {
267 | generatedEntities.add(model.name);
268 | const nonScalarFields = getNonScalarFields(model);
269 | nonScalarFields.forEach((field) => {
270 | if (field.type !== 'field') return;
271 | const enumType = determineEnum(field);
272 | if (enumType && enumType.type === 'enum') {
273 | if (generatedEntities.has(enumType.name)) return;
274 | const sourceFile = project.createSourceFile(
275 | `src/lib/server/trpc/${
276 | enumType && enumType.type === 'enum'
277 | ? `enums/${generateEnumFilename(enumType.name)}`
278 | : `models/${generateModelFilename(model.name)}`
279 | }.ts`,
280 | '',
281 | { overwrite: true }
282 | );
283 | generateEnumSchema(sourceFile, enumType);
284 | generatedEntities.add(enumType.name);
285 | } else {
286 | const dependantModel = getModelByName(field.fieldType as string);
287 | if (generatedEntities.has(field.fieldType as string)) return;
288 | if (dependantModel && dependantModel.type === 'model') {
289 | generatedEntities.add(dependantModel.name);
290 | generateSchemaForModel(project, dependantModel, generatedEntities);
291 | }
292 | }
293 | });
294 |
295 | const mainModelFile = project.createSourceFile(
296 | `src/lib/server/trpc/models/${generateModelFilename(model.name)}.ts`,
297 | '',
298 | { overwrite: true }
299 | );
300 | generateModelSchema(mainModelFile, model);
301 | };
302 |
303 | export const prepareApprouter = (
304 | sourceFile: SourceFile,
305 | routerName: string,
306 | routerFile: string
307 | ) => {
308 | const existingImport = sourceFile.getImportDeclaration((declaration) => {
309 | return declaration.getModuleSpecifierValue() === `./subrouters/${routerFile}`;
310 | });
311 |
312 | if (!existingImport) {
313 | sourceFile.addImportDeclaration({
314 | moduleSpecifier: `./subrouters/${routerFile}`,
315 | namedImports: [routerName]
316 | });
317 | }
318 |
319 | const appRouterDeclaration = sourceFile.getVariableDeclaration('appRouter');
320 |
321 | const existingArgument = appRouterDeclaration
322 | ?.getInitializerIfKindOrThrow(SyntaxKind.CallExpression)
323 | .getArguments()
324 | .find((arg) => arg.getText() === routerName);
325 |
326 | if (!existingArgument) {
327 | appRouterDeclaration
328 | ?.getInitializerIfKindOrThrow(SyntaxKind.CallExpression)
329 | .addArgument(routerName);
330 | }
331 | };
332 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/src/lib/naming.ts:
--------------------------------------------------------------------------------
1 | export const generateZodModelName = (name: string) => {
2 | return `${name.toLowerCase()}Model`;
3 | };
4 |
5 | export const generateEnumName = (name: string) => {
6 | return `${name.toLowerCase()}Enum`;
7 | };
8 |
9 | export const generateCompleteModelName = (name: string) => {
10 | return `Complete${name}Model`;
11 | };
12 |
13 | export const generateZodEnumName = (name: string) => {
14 | return `${name.toLowerCase()}EnumModel`;
15 | };
16 |
17 | export const generateRouterFilename = (name: string) => {
18 | return `${name.toLowerCase()}-router`;
19 | };
20 |
21 | export const generateModelFilename = (name: string) => {
22 | return `${name.toLowerCase()}`;
23 | };
24 |
25 | export const generateEnumFilename = (name: string) => {
26 | return `${name.toLowerCase()}`;
27 | };
28 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/src/lib/parser.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from 'fs';
2 | import { getSchema } from '@mrleebo/prisma-ast';
3 |
4 | export const getModelByName = (modelName: string) => {
5 | const schema = getSchema(readFileSync('prisma/schema.prisma', { encoding: 'utf8' }));
6 | return schema.list.find(
7 | (item) => item.type === 'model' && item.name.toLowerCase() === modelName.toLowerCase()
8 | );
9 | };
10 |
11 | export const getAllModels = () => {
12 | const schema = getSchema(readFileSync('prisma/schema.prisma', { encoding: 'utf8' }));
13 | return schema.list.filter((item) => item.type === 'model');
14 | };
15 |
16 | export const getAllEnums = () => {
17 | const schema = getSchema(readFileSync('prisma/schema.prisma', { encoding: 'utf8' }));
18 | return schema.list.filter((item) => item.type === 'enum');
19 | };
20 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/src/templates/base/router.ts.ejs:
--------------------------------------------------------------------------------
1 | import { router, publicProcedure, mergeRouters } from './trpc';
2 |
3 | const defaultRouter = router({
4 | greeting: publicProcedure.query(() => 'hello webstone tRPC')
5 | });
6 |
7 | export const appRouter = mergeRouters(defaultRouter);
8 |
9 | export type AppRouter = typeof appRouter;
10 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/src/templates/base/trpc.ts.ejs:
--------------------------------------------------------------------------------
1 | import { initTRPC } from 'webstone-plugin-trpc-web';
2 | const t = initTRPC.create();
3 |
4 | export const middleware = t.middleware;
5 | export const router = t.router;
6 | export const publicProcedure = t.procedure;
7 | export const mergeRouters = t.mergeRouters;
8 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/src/templates/subrouter.ejs:
--------------------------------------------------------------------------------
1 | import { router, publicProcedure } from '../trpc';
2 |
3 | export const <%=props.lowercaseSingular%>Router = router({
4 | create<%=props.capitalizedSingular%>: publicProcedure
5 | .input(<%=props.zodModelName%>)
6 | .mutation(({ input }) => {
7 | return input;
8 | }),
9 | getAll<%=props.capitalizedPlural%>: publicProcedure.query(() => {
10 | return [];
11 | }),
12 | get<%=props.capitalizedSingular%> : publicProcedure.input(<%=props.idFieldType%>).query(({ input }) => {
13 | return input;
14 | }),
15 | update<%=props.capitalizedSingular%>: publicProcedure.input(<%=props.zodModelName%>).mutation(({ input }) => {
16 | return input;
17 | }),
18 | delete<%=props.capitalizedSingular%>: publicProcedure.input(<%=props.idFieldType%>).mutation(({ input }) => {
19 | return input;
20 | })
21 | });
22 |
23 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/tests/lib/generate.spec.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'uvu';
2 | import * as assert from 'uvu/assert';
3 | import sinon from 'sinon';
4 | import fs from 'node:fs';
5 | import {
6 | getNonScalarFields,
7 | determineEnum,
8 | mapZodType,
9 | prepareApprouter,
10 | getIDType
11 | } from '../../src/lib/generate';
12 | import { Field, Model } from '@mrleebo/prisma-ast';
13 | import { Project, SourceFile } from 'ts-morph';
14 |
15 | test.after.each(() => {
16 | sinon.restore();
17 | });
18 |
19 | test('should return non-scalar fields', async () => {
20 | const model: Model = {
21 | name: 'TestModel',
22 | type: 'model',
23 | properties: [
24 | {
25 | type: 'field',
26 | fieldType: 'String',
27 | name: 'stringField'
28 | },
29 | {
30 | type: 'field',
31 | fieldType: 'Non-Scalar',
32 | name: 'nonScalarField'
33 | },
34 | {
35 | type: 'field',
36 | fieldType: 'Int',
37 | name: 'numberField'
38 | }
39 | ]
40 | };
41 | const nonScalarFields = getNonScalarFields(model);
42 |
43 | assert.is(nonScalarFields.length, 1);
44 | assert.is(nonScalarFields[0].type === 'field' && nonScalarFields[0].name, 'nonScalarField');
45 | assert.equal(nonScalarFields, [
46 | {
47 | type: 'field',
48 | fieldType: 'Non-Scalar',
49 | name: 'nonScalarField'
50 | }
51 | ]);
52 | });
53 |
54 | test('should determine enum', async () => {
55 | const fakeReadFileSync = sinon.fake.returns(`
56 | generator client {
57 | provider = "prisma-client-js"
58 | }
59 |
60 | datasource db {
61 | provider = "postgresql"
62 | url = env("DATABASE_URL")
63 | }
64 |
65 | model User {
66 | id Int @id @default(autoincrement())
67 | createdAt DateTime @default(now())
68 | email String @unique
69 | name String?
70 | role Role? @default(USER)
71 | postId Int?
72 | posts Post[]
73 | }
74 |
75 | model Post {
76 | id Int @id @default(autoincrement())
77 | user User @relation(fields: [userId], references: [id])
78 | role Role
79 | userId Int
80 | }
81 |
82 | enum Role {
83 | USER
84 | ADMIN
85 | }
86 | `);
87 |
88 | sinon.replace(fs, 'readFileSync', fakeReadFileSync);
89 |
90 | const field: Field = {
91 | type: 'field',
92 | fieldType: 'Role',
93 | name: 'Role'
94 | };
95 |
96 | const determinedEnum = determineEnum(field);
97 |
98 | assert.ok(determinedEnum);
99 | assert.is(determinedEnum?.type, 'enum');
100 | assert.is(determinedEnum.type === 'enum' && determinedEnum?.name, 'Role');
101 | assert.is(determinedEnum.type === 'enum' && determinedEnum?.enumerators.length, 2);
102 | });
103 |
104 | test("should determine enum doesn't exist", async () => {
105 | const fakeReadFileSync = sinon.fake.returns(`
106 | generator client {
107 | provider = "prisma-client-js"
108 | }
109 |
110 | datasource db {
111 | provider = "postgresql"
112 | url = env("DATABASE_URL")
113 | }
114 |
115 | model User {
116 | id Int @id @default(autoincrement())
117 | createdAt DateTime @default(now())
118 | email String @unique
119 | name String?
120 | role Role? @default(USER)
121 | postId Int?
122 | posts Post[]
123 | }
124 |
125 | model Post {
126 | id Int @id @default(autoincrement())
127 | user User @relation(fields: [userId], references: [id])
128 | role Role
129 | userId Int
130 | }
131 |
132 | enum Role {
133 | USER
134 | ADMIN
135 | }
136 | `);
137 |
138 | sinon.replace(fs, 'readFileSync', fakeReadFileSync);
139 |
140 | const field: Field = {
141 | type: 'field',
142 | fieldType: 'Title',
143 | name: 'Title'
144 | };
145 |
146 | const determinedEnum = determineEnum(field);
147 | assert.not.ok(determinedEnum);
148 | });
149 |
150 | test('map zod type to string', async () => {
151 | const field: Field = {
152 | type: 'field',
153 | fieldType: 'String',
154 | name: 'stringField'
155 | };
156 |
157 | const mappedType = mapZodType(field);
158 | assert.is(mappedType, 'z.string()');
159 | });
160 |
161 | test('map zod type to string array', async () => {
162 | const field: Field = {
163 | type: 'field',
164 | fieldType: 'String',
165 | name: 'stringField',
166 | array: true
167 | };
168 |
169 | const mappedType = mapZodType(field);
170 | assert.is(mappedType, 'z.string().array()');
171 | });
172 |
173 | test('map zod type to optional string array', async () => {
174 | const field: Field = {
175 | type: 'field',
176 | fieldType: 'String',
177 | name: 'stringField',
178 | array: true,
179 | optional: true
180 | };
181 |
182 | const mappedType = mapZodType(field);
183 | assert.is(mappedType, 'z.string().array().nullish()');
184 | });
185 |
186 | test('map zod type to number', async () => {
187 | const field: Field = {
188 | type: 'field',
189 | fieldType: 'Int',
190 | name: 'numberField'
191 | };
192 |
193 | const mappedType = mapZodType(field);
194 | assert.is(mappedType, 'z.number().int()');
195 | });
196 |
197 | test('map zod type to boolean', async () => {
198 | const field: Field = {
199 | type: 'field',
200 | fieldType: 'Boolean',
201 | name: 'booleanField'
202 | };
203 |
204 | const mappedType = mapZodType(field);
205 | assert.is(mappedType, 'z.boolean()');
206 | });
207 |
208 | test('map zod type to BigInt', async () => {
209 | const field: Field = {
210 | type: 'field',
211 | fieldType: 'BigInt',
212 | name: 'bigIntField'
213 | };
214 |
215 | const mappedType = mapZodType(field);
216 | assert.is(mappedType, 'z.bigint()');
217 | });
218 |
219 | test('map zod type to DateTime', async () => {
220 | const field: Field = {
221 | type: 'field',
222 | fieldType: 'DateTime',
223 | name: 'dateField'
224 | };
225 |
226 | const mappedType = mapZodType(field);
227 | assert.is(mappedType, 'z.date()');
228 | });
229 |
230 | test('map zod type to Float', async () => {
231 | const field: Field = {
232 | type: 'field',
233 | fieldType: 'Float',
234 | name: 'floatField'
235 | };
236 |
237 | const mappedType = mapZodType(field);
238 | assert.is(mappedType, 'z.number()');
239 | });
240 |
241 | test('map zod type to Float', async () => {
242 | const field: Field = {
243 | type: 'field',
244 | fieldType: 'Float',
245 | name: 'floatField'
246 | };
247 |
248 | const mappedType = mapZodType(field);
249 | assert.is(mappedType, 'z.number()');
250 | });
251 |
252 | test('map zod type to Decimal', async () => {
253 | const field: Field = {
254 | type: 'field',
255 | fieldType: 'Decimal',
256 | name: 'decimalField'
257 | };
258 |
259 | const mappedType = mapZodType(field);
260 | assert.is(mappedType, 'z.number()');
261 | });
262 |
263 | test('should import and extend the approuter', async () => {
264 | const project = new Project({ useInMemoryFileSystem: true });
265 |
266 | const sourceFile: SourceFile = project.createSourceFile(
267 | '/testRouter.ts',
268 | `
269 | import { router, publicProcedure, mergeRouters } from './trpc';
270 |
271 | const defaultRouter = router({
272 | greeting: publicProcedure.query(() => 'hello webstone tRPC')
273 | });
274 |
275 | export const appRouter = mergeRouters(defaultRouter);
276 |
277 | export type AppRouter = typeof appRouter;
278 |
279 | `
280 | );
281 |
282 | prepareApprouter(sourceFile, 'testRouter', 'testRouter');
283 |
284 | const imports = sourceFile.getImportDeclarations();
285 |
286 | const namedImports = imports.flatMap((imp) =>
287 | imp.getNamedImports().map((namedImp) => namedImp.getText())
288 | );
289 |
290 | const moduleSpecifiers = imports.map((imp) => imp.getModuleSpecifierValue());
291 |
292 | assert.is(imports.length, 2);
293 | assert.is(namedImports.length, 4);
294 |
295 | // check named imports
296 | assert.ok(namedImports.includes('testRouter'));
297 |
298 | // check module specifiers
299 | assert.ok(moduleSpecifiers.includes('./subrouters/testRouter'));
300 | });
301 |
302 | test('should return string as id type', async () => {
303 | const model: Model = {
304 | name: 'TestModel',
305 | type: 'model',
306 | properties: [
307 | {
308 | type: 'field',
309 | fieldType: 'String',
310 | name: 'stringField',
311 | attributes: [
312 | {
313 | name: 'id',
314 | type: 'attribute',
315 | kind: 'field'
316 | }
317 | ]
318 | },
319 | {
320 | type: 'field',
321 | fieldType: 'Non-Scalar',
322 | name: 'nonScalarField'
323 | },
324 | {
325 | type: 'field',
326 | fieldType: 'Int',
327 | name: 'numberField'
328 | }
329 | ]
330 | };
331 |
332 | const idType = getIDType(model);
333 | assert.is(idType, 'z.string()');
334 | });
335 |
336 | test('should return number as id type', async () => {
337 | const model: Model = {
338 | name: 'TestModel',
339 | type: 'model',
340 | properties: [
341 | {
342 | type: 'field',
343 | fieldType: 'Int',
344 | name: 'stringField',
345 | attributes: [
346 | {
347 | name: 'id',
348 | type: 'attribute',
349 | kind: 'field'
350 | }
351 | ]
352 | },
353 | {
354 | type: 'field',
355 | fieldType: 'Non-Scalar',
356 | name: 'nonScalarField'
357 | },
358 | {
359 | type: 'field',
360 | fieldType: 'Int',
361 | name: 'numberField'
362 | }
363 | ]
364 | };
365 |
366 | const idType = getIDType(model);
367 | assert.is(idType, 'z.number().int()');
368 | });
369 |
370 | test('should return error, because no ID provided', async () => {
371 | try {
372 | const model: Model = {
373 | name: 'TestModel',
374 | type: 'model',
375 | properties: [
376 | {
377 | type: 'field',
378 | fieldType: 'Int',
379 | name: 'stringField'
380 | },
381 | {
382 | type: 'field',
383 | fieldType: 'Non-Scalar',
384 | name: 'nonScalarField'
385 | },
386 | {
387 | type: 'field',
388 | fieldType: 'Int',
389 | name: 'numberField'
390 | }
391 | ]
392 | };
393 | getIDType(model);
394 | } catch (error) {
395 | assert.is(error.message, 'ID field not found');
396 | }
397 | });
398 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/tests/lib/naming.spec.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'uvu';
2 | import * as assert from 'uvu/assert';
3 | import {
4 | generateZodEnumName,
5 | generateZodModelName,
6 | generateRouterFilename,
7 | generateEnumFilename,
8 | generateModelFilename,
9 | generateCompleteModelName,
10 | generateEnumName
11 | } from '../../src/lib/naming';
12 |
13 | test('should return zod model name', async () => {
14 | const modelName = generateZodModelName('User');
15 | assert.is(modelName, 'userModel');
16 | });
17 |
18 | test('should return zod enum name', async () => {
19 | const enumName = generateZodEnumName('Role');
20 | assert.is(enumName, 'roleEnumModel');
21 | });
22 |
23 | test('should return subrouter filename', async () => {
24 | const filename = generateRouterFilename('User');
25 | assert.is(filename, 'user-router');
26 | });
27 |
28 | test('should return model filename', async () => {
29 | const filename = generateModelFilename('User');
30 | assert.is(filename, 'user');
31 | });
32 |
33 | test('should return enum filename', async () => {
34 | const filename = generateEnumFilename('Role');
35 | assert.is(filename, 'role');
36 | });
37 |
38 | test('should return enum name', async () => {
39 | const enumName = generateEnumName('Role');
40 | assert.is(enumName, 'roleEnum');
41 | });
42 |
43 | test('should return complete name', async () => {
44 | const completeName = generateCompleteModelName('User');
45 | assert.is(completeName, 'CompleteUserModel');
46 | });
47 |
48 | test.run();
49 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/tests/lib/parser.spec.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'uvu';
2 | import * as assert from 'uvu/assert';
3 | import sinon from 'sinon';
4 | import fs from 'fs';
5 | import { getAllEnums, getAllModels, getModelByName } from '../../src/lib/parser';
6 |
7 | test.after.each(() => {
8 | sinon.restore();
9 | });
10 |
11 | test('should get all enums', async () => {
12 | const fakeReadFileSync = sinon.fake.returns(`
13 | generator client {
14 | provider = "prisma-client-js"
15 | }
16 |
17 | datasource db {
18 | provider = "postgresql"
19 | url = env("DATABASE_URL")
20 | }
21 |
22 | model User {
23 | id Int @id @default(autoincrement())
24 | createdAt DateTime @default(now())
25 | email String @unique
26 | name String?
27 | role Role? @default(USER)
28 | postId Int?
29 | posts Post[]
30 | }
31 |
32 | model Post {
33 | id Int @id @default(autoincrement())
34 | user User @relation(fields: [userId], references: [id])
35 | role Role
36 | userId Int
37 | }
38 |
39 | enum Role {
40 | USER
41 | ADMIN
42 | }
43 | `);
44 |
45 | sinon.replace(fs, 'readFileSync', fakeReadFileSync);
46 |
47 | const enums = getAllEnums();
48 |
49 | assert.is(enums.length, 1);
50 | assert.is(enums[0].type === 'enum' && enums[0].name, 'Role');
51 | assert.is(enums[0].type, 'enum');
52 | });
53 |
54 | test('should return all models', async () => {
55 | const fakeReadFileSync = sinon.fake.returns(`
56 | generator client {
57 | provider = "prisma-client-js"
58 | }
59 |
60 | datasource db {
61 | provider = "postgresql"
62 | url = env("DATABASE_URL")
63 | }
64 |
65 | model User {
66 | id Int @id @default(autoincrement())
67 | createdAt DateTime @default(now())
68 | email String @unique
69 | name String?
70 | role Role? @default(USER)
71 | postId Int?
72 | posts Post[]
73 | }
74 |
75 | model Post {
76 | id Int @id @default(autoincrement())
77 | user User @relation(fields: [userId], references: [id])
78 | role Role
79 | userId Int
80 | }
81 |
82 | enum Role {
83 | USER
84 | ADMIN
85 | }
86 | `);
87 |
88 | sinon.replace(fs, 'readFileSync', fakeReadFileSync);
89 | const models = getAllModels();
90 |
91 | assert.is(models.length, 2);
92 | assert.is(models[0].type, 'model');
93 | assert.is(models[0].type === 'model' && models[0].name, 'User');
94 | });
95 |
96 | test('should get single model by name', async () => {
97 | const fakeReadFileSync = sinon.fake.returns(`
98 | generator client {
99 | provider = "prisma-client-js"
100 | }
101 |
102 | datasource db {
103 | provider = "postgresql"
104 | url = env("DATABASE_URL")
105 | }
106 |
107 | model User {
108 | id Int @id @default(autoincrement())
109 | createdAt DateTime @default(now())
110 | email String @unique
111 | name String?
112 | role Role? @default(USER)
113 | postId Int?
114 | posts Post[]
115 | }
116 |
117 | model Post {
118 | id Int @id @default(autoincrement())
119 | user User @relation(fields: [userId], references: [id])
120 | role Role
121 | userId Int
122 | }
123 |
124 | enum Role {
125 | USER
126 | ADMIN
127 | }
128 | `);
129 |
130 | sinon.replace(fs, 'readFileSync', fakeReadFileSync);
131 |
132 | const model = getModelByName('User');
133 |
134 | assert.ok(model);
135 | assert.is(model?.type, 'model');
136 | assert.is(model?.type === 'model' && model?.name, 'User');
137 | assert.is(model?.type === 'model' && model?.properties.length, 7);
138 | });
139 |
140 | test("should return undefined if models doesn't exist", async () => {
141 | const fakeReadFileSync = sinon.fake.returns(`
142 | generator client {
143 | provider = "prisma-client-js"
144 | }
145 |
146 | datasource db {
147 | provider = "postgresql"
148 | url = env("DATABASE_URL")
149 | }
150 |
151 | model User {
152 | id Int @id @default(autoincrement())
153 | createdAt DateTime @default(now())
154 | email String @unique
155 | name String?
156 | role Role? @default(USER)
157 | postId Int?
158 | posts Post[]
159 | }
160 |
161 | model Post {
162 | id Int @id @default(autoincrement())
163 | user User @relation(fields: [userId], references: [id])
164 | role Role
165 | userId Int
166 | }
167 |
168 | enum Role {
169 | USER
170 | ADMIN
171 | }
172 | `);
173 |
174 | sinon.replace(fs, 'readFileSync', fakeReadFileSync);
175 |
176 | const model = getModelByName('User2');
177 |
178 | assert.not.ok(model);
179 | });
180 |
181 | test.run();
182 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "outDir": "./build",
5 | "module": "commonjs",
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "allowSyntheticDefaultImports": true,
9 | "experimentalDecorators": true,
10 | "skipLibCheck": true,
11 | "declaration": true,
12 | "moduleResolution": "Node",
13 | "rootDir": "src",
14 | "declarationDir": "./build/types"
15 | },
16 | "include": ["src/**/*.ts"],
17 | "exclude": ["src/fixtures", "src/**/*.test.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/.eslintignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
5 | plugins: ['svelte3', '@typescript-eslint'],
6 | ignorePatterns: ['*.cjs'],
7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
8 | settings: {
9 | 'svelte3/typescript': () => require('typescript')
10 | },
11 | parserOptions: {
12 | sourceType: 'module',
13 | ecmaVersion: 2020
14 | },
15 | env: {
16 | browser: true,
17 | es2017: true,
18 | node: true
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/.prettierignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte"],
7 | "pluginSearchDirs": ["."],
8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # webstone-plugin-trpc-web
2 |
3 | ## 0.1.0
4 |
5 | ### Minor Changes
6 |
7 | - 2826fcb: Release a beta version of the tRPC Webstone Plugin.
8 | - 23575e3: Release the webstone-plugin-request-logger package.
9 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/README.md:
--------------------------------------------------------------------------------
1 | # tRPC Web - Webstone Plugin
2 |
3 | ## Installation
4 |
5 | Install this plugin with the following command:
6 |
7 | ```
8 | npm install -D webstone-plugin-trpc-web
9 | ```
10 |
11 | ## Usage
12 |
13 | > **_Document how developers can use this plugin. Do they import a Svelte component? Is there a Svelte action they can use?_**
14 |
15 | ## Learn more about Webstone Plugins
16 |
17 | This plugin is part of a wider ecosystem called [Webstone Plugins](https://github.com/WebstoneHQ/webstone).
18 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webstone-plugin-trpc-web",
3 | "version": "0.1.0",
4 | "scripts": {
5 | "dev": "vite dev",
6 | "build": "svelte-kit sync && svelte-package --input src/lib/plugin && cp package.json ./dist/",
7 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
8 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
9 | "lint": "prettier --plugin-search-dir . --check . && eslint .",
10 | "format": "prettier --plugin-search-dir . --write .",
11 | "prepublishOnly": "pnpm remove webstone-plugin-trpc-cli @webstone/cli"
12 | },
13 | "devDependencies": {
14 | "@playwright/test": "^1.37.0",
15 | "@sveltejs/adapter-auto": "^2.1.0",
16 | "@sveltejs/kit": "^1.22.5",
17 | "@sveltejs/package": "^2.2.1",
18 | "@typescript-eslint/eslint-plugin": "^6.3.0",
19 | "@typescript-eslint/parser": "^6.3.0",
20 | "@webstone/cli": "workspace:^0.13.0",
21 | "eslint": "^8.46.0",
22 | "eslint-config-prettier": "^9.0.0",
23 | "eslint-plugin-svelte3": "^4.0.0",
24 | "prettier": "^3.0.1",
25 | "prettier-plugin-svelte": "^3.0.3",
26 | "svelte": "^4.1.2",
27 | "svelte-check": "^3.4.6",
28 | "tslib": "^2.6.1",
29 | "typescript": "^5.1.6",
30 | "vite": "^4.4.9",
31 | "webstone-plugin-trpc-cli": "workspace:^0.2.0"
32 | },
33 | "type": "module",
34 | "private": true,
35 | "dependencies": {
36 | "@trpc/client": "^10.37.1",
37 | "@trpc/server": "^10.37.1",
38 | "zod": "^3.21.4"
39 | },
40 | "exports": {
41 | "./package.json": "./package.json",
42 | ".": {
43 | "types": "./index.d.ts",
44 | "svelte": "./index.js",
45 | "default": "./index.js"
46 | }
47 | },
48 | "files": [
49 | "dist"
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import type { PlaywrightTestConfig } from '@playwright/test';
2 |
3 | const config: PlaywrightTestConfig = {
4 | webServer: {
5 | command: 'npm run build && npm run preview',
6 | port: 4173
7 | }
8 | };
9 |
10 | export default config;
11 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/src/app.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // See https://kit.svelte.dev/docs/types#app
4 | // for information about these interfaces
5 | // and what to do when importing types
6 | declare namespace App {
7 | // interface Locals {}
8 | // interface PageData {}
9 | // interface Error {}
10 | // interface Platform {}
11 | }
12 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/src/lib/plugin/client.ts:
--------------------------------------------------------------------------------
1 | import type { AnyRouter, ClientDataTransformerOptions } from '@trpc/server';
2 | import {
3 | createTRPCProxyClient,
4 | httpBatchLink,
5 | type HTTPHeaders,
6 | type TRPCLink
7 | } from '@trpc/client';
8 |
9 | export function createTrpcClient({
10 | endpointUrl = '/trpc',
11 | loadFetch,
12 | transformer,
13 | headers
14 | }: {
15 | endpointUrl: string;
16 | loadFetch?: typeof window.fetch;
17 | transformer?: ClientDataTransformerOptions;
18 | headers?: HTTPHeaders | (() => HTTPHeaders | Promise);
19 | }) {
20 | const link: TRPCLink = httpBatchLink({
21 | url: endpointUrl,
22 | ...(loadFetch && { fetch: loadFetch, headers })
23 | });
24 |
25 | return createTRPCProxyClient({ transformer, links: [link] });
26 | }
27 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/src/lib/plugin/handler.ts:
--------------------------------------------------------------------------------
1 | // thank you icflorescu for the inspiration in https://github.com/icflorescu/trpc-sveltekit/blob/main/package/src/server.ts
2 |
3 | import type { Handle, RequestEvent } from '@sveltejs/kit';
4 | import type {
5 | AnyRouter,
6 | Dict,
7 | inferRouterContext,
8 | inferRouterError,
9 | TRPCError
10 | } from '@trpc/server';
11 | import type { HTTPRequest } from '@trpc/server/dist/http/internals/types';
12 | import { resolveHTTPResponse, type ResponseMeta } from '@trpc/server/http';
13 | import type { TRPCResponse } from '@trpc/server/rpc';
14 |
15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 | export const createTrpcHandler = ({
17 | endpointURL = '/trpc',
18 | router,
19 | createContext,
20 | responseMeta,
21 | onError
22 | }: {
23 | /**
24 | * URL the Handler is mounted on
25 | */
26 | endpointURL: string;
27 |
28 | /**
29 | * TRPC router
30 | */
31 | router: Router;
32 | createContext?: (event: RequestEvent) => Promise>;
33 | responseMeta?: (options: {
34 | data: TRPCResponse>[];
35 | ctx?: inferRouterContext;
36 | paths?: string[];
37 | type: 'query' | 'mutation' | 'subscription' | 'unknown';
38 | errors: TRPCError[];
39 | }) => ResponseMeta;
40 |
41 | onError?: (options: {
42 | error: TRPCError;
43 | type: 'query' | 'mutation' | 'subscription' | 'unknown';
44 | path?: string;
45 | input: unknown;
46 | ctx?: inferRouterContext;
47 | req: HTTPRequest;
48 | }) => void;
49 | }): Handle => {
50 | const trpcHandler: Handle = async ({ event, resolve }) => {
51 | if (event.url.pathname.startsWith(endpointURL)) {
52 | const request = event.request as Request & {
53 | headers: Dict;
54 | };
55 |
56 | const req = {
57 | method: request.method,
58 | headers: request.headers,
59 | query: event.url.searchParams,
60 | body: await request.text()
61 | };
62 |
63 | const httpResponse = await resolveHTTPResponse({
64 | router,
65 | req,
66 | path: event.url.pathname.substring(endpointURL.length + 1),
67 | createContext: async () => createContext?.(event),
68 | responseMeta,
69 | onError
70 | });
71 |
72 | const { status, headers, body } = httpResponse as {
73 | status: number;
74 | headers: Record;
75 | body: string;
76 | };
77 |
78 | return new Response(body, { status, headers });
79 | }
80 |
81 | return await resolve(event);
82 | };
83 |
84 | return trpcHandler;
85 | };
86 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/src/lib/plugin/index.ts:
--------------------------------------------------------------------------------
1 | export * from './handler';
2 | export { z } from 'zod';
3 | export * from '@trpc/server';
4 | export * from './client';
5 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 | Welcome to your library project
2 | Create your package using @sveltejs/package and preview/showcase your work with SvelteKit
3 | Visit kit.svelte.dev to read the documentation
4 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WebstoneHQ/webstone-plugins/5296c66e4191ea32b09b70bf728fc023f0f1ccc4/packages/plugin-trpc/web/static/favicon.png
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import { vitePreprocess } from '@sveltejs/kit/vite';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://github.com/sveltejs/svelte-preprocess
7 | // for more information about preprocessors
8 | preprocess: vitePreprocess(),
9 |
10 | kit: {
11 | adapter: adapter()
12 | }
13 | };
14 |
15 | export default config;
16 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true
12 | }
13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
14 | //
15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
16 | // from the referenced tsconfig.json - TypeScript does not merge them in
17 | }
18 |
--------------------------------------------------------------------------------
/packages/plugin-trpc/web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import type { UserConfig } from 'vite';
3 |
4 | const config: UserConfig = {
5 | plugins: [sveltekit()]
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | // playwright.config.ts
2 | import { type PlaywrightTestConfig } from "@playwright/test";
3 |
4 | const config: PlaywrightTestConfig = {
5 | testIgnore: "**/_dev-app/**",
6 | use: {
7 | baseURL: "http://localhost:5173",
8 | screenshot: "only-on-failure",
9 | video: "retain-on-failure",
10 | },
11 | outputDir: "tests/e2e/test-results",
12 | };
13 | export default config;
14 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/**"
3 | - "!_dev-app/**"
4 | - "!packages/create-webstone-app/templates/**"
5 |
--------------------------------------------------------------------------------
/tests/e2e/1-web-pages/create-and-delete.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 | import { execSync } from "child_process";
3 | import { resolve } from "path";
4 | import { sleep } from "../globals";
5 |
6 | const devAppPath = resolve("./_dev-app");
7 |
8 | test.describe("web/page/create & web/page/delete", () => {
9 | test("creates and deletes an About Us page", async ({ page }) => {
10 | execSync("pnpm ws web route create 'About Us' --types '+page.svelte'", {
11 | cwd: devAppPath,
12 | });
13 | await page.goto("/about-us");
14 | await expect(page.locator("h1")).toContainText("About Us");
15 | await page.goto("/");
16 | execSync("pnpm ws web route delete 'About Us'", {
17 | cwd: devAppPath,
18 | });
19 |
20 | // The previous `execSync` call to delete /about-us results in a dev server restart.
21 | // Let's wait a tiny bit for the restart to complete.
22 | await sleep(300);
23 |
24 | await page.goto("/about-us");
25 | await expect(page.locator("h1")).toContainText("404");
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/tests/e2e/2-api-endpoints/create-and-delete.spec.ts:
--------------------------------------------------------------------------------
1 | import { APIResponse, expect, test } from "@playwright/test";
2 | import { execSync } from "child_process";
3 | import { resolve } from "path";
4 | import { sleep } from "../globals";
5 |
6 | const devAppPath = resolve("./_dev-app");
7 |
8 | test.describe("web/api/create & web/api/delete", () => {
9 | test("creates and deletes CRUD API endpoints for /api/users", async ({
10 | request,
11 | }) => {
12 | execSync("pnpm ws web api create /api/users", {
13 | cwd: devAppPath,
14 | });
15 |
16 | let response: APIResponse;
17 | response = await request.get("/api/users");
18 | expect(response.ok()).toBeTruthy();
19 | expect(response.status()).toEqual(200);
20 | expect(await response.text()).toEqual("GET /api/users => Ok.");
21 |
22 | response = await request.post("/api/users");
23 | expect(response.ok()).toBeTruthy();
24 | expect(response.status()).toEqual(200);
25 | expect(await response.text()).toEqual("POST /api/users => Ok.");
26 |
27 | response = await request.delete("/api/users/123");
28 | expect(response.ok()).toBeTruthy();
29 | expect(response.status()).toEqual(200);
30 | expect(await response.text()).toEqual("DELETE /api/users/123 => Ok.");
31 |
32 | response = await request.patch("/api/users/123");
33 | expect(response.ok()).toBeTruthy();
34 | expect(response.status()).toEqual(200);
35 | expect(await response.text()).toEqual("PATCH /api/users/123 => Ok.");
36 |
37 | response = await request.put("/api/users/123");
38 | expect(response.ok()).toBeTruthy();
39 | expect(response.status()).toEqual(200);
40 | expect(await response.text()).toEqual("PUT /api/users/123 => Ok.");
41 |
42 | execSync("pnpm ws web api delete /api/users", {
43 | cwd: devAppPath,
44 | });
45 |
46 | // The previous `execSync` call to delete /api/users results in a dev server restart.
47 | // Let's wait a tiny bit for the restart to complete.
48 | await sleep(300);
49 |
50 | response = await request.get("/api/users");
51 | expect(response.status()).toEqual(404);
52 |
53 | response = await request.post("/api/users");
54 | expect(response.status()).toEqual(404);
55 |
56 | response = await request.delete("/api/users/123");
57 | expect(response.status()).toEqual(404);
58 |
59 | response = await request.patch("/api/users/123");
60 | expect(response.status()).toEqual(404);
61 |
62 | response = await request.put("/api/users/123");
63 | expect(response.status()).toEqual(404);
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/tests/e2e/globals.ts:
--------------------------------------------------------------------------------
1 | export const sleep = async (ms: number) => {
2 | return new Promise((resolve) => setTimeout(resolve, ms));
3 | };
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "noImplicitAny": true,
7 | "noImplicitThis": true,
8 | "noUnusedLocals": true,
9 | "sourceMap": false,
10 | "inlineSourceMap": true,
11 | "strict": true,
12 | "declaration": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------