├── .gitignore ├── README.md ├── graphql-codegen.yml ├── graphql ├── App.ts ├── Book.ts ├── Context.ts ├── Mutation.ts ├── Query.ts ├── build-schema.ts ├── gen │ └── nxs.gen.ts ├── schema.graphql ├── schema.ts └── server.ts ├── images ├── demo.gif ├── editor.png ├── screenie.png ├── ss1.png └── types.png ├── index.html ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── Mutation.vue ├── Query.vue ├── assets │ └── logo.png ├── components │ └── HelloWorld.vue ├── generated │ └── graphql.ts ├── main.ts ├── shims-vue.d.ts └── vite-env.d.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Type Safe GraphQL with Vue 3 2 | 3 | This is an article walking through constructing a stack to do type safe GraphQL with Vue 3. It uses: 4 | 5 | - [graphql](https://graphql.org/) (graphql spec) 6 | - [express-graphql](https://github.com/graphql/express-graphql) (express graphql middleware for the server) 7 | - [urql](https://formidable.com/open-source/urql/) (and `@urql/vue`) for the front-end client 8 | - [nexus-graphql](https://nexusjs.org/) library for generating GraphQL schema using TypeScript objects 9 | - [nexus-decorators](https://github.com/graphql-nexus/nexus-decorators) experimental decorator syntax for use with `nexus-graphql` 10 | - [graphql-code-generator](https://www.graphql-code-generator.com/) generate TypeScript types from `gql` queries 11 | 12 | It's a lot of moving parts. We will create a non type safe app with Vue 3 and urql, then make it type safe starting from the back-end and work our way to the front-end. 13 | 14 | ## Getting Started 15 | 16 | Create a new Vue 3 project with: 17 | 18 | ```sh 19 | yarn create vite vue-urql-nexus --template vue-ts 20 | ``` 21 | 22 | ## Create a GraphQL Server 23 | 24 | The first thing we need is a GraphQL server to test. Like every GraphQL tutorial, be prepared to install a LOT of dependencies. 25 | 26 | For now, we need these dependencies: 27 | 28 | ```sh 29 | yarn add ts-node graphql express-graphql express cors @types/express @types/cors --dev 30 | ``` 31 | 32 | Now make `graphql/server.ts` and add some code: 33 | 34 | ```ts 35 | import { graphqlHTTP } from "express-graphql"; 36 | import express from "express"; 37 | import { buildSchema } from "graphql"; 38 | import cors from "cors"; 39 | 40 | const graphqlSchema = buildSchema(` 41 | type Book { 42 | title: String! 43 | year: Int! 44 | author: String! 45 | } 46 | 47 | type App { 48 | books: [Book!] 49 | } 50 | 51 | type Query { 52 | app: App! 53 | } 54 | 55 | input BookInput { 56 | title: String! 57 | year: Int! 58 | author: String! 59 | } 60 | 61 | type Mutation { 62 | addBook(input: BookInput!): Book! 63 | } 64 | 65 | `); 66 | 67 | const app = express(); 68 | 69 | const context = { 70 | app: { 71 | books: [{ title: "Some title", year: 1980, author: "Lachlan" }], 72 | }, 73 | }; 74 | 75 | app.use(cors()); 76 | app.use( 77 | "/graphql", 78 | graphqlHTTP(() => { 79 | return { 80 | schema: graphqlSchema, 81 | graphiql: true, 82 | context, 83 | rootValue: { 84 | app: (_: any, ctx: typeof context) => { 85 | return ctx.app; 86 | }, 87 | addBook: ( 88 | args: { input: { title: string; year: number; author: string } }, 89 | ctx: typeof context 90 | ) => { 91 | const book = args.input; 92 | ctx.app.books.push(book); 93 | return book; 94 | }, 95 | }, 96 | }; 97 | }) 98 | ); 99 | 100 | app.listen(4000, () => { 101 | console.log("Started server on 4000"); 102 | }); 103 | ``` 104 | 105 | Quite a bit going on here. We introduce `input`, `type`, `Query` and `Mutation`. We use a singleton to maintain state. Notice a problem - we have duplicate types, in the GraphQL schema and in the `addBook` function. Not ideal... two sources of truth. More on this later. 106 | 107 | Grab the `tsonfig.json` from this repository, or configure it yourself, then start the server with `yarn ts-node graphql/server.ts`. You will need to restart every time you change it, or you could use `ts-node-dev`. Anyway, what this is doing is declaring a basic schema - I'll be storing all my data under `app`. 108 | 109 | If you go to `http://localhost:4000` you can query it. You can also add a new book with a mutation: 110 | 111 | ```graphql 112 | query Books { 113 | app { 114 | books { 115 | title 116 | } 117 | } 118 | } 119 | 120 | mutation AddBook($input: BookInput!) { 121 | addBook(input: $input) { 122 | title 123 | } 124 | } 125 | 126 | # variables 127 | # { 128 | # "input": { 129 | # "title": "New title", 130 | # "year": 1990, 131 | # "author": "Lachlan" 132 | # } 133 | # } 134 | ``` 135 | 136 | ![](./images/screenie.png) 137 | 138 | ## Using urql with Vue 139 | 140 | Add `@urql/vue`: 141 | 142 | ```sh 143 | yarn add @urql/vue 144 | ``` 145 | 146 | Create a new client in `App.vue`. You should also create `Query.vue` and `Mutation.vue`. 147 | 148 | ```html 149 | 152 | 153 | 170 | ``` 171 | 172 | ## Query with urql 173 | 174 | Now let's query using urql. Add this to `Query.vue`: 175 | 176 | ```html 177 | 192 | 193 | 221 | ``` 222 | 223 | Here's the main parts: 224 | 225 | - Make a query with `gql`. I'm using the same one we tested in the browser with. 226 | - Use `useQuery` to launch the query. It returns a `reactive` object with some useful properties. 227 | - Render the result. 228 | 229 | Start Vite with `yarn dev` and head to the browser. You should see a single book title rendered. 230 | 231 | ## Reactive Mutations with urql 232 | 233 | Let's add a mutation. Update `Mutation.vue`: 234 | 235 | ```html 236 | 250 | 251 | 292 | ``` 293 | 294 | All the types in the `gql` must match up to the ones your declared in your schema. 295 | 296 | Finally, the cool part - sumbit the form and you'll see the UI is automatically updated with the new book you created! The magic of urql and Vue. 297 | 298 | ![](./images/demo.gif) 299 | 300 | ## Solving the Typing Problem 301 | 302 | So everything works, but it's not type safe. Getting this to be type safe is a mission, are you ready? We will use a combination of tools, each more complex than the last:our 303 | 304 | - [graphql-code-generator](https://www.graphql-code-generator.com/). Creates types from your schema from `gql`. It extracts them from your `vue` files. 305 | - [graphql-nexus](https://nexusjs.org/). Generate schema from types. 306 | - [nexus-decorators](https://github.com/graphql-nexus/nexus-decorators). Works with graphql-nexus. Decorate classes which are turned into a schema. 307 | 308 | If we set it up correctly, everything will be type safe and we will generate the correct schema from the definitions. As a reminder, the schema looks like this: 309 | 310 | ```graphql 311 | type Book { 312 | title: String! 313 | year: Int! 314 | author: String! 315 | } 316 | 317 | type App { 318 | books: [Book!] 319 | } 320 | 321 | type Query { 322 | app: App! 323 | } 324 | 325 | input BookInput { 326 | title: String! 327 | year: Int! 328 | author: String! 329 | } 330 | 331 | type Mutation { 332 | addBook(input: BookInput!): Book! 333 | } 334 | ``` 335 | 336 | ## Creating a Schema with GraphQL Nexus 337 | 338 | Add the dependencies: 339 | 340 | ```ts 341 | yarn add nexus nexus-decorators --dev 342 | ``` 343 | 344 | Inside of `graphql/schema.ts` add the following: 345 | 346 | ```ts 347 | import path from "path"; 348 | import { makeSchema } from "nexus"; 349 | 350 | export const graphqlSchema = makeSchema({ 351 | types: [], 352 | shouldGenerateArtifacts: true, 353 | shouldExitAfterGenerateArtifacts: Boolean(process.env.GRAPHQL_CODEGEN), 354 | outputs: { 355 | typegen: path.join(__dirname, "gen/nxs.gen.ts"), 356 | schema: path.join(__dirname, "schema.graphql"), 357 | }, 358 | contextType: { 359 | module: path.join(__dirname, "./Context.ts"), 360 | export: "Context", 361 | }, 362 | }); 363 | ``` 364 | 365 | This will be how we create our schema and our types. The schema will be in `graphql/schema.graphql` and the types in `graphql/gen/nxs.gen.ts`. 366 | 367 | Add a simple `Context` to `graphql/Context.ts`. This is how we will get type safety on the context variable. 368 | 369 | ```ts 370 | export abstract class Context { 371 | } 372 | ``` 373 | 374 | You can create the schema by running `ts-node graphql/schema.ts`. I'd recommend making a file: `graphql/build-schema.ts` and adding: 375 | 376 | ```ts 377 | process.env.GRAPHQL_CODEGEN = "true"; 378 | import "./schema"; 379 | ``` 380 | 381 | Better yet, make it a script in `package.json`: 382 | 383 | ```json 384 | { 385 | "scripts": { 386 | "build-schema": "ts-node --transpile-only graphql/schema.ts" 387 | } 388 | } 389 | ``` 390 | 391 | Now just run `yarn build-schema`. The `GRAPHQL_CODEGEN` will be useful later when starting the server. 392 | 393 | The code generates two files. One is `graphql/schema.graphql`. It's simple: 394 | 395 | ```graphql 396 | ### This file was generated by Nexus Schema 397 | ### Do not make changes to this file directly 398 | 399 | 400 | type Query { 401 | ok: Boolean! 402 | } 403 | ``` 404 | 405 | Another is `graphql/gen/nxs.gen.ts` is more or less impossible to read - but take a look anyway. 406 | 407 | ## Adding Context, App and Book Entities 408 | 409 | Let's add `Context`, `App` and `Book` types. They are defined in `graphql`. Start with `Context`: 410 | 411 | ```ts 412 | // graphql/Context.ts 413 | import { App } from "./App"; 414 | import { Book } from "./Book"; 415 | 416 | export abstract class Context { 417 | abstract books: Book[] 418 | app = new App(this) 419 | } 420 | ``` 421 | 422 | Next, `Book`: 423 | 424 | ```ts 425 | // graphql/Book.ts 426 | import { nxs } from "nexus-decorators"; 427 | 428 | export interface BookDetails { 429 | title: string; 430 | author: string; 431 | year: number; 432 | } 433 | 434 | @nxs.objectType({ 435 | description: "Represents a book", 436 | }) 437 | export class Book { 438 | constructor(private details: BookDetails) {} 439 | 440 | @nxs.field.nonNull.string() 441 | get title() { 442 | return this.details.title; 443 | } 444 | 445 | @nxs.field.nonNull.string() 446 | get author() { 447 | return this.details.author; 448 | } 449 | 450 | @nxs.field.nonNull.int() 451 | get year() { 452 | return this.details.year; 453 | } 454 | } 455 | 456 | ``` 457 | 458 | And finally, `App`: 459 | 460 | ```ts 461 | // graphql/App.ts 462 | import { nxs } from "nexus-decorators"; 463 | import { Book } from "./Book"; 464 | import { Context } from "./Context"; 465 | 466 | export class App { 467 | constructor(private ctx: Context) {} 468 | 469 | @nxs.field.nonNull.list.nonNull.type(() => Book, { 470 | description: "All books in the system", 471 | }) 472 | get books() { 473 | return this.ctx.books 474 | } 475 | } 476 | ``` 477 | 478 | We use `nxs` to decorate everything we'd like to expose via GraphQL - this will give us our GraphQL schema and TypeScript definitions. 479 | 480 | Add the `Book` type in `schema.ts`: 481 | 482 | ```ts 483 | import path from "path"; 484 | import { makeSchema } from "nexus"; 485 | import { Book } from "./Book"; 486 | 487 | export const graphqlSchema = makeSchema({ 488 | types: [Book], 489 | 490 | // ... 491 | }); 492 | ``` 493 | 494 | Regenerate the schema: `yarn build-schema`. 495 | 496 | ```graphql 497 | ### This file was generated by Nexus Schema 498 | ### Do not make changes to this file directly 499 | 500 | 501 | """Represents a book""" 502 | type Book { 503 | author: String! 504 | title: String! 505 | year: Int! 506 | } 507 | 508 | type Query { 509 | ok: Boolean! 510 | } 511 | ``` 512 | 513 | Getting closer. It's worth seeing how `nxs.gen.ts` changed, too. Here are some of the more important ones: 514 | 515 | ```ts 516 | export interface NexusGenFieldTypes { 517 | Book: { // field return type 518 | author: string; // String! 519 | title: string; // String! 520 | year: number; // Int! 521 | } 522 | Query: { // field return type 523 | ok: boolean; // Boolean! 524 | } 525 | } 526 | 527 | export interface NexusGenFieldTypeNames { 528 | Book: { // field return type name 529 | author: 'String' 530 | title: 'String' 531 | year: 'Int' 532 | } 533 | Query: { // field return type name 534 | ok: 'Boolean' 535 | } 536 | } 537 | 538 | // ... 539 | 540 | export interface NexusGenTypes { 541 | context: Context; 542 | } 543 | ``` 544 | 545 | ## Adding a Query Entity 546 | 547 | Add `graphql/Query.ts`: 548 | 549 | ```ts 550 | import { nxs } from "nexus-decorators"; 551 | import { App } from "./App"; 552 | import { Book } from "./Book"; 553 | import { Context } from "./Context"; 554 | import { NexusGenTypes } from "./gen/nxs.gen" 555 | 556 | @nxs.objectType({ 557 | description: "root query", 558 | }) 559 | export class Query { 560 | constructor(private ctx: Context) {} 561 | 562 | @nxs.field.nonNull.type(() => App) 563 | app(_: any, ctx: NexusGenTypes["context"]) { 564 | return ctx.app; 565 | } 566 | 567 | @nxs.field.nonNull.list.nonNull.type(() => Book) 568 | books(_: any, ctx: NexusGenTypes["context"]) { 569 | return ctx.app.books; 570 | } 571 | } 572 | ``` 573 | 574 | Now re-generate the schema and see the generated `schema.grapql`: 575 | 576 | ```graphql 577 | ### This file was generated by Nexus Schema 578 | ### Do not make changes to this file directly 579 | 580 | 581 | type App { 582 | """All books in the system""" 583 | books: [Book!]! 584 | } 585 | 586 | """Represents a book""" 587 | type Book { 588 | author: String! 589 | title: String! 590 | year: Int! 591 | } 592 | 593 | """root query""" 594 | type Query { 595 | app: App! 596 | books: [Book!]! 597 | } 598 | ``` 599 | 600 | Great! We are almost back to our original schema. This is type safe, though - everything is written in TypeScript using classes and decorators. 601 | 602 | Update `graphql/schema.ts` to include `Query`: 603 | 604 | ```ts 605 | // ... 606 | import { Book } from "./Book"; 607 | import { Query } from "./Query"; 608 | 609 | export const graphqlSchema = makeSchema({ 610 | types: [Book, Query], 611 | // ... 612 | }) 613 | ``` 614 | 615 | Use the new `Context` object in `graphql/server.ts`. We don't need the `rootValue` resolver anymore - this is handled via the `Context` (not entirely clear how this works yet - need to find out). 616 | 617 | ```ts 618 | import { graphqlHTTP } from "express-graphql"; 619 | import express from "express"; 620 | import cors from "cors"; 621 | import { graphqlSchema } from "./schema"; 622 | import { Context } from "./Context"; 623 | import { Book } from "./Book"; 624 | 625 | const app = express(); 626 | 627 | class ServerContext extends Context { 628 | books = [new Book({ title: "My book", author: "Lachlan", year: 1990 })]; 629 | } 630 | 631 | const context = new ServerContext() 632 | 633 | app.use(cors()); 634 | app.use( 635 | "/graphql", 636 | graphqlHTTP(() => { 637 | return { 638 | schema: graphqlSchema, 639 | graphiql: true, 640 | context, 641 | }; 642 | }) 643 | ); 644 | 645 | app.listen(4000, () => { 646 | console.log("Started server on 4000"); 647 | }); 648 | ``` 649 | 650 | Start the server and test it out. The Vue app should be loading the books. The mutation won't work yet - let's define that now. 651 | 652 | ## Mutations with GraphQL Nexus 653 | 654 | Add `graphql/Mutation.ts`: 655 | 656 | ```ts 657 | import { inputObjectType, mutationType, nonNull } from "nexus"; 658 | 659 | export const Mutation = mutationType({ 660 | definition (t) { 661 | t.field('addBook', { 662 | type: 'Book', 663 | description: "Add a book", 664 | args: { 665 | input: nonNull(inputObjectType({ 666 | name: 'BookInput', 667 | definition (t) { 668 | t.nonNull.string('title') 669 | t.nonNull.string('author') 670 | t.nonNull.int('year') 671 | } 672 | })) 673 | }, 674 | async resolve (root, args, ctx) { 675 | return ctx.app.addBook(args.input) 676 | } 677 | }) 678 | } 679 | }) 680 | ``` 681 | 682 | This one is not using `nexus-decorators`. There's probably some way to do it with that, but I don't know it, so I am using the original `nexus` object syntax. 683 | 684 | Update `graphql/App.ts` to define the `addBook` method: 685 | 686 | ```ts 687 | import { nxs } from "nexus-decorators"; 688 | import { Book, BookDetails } from "./Book"; 689 | import { Context } from "./Context"; 690 | 691 | export class App { 692 | constructor(private ctx: Context) {} 693 | 694 | @nxs.field.nonNull.list.nonNull.type(() => Book, { 695 | description: "All books in the system", 696 | }) 697 | get books() { 698 | return this.ctx.books 699 | } 700 | 701 | addBook (details: BookDetails) { 702 | const book = new Book(details) 703 | this.ctx.books.push(book) 704 | return book 705 | } 706 | } 707 | ``` 708 | 709 | Update the schema: 710 | 711 | ```ts 712 | // ... 713 | import { Query } from "./Query"; 714 | import { Mutation } from "./Mutation"; 715 | 716 | export const graphqlSchema = makeSchema({ 717 | types: [Book, Query, Mutation], 718 | // ... 719 | }) 720 | ``` 721 | 722 | Re-generate with `yarn build-schemas`. Now we have our `BookInput` and `Mutation` again! 723 | 724 | ```ts 725 | ### This file was generated by Nexus Schema 726 | ### Do not make changes to this file directly 727 | 728 | 729 | type App { 730 | """All books in the system""" 731 | books: [Book!]! 732 | } 733 | 734 | """Represents a book""" 735 | type Book { 736 | author: String! 737 | title: String! 738 | year: Int! 739 | } 740 | 741 | input BookInput { 742 | author: String! 743 | title: String! 744 | year: Int! 745 | } 746 | 747 | type Mutation { 748 | """Add a book""" 749 | addBook(input: BookInput!): Book 750 | } 751 | 752 | """root query""" 753 | type Query { 754 | app: App! 755 | books: [Book!]! 756 | } 757 | ``` 758 | 759 | Now the app is working again, as expected! 760 | 761 | ## Type Safe Front-End 762 | 763 | The back-end is type safe - but the front-end is not. We can do something about that using [`graphql-code-generator`](https://www.graphql-code-generator.com/). 764 | 765 | Add a bunch of dependencies to `package.json` and install: 766 | 767 | ```json 768 | { 769 | "devDependencies": { 770 | "@graphql-codegen/add": "^2.0.2", 771 | "@graphql-codegen/cli": "^1.21.6", 772 | "@graphql-codegen/typed-document-node": "^1.18.9", 773 | "@graphql-codegen/typescript": "^1.22.4", 774 | "@graphql-codegen/typescript-operations": "^1.18.3", 775 | "@graphql-typed-document-node/core": "^3.1.0" 776 | } 777 | } 778 | ``` 779 | 780 | GraphQL Code Generator will extract the queries from `gql` tags and create TypeScript definitions. Let's configure it. Add the following to `graphql-codegen.yml` 781 | 782 | ```yml 783 | overwrite: true 784 | schema: './graphql/schema.graphql' 785 | documents: 'src/**/*.vue' 786 | generates: 787 | src/generated/graphql.ts: 788 | config: 789 | immutableTypes: true 790 | useTypeImports: true 791 | preResolveTypes: true 792 | onlyOperationTypes: true 793 | avoidOptionals: true 794 | enumsAsTypes: true 795 | plugins: 796 | - add: 797 | content: '/* eslint-disable */' 798 | - 'typescript' 799 | - 'typescript-operations' 800 | - 'typed-document-node' 801 | ``` 802 | 803 | Also add a script: 804 | 805 | ```json 806 | { 807 | "scripts": { 808 | "codegen": "graphql-codegen --config ${PWD}/graphql-codegen.yml", 809 | } 810 | } 811 | ``` 812 | 813 | Try running `yarn codegen`. The output is kind of readable: 814 | 815 | ```ts 816 | /* eslint-disable */ 817 | import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 818 | export type Maybe = T | null; 819 | export type Exact = { [K in keyof T]: T[K] }; 820 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 821 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 822 | /** All built-in and custom scalars, mapped to their actual values */ 823 | export type Scalars = { 824 | ID: string; 825 | String: string; 826 | Boolean: boolean; 827 | Int: number; 828 | Float: number; 829 | }; 830 | 831 | 832 | 833 | export type BookInput = { 834 | readonly author: Scalars['String']; 835 | readonly title: Scalars['String']; 836 | readonly year: Scalars['Int']; 837 | }; 838 | 839 | 840 | 841 | export type AddBookMutationVariables = Exact<{ 842 | input: BookInput; 843 | }>; 844 | 845 | 846 | export type AddBookMutation = { readonly __typename?: 'Mutation', readonly addBook: Maybe<{ readonly __typename?: 'Book', readonly title: string }> }; 847 | 848 | export type GetBooksQueryVariables = Exact<{ [key: string]: never; }>; 849 | 850 | 851 | export type GetBooksQuery = { readonly __typename?: 'Query', readonly app: { readonly __typename?: 'App', readonly books: ReadonlyArray<{ readonly __typename?: 'Book', readonly title: string }> } }; 852 | 853 | 854 | export const AddBookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddBook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BookInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addBook"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]} as unknown as DocumentNode; 855 | export const GetBooksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetBooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"books"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]} as unknown as DocumentNode; 856 | ``` 857 | 858 | Let's try it out. We will use these instead of inlining the queries as strings. Update `Query.vue`: 859 | 860 | ```html 861 | 891 | ``` 892 | 893 | The `GetBooksDocument` was generated by GraphQL Code Generator, derived from `GetBooks` (which we no longer need to assign - just doing `gql` and naming the query is enough). We now have type completion. 894 | 895 | ![](./images/editor.png) 896 | 897 | Update `Mutation.vue` too - we can even get type safety around the inputs to mutations! We are already getting some value - `year` cannot be null, and this is reflected correctly: 898 | 899 | ![](./images/types.png) 900 | 901 | Everything works again - but it's now type safe by default. 902 | 903 | ## Conclusion and Improvements 904 | 905 | We are generating a lot of code. Consider running both the schema generation and front-end code generation in watch mode, using something like `concurrently`. Then you can get the latest types as soon as you save. 906 | 907 | Some improvements would be exploring how to make more complex queries, use fragements, etc. I'm excited to continue learning about GraphQL and how to use it with urql. 908 | -------------------------------------------------------------------------------- /graphql-codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: './graphql/schema.graphql' 3 | documents: 'src/**/*.vue' 4 | generates: 5 | src/generated/graphql.ts: 6 | config: 7 | immutableTypes: true 8 | useTypeImports: true 9 | preResolveTypes: true 10 | onlyOperationTypes: true 11 | avoidOptionals: true 12 | enumsAsTypes: true 13 | plugins: 14 | - add: 15 | content: '/* eslint-disable */' 16 | - 'typescript' 17 | - 'typescript-operations' 18 | - 'typed-document-node' -------------------------------------------------------------------------------- /graphql/App.ts: -------------------------------------------------------------------------------- 1 | import { nxs } from "nexus-decorators"; 2 | import { Book, BookDetails } from "./Book"; 3 | import { Context } from "./Context"; 4 | 5 | export class App { 6 | constructor(private ctx: Context) {} 7 | 8 | @nxs.field.nonNull.list.nonNull.type(() => Book, { 9 | description: "All books in the system", 10 | }) 11 | get books() { 12 | return this.ctx.books 13 | } 14 | 15 | addBook (details: BookDetails) { 16 | const book = new Book(details) 17 | this.ctx.books.push(book) 18 | return book 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /graphql/Book.ts: -------------------------------------------------------------------------------- 1 | import { nxs } from "nexus-decorators"; 2 | 3 | export interface BookDetails { 4 | title: string; 5 | author: string; 6 | year: number; 7 | } 8 | 9 | @nxs.objectType({ 10 | description: "Represents a book", 11 | }) 12 | export class Book { 13 | constructor(private details: BookDetails) {} 14 | 15 | @nxs.field.nonNull.string() 16 | get title() { 17 | return this.details.title; 18 | } 19 | 20 | @nxs.field.nonNull.string() 21 | get author() { 22 | return this.details.author; 23 | } 24 | 25 | @nxs.field.nonNull.int() 26 | get year() { 27 | return this.details.year; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /graphql/Context.ts: -------------------------------------------------------------------------------- 1 | import { App } from "./App"; 2 | import { Book } from "./Book"; 3 | 4 | export abstract class Context { 5 | abstract books: Book[] 6 | app = new App(this) 7 | } -------------------------------------------------------------------------------- /graphql/Mutation.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType, mutationType, nonNull } from "nexus"; 2 | 3 | export const Mutation = mutationType({ 4 | definition (t) { 5 | t.field('addBook', { 6 | type: 'Book', 7 | description: "Add a book", 8 | args: { 9 | input: nonNull(inputObjectType({ 10 | name: 'BookInput', 11 | definition (t) { 12 | t.nonNull.string('title') 13 | t.nonNull.string('author') 14 | t.nonNull.int('year') 15 | } 16 | })) 17 | }, 18 | async resolve (root, args, ctx) { 19 | return ctx.app.addBook(args.input) 20 | } 21 | }) 22 | } 23 | }) -------------------------------------------------------------------------------- /graphql/Query.ts: -------------------------------------------------------------------------------- 1 | import { nxs } from "nexus-decorators"; 2 | import { App } from "./App"; 3 | import { Book } from "./Book"; 4 | import { Context } from "./Context"; 5 | import { NexusGenTypes } from "./gen/nxs.gen" 6 | 7 | @nxs.objectType({ 8 | description: "root query", 9 | }) 10 | export class Query { 11 | constructor(private ctx: Context) {} 12 | 13 | @nxs.field.nonNull.type(() => App) 14 | app(_: any, ctx: NexusGenTypes["context"]) { 15 | return ctx.app; 16 | } 17 | 18 | @nxs.field.nonNull.list.nonNull.type(() => Book) 19 | books(_: any, ctx: NexusGenTypes["context"]) { 20 | return ctx.app.books; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /graphql/build-schema.ts: -------------------------------------------------------------------------------- 1 | process.env.GRAPHQL_CODEGEN = "true"; 2 | import "./schema"; 3 | -------------------------------------------------------------------------------- /graphql/gen/nxs.gen.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was generated by Nexus Schema 3 | * Do not make changes to this file directly 4 | */ 5 | 6 | 7 | import type { Context } from "./../Context" 8 | import type { Book } from "./../Book" 9 | import type { Query } from "./../Query" 10 | 11 | 12 | 13 | 14 | declare global { 15 | interface NexusGen extends NexusGenTypes {} 16 | } 17 | 18 | export interface NexusGenInputs { 19 | BookInput: { // input type 20 | author: string; // String! 21 | title: string; // String! 22 | year: number; // Int! 23 | } 24 | } 25 | 26 | export interface NexusGenEnums { 27 | } 28 | 29 | export interface NexusGenScalars { 30 | String: string 31 | Int: number 32 | Float: number 33 | Boolean: boolean 34 | ID: string 35 | } 36 | 37 | export interface NexusGenObjects { 38 | App: {}; 39 | Book: Book; 40 | Mutation: {}; 41 | Query: Query; 42 | } 43 | 44 | export interface NexusGenInterfaces { 45 | } 46 | 47 | export interface NexusGenUnions { 48 | } 49 | 50 | export type NexusGenRootTypes = NexusGenObjects 51 | 52 | export type NexusGenAllTypes = NexusGenRootTypes & NexusGenScalars 53 | 54 | export interface NexusGenFieldTypes { 55 | App: { // field return type 56 | books: NexusGenRootTypes['Book'][]; // [Book!]! 57 | } 58 | Book: { // field return type 59 | author: string; // String! 60 | title: string; // String! 61 | year: number; // Int! 62 | } 63 | Mutation: { // field return type 64 | addBook: NexusGenRootTypes['Book'] | null; // Book 65 | } 66 | Query: { // field return type 67 | app: NexusGenRootTypes['App']; // App! 68 | books: NexusGenRootTypes['Book'][]; // [Book!]! 69 | } 70 | } 71 | 72 | export interface NexusGenFieldTypeNames { 73 | App: { // field return type name 74 | books: 'Book' 75 | } 76 | Book: { // field return type name 77 | author: 'String' 78 | title: 'String' 79 | year: 'Int' 80 | } 81 | Mutation: { // field return type name 82 | addBook: 'Book' 83 | } 84 | Query: { // field return type name 85 | app: 'App' 86 | books: 'Book' 87 | } 88 | } 89 | 90 | export interface NexusGenArgTypes { 91 | Mutation: { 92 | addBook: { // args 93 | input: NexusGenInputs['BookInput']; // BookInput! 94 | } 95 | } 96 | } 97 | 98 | export interface NexusGenAbstractTypeMembers { 99 | } 100 | 101 | export interface NexusGenTypeInterfaces { 102 | } 103 | 104 | export type NexusGenObjectNames = keyof NexusGenObjects; 105 | 106 | export type NexusGenInputNames = keyof NexusGenInputs; 107 | 108 | export type NexusGenEnumNames = never; 109 | 110 | export type NexusGenInterfaceNames = never; 111 | 112 | export type NexusGenScalarNames = keyof NexusGenScalars; 113 | 114 | export type NexusGenUnionNames = never; 115 | 116 | export type NexusGenObjectsUsingAbstractStrategyIsTypeOf = never; 117 | 118 | export type NexusGenAbstractsUsingStrategyResolveType = never; 119 | 120 | export type NexusGenFeaturesConfig = { 121 | abstractTypeStrategies: { 122 | isTypeOf: false 123 | resolveType: true 124 | __typename: false 125 | } 126 | } 127 | 128 | export interface NexusGenTypes { 129 | context: Context; 130 | inputTypes: NexusGenInputs; 131 | rootTypes: NexusGenRootTypes; 132 | inputTypeShapes: NexusGenInputs & NexusGenEnums & NexusGenScalars; 133 | argTypes: NexusGenArgTypes; 134 | fieldTypes: NexusGenFieldTypes; 135 | fieldTypeNames: NexusGenFieldTypeNames; 136 | allTypes: NexusGenAllTypes; 137 | typeInterfaces: NexusGenTypeInterfaces; 138 | objectNames: NexusGenObjectNames; 139 | inputNames: NexusGenInputNames; 140 | enumNames: NexusGenEnumNames; 141 | interfaceNames: NexusGenInterfaceNames; 142 | scalarNames: NexusGenScalarNames; 143 | unionNames: NexusGenUnionNames; 144 | allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames']; 145 | allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames']; 146 | allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes'] 147 | abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames']; 148 | abstractTypeMembers: NexusGenAbstractTypeMembers; 149 | objectsUsingAbstractStrategyIsTypeOf: NexusGenObjectsUsingAbstractStrategyIsTypeOf; 150 | abstractsUsingStrategyResolveType: NexusGenAbstractsUsingStrategyResolveType; 151 | features: NexusGenFeaturesConfig; 152 | } 153 | 154 | 155 | declare global { 156 | interface NexusGenPluginTypeConfig { 157 | } 158 | interface NexusGenPluginInputTypeConfig { 159 | } 160 | interface NexusGenPluginFieldConfig { 161 | } 162 | interface NexusGenPluginInputFieldConfig { 163 | } 164 | interface NexusGenPluginSchemaConfig { 165 | } 166 | interface NexusGenPluginArgConfig { 167 | } 168 | } -------------------------------------------------------------------------------- /graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | ### This file was generated by Nexus Schema 2 | ### Do not make changes to this file directly 3 | 4 | 5 | type App { 6 | """All books in the system""" 7 | books: [Book!]! 8 | } 9 | 10 | """Represents a book""" 11 | type Book { 12 | author: String! 13 | title: String! 14 | year: Int! 15 | } 16 | 17 | input BookInput { 18 | author: String! 19 | title: String! 20 | year: Int! 21 | } 22 | 23 | type Mutation { 24 | """Add a book""" 25 | addBook(input: BookInput!): Book 26 | } 27 | 28 | """root query""" 29 | type Query { 30 | app: App! 31 | books: [Book!]! 32 | } 33 | -------------------------------------------------------------------------------- /graphql/schema.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { makeSchema } from "nexus"; 3 | import { Book } from "./Book"; 4 | import { Query } from "./Query"; 5 | import { Mutation } from "./Mutation"; 6 | 7 | export const graphqlSchema = makeSchema({ 8 | types: [Book, Query, Mutation], 9 | shouldGenerateArtifacts: true, 10 | shouldExitAfterGenerateArtifacts: Boolean(process.env.GRAPHQL_CODEGEN), 11 | outputs: { 12 | typegen: path.join(__dirname, "gen/nxs.gen.ts"), 13 | schema: path.join(__dirname, "schema.graphql"), 14 | }, 15 | contextType: { 16 | module: path.join(__dirname, "./Context.ts"), 17 | export: "Context", 18 | }, 19 | }); -------------------------------------------------------------------------------- /graphql/server.ts: -------------------------------------------------------------------------------- 1 | import { graphqlHTTP } from "express-graphql"; 2 | import express from "express"; 3 | import cors from "cors"; 4 | import { graphqlSchema } from "./schema"; 5 | import { Context } from "./Context"; 6 | import { Book } from "./Book"; 7 | 8 | const app = express(); 9 | 10 | class ServerContext extends Context { 11 | books = [new Book({ title: "My book", author: "Lachlan", year: 1990 })]; 12 | } 13 | 14 | const context = new ServerContext() 15 | 16 | app.use(cors()); 17 | app.use( 18 | "/graphql", 19 | graphqlHTTP(() => { 20 | return { 21 | schema: graphqlSchema, 22 | graphiql: true, 23 | context, 24 | }; 25 | }) 26 | ); 27 | 28 | app.listen(4000, () => { 29 | console.log("Started server on 4000"); 30 | }); 31 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmiller1990/vue-3-urql-example/9b8edc2020e97550538041f39a9424a739b72947/images/demo.gif -------------------------------------------------------------------------------- /images/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmiller1990/vue-3-urql-example/9b8edc2020e97550538041f39a9424a739b72947/images/editor.png -------------------------------------------------------------------------------- /images/screenie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmiller1990/vue-3-urql-example/9b8edc2020e97550538041f39a9424a739b72947/images/screenie.png -------------------------------------------------------------------------------- /images/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmiller1990/vue-3-urql-example/9b8edc2020e97550538041f39a9424a739b72947/images/ss1.png -------------------------------------------------------------------------------- /images/types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmiller1990/vue-3-urql-example/9b8edc2020e97550538041f39a9424a739b72947/images/types.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-urql-nexus", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "server": "yarn ts-node graphql/server.ts", 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "serve": "vite preview", 9 | "codegen": "graphql-codegen --config ${PWD}/graphql-codegen.yml", 10 | "build-schema": "ts-node --transpile-only graphql/schema.ts" 11 | }, 12 | "dependencies": { 13 | "@urql/vue": "^0.4.3", 14 | "vue": "^3.0.5" 15 | }, 16 | "devDependencies": { 17 | "@graphql-codegen/add": "^2.0.2", 18 | "@graphql-codegen/cli": "^1.21.6", 19 | "@graphql-codegen/typed-document-node": "^1.18.9", 20 | "@graphql-codegen/typescript": "^1.22.4", 21 | "@graphql-codegen/typescript-operations": "^1.18.3", 22 | "@graphql-typed-document-node/core": "^3.1.0", 23 | "@types/cors": "^2.8.12", 24 | "@types/express": "^4.17.13", 25 | "@vitejs/plugin-vue": "^1.3.0", 26 | "@vue/compiler-sfc": "^3.0.5", 27 | "cors": "^2.8.5", 28 | "express": "^4.17.1", 29 | "express-graphql": "^0.12.0", 30 | "graphql": "^15.5.1", 31 | "nexus": "^1.1.0", 32 | "nexus-decorators": "^0.2.0", 33 | "ts-node": "^10.1.0", 34 | "ts-node-dev": "^1.1.8", 35 | "typescript": "^4.3.2", 36 | "vite": "^2.4.4", 37 | "vue-tsc": "^0.2.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmiller1990/vue-3-urql-example/9b8edc2020e97550538041f39a9424a739b72947/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 23 | -------------------------------------------------------------------------------- /src/Mutation.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /src/Query.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmiller1990/vue-3-urql-example/9b8edc2020e97550538041f39a9424a739b72947/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 53 | 54 | 71 | -------------------------------------------------------------------------------- /src/generated/graphql.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 3 | export type Maybe = T | null; 4 | export type Exact = { [K in keyof T]: T[K] }; 5 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 6 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 7 | /** All built-in and custom scalars, mapped to their actual values */ 8 | export type Scalars = { 9 | ID: string; 10 | String: string; 11 | Boolean: boolean; 12 | Int: number; 13 | Float: number; 14 | }; 15 | 16 | 17 | 18 | export type BookInput = { 19 | readonly author: Scalars['String']; 20 | readonly title: Scalars['String']; 21 | readonly year: Scalars['Int']; 22 | }; 23 | 24 | 25 | 26 | export type AddBookMutationVariables = Exact<{ 27 | input: BookInput; 28 | }>; 29 | 30 | 31 | export type AddBookMutation = { readonly __typename?: 'Mutation', readonly addBook: Maybe<{ readonly __typename?: 'Book', readonly title: string }> }; 32 | 33 | export type GetBooksQueryVariables = Exact<{ [key: string]: never; }>; 34 | 35 | 36 | export type GetBooksQuery = { readonly __typename?: 'Query', readonly app: { readonly __typename?: 'App', readonly books: ReadonlyArray<{ readonly __typename?: 'Book', readonly title: string }> } }; 37 | 38 | 39 | export const AddBookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddBook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BookInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addBook"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]} as unknown as DocumentNode; 40 | export const GetBooksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetBooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"books"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]} as unknown as DocumentNode; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()] 7 | }) 8 | --------------------------------------------------------------------------------