├── .cargo └── config.toml ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .gitmodules ├── .npmignore ├── Cargo.toml ├── README.md ├── __test__ └── index.spec.ts ├── build.rs ├── examples └── basic │ ├── generated.graphql │ ├── index.ts │ ├── package.json │ ├── src │ ├── schema.ts │ └── types.ts │ ├── tsconfig.json │ ├── tsgql.config.js │ └── yarn.lock ├── index.js ├── npm ├── android-arm64 │ ├── README.md │ └── package.json ├── darwin-arm64 │ ├── README.md │ └── package.json ├── darwin-x64 │ ├── README.md │ └── package.json ├── freebsd-x64 │ ├── README.md │ └── package.json ├── linux-arm-gnueabihf │ ├── README.md │ └── package.json ├── linux-arm64-gnu │ ├── README.md │ └── package.json ├── linux-arm64-musl │ ├── README.md │ └── package.json ├── linux-x64-gnu │ ├── README.md │ └── package.json ├── linux-x64-musl │ ├── README.md │ └── package.json ├── win32-arm64-msvc │ ├── README.md │ └── package.json └── win32-x64-msvc │ ├── README.md │ └── package.json ├── package.json ├── rustfmt.toml ├── schema.graphql ├── src ├── codegen.rs ├── lib.rs ├── main.rs └── node.rs ├── tsconfig.json ├── tsgql.config.js ├── type-reduce ├── .gitignore ├── LICENSE ├── esbuild.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src │ ├── __tests__ │ │ └── reduce.test.ts │ ├── index.ts │ ├── lib.ts │ └── types.ts ├── tsconfig.json └── yarn.lock └── yarn.lock /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | linker = "aarch64-linux-gnu-gcc" 3 | 4 | [target.aarch64-unknown-linux-musl] 5 | linker = "aarch64-linux-musl-gcc" 6 | rustflags = ["-C", "target-feature=-crt-static"] 7 | 8 | [target.armv7-unknown-linux-gnueabihf] 9 | linker = "arm-linux-gnueabihf-gcc" 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | env: 3 | DEBUG: napi:* 4 | APP_NAME: core 5 | MACOSX_DEPLOYMENT_TARGET: '10.13' 6 | 'on': 7 | push: 8 | branches: 9 | - master 10 | tags-ignore: 11 | - '**' 12 | pull_request: null 13 | jobs: 14 | build: 15 | if: '!contains(github.event.head_commit.message, ''skip ci'')' 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | settings: 20 | - host: macos-latest 21 | target: x86_64-apple-darwin 22 | build: yarn build 23 | - host: windows-latest 24 | build: yarn build 25 | target: x86_64-pc-windows-msvc 26 | # - host: windows-latest 27 | # build: | 28 | # export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=32; 29 | # export CARGO_PROFILE_RELEASE_LTO=false 30 | # yarn build --target i686-pc-windows-msvc 31 | # target: i686-pc-windows-msvc 32 | # setup: | 33 | # choco install nodejs-lts --x86 -y --force 34 | # echo "C:\Program Files (x86)\nodejs" >> $GITHUB_PATH 35 | - host: ubuntu-latest 36 | target: x86_64-unknown-linux-gnu 37 | docker: | 38 | docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REGISTRY_URL 39 | docker pull $DOCKER_REGISTRY_URL/napi-rs/napi-rs/nodejs-rust:lts-debian 40 | docker tag $DOCKER_REGISTRY_URL/napi-rs/napi-rs/nodejs-rust:lts-debian builder 41 | build: | 42 | docker run --rm -v ~/.cargo/git:/root/.cargo/git -v ~/.cargo/registry:/root/.cargo/registry -v $(pwd):/build -w /build builder yarn build && strip core.linux-x64-gnu.node 43 | - host: ubuntu-latest 44 | target: x86_64-unknown-linux-musl 45 | docker: | 46 | docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REGISTRY_URL 47 | docker pull $DOCKER_REGISTRY_URL/napi-rs/napi-rs/nodejs-rust:lts-alpine 48 | docker tag $DOCKER_REGISTRY_URL/napi-rs/napi-rs/nodejs-rust:lts-alpine builder 49 | build: docker run --rm -v ~/.cargo/git:/root/.cargo/git -v ~/.cargo/registry:/root/.cargo/registry -v $(pwd):/build -w /build builder yarn build && strip core.linux-x64-musl.node 50 | - host: macos-latest 51 | target: aarch64-apple-darwin 52 | build: | 53 | sudo rm -Rf /Library/Developer/CommandLineTools/SDKs/*; 54 | export CC=$(xcrun -f clang); 55 | export CXX=$(xcrun -f clang++); 56 | SYSROOT=$(xcrun --sdk macosx --show-sdk-path); 57 | export CFLAGS="-isysroot $SYSROOT -isystem $SYSROOT"; 58 | yarn build --target=aarch64-apple-darwin 59 | strip -x *.node 60 | - host: ubuntu-latest 61 | target: aarch64-unknown-linux-gnu 62 | setup: | 63 | sudo apt-get install g++-aarch64-linux-gnu gcc-aarch64-linux-gnu -y 64 | build: | 65 | yarn build --target=aarch64-unknown-linux-gnu 66 | aarch64-linux-gnu-strip core.linux-arm64-gnu.node 67 | - host: ubuntu-latest 68 | target: armv7-unknown-linux-gnueabihf 69 | setup: | 70 | sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y 71 | build: | 72 | yarn build --target=armv7-unknown-linux-gnueabihf 73 | arm-linux-gnueabihf-strip core.linux-arm-gnueabihf.node 74 | - host: ubuntu-latest 75 | target: aarch64-linux-android 76 | build: | 77 | export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang" 78 | yarn build --target aarch64-linux-android 79 | - host: ubuntu-latest 80 | target: aarch64-unknown-linux-musl 81 | downloadTarget: aarch64-unknown-linux-musl 82 | docker: | 83 | docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REGISTRY_URL 84 | docker pull ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine 85 | docker tag ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine builder 86 | build: | 87 | docker run --rm -v ~/.cargo/git:/root/.cargo/git -v ~/.cargo/registry:/root/.cargo/registry -v $(pwd):/build -w /build builder sh -c "yarn build --target=aarch64-unknown-linux-musl && /aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip core.linux-arm64-musl.node" 88 | - host: windows-latest 89 | target: aarch64-pc-windows-msvc 90 | build: yarn build --target aarch64-pc-windows-msvc 91 | name: stable - ${{ matrix.settings.target }} - node@14 92 | runs-on: ${{ matrix.settings.host }} 93 | steps: 94 | - uses: actions/checkout@v2 95 | with: 96 | submodules: recursive 97 | - name: Setup node 98 | uses: actions/setup-node@v2 99 | with: 100 | node-version: 14 101 | check-latest: true 102 | - name: Install 103 | uses: actions-rs/toolchain@v1 104 | with: 105 | profile: minimal 106 | override: true 107 | toolchain: stable 108 | target: ${{ matrix.settings.target }} 109 | - name: Generate Cargo.lock 110 | uses: actions-rs/cargo@v1 111 | with: 112 | command: generate-lockfile 113 | - name: Cache cargo registry 114 | uses: actions/cache@v2 115 | with: 116 | path: ~/.cargo/registry 117 | key: ${{ matrix.settings.target }}-node@14-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }} 118 | - name: Cache cargo index 119 | uses: actions/cache@v2 120 | with: 121 | path: ~/.cargo/git 122 | key: ${{ matrix.settings.target }}-node@14-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }} 123 | - name: Cache NPM dependencies 124 | uses: actions/cache@v2 125 | with: 126 | path: node_modules 127 | key: npm-cache-${{ matrix.settings.target }}-node@14-${{ hashFiles('yarn.lock') }} 128 | - name: Pull latest image 129 | run: ${{ matrix.settings.docker }} 130 | env: 131 | DOCKER_REGISTRY_URL: ghcr.io 132 | DOCKER_USERNAME: ${{ github.actor }} 133 | DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 134 | if: ${{ matrix.settings.docker }} 135 | - name: Setup toolchain 136 | run: ${{ matrix.settings.setup }} 137 | if: ${{ matrix.settings.setup }} 138 | shell: bash 139 | - name: Install dependencies 140 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 141 | - name: Build 142 | run: ${{ matrix.settings.build }} 143 | shell: bash 144 | - name: Upload artifact 145 | uses: actions/upload-artifact@v2 146 | with: 147 | name: bindings-${{ matrix.settings.target }} 148 | path: ${{ env.APP_NAME }}.*.node 149 | test-macOS-windows-binding: 150 | name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} 151 | needs: 152 | - build 153 | strategy: 154 | fail-fast: false 155 | matrix: 156 | settings: 157 | - host: windows-latest 158 | target: x86_64-pc-windows-msvc 159 | node: 160 | - '12' 161 | - '14' 162 | - '16' 163 | runs-on: ${{ matrix.settings.host }} 164 | steps: 165 | - uses: actions/checkout@v2 166 | with: 167 | submodules: recursive 168 | - name: Setup node 169 | uses: actions/setup-node@v2 170 | with: 171 | node-version: ${{ matrix.node }} 172 | check-latest: true 173 | - name: Cache NPM dependencies 174 | uses: actions/cache@v2 175 | with: 176 | path: node_modules 177 | key: npm-cache-test-${{ matrix.settings.target }}-${{ matrix.node }}-${{ hashFiles('yarn.lock') }} 178 | - name: Install dependencies 179 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 180 | - name: Download artifacts 181 | uses: actions/download-artifact@v2 182 | with: 183 | name: bindings-${{ matrix.settings.target }} 184 | path: . 185 | - name: List packages 186 | run: ls -R . 187 | shell: bash 188 | - name: Test bindings 189 | run: yarn test 190 | test-linux-x64-gnu-binding: 191 | name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }} 192 | needs: 193 | - build 194 | strategy: 195 | fail-fast: false 196 | matrix: 197 | node: 198 | - '12' 199 | - '14' 200 | - '16' 201 | runs-on: ubuntu-latest 202 | steps: 203 | - uses: actions/checkout@v2 204 | with: 205 | submodules: recursive 206 | - name: Setup node 207 | uses: actions/setup-node@v2 208 | with: 209 | node-version: ${{ matrix.node }} 210 | check-latest: true 211 | - name: Cache NPM dependencies 212 | uses: actions/cache@v2 213 | with: 214 | path: node_modules 215 | key: npm-cache-test-linux-x64-gnu-${{ matrix.node }}-${{ hashFiles('yarn.lock') }} 216 | - name: Install dependencies 217 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 218 | - name: Download artifacts 219 | uses: actions/download-artifact@v2 220 | with: 221 | name: bindings-x86_64-unknown-linux-gnu 222 | path: . 223 | - name: List packages 224 | run: ls -R . 225 | shell: bash 226 | - name: Test bindings 227 | run: docker run --rm -v $(pwd):/core -w /core node:${{ matrix.node }}-slim yarn test 228 | test-linux-x64-musl-binding: 229 | name: Test bindings on x86_64-unknown-linux-musl - node@${{ matrix.node }} 230 | needs: 231 | - build 232 | strategy: 233 | fail-fast: false 234 | matrix: 235 | node: 236 | - '12' 237 | - '14' 238 | - '16' 239 | runs-on: ubuntu-latest 240 | steps: 241 | - uses: actions/checkout@v2 242 | with: 243 | submodules: recursive 244 | - name: Setup node 245 | uses: actions/setup-node@v2 246 | with: 247 | node-version: ${{ matrix.node }} 248 | check-latest: true 249 | - name: Cache NPM dependencies 250 | uses: actions/cache@v2 251 | with: 252 | path: node_modules 253 | key: npm-cache-test-x86_64-unknown-linux-musl-${{ matrix.node }}-${{ hashFiles('yarn.lock') }} 254 | - name: Install dependencies 255 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 256 | - name: Download artifacts 257 | uses: actions/download-artifact@v2 258 | with: 259 | name: bindings-x86_64-unknown-linux-musl 260 | path: . 261 | - name: List packages 262 | run: ls -R . 263 | shell: bash 264 | - name: Test bindings 265 | run: docker run --rm -v $(pwd):/core -w /core node:${{ matrix.node }}-alpine yarn test 266 | test-linux-aarch64-gnu-binding: 267 | name: Test bindings on aarch64-unknown-linux-gnu - node@${{ matrix.node }} 268 | needs: 269 | - build 270 | strategy: 271 | fail-fast: false 272 | matrix: 273 | node: 274 | - '12' 275 | - '14' 276 | - '16' 277 | runs-on: ubuntu-latest 278 | steps: 279 | - run: docker run --rm --privileged multiarch/qemu-user-static:register --reset 280 | - uses: actions/checkout@v2 281 | with: 282 | submodules: recursive 283 | - name: Download artifacts 284 | uses: actions/download-artifact@v2 285 | with: 286 | name: bindings-aarch64-unknown-linux-gnu 287 | path: . 288 | - name: List packages 289 | run: ls -R . 290 | shell: bash 291 | - name: Setup and run tests 292 | uses: docker://multiarch/ubuntu-core:arm64-focal 293 | with: 294 | args: | 295 | sh -c " 296 | apt-get update && \ 297 | apt-get install -y ca-certificates gnupg2 curl apt-transport-https && \ 298 | curl -sL https://deb.nodesource.com/setup_${{ matrix.node }}.x | bash - && \ 299 | apt-get install -y nodejs && \ 300 | npm install -g yarn && \ 301 | yarn install --ignore-scripts --registry https://registry.npmjs.org --network-timeout 300000 && \ 302 | yarn test && \ 303 | ls -la 304 | " 305 | test-linux-aarch64-musl-binding: 306 | name: Test bindings on aarch64-unknown-linux-musl - node@${{ matrix.node }} 307 | needs: 308 | - build 309 | runs-on: ubuntu-latest 310 | steps: 311 | - run: docker run --rm --privileged multiarch/qemu-user-static:register --reset 312 | - uses: actions/checkout@v2 313 | with: 314 | submodules: recursive 315 | - name: Download artifacts 316 | uses: actions/download-artifact@v2 317 | with: 318 | name: bindings-aarch64-unknown-linux-musl 319 | path: . 320 | - name: List packages 321 | run: ls -R . 322 | shell: bash 323 | - name: Setup and run tests 324 | uses: docker://multiarch/alpine:aarch64-latest-stable 325 | with: 326 | args: | 327 | sh -c " 328 | apk add nodejs npm && \ 329 | npm install -g yarn && \ 330 | yarn install --ignore-scripts --registry https://registry.npmjs.org --network-timeout 300000 && \ 331 | npm test 332 | " 333 | test-linux-arm-gnueabihf-binding: 334 | name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }} 335 | needs: 336 | - build 337 | strategy: 338 | fail-fast: false 339 | matrix: 340 | node: 341 | - '12' 342 | - '14' 343 | - '16' 344 | runs-on: ubuntu-latest 345 | steps: 346 | - run: docker run --rm --privileged multiarch/qemu-user-static:register --reset 347 | - uses: actions/checkout@v2 348 | with: 349 | submodules: recursive 350 | - name: Download artifacts 351 | uses: actions/download-artifact@v2 352 | with: 353 | name: bindings-armv7-unknown-linux-gnueabihf 354 | path: . 355 | - name: List packages 356 | run: ls -R . 357 | shell: bash 358 | - name: Setup and run tests 359 | uses: docker://multiarch/ubuntu-core:armhf-focal 360 | with: 361 | args: | 362 | sh -c " 363 | apt-get update && \ 364 | apt-get install -y ca-certificates gnupg2 curl apt-transport-https && \ 365 | curl -sL https://deb.nodesource.com/setup_${{ matrix.node }}.x | bash - && \ 366 | apt-get install -y nodejs && \ 367 | npm install -g yarn && \ 368 | yarn install --ignore-scripts --registry https://registry.npmjs.org --network-timeout 300000 && \ 369 | yarn test && \ 370 | ls -la 371 | " 372 | publish: 373 | name: Publish 374 | runs-on: ubuntu-latest 375 | needs: 376 | - test-linux-x64-gnu-binding 377 | - test-linux-x64-musl-binding 378 | - test-linux-aarch64-gnu-binding 379 | - test-linux-arm-gnueabihf-binding 380 | - test-macOS-windows-binding 381 | - test-linux-aarch64-musl-binding 382 | steps: 383 | - uses: actions/checkout@v2 384 | with: 385 | submodules: recursive 386 | - name: Setup node 387 | uses: actions/setup-node@v2 388 | with: 389 | node-version: 14 390 | check-latest: true 391 | - name: Cache NPM dependencies 392 | uses: actions/cache@v2 393 | with: 394 | path: node_modules 395 | key: npm-cache-ubuntu-latest-${{ hashFiles('yarn.lock') }} 396 | restore-keys: | 397 | npm-cache- 398 | - name: Install dependencies 399 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 400 | - name: Download all artifacts 401 | uses: actions/download-artifact@v2 402 | with: 403 | path: artifacts 404 | - name: Move artifacts 405 | run: yarn artifacts 406 | - name: List packages 407 | run: ls -R ./npm 408 | shell: bash 409 | - name: Publish 410 | run: | 411 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 412 | npm publish --access public 413 | env: 414 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 415 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 416 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vscode/ 4 | node_modules/ 5 | dist/* 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "apollo-rs"] 2 | path = apollo-rs 3 | url = https://github.com/apollographql/apollo-rs 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .cargo 4 | .github 5 | npm 6 | .eslintrc 7 | .prettierignore 8 | rustfmt.toml 9 | yarn.lock 10 | *.node 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tsgql" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [features] 10 | default = ["node"] 11 | node = ["napi", "napi-derive", "napi-build"] 12 | 13 | [dependencies] 14 | napi = { version = "1", optional = true, features=["serde-json"] } 15 | napi-derive = { version = "1", optional = true } 16 | anyhow = "1.0.44" 17 | once_cell = "1.8.0" 18 | serde = { version = "1.0.130", features = ["derive"] } 19 | serde_json = "1.0.68" 20 | swc = "0.71.0" 21 | swc_common = "0.13.5" 22 | swc_ecmascript = "0.77.0" 23 | apollo-encoder = { path = "./apollo-rs/crates/apollo-encoder" } 24 | indoc = "1.0.3" 25 | 26 | [target.'cfg(all(any(windows, unix), target_arch = "x86_64", not(target_env = "musl")))'.dependencies] 27 | mimalloc = {version = "0.1"} 28 | 29 | [build-dependencies] 30 | napi-build = { version = "1", optional = true } 31 | 32 | [profile.release] 33 | lto = true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tsgql 2 | Making Typescript and GraphQL a pleasant experience. 3 | 4 | ## What is this? 5 | tsgql is an experimental GraphQL code generator that takes a different approach, instead of generating types from your GraphQL schema, you define your Typescript types first and use that to generate the schema. 6 | 7 | This allows you to use the flexibility and expressiveness of Typescript's type system to create GraphQL schemas and reduce boilerplate, unlike alternatives such as [type-graphql](https://github.com/MichalLytek/type-graphql), which force you to use clunky and cumbersome classes/decorators. 8 | 9 | It allows you to do things like this: 10 | ```typescript 11 | export type User = { 12 | id: string; 13 | name: string; 14 | age: number; 15 | }; 16 | 17 | export type Mutation = { 18 | updateUser: (args: { id: User['id'], fields: Partial> }) => Promise 19 | } 20 | ``` 21 | Which generates the following GraphQL schema: 22 | ```graphql 23 | type User { 24 | id: String! 25 | name: String! 26 | age: Int! 27 | } 28 | 29 | input UpdateUserInputFields { 30 | name: String 31 | age: Int 32 | } 33 | 34 | type Mutation { 35 | updateUser(id: String!, fields: UpdateUserInputFields!): User! 36 | } 37 | 38 | ``` 39 | 40 | Check out the examples folder for sample usage. 41 | 42 | [Read more here](https://zackoverflow.dev/writing/tsgql). 43 | 44 | ## How it works 45 | tsgql uses SWC to parse your Typescript schema into an AST, which is then walked and used to generate a GraphQL schema with [apollo-encoder](https://github.com/apollographql/apollo-rs/tree/main/crates/apollo-encoder). 46 | 47 | Before the SWC step, we run your Typescript types through the TS Compiler API, which reduces them into a simpler form. This is necessary because SWC provides no such type system utilities. 48 | 49 | This is an experimental internal tool we use at Modfy, and is subject to breaking changes and general instability. 50 | -------------------------------------------------------------------------------- /__test__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | const { loadBinding } = require("@node-rs/helper"); 4 | const native = loadBinding(__dirname, "../core", "@modfy/tsgql"); 5 | 6 | test('works', (t) => { 7 | const types = ` 8 | type User = { 9 | id: number, 10 | name: string 11 | age?: number 12 | }` 13 | const out = native.generateSchema(types, JSON.stringify({User: 0}), `{ 14 | "syntax": "typescript", 15 | "tsx": true, 16 | "decorators": false, 17 | "dynamicImport": false 18 | }`) 19 | 20 | t.is(out, `type User { 21 | id: Int! 22 | name: String! 23 | age: Int 24 | } 25 | `) 26 | }) 27 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "node")] 2 | extern crate napi_build; 3 | 4 | fn main() { 5 | #[cfg(feature = "node")] 6 | napi_build::setup(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/basic/generated.graphql: -------------------------------------------------------------------------------- 1 | type RedJungleFowl { 2 | id: String! 3 | name: String! 4 | weight: Int! 5 | } 6 | type User { 7 | id: String! 8 | name: String! 9 | karma: Int 10 | } 11 | input GetUserInput { 12 | id: String 13 | name: String 14 | karma: Int 15 | } 16 | type Foo { 17 | id: String! 18 | name: String! 19 | karma: Int 20 | } 21 | type Query { 22 | getUser(user: GetUserInput, karma: Int): [User] 23 | } 24 | type CreateUserOutput { 25 | name: String! 26 | } 27 | type Mutation { 28 | createUser(name: String!, karma: Int): CreateUserOutput! 29 | updateUser(user: GetUserInput): User 30 | getRedJungleFowl(name: String, weight: Int): RedJungleFowl 31 | } 32 | -------------------------------------------------------------------------------- /examples/basic/index.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Zack Radisic", 6 | "license": "MIT", 7 | "scripts": { 8 | "generate": "node ../../index.js ./examples/basic/tsgql.config.js" 9 | }, 10 | "dependencies": { 11 | "typescript": "^4.4.4" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/basic/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { Input } from '../../../'; 2 | import { RedJungleFowl } from './types'; 3 | 4 | // Export imported types to tell tsgql to generate them 5 | export { RedJungleFowl }; 6 | 7 | export type User = { 8 | id: string; 9 | name: string; 10 | karma: number | null; 11 | }; 12 | 13 | // Here we use the `Input` marker type to tell tsgql 14 | // that `GetUserInput` should generate a GraphQL Input. 15 | // 16 | // Notice how we can use Typescript's `Partial` utility type 17 | // to make every field of `User` optional! 18 | export type GetUserInput = Input> 19 | 20 | // Type aliases work as well 21 | export type Foo = User; 22 | 23 | export type Query = { 24 | // This field will have two arguments 25 | getUser: (input: { 26 | user?: GetUserInput; 27 | karma?: number; 28 | }) => Promise; 29 | }; 30 | 31 | export type Mutation = { 32 | // Since the input is not a named and an exported type, tsgql will 33 | // generate an Input with the name `CreateUserInput` for you. 34 | createUser: (input: Omit) => Promise<{ name: string }>; 35 | 36 | updateUser: ( 37 | input: { user?: GetUserInput }, 38 | ) => Promise; 39 | 40 | getRedJungleFowl: (input: Partial>) => Promise 41 | }; 42 | -------------------------------------------------------------------------------- /examples/basic/src/types.ts: -------------------------------------------------------------------------------- 1 | export type RedJungleFowl = { 2 | id: string; 3 | name: string; 4 | weight: number; 5 | } -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "rootDir": "./src", 6 | // use Node's module resolution algorithm, instead of the legacy TS one 7 | "moduleResolution": "node", 8 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 9 | "skipLibCheck": true, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/basic/tsgql.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tsconfigPath: "./tsconfig.json", 3 | schema: "./src/schema.ts", 4 | out: "./generated.graphql", 5 | } -------------------------------------------------------------------------------- /examples/basic/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | typescript@3.4: 6 | version "3.4.5" 7 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" 8 | integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { loadBinding } = require("@node-rs/helper"); 3 | const { createReducer } = require("./dist"); 4 | 5 | /** 6 | * __dirname means load native addon from current dir 7 | * 'core' is the name of native addon 8 | * the second arguments was decided by `napi.name` field in `package.json` 9 | * the third arguments was decided by `name` field in `package.json` 10 | * `loadBinding` helper will load `core.[PLATFORM].node` from `__dirname` first 11 | * If failed to load addon, it will fallback to load from `@modfy/tsgql-[PLATFORM]` 12 | */ 13 | const native = loadBinding(__dirname, "core", "@modfy/tsgql"); 14 | 15 | const defaultArgs = { 16 | tsconfigPath: "./tsconfig.json", 17 | schema: "./src/schema.ts", 18 | out: "./schema.graphql", 19 | }; 20 | 21 | const readConfig = (path) => { 22 | const conf = require(path) 23 | if (!conf.tsconfigPath) conf.tsconfigPath = defaultArgs.tsconfigPath 24 | if (!conf.schema) conf.tsconfigPath = defaultArgs.schema 25 | if (!conf.out) conf.tsconfigPath = defaultArgs.out 26 | return conf 27 | } 28 | 29 | const run = () => { 30 | let path = "./tsgql.config.js"; 31 | if (process.argv.length > 2) { 32 | path = process.argv[2]; 33 | } 34 | const { tsconfigPath, schema, out } = readConfig(path) 35 | 36 | const reducer = createReducer({tsconfigPath, path: schema}) 37 | const [reduced, manifest] = reducer.generate(); 38 | 39 | const contents = native.generateSchema( 40 | reduced, 41 | JSON.stringify(manifest), 42 | `{ 43 | "syntax": "typescript", 44 | "tsx": true, 45 | "decorators": false, 46 | "dynamicImport": false 47 | }`, 48 | ); 49 | 50 | fs.writeFileSync(out, contents); 51 | }; 52 | 53 | run(); 54 | -------------------------------------------------------------------------------- /npm/android-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-android-arm64` 2 | 3 | This is the **aarch64-linux-android** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/android-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-android-arm64", 3 | "version": "0.0.0", 4 | "os": [ 5 | "android" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "core.android-arm64.node", 11 | "files": [ 12 | "core.android-arm64.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /npm/darwin-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-darwin-arm64` 2 | 3 | This is the **aarch64-apple-darwin** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/darwin-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-darwin-arm64", 3 | "version": "0.0.0", 4 | "os": [ 5 | "darwin" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "core.darwin-arm64.node", 11 | "files": [ 12 | "core.darwin-arm64.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /npm/darwin-x64/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-darwin-x64` 2 | 3 | This is the **x86_64-apple-darwin** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/darwin-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-darwin-x64", 3 | "version": "0.0.0", 4 | "os": [ 5 | "darwin" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "core.darwin-x64.node", 11 | "files": [ 12 | "core.darwin-x64.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /npm/freebsd-x64/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-freebsd-x64` 2 | 3 | This is the **x86_64-unknown-freebsd** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/freebsd-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-freebsd-x64", 3 | "version": "0.0.0", 4 | "os": [ 5 | "freebsd" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "core.freebsd-x64.node", 11 | "files": [ 12 | "core.freebsd-x64.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /npm/linux-arm-gnueabihf/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-linux-arm-gnueabihf` 2 | 3 | This is the **armv7-unknown-linux-gnueabihf** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/linux-arm-gnueabihf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-linux-arm-gnueabihf", 3 | "version": "0.0.0", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "arm" 9 | ], 10 | "main": "core.linux-arm-gnueabihf.node", 11 | "files": [ 12 | "core.linux-arm-gnueabihf.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /npm/linux-arm64-gnu/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-linux-arm64-gnu` 2 | 3 | This is the **aarch64-unknown-linux-gnu** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/linux-arm64-gnu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-linux-arm64-gnu", 3 | "version": "0.0.0", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "core.linux-arm64-gnu.node", 11 | "files": [ 12 | "core.linux-arm64-gnu.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /npm/linux-arm64-musl/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-linux-arm64-musl` 2 | 3 | This is the **aarch64-unknown-linux-musl** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/linux-arm64-musl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-linux-arm64-musl", 3 | "version": "0.0.0", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "core.linux-arm64-musl.node", 11 | "files": [ 12 | "core.linux-arm64-musl.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /npm/linux-x64-gnu/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-linux-x64-gnu` 2 | 3 | This is the **x86_64-unknown-linux-gnu** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/linux-x64-gnu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-linux-x64-gnu", 3 | "version": "0.0.0", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "core.linux-x64-gnu.node", 11 | "files": [ 12 | "core.linux-x64-gnu.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /npm/linux-x64-musl/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-linux-x64-musl` 2 | 3 | This is the **x86_64-unknown-linux-musl** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/linux-x64-musl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-linux-x64-musl", 3 | "version": "0.0.0", 4 | "os": [ 5 | "linux" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "core.linux-x64-musl.node", 11 | "files": [ 12 | "core.linux-x64-musl.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /npm/win32-arm64-msvc/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-win32-arm64-msvc` 2 | 3 | This is the **aarch64-pc-windows-msvc** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/win32-arm64-msvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-win32-arm64-msvc", 3 | "version": "0.0.0", 4 | "os": [ 5 | "win32" 6 | ], 7 | "cpu": [ 8 | "arm64" 9 | ], 10 | "main": "core.win32-arm64-msvc.node", 11 | "files": [ 12 | "core.win32-arm64-msvc.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /npm/win32-x64-msvc/README.md: -------------------------------------------------------------------------------- 1 | # `@modfy/tsgql-win32-x64-msvc` 2 | 3 | This is the **x86_64-pc-windows-msvc** binary for `@modfy/tsgql` 4 | -------------------------------------------------------------------------------- /npm/win32-x64-msvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql-win32-x64-msvc", 3 | "version": "0.0.0", 4 | "os": [ 5 | "win32" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "core.win32-x64-msvc.node", 11 | "files": [ 12 | "core.win32-x64-msvc.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@modfy/tsgql", 3 | "types": "./dist", 4 | "version": "0.0.1", 5 | "napi": { 6 | "name": "core", 7 | "triples": { 8 | "additional": [ 9 | "aarch64-apple-darwin", 10 | "aarch64-linux-android", 11 | "aarch64-unknown-linux-gnu", 12 | "aarch64-unknown-linux-musl", 13 | "aarch64-pc-windows-msvc", 14 | "armv7-unknown-linux-gnueabihf", 15 | "x86_64-unknown-linux-musl" 16 | ] 17 | } 18 | }, 19 | "license": "MIT", 20 | "dependencies": { 21 | "@node-rs/helper": "^1.2.1" 22 | }, 23 | "devDependencies": { 24 | "@napi-rs/cli": "^1.2.1", 25 | "@swc-node/register": "^1.4.0", 26 | "@types/node": "^16.11.7", 27 | "ava": "^3.15.0" 28 | }, 29 | "engines": { 30 | "node": ">= 10" 31 | }, 32 | "publishConfig": { 33 | "registry": "https://registry.npmjs.org/", 34 | "access": "public" 35 | }, 36 | "scripts": { 37 | "artifacts": "napi artifacts", 38 | "build": "napi build --platform --release", 39 | "build:debug": "napi build --platform", 40 | "prepublishOnly": "napi prepublish -t npm", 41 | "version": "napi version", 42 | "test": "ava" 43 | }, 44 | "ava": { 45 | "require": [ 46 | "@swc-node/register" 47 | ], 48 | "extensions": [ 49 | "ts" 50 | ], 51 | "environmentVariables": { 52 | "TS_NODE_PROJECT": "./tsconfig.json" 53 | }, 54 | "files": [ 55 | "__test__/**" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 4 -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | id: String! 3 | name: String! 4 | karma: Int! 5 | } 6 | type Loot { 7 | rarity: Int! 8 | name: String! 9 | } 10 | type Player { 11 | user: User! 12 | level: Int! 13 | } 14 | input GetUserInput { 15 | id: String 16 | name: String 17 | karma: Int 18 | } 19 | type Query { 20 | getUser(user: GetUserInput, karma: Int): [User] 21 | } 22 | input CreateUserInput { 23 | name: String 24 | karma: Int 25 | } 26 | type Mutation { 27 | createUser(user: CreateUserInput!): User 28 | createLoot(name: String): CreateLootOutput! 29 | } 30 | type CreateLootOutput { 31 | success: Boolean! 32 | loot: Loot 33 | } 34 | -------------------------------------------------------------------------------- /src/codegen.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | 4 | use apollo_encoder::{Field, InputField, InputObjectDef, InputValue, ObjectDef, Schema, Type_}; 5 | use swc::{config::ParseOptions, try_with_handler, Compiler}; 6 | use swc_common::{FileName, FilePathMapping, SourceMap}; 7 | use swc_ecmascript::ast::{ 8 | BindingIdent, Decl, Expr, Module, ModuleItem, Stmt, TsArrayType, TsEntityName, TsFnParam, 9 | TsKeywordType, TsKeywordTypeKind, TsPropertySignature, TsType, TsTypeAnn, TsTypeElement, 10 | TsTypeParamInstantiation, TsTypeRef, TsUnionOrIntersectionType, TsUnionType, 11 | }; 12 | use swc_ecmascript::ast::{Program, TsFnOrConstructorType, TsFnType}; 13 | 14 | use anyhow::{Context, Result}; 15 | 16 | pub fn generate_schema(prog: Module, manifest: HashMap) -> Result { 17 | let mut ctx = CodeGenCtx::new(manifest); 18 | ctx.parse(prog)?; 19 | Ok(ctx.finish()) 20 | } 21 | 22 | #[derive(Clone, Debug)] 23 | enum FieldKind { 24 | Input, 25 | Object, 26 | } 27 | 28 | impl FieldKind { 29 | pub fn name(&self) -> String { 30 | match self { 31 | Self::Input => "Input".into(), 32 | Self::Object => "Object".into(), 33 | } 34 | } 35 | } 36 | 37 | #[derive(Debug)] 38 | pub enum KeyedGraphQLKind { 39 | Object(ObjectDef), 40 | Input(InputObjectDef), 41 | } 42 | 43 | #[derive(Debug)] 44 | pub enum GraphQLKind { 45 | Object, 46 | Input, 47 | Enum, 48 | } 49 | 50 | pub enum ComputeNameKind<'a> { 51 | Input(&'a str, usize), 52 | Output, 53 | } 54 | 55 | impl GraphQLKind { 56 | pub fn from_u8(val: u8) -> Option { 57 | match val { 58 | 0 => Some(GraphQLKind::Object), 59 | 1 => Some(GraphQLKind::Input), 60 | 2 => Some(GraphQLKind::Enum), 61 | _ => None, 62 | } 63 | } 64 | } 65 | 66 | #[derive(Clone, Debug)] 67 | enum ParsedField { 68 | Input(InputField), 69 | Object(Field), 70 | } 71 | 72 | impl ParsedField { 73 | pub fn input(self) -> Option { 74 | match self { 75 | Self::Input(input) => Some(input), 76 | Self::Object(_) => None, 77 | } 78 | } 79 | 80 | pub fn object(self) -> Option { 81 | match self { 82 | Self::Input(_) => None, 83 | Self::Object(f) => Some(f), 84 | } 85 | } 86 | 87 | pub fn new(kind: FieldKind, name: String, type_: Type_) -> Self { 88 | match kind { 89 | FieldKind::Input => Self::Input(InputField::new(name, type_)), 90 | FieldKind::Object => Self::Object(Field::new(name, type_)), 91 | } 92 | } 93 | 94 | pub fn with_args( 95 | kind: FieldKind, 96 | name: String, 97 | type_: Type_, 98 | args: Vec, 99 | ) -> Option { 100 | if let Self::Object(mut field) = Self::new(kind, name, type_) { 101 | args.into_iter().for_each(|f| field.arg(f)); 102 | Some(Self::Object(field)) 103 | } else { 104 | None 105 | } 106 | } 107 | } 108 | 109 | /// The main struct used for generating GraphQL schemas from a widened 110 | /// tsgql Typescript schema input. 111 | /// 112 | /// TODO: This code was written hastily and kind of confusing, should probably refactor 113 | /// it to use SWC's Visitor trait + state variables. 114 | /// Some good examples are in the Next.js [repo](https://github.com/vercel/next.js/tree/canary/packages/next/build/swc/src) 115 | struct CodeGenCtx { 116 | schema: Schema, 117 | manifest: HashMap, 118 | 119 | /// True when we are parsing the inputs of a field with arguments 120 | parsing_inputs: bool, 121 | /// True when we are parsing the output of a field with arguments 122 | parsing_output: bool, 123 | } 124 | 125 | impl CodeGenCtx { 126 | /// `manifest` is generated from the first pass in the Typescript compiler API code 127 | fn new(manifest: HashMap) -> Self { 128 | let schema = Schema::new(); 129 | Self { 130 | schema, 131 | manifest, 132 | parsing_inputs: false, 133 | parsing_output: false, 134 | } 135 | } 136 | 137 | fn parse(&mut self, prog: Module) -> Result<()> { 138 | for item in prog.body { 139 | match item { 140 | ModuleItem::Stmt(stmt) => { 141 | self.parse_statement(stmt)?; 142 | } 143 | ModuleItem::ModuleDecl(_) => {} 144 | } 145 | } 146 | Ok(()) 147 | } 148 | 149 | fn parse_statement(&mut self, stmt: Stmt) -> Result<()> { 150 | match stmt { 151 | Stmt::Decl(Decl::TsTypeAlias(alias)) => { 152 | let ident = alias.id.sym.as_ref(); 153 | match self.manifest.get(ident) { 154 | Some(&GraphQLKind::Input) => { 155 | let mut input_def = InputObjectDef::new(ident.to_string()); 156 | self.parse_typed_fields(FieldKind::Input, &alias.type_ann)? 157 | .into_iter() 158 | .for_each(|f| input_def.field(f.input().unwrap())); 159 | 160 | self.schema.input(input_def); 161 | } 162 | Some(_) => { 163 | let mut object_def = ObjectDef::new(ident.to_string()); 164 | self.parse_typed_fields(FieldKind::Object, &alias.type_ann)? 165 | .into_iter() 166 | .for_each(|f| object_def.field(f.object().unwrap())); 167 | 168 | self.schema.object(object_def); 169 | } 170 | // Skip types not in the manifest 171 | None => {} 172 | } 173 | Ok(()) 174 | } 175 | _ => todo!(), 176 | } 177 | } 178 | 179 | fn parse_typed_fields( 180 | &mut self, 181 | field_kind: FieldKind, 182 | type_ann: &TsType, 183 | ) -> Result> { 184 | let mut fields: Vec = Vec::new(); 185 | match type_ann { 186 | TsType::TsTypeLit(lit) => { 187 | for member in &lit.members { 188 | match member { 189 | TsTypeElement::TsPropertySignature(prop_sig) => { 190 | fields.push(self.parse_field(field_kind.clone(), prop_sig)?); 191 | } 192 | r => return Err(anyhow::anyhow!("Invalid property type: {:?}", r)), 193 | } 194 | } 195 | } 196 | r => todo!("Not implemented parsing in this context: {:?}", r), 197 | }; 198 | 199 | Ok(fields) 200 | } 201 | 202 | fn parse_field( 203 | &mut self, 204 | kind: FieldKind, 205 | prop_sig: &TsPropertySignature, 206 | ) -> Result { 207 | let key = match &*prop_sig.key { 208 | Expr::Ident(ident) => ident.sym.to_string(), 209 | _ => return Err(anyhow::anyhow!("Invalid property signature type")), 210 | }; 211 | 212 | match self.parse_type( 213 | &key, 214 | &prop_sig.type_ann.as_ref().unwrap().type_ann, 215 | prop_sig.optional, 216 | )? { 217 | (ty, None) => Ok(ParsedField::new(kind, key, ty)), 218 | (ty, Some(args)) => match ParsedField::with_args(kind, key, ty, args) { 219 | None => Err(anyhow::anyhow!( 220 | "Only ObjectDefs can contain input fields with args" 221 | )), 222 | Some(field) => Ok(field), 223 | }, 224 | } 225 | } 226 | 227 | /// Returns the type of a GraphQL field, returning arguments if it has any. 228 | /// `field_name` is used to generate names for Inputs, and can be an empty string 229 | /// if you don't expect the Typescript type to be a function 230 | /// 231 | /// Important thing to note is that we only consider a sub-set of types, because 232 | /// the Typescript code is widened by the Typescript Compiler API before we receive it. 233 | fn parse_type( 234 | &mut self, 235 | field_name: &str, 236 | type_ann: &TsType, 237 | optional: bool, 238 | ) -> Result<(Type_, Option>)> { 239 | let (ty, args) = match type_ann { 240 | TsType::TsKeywordType(TsKeywordType { kind, .. }) => { 241 | (Self::parse_keyword_type(kind)?, None) 242 | } 243 | TsType::TsArrayType(TsArrayType { elem_type, .. }) => { 244 | match (self.parsing_output, &**elem_type) { 245 | (true, TsType::TsTypeLit(_)) => { 246 | let name = Self::compute_new_name(ComputeNameKind::Output, field_name); 247 | self.parse_type_literal(FieldKind::Object, &name, elem_type)?; 248 | ( 249 | Type_::List { 250 | ty: Box::new(Type_::NamedType { name }), 251 | }, 252 | None, 253 | ) 254 | } 255 | _ => { 256 | ( 257 | Type_::List { 258 | // TODO: There is no way to set non-nullable array elements in TS, 259 | // meaning we cant represent [Int!]! 260 | ty: Box::new(self.parse_type(field_name, elem_type, true)?.0), 261 | }, 262 | None, 263 | ) 264 | } 265 | } 266 | } 267 | TsType::TsTypeRef(TsTypeRef { 268 | type_name, 269 | type_params, 270 | .. 271 | }) => self.parse_type_ref(field_name, type_name, type_params)?, 272 | TsType::TsFnOrConstructorType(TsFnOrConstructorType::TsFnType(TsFnType { 273 | params, 274 | // `type_ann` here is return type 275 | type_ann, 276 | .. 277 | })) => { 278 | if params.len() != 1 { 279 | return Err(anyhow::anyhow!("Expected only one parameter for field arg")); 280 | } 281 | 282 | let input = ¶ms[0]; 283 | 284 | let lit = match input { 285 | TsFnParam::Ident(BindingIdent { type_ann, .. }) => { 286 | if let Some(TsTypeAnn { type_ann, .. }) = type_ann { 287 | match **type_ann { 288 | TsType::TsTypeLit(ref lit) => Some(lit), 289 | _ => None, 290 | } 291 | } else { 292 | None 293 | } 294 | } 295 | _ => None, 296 | }; 297 | 298 | let lit = match lit { 299 | None => return Err(anyhow::anyhow!("Type of Field args can only be objects")), 300 | Some(lit) => lit, 301 | }; 302 | 303 | let member_count = lit.members.len(); 304 | self.parsing_inputs = true; 305 | let args = lit 306 | .members 307 | .iter() 308 | .map(|f| self.parse_arg_member(field_name, f, member_count)) 309 | .collect::>>()?; 310 | self.parsing_inputs = false; 311 | 312 | self.parsing_output = true; 313 | // Last param can be anything here, since we don't know if the return type is 314 | // optional until we parse it. `self.parse_type()` will make sure to return 315 | // the correct type if we are parsing return type 316 | let (ret_ty, _) = self.parse_type(field_name, &type_ann.type_ann, true)?; 317 | self.parsing_output = false; 318 | 319 | return Ok((ret_ty, Some(args))); 320 | } 321 | TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(uni)) => { 322 | let typ = Self::unwrap_union(uni)?; 323 | return self.parse_type(field_name, typ, true); 324 | } 325 | // TODO: Move TsTypeLit in here 326 | r => { 327 | println!("{:?}", r); 328 | todo!(); 329 | } 330 | }; 331 | 332 | if !optional { 333 | return Ok((Type_::NonNull { ty: Box::new(ty) }, args)); 334 | } 335 | 336 | Ok((ty, args)) 337 | } 338 | 339 | fn parse_type_ref( 340 | &mut self, 341 | field_name: &str, 342 | type_name: &TsEntityName, 343 | type_params: &Option, 344 | ) -> Result<(Type_, Option>)> { 345 | if let TsEntityName::Ident(ident) = type_name { 346 | if ident.sym.to_string() != "Promise" { 347 | match self.manifest.get(ident.sym.as_ref()) { 348 | Some(&GraphQLKind::Object) if self.parsing_inputs => { 349 | return Err(anyhow::anyhow!( 350 | "Field args can only be Inputs (check: {})", 351 | ident.sym.as_ref() 352 | )); 353 | } 354 | Some(&GraphQLKind::Input) if !self.parsing_inputs => { 355 | return Err(anyhow::anyhow!( 356 | "Field type can't be an Input (check: {})", 357 | ident.sym.as_ref() 358 | )); 359 | } 360 | Some(_) => Ok(( 361 | Type_::NamedType { 362 | name: ident.sym.to_string(), 363 | }, 364 | None, 365 | )), 366 | None => return Err(anyhow::anyhow!("Undefined type: {}", ident.sym.as_ref())), 367 | } 368 | } else { 369 | match type_params { 370 | None => Err(anyhow::anyhow!("Missing type parameter for Promise")), 371 | Some(TsTypeParamInstantiation { params, .. }) => { 372 | match params.len() { 373 | 1 => {} 374 | other => { 375 | return Err(anyhow::anyhow!( 376 | "Invalid amount of type parameters for Promise: {}", 377 | other 378 | )) 379 | } 380 | } 381 | let typ = ¶ms[0]; 382 | 383 | // Somewhat confusing, but if we are here then we are parsing return of 384 | // a field with arguments, meaning we don't know the optionality of the 385 | // return type until we unwrap it from the Promise, meaning we should 386 | // discard the `optional` param and return here 387 | // 388 | // Maybe we should move this match branch into its own dedicated function, 389 | // and when we parse the return we call that instead of this function. 390 | match &**typ { 391 | TsType::TsUnionOrIntersectionType( 392 | TsUnionOrIntersectionType::TsUnionType(u), 393 | ) if Self::is_nullable_union(typ) => { 394 | let non_null = Self::unwrap_union(u)?; 395 | match non_null { 396 | TsType::TsTypeLit(_) => { 397 | let name = Self::compute_new_name( 398 | ComputeNameKind::Output, 399 | field_name, 400 | ); 401 | self.parse_type_literal( 402 | FieldKind::Object, 403 | &name, 404 | non_null, 405 | )?; 406 | 407 | Ok((Type_::NamedType { name }, None)) 408 | } 409 | _ => self.parse_type(field_name, non_null, true), 410 | } 411 | } 412 | TsType::TsTypeLit(_) => { 413 | let name = 414 | Self::compute_new_name(ComputeNameKind::Output, field_name); 415 | self.parse_type_literal(FieldKind::Object, &name, typ)?; 416 | Ok(( 417 | Type_::NonNull { 418 | ty: Box::new(Type_::NamedType { name }), 419 | }, 420 | None, 421 | )) 422 | } 423 | _ => self.parse_type("", typ, false), 424 | } 425 | } 426 | } 427 | } 428 | } else { 429 | todo!() 430 | } 431 | } 432 | 433 | fn parse_arg_member( 434 | &mut self, 435 | field_name: &str, 436 | member: &TsTypeElement, 437 | member_count: usize, 438 | ) -> Result { 439 | match member { 440 | TsTypeElement::TsPropertySignature(prop_sig) => { 441 | let ident = match &*prop_sig.key { 442 | Expr::Ident(ident) => ident, 443 | _ => todo!(), 444 | }; 445 | 446 | let type_ann = match &prop_sig.type_ann { 447 | Some(t) => t, 448 | None => return Err(anyhow::anyhow!("Missing property")), 449 | }; 450 | 451 | let name = ident.sym.as_ref(); 452 | 453 | let type_ = match &*type_ann.type_ann { 454 | TsType::TsTypeLit(_) => { 455 | let input_name = Self::compute_new_name( 456 | ComputeNameKind::Input(name, member_count), 457 | field_name, 458 | ); 459 | self.parse_arg_type_literal( 460 | &input_name, 461 | &type_ann.type_ann, 462 | prop_sig.optional, 463 | )? 464 | } 465 | TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType( 466 | uni, 467 | )) => { 468 | if !Self::is_nullable_union(&*type_ann.type_ann) { 469 | return Err(anyhow::anyhow!("Unions as field args must be nullable")); 470 | } 471 | let unwrapped = Self::unwrap_union(uni)?; 472 | match unwrapped { 473 | TsType::TsTypeLit(_) => { 474 | let input_name = Self::compute_new_name( 475 | ComputeNameKind::Input(name, member_count), 476 | field_name, 477 | ); 478 | self.parse_arg_type_literal(&input_name, unwrapped, true)? 479 | } 480 | _ => { 481 | let (ty, _) = self.parse_type(name, unwrapped, true)?; 482 | ty 483 | } 484 | } 485 | } 486 | ty => { 487 | let (ty, _) = self.parse_type(name, ty, prop_sig.optional)?; 488 | ty 489 | } 490 | }; 491 | 492 | Ok(InputValue::new(ident.sym.to_string(), type_)) 493 | } 494 | _ => Err(anyhow::anyhow!( 495 | "Field args input can only contain properties" 496 | )), 497 | } 498 | } 499 | 500 | fn parse_arg_type_literal(&mut self, name: &str, ty: &TsType, optional: bool) -> Result { 501 | self.parse_type_literal(FieldKind::Input, name, ty)?; 502 | 503 | if !optional { 504 | Ok(Type_::NonNull { 505 | ty: Box::new(Type_::NamedType { 506 | name: name.to_string(), 507 | }), 508 | }) 509 | } else { 510 | Ok(Type_::NamedType { 511 | name: name.to_string(), 512 | }) 513 | } 514 | } 515 | 516 | fn parse_type_literal(&mut self, kind: FieldKind, new_name: &str, ty: &TsType) -> Result<()> { 517 | match kind { 518 | FieldKind::Input => { 519 | let mut input_def = InputObjectDef::new(new_name.into()); 520 | 521 | self.parse_typed_fields(FieldKind::Input, ty)? 522 | .into_iter() 523 | .for_each(|f| input_def.field(f.input().unwrap())); 524 | 525 | self.schema.input(input_def); 526 | 527 | Ok(()) 528 | } 529 | FieldKind::Object => { 530 | let mut object_def = ObjectDef::new(new_name.into()); 531 | 532 | self.parse_typed_fields(FieldKind::Object, ty)? 533 | .into_iter() 534 | .for_each(|f| object_def.field(f.object().unwrap())); 535 | 536 | self.schema.object(object_def); 537 | 538 | Ok(()) 539 | } 540 | } 541 | } 542 | 543 | fn finish(self) -> String { 544 | self.schema.finish() 545 | } 546 | } 547 | 548 | impl CodeGenCtx { 549 | fn is_nullable(ty: &TsType) -> bool { 550 | match ty { 551 | TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(_)) => { 552 | Self::is_nullable_union(ty) 553 | } 554 | TsType::TsKeywordType(TsKeywordType { kind, .. }) => matches!( 555 | kind, 556 | TsKeywordTypeKind::TsNullKeyword | TsKeywordTypeKind::TsUndefinedKeyword 557 | ), 558 | // For now just assume type references and others are non-null 559 | _ => false, 560 | } 561 | } 562 | 563 | /// Return true if type is like: `T | null or T | undefined` 564 | fn is_nullable_union(ty: &TsType) -> bool { 565 | match ty { 566 | TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType( 567 | TsUnionType { types, .. }, 568 | )) => types.iter().any(|ty| Self::is_nullable(ty)), 569 | _ => false, 570 | } 571 | } 572 | 573 | /// Return the first non-nullable type of a union. This will error if there is no 574 | /// nullable type present. 575 | /// 576 | /// ``` 577 | /// Ex: "User | null" -> User 578 | /// "User | string" -> Error 579 | /// ``` 580 | fn unwrap_union(ty: &TsUnionType) -> Result<&TsType> { 581 | match ty.types.iter().find(|t| !Self::is_nullable(t)) { 582 | None => Err(anyhow::anyhow!("No non-nullable type found in union")), 583 | Some(t) => Ok(t), 584 | } 585 | } 586 | 587 | /// Computes a name for a new Input type. The resulting name depends on the variant of 588 | /// `ComputeNameKind`. If it is `ComputeNameKind::Input` we try to generate a unique 589 | /// input name based on `field_name` and `param_name` 590 | /// 591 | /// Otherwise, we also concatenate the name of the param 592 | fn compute_new_name(kind: ComputeNameKind, field_name: &str) -> String { 593 | match kind { 594 | ComputeNameKind::Output => { 595 | format!("{}{}", upper_camel_case(field_name), "Output") 596 | } 597 | ComputeNameKind::Input(_param_name, member_count) if member_count == 1 => { 598 | format!("{}{}", upper_camel_case(field_name), "Input",) 599 | } 600 | ComputeNameKind::Input(param_name, _) => { 601 | format!( 602 | "{}{}{}", 603 | upper_camel_case(field_name), 604 | "Input", 605 | upper_camel_case(param_name) 606 | ) 607 | } 608 | } 609 | } 610 | 611 | fn parse_keyword_type(kind: &TsKeywordTypeKind) -> Result { 612 | match kind { 613 | TsKeywordTypeKind::TsNumberKeyword => Ok(Type_::NamedType { name: "Int".into() }), 614 | TsKeywordTypeKind::TsStringKeyword => Ok(Type_::NamedType { 615 | name: "String".into(), 616 | }), 617 | TsKeywordTypeKind::TsBooleanKeyword => Ok(Type_::NamedType { 618 | name: "Boolean".into(), 619 | }), 620 | // TODO: Scalar types like BigInt 621 | r => todo!("Unsupported keyword type: {:?}", r), 622 | } 623 | } 624 | } 625 | 626 | fn upper_camel_case(s: &str) -> String { 627 | s.chars() 628 | .next() 629 | .iter() 630 | .map(|c| c.to_ascii_uppercase()) 631 | .chain(s.chars().skip(1)) 632 | .collect::() 633 | } 634 | 635 | pub fn parse_ts(s: &str, opts: &str) -> Result { 636 | let cm = Arc::new(SourceMap::new(FilePathMapping::empty())); 637 | let c = Arc::new(Compiler::new(cm)); 638 | 639 | try_with_handler(c.cm.clone(), |handler| { 640 | let opts: ParseOptions = serde_json::from_str(opts).unwrap(); 641 | 642 | let fm = c.cm.new_source_file(FileName::Anon, s.into()); 643 | let program = c 644 | .parse_js( 645 | fm, 646 | handler, 647 | opts.target, 648 | opts.syntax, 649 | opts.is_module, 650 | opts.comments, 651 | ) 652 | .context("failed to parse code")?; 653 | 654 | Ok(program) 655 | }) 656 | } 657 | 658 | #[cfg(test)] 659 | mod tests { 660 | use super::*; 661 | use indoc::indoc; 662 | 663 | fn get_prog(src: &str) -> Program { 664 | parse_ts( 665 | src, 666 | "{ 667 | \"syntax\": \"typescript\", 668 | \"tsx\": true, 669 | \"decorators\": false, 670 | \"dynamicImport\": false 671 | }", 672 | ) 673 | .unwrap() 674 | } 675 | 676 | fn test(src: &str, expected: &str, mani: Vec<(&str, GraphQLKind)>) { 677 | let prog = get_prog(src); 678 | 679 | let mut map: HashMap = HashMap::new(); 680 | mani.into_iter().for_each(|(k, v)| { 681 | map.insert(k.into(), v); 682 | }); 683 | 684 | let mut gen = CodeGenCtx::new(map); 685 | 686 | gen.parse(prog.module().unwrap()).unwrap(); 687 | let out = gen.finish(); 688 | println!("{}", out); 689 | assert_eq!(expected, out); 690 | } 691 | 692 | fn test_expect_err(src: &str, mani: Vec<(&str, GraphQLKind)>) { 693 | let prog = get_prog(src); 694 | let mut map: HashMap = HashMap::new(); 695 | mani.into_iter().for_each(|(k, v)| { 696 | map.insert(k.into(), v); 697 | }); 698 | let mut gen = CodeGenCtx::new(map); 699 | match gen.parse(prog.module().unwrap()) { 700 | Err(_) => {} 701 | Ok(_) => { 702 | println!("Output: {}", gen.finish()); 703 | panic!("Expected error") 704 | } 705 | } 706 | } 707 | 708 | #[test] 709 | fn it_parses_field_basic_types() { 710 | let src = " 711 | type User = { id: string; name: string; karma: number; active: boolean; } 712 | type Player = { user: User; level: number; } 713 | "; 714 | test( 715 | src, 716 | indoc! { r#" 717 | type User { 718 | id: String! 719 | name: String! 720 | karma: Int! 721 | active: Boolean! 722 | } 723 | type Player { 724 | user: User! 725 | level: Int! 726 | } 727 | "# }, 728 | vec![ 729 | ("User", GraphQLKind::Object), 730 | ("Player", GraphQLKind::Object), 731 | ], 732 | ); 733 | 734 | // Optionals 735 | let src = " 736 | type User = { id?: string; name?: string; karma?: number; } 737 | type Player = { user: User; level?: number; } 738 | "; 739 | 740 | test( 741 | src, 742 | indoc! { r#" 743 | type User { 744 | id: String 745 | name: String 746 | karma: Int 747 | } 748 | type Player { 749 | user: User! 750 | level: Int 751 | } 752 | "# }, 753 | vec![ 754 | ("User", GraphQLKind::Object), 755 | ("Player", GraphQLKind::Object), 756 | ], 757 | ); 758 | } 759 | 760 | #[test] 761 | fn it_fails_when_a_field_is_an_input() { 762 | // Basic 763 | let src = " 764 | type User = { id: string; name: string; karma: number; } 765 | type AnInput = { dummy: string } 766 | type Player = { user: User[]; woops: AnInput; } 767 | "; 768 | test_expect_err( 769 | src, 770 | vec![ 771 | ("User", GraphQLKind::Object), 772 | ("AnInput", GraphQLKind::Input), 773 | ("Player", GraphQLKind::Object), 774 | ], 775 | ); 776 | 777 | // Field with args 778 | let src = " 779 | type AnInput = { id: string; name: string; karma: number; } 780 | type Query = { doSomething: (input: { whatever: string }) => Promise } 781 | "; 782 | test_expect_err( 783 | src, 784 | vec![ 785 | ("AnInput", GraphQLKind::Input), 786 | ("Query", GraphQLKind::Object), 787 | ], 788 | ); 789 | } 790 | 791 | #[test] 792 | fn it_parses_array_fields() { 793 | // Basic 794 | let src = " 795 | type User = { id: string; name: string; karma: number; } 796 | type Player = { user: User[]; level: number[]; } 797 | "; 798 | test( 799 | src, 800 | indoc! { r#" 801 | type User { 802 | id: String! 803 | name: String! 804 | karma: Int! 805 | } 806 | type Player { 807 | user: [User]! 808 | level: [Int]! 809 | } 810 | "# }, 811 | vec![ 812 | ("User", GraphQLKind::Object), 813 | ("Player", GraphQLKind::Object), 814 | ], 815 | ); 816 | 817 | // Optional 818 | let src = " 819 | type User = { id: string; name: string; karma: number; } 820 | type Player = { user?: User[]; level?: number[]; } 821 | "; 822 | test( 823 | src, 824 | indoc! { r#" 825 | type User { 826 | id: String! 827 | name: String! 828 | karma: Int! 829 | } 830 | type Player { 831 | user: [User] 832 | level: [Int] 833 | } 834 | "# }, 835 | vec![ 836 | ("User", GraphQLKind::Object), 837 | ("Player", GraphQLKind::Object), 838 | ], 839 | ); 840 | 841 | // Nested 842 | let src = " 843 | type User = { id: string[][]; name: string; karma: number; } 844 | type Player = { user?: User[][]; level?: number[][]; } 845 | "; 846 | test( 847 | src, 848 | indoc! { r#" 849 | type User { 850 | id: [[String]]! 851 | name: String! 852 | karma: Int! 853 | } 854 | type Player { 855 | user: [[User]] 856 | level: [[Int]] 857 | } 858 | "# }, 859 | vec![ 860 | ("User", GraphQLKind::Object), 861 | ("Player", GraphQLKind::Object), 862 | ], 863 | ); 864 | } 865 | 866 | #[cfg(test)] 867 | mod args_tests { 868 | use super::*; 869 | 870 | #[test] 871 | fn it_parses_fields_with_args() { 872 | // Basic 873 | let src = " 874 | type User = { id: string; name: string; karma: number; } 875 | type Query = { findUser: (args: { id?: string, name?: string, test: string[] }) => Promise; } 876 | "; 877 | test( 878 | src, 879 | indoc! { r#" 880 | type User { 881 | id: String! 882 | name: String! 883 | karma: Int! 884 | } 885 | type Query { 886 | findUser(id: String, name: String, test: [String]!): [User]! 887 | } 888 | "# }, 889 | vec![ 890 | ("User", GraphQLKind::Object), 891 | ("Query", GraphQLKind::Object), 892 | ], 893 | ); 894 | 895 | // With pre-defined input 896 | let src = " 897 | type User = { id: string; name: string; karma: number; } 898 | type FindUserInput = { name: string, id?: string } 899 | type Query = { findUser: (args: { input: FindUserInput }) => Promise; } 900 | "; 901 | test( 902 | src, 903 | indoc! { r#" 904 | type User { 905 | id: String! 906 | name: String! 907 | karma: Int! 908 | } 909 | input FindUserInput { 910 | name: String! 911 | id: String 912 | } 913 | type Query { 914 | findUser(input: FindUserInput!): User 915 | } 916 | "# }, 917 | vec![ 918 | ("User", GraphQLKind::Object), 919 | ("FindUserInput", GraphQLKind::Input), 920 | ("Query", GraphQLKind::Object), 921 | ], 922 | ); 923 | 924 | // Mix and match 925 | let src = " 926 | type User = { id: string; name: string; karma: number; } 927 | type FindUserInput = { name: string, id?: string } 928 | type Query = { findUser: (args: { input: FindUserInput, karma?: number }) => Promise; } 929 | "; 930 | test( 931 | src, 932 | indoc! { r#" 933 | type User { 934 | id: String! 935 | name: String! 936 | karma: Int! 937 | } 938 | input FindUserInput { 939 | name: String! 940 | id: String 941 | } 942 | type Query { 943 | findUser(input: FindUserInput!, karma: Int): User! 944 | } 945 | "# }, 946 | vec![ 947 | ("User", GraphQLKind::Object), 948 | ("FindUserInput", GraphQLKind::Input), 949 | ("Query", GraphQLKind::Object), 950 | ], 951 | ); 952 | } 953 | 954 | #[test] 955 | fn it_should_fail_when_given_multiple_args() { 956 | // Inlined type literal 957 | let src = " 958 | type User = { id: string; name: string; karma: number; } 959 | type Query = { findUser: (args: { name: string }, woops: { karma: number }) => Promise; } 960 | "; 961 | test_expect_err( 962 | src, 963 | vec![ 964 | ("User", GraphQLKind::Object), 965 | ("Query", GraphQLKind::Object), 966 | ], 967 | ); 968 | } 969 | 970 | #[test] 971 | fn it_parses_type_literal_args() { 972 | let src = " 973 | type User = { id: string; name: string; karma: number; } 974 | type Query = { findUser: (args: { user: { name?: string, karma?: number } }) => Promise; } 975 | "; 976 | test( 977 | src, 978 | indoc! { r#" 979 | type User { 980 | id: String! 981 | name: String! 982 | karma: Int! 983 | } 984 | input FindUserInput { 985 | name: String 986 | karma: Int 987 | } 988 | type Query { 989 | findUser(user: FindUserInput!): User! 990 | } 991 | "# }, 992 | vec![ 993 | ("User", GraphQLKind::Object), 994 | ("Query", GraphQLKind::Object), 995 | ], 996 | ); 997 | } 998 | 999 | #[test] 1000 | fn it_parses_type_literal_and_keyword_args() { 1001 | let src = " 1002 | type User = { id: string; name: string; karma: number; } 1003 | type Query = { findUser: (args: { user: { name?: string, karma?: number }, karma?: number }) => Promise; } 1004 | "; 1005 | test( 1006 | src, 1007 | indoc! { r#" 1008 | type User { 1009 | id: String! 1010 | name: String! 1011 | karma: Int! 1012 | } 1013 | input FindUserInputUser { 1014 | name: String 1015 | karma: Int 1016 | } 1017 | type Query { 1018 | findUser(user: FindUserInputUser!, karma: Int): User! 1019 | } 1020 | "# }, 1021 | vec![ 1022 | ("User", GraphQLKind::Object), 1023 | ("Query", GraphQLKind::Object), 1024 | ], 1025 | ); 1026 | } 1027 | 1028 | #[test] 1029 | fn it_parses_multiple_type_literal_args() { 1030 | let src = " 1031 | type User = { id: string; name: string; karma: number; } 1032 | type Query = { findUser: (args: { user: { name?: string, karma?: number }, other?: { id: string } }) => Promise; } 1033 | "; 1034 | test( 1035 | src, 1036 | indoc! { r#" 1037 | type User { 1038 | id: String! 1039 | name: String! 1040 | karma: Int! 1041 | } 1042 | input FindUserInputUser { 1043 | name: String 1044 | karma: Int 1045 | } 1046 | input FindUserInputOther { 1047 | id: String! 1048 | } 1049 | type Query { 1050 | findUser(user: FindUserInputUser!, other: FindUserInputOther): User! 1051 | } 1052 | "# }, 1053 | vec![ 1054 | ("User", GraphQLKind::Object), 1055 | ("Query", GraphQLKind::Object), 1056 | ], 1057 | ); 1058 | } 1059 | 1060 | #[test] 1061 | fn it_should_fail_when_an_arg_isnt_an_input() { 1062 | let src = " 1063 | type User = { id: string; name: string; karma: number; } 1064 | type NotAnInput = { id: string } 1065 | type Query = { findUser: (args: { input: NotAnInput }) => Promise; } 1066 | "; 1067 | test_expect_err( 1068 | src, 1069 | vec![ 1070 | ("User", GraphQLKind::Object), 1071 | ("NotAnInput", GraphQLKind::Object), 1072 | ("Query", GraphQLKind::Object), 1073 | ], 1074 | ); 1075 | } 1076 | 1077 | #[test] 1078 | fn it_should_identify_promised_return_type() { 1079 | // Optionals 1080 | let src = " 1081 | type User = { id: string; name: string; karma: number; } 1082 | type Query = { findUser: (args: { id: string }) => Promise; } 1083 | "; 1084 | test( 1085 | src, 1086 | indoc! { r#" 1087 | type User { 1088 | id: String! 1089 | name: String! 1090 | karma: Int! 1091 | } 1092 | type Query { 1093 | findUser(id: String!): User 1094 | } 1095 | "# }, 1096 | vec![ 1097 | ("User", GraphQLKind::Object), 1098 | ("Query", GraphQLKind::Object), 1099 | ], 1100 | ); 1101 | let src = " 1102 | type User = { id: string; name: string; karma: number; } 1103 | type Query = { findUser: (args: { id: string }) => Promise; } 1104 | "; 1105 | test( 1106 | src, 1107 | indoc! { r#" 1108 | type User { 1109 | id: String! 1110 | name: String! 1111 | karma: Int! 1112 | } 1113 | type Query { 1114 | findUser(id: String!): User 1115 | } 1116 | "# }, 1117 | vec![ 1118 | ("User", GraphQLKind::Object), 1119 | ("Query", GraphQLKind::Object), 1120 | ], 1121 | ); 1122 | let src = " 1123 | type User = { id: string; name: string; karma: number; } 1124 | type Query = { findUser: (args: { id: string }) => Promise; } 1125 | "; 1126 | test( 1127 | src, 1128 | indoc! { r#" 1129 | type User { 1130 | id: String! 1131 | name: String! 1132 | karma: Int! 1133 | } 1134 | type Query { 1135 | findUser(id: String!): String! 1136 | } 1137 | "# }, 1138 | vec![ 1139 | ("User", GraphQLKind::Object), 1140 | ("Query", GraphQLKind::Object), 1141 | ], 1142 | ); 1143 | } 1144 | 1145 | #[test] 1146 | fn it_should_identify_promised_return_type_literal() { 1147 | let src = " 1148 | type User = { id: string; name: string; karma: number; } 1149 | type Query = { 1150 | findUser: (args: { id: string }) => Promise<{ user: User, success: boolean}>; 1151 | } 1152 | "; 1153 | test( 1154 | src, 1155 | indoc! { r#" 1156 | type User { 1157 | id: String! 1158 | name: String! 1159 | karma: Int! 1160 | } 1161 | type FindUserOutput { 1162 | user: User! 1163 | success: Boolean! 1164 | } 1165 | type Query { 1166 | findUser(id: String!): FindUserOutput! 1167 | } 1168 | "# }, 1169 | vec![ 1170 | ("User", GraphQLKind::Object), 1171 | ("Query", GraphQLKind::Object), 1172 | ], 1173 | ); 1174 | 1175 | let src = " 1176 | type User = { id: string; name: string; karma: number; } 1177 | type Query = { 1178 | findUser: (args: { id: string }) => Promise<{ user: User, success: boolean} | null>; 1179 | } 1180 | "; 1181 | test( 1182 | src, 1183 | indoc! { r#" 1184 | type User { 1185 | id: String! 1186 | name: String! 1187 | karma: Int! 1188 | } 1189 | type FindUserOutput { 1190 | user: User! 1191 | success: Boolean! 1192 | } 1193 | type Query { 1194 | findUser(id: String!): FindUserOutput 1195 | } 1196 | "# }, 1197 | vec![ 1198 | ("User", GraphQLKind::Object), 1199 | ("Query", GraphQLKind::Object), 1200 | ], 1201 | ); 1202 | 1203 | let src = " 1204 | type User = { id: string; name: string; karma: number; } 1205 | type Mutation = { 1206 | updateUser: (input: { user: { id?: string; name?: string; };}) => Promise<{ id: string; name: string; karma: number;}[] | null>; 1207 | } 1208 | "; 1209 | test( 1210 | src, 1211 | indoc! { r#" 1212 | type User { 1213 | id: String! 1214 | name: String! 1215 | karma: Int! 1216 | } 1217 | input UpdateUserInput { 1218 | id: String 1219 | name: String 1220 | } 1221 | type UpdateUserOutput { 1222 | id: String! 1223 | name: String! 1224 | karma: Int! 1225 | } 1226 | type Mutation { 1227 | updateUser(user: UpdateUserInput!): [UpdateUserOutput] 1228 | } 1229 | "# }, 1230 | vec![ 1231 | ("User", GraphQLKind::Object), 1232 | ("GetUserInput", GraphQLKind::Input), 1233 | ("Query", GraphQLKind::Object), 1234 | ("Mutation", GraphQLKind::Object), 1235 | ], 1236 | ); 1237 | 1238 | let src = " 1239 | type User = { id: string; name: string; karma: number; } 1240 | type Mutation = { 1241 | updateUser: (input: { user?: { id?: string; name?: string; } | undefined; }) => Promise; 1242 | } 1243 | "; 1244 | test( 1245 | src, 1246 | indoc! { r#" 1247 | type User { 1248 | id: String! 1249 | name: String! 1250 | karma: Int! 1251 | } 1252 | input UpdateUserInput { 1253 | id: String 1254 | name: String 1255 | } 1256 | type Mutation { 1257 | updateUser(user: UpdateUserInput): Boolean! 1258 | } 1259 | "# }, 1260 | vec![ 1261 | ("User", GraphQLKind::Object), 1262 | ("GetUserInput", GraphQLKind::Input), 1263 | ("Query", GraphQLKind::Object), 1264 | ("Mutation", GraphQLKind::Object), 1265 | ], 1266 | ); 1267 | } 1268 | } 1269 | } 1270 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod codegen; 2 | 3 | pub use codegen::*; 4 | 5 | #[cfg(feature = "node")] 6 | #[macro_use] 7 | extern crate napi_derive; 8 | #[cfg(feature = "node")] 9 | mod node; 10 | 11 | #[cfg(feature = "node")] 12 | pub use node::*; 13 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "node"))] 2 | fn main() { 3 | use std::fs::{self}; 4 | use tsgql::{generate_schema, parse_ts}; 5 | let filepath = std::env::args().nth(2).unwrap(); 6 | let outpath = std::env::args() 7 | .nth(3) 8 | .unwrap_or_else(|| "./generated.schema".into()); 9 | 10 | println!("filepath={}, outpath={}", filepath, outpath); 11 | 12 | let code = fs::read_to_string(filepath).expect("failed to read file"); 13 | let prog = parse_ts( 14 | code.as_str(), 15 | "{ 16 | \"syntax\": \"typescript\", 17 | \"tsx\": true, 18 | \"decorators\": false, 19 | \"dynamicImport\": false 20 | }", 21 | ) 22 | .unwrap(); 23 | 24 | // generate_schema(prog) 25 | } 26 | 27 | #[cfg(feature = "node")] 28 | fn main() {} 29 | -------------------------------------------------------------------------------- /src/node.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fs}; 2 | 3 | use napi::{CallContext, Error, JsNumber, JsObject, JsString, Result}; 4 | 5 | use crate::{generate_schema, parse_ts, GraphQLKind}; 6 | 7 | #[cfg(all( 8 | any(windows, unix), 9 | target_arch = "x86_64", 10 | not(target_env = "musl"), 11 | not(debug_assertions) 12 | ))] 13 | #[global_allocator] 14 | static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; 15 | 16 | #[module_exports] 17 | fn init(mut exports: JsObject) -> Result<()> { 18 | exports.create_named_method("generateSchema", generate)?; 19 | Ok(()) 20 | } 21 | 22 | #[js_function(4)] 23 | fn generate(ctx: CallContext) -> Result { 24 | let code = ctx.get::(0)?.into_utf8()?; 25 | let manifest = ctx.get::(1)?.into_utf8()?; 26 | let opts = ctx.get::(2)?.into_utf8()?; 27 | 28 | let manifest_raw: HashMap = serde_json::from_str(manifest.as_str()?)?; 29 | 30 | let mut manifest: HashMap = HashMap::with_capacity(manifest_raw.len()); 31 | manifest_raw.into_iter().for_each(|(s, val)| { 32 | manifest.insert(s, GraphQLKind::from_u8(val).unwrap()); 33 | }); 34 | 35 | let prog = match parse_ts(code.as_str()?, opts.as_str()?) { 36 | Ok(p) => p, 37 | Err(e) => return Err(Error::new(napi::Status::Unknown, format!("{:?}", e))), 38 | }; 39 | 40 | let output = match generate_schema(prog.module().unwrap(), manifest) { 41 | Ok(output) => output, 42 | Err(e) => return Err(Error::new(napi::Status::Unknown, format!("{:?}", e))), 43 | }; 44 | 45 | ctx.env.create_string(&output) 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "strict": true, 5 | "moduleResolution": "node", 6 | "module": "CommonJS", 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "include": ["."], 13 | "exclude": ["node_modules"] 14 | } -------------------------------------------------------------------------------- /tsgql.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tsconfigPath: "./type-reduce/tsconfig.json", 3 | schema: "./type-reduce/src/input.ts", 4 | out: "./schema.graphql", 5 | } -------------------------------------------------------------------------------- /type-reduce/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /type-reduce/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Zack Radisic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /type-reduce/esbuild.js: -------------------------------------------------------------------------------- 1 | require('esbuild').buildSync({ 2 | entryPoints: ['./src/index.js'], 3 | bundle: true, 4 | outfile: 'dist/index.js', 5 | platform: 'node' 6 | }) -------------------------------------------------------------------------------- /type-reduce/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | testMatch: [ 4 | '**/__tests__/**/*.+(ts|tsx|js)', 5 | '**/?(*.)+(spec|test).+(ts|tsx|js)', 6 | ], 7 | transform: { 8 | '^.+\\.(t|j)sx?$': ['@swc/jest'], 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /type-reduce/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@nodelib/fs.scandir": { 8 | "version": "2.1.5", 9 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 10 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 11 | "requires": { 12 | "@nodelib/fs.stat": "2.0.5", 13 | "run-parallel": "^1.1.9" 14 | } 15 | }, 16 | "@nodelib/fs.stat": { 17 | "version": "2.0.5", 18 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 19 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" 20 | }, 21 | "@nodelib/fs.walk": { 22 | "version": "1.2.8", 23 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 24 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 25 | "requires": { 26 | "@nodelib/fs.scandir": "2.1.5", 27 | "fastq": "^1.6.0" 28 | } 29 | }, 30 | "@sindresorhus/is": { 31 | "version": "0.14.0", 32 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", 33 | "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" 34 | }, 35 | "@szmarczak/http-timer": { 36 | "version": "1.1.2", 37 | "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", 38 | "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", 39 | "requires": { 40 | "defer-to-connect": "^1.0.1" 41 | } 42 | }, 43 | "@ts-morph/bootstrap": { 44 | "version": "0.11.0", 45 | "resolved": "https://registry.npmjs.org/@ts-morph/bootstrap/-/bootstrap-0.11.0.tgz", 46 | "integrity": "sha512-wS75U9u5Xb8PD5KdE9lsQUB4SnQBH63Mhoi/ISJ/E+kEQiz9pjnFsydQGrJIIe+IdkzT++nk7iwHLQ4PBpoDFw==", 47 | "requires": { 48 | "@ts-morph/common": "~0.11.0" 49 | } 50 | }, 51 | "@ts-morph/common": { 52 | "version": "0.11.0", 53 | "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.11.0.tgz", 54 | "integrity": "sha512-Ti2tpROSVHlBoNiJKVUYPNk/yCPb1Bcly4RsSwC2F9a8PMMYfEYovRcghTu6N5o66Jq0yvPXN3uNaO4a3zskTA==", 55 | "requires": { 56 | "fast-glob": "^3.2.7", 57 | "minimatch": "^3.0.4", 58 | "mkdirp": "^1.0.4", 59 | "path-browserify": "^1.0.1" 60 | } 61 | }, 62 | "@types/node": { 63 | "version": "16.11.1", 64 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz", 65 | "integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==", 66 | "dev": true 67 | }, 68 | "abbrev": { 69 | "version": "1.1.1", 70 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 71 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 72 | }, 73 | "ansi-align": { 74 | "version": "3.0.1", 75 | "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", 76 | "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", 77 | "requires": { 78 | "string-width": "^4.1.0" 79 | } 80 | }, 81 | "ansi-regex": { 82 | "version": "5.0.1", 83 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 84 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 85 | }, 86 | "ansi-styles": { 87 | "version": "4.3.0", 88 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 89 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 90 | "requires": { 91 | "color-convert": "^2.0.1" 92 | } 93 | }, 94 | "anymatch": { 95 | "version": "3.1.2", 96 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 97 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 98 | "requires": { 99 | "normalize-path": "^3.0.0", 100 | "picomatch": "^2.0.4" 101 | } 102 | }, 103 | "balanced-match": { 104 | "version": "1.0.2", 105 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 106 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 107 | }, 108 | "binary-extensions": { 109 | "version": "2.2.0", 110 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 111 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" 112 | }, 113 | "boxen": { 114 | "version": "5.1.2", 115 | "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", 116 | "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", 117 | "requires": { 118 | "ansi-align": "^3.0.0", 119 | "camelcase": "^6.2.0", 120 | "chalk": "^4.1.0", 121 | "cli-boxes": "^2.2.1", 122 | "string-width": "^4.2.2", 123 | "type-fest": "^0.20.2", 124 | "widest-line": "^3.1.0", 125 | "wrap-ansi": "^7.0.0" 126 | } 127 | }, 128 | "brace-expansion": { 129 | "version": "1.1.11", 130 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 131 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 132 | "requires": { 133 | "balanced-match": "^1.0.0", 134 | "concat-map": "0.0.1" 135 | } 136 | }, 137 | "braces": { 138 | "version": "3.0.2", 139 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 140 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 141 | "requires": { 142 | "fill-range": "^7.0.1" 143 | } 144 | }, 145 | "cacheable-request": { 146 | "version": "6.1.0", 147 | "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", 148 | "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", 149 | "requires": { 150 | "clone-response": "^1.0.2", 151 | "get-stream": "^5.1.0", 152 | "http-cache-semantics": "^4.0.0", 153 | "keyv": "^3.0.0", 154 | "lowercase-keys": "^2.0.0", 155 | "normalize-url": "^4.1.0", 156 | "responselike": "^1.0.2" 157 | }, 158 | "dependencies": { 159 | "get-stream": { 160 | "version": "5.2.0", 161 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 162 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 163 | "requires": { 164 | "pump": "^3.0.0" 165 | } 166 | }, 167 | "lowercase-keys": { 168 | "version": "2.0.0", 169 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", 170 | "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" 171 | } 172 | } 173 | }, 174 | "camelcase": { 175 | "version": "6.2.0", 176 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", 177 | "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==" 178 | }, 179 | "chalk": { 180 | "version": "4.1.2", 181 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 182 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 183 | "requires": { 184 | "ansi-styles": "^4.1.0", 185 | "supports-color": "^7.1.0" 186 | }, 187 | "dependencies": { 188 | "has-flag": { 189 | "version": "4.0.0", 190 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 191 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 192 | }, 193 | "supports-color": { 194 | "version": "7.2.0", 195 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 196 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 197 | "requires": { 198 | "has-flag": "^4.0.0" 199 | } 200 | } 201 | } 202 | }, 203 | "chokidar": { 204 | "version": "3.5.2", 205 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", 206 | "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", 207 | "requires": { 208 | "anymatch": "~3.1.2", 209 | "braces": "~3.0.2", 210 | "fsevents": "~2.3.2", 211 | "glob-parent": "~5.1.2", 212 | "is-binary-path": "~2.1.0", 213 | "is-glob": "~4.0.1", 214 | "normalize-path": "~3.0.0", 215 | "readdirp": "~3.6.0" 216 | } 217 | }, 218 | "ci-info": { 219 | "version": "2.0.0", 220 | "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", 221 | "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" 222 | }, 223 | "cli-boxes": { 224 | "version": "2.2.1", 225 | "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", 226 | "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" 227 | }, 228 | "clone-response": { 229 | "version": "1.0.2", 230 | "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", 231 | "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", 232 | "requires": { 233 | "mimic-response": "^1.0.0" 234 | } 235 | }, 236 | "code-block-writer": { 237 | "version": "10.1.1", 238 | "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-10.1.1.tgz", 239 | "integrity": "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==" 240 | }, 241 | "color-convert": { 242 | "version": "2.0.1", 243 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 244 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 245 | "requires": { 246 | "color-name": "~1.1.4" 247 | } 248 | }, 249 | "color-name": { 250 | "version": "1.1.4", 251 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 252 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 253 | }, 254 | "concat-map": { 255 | "version": "0.0.1", 256 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 257 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 258 | }, 259 | "configstore": { 260 | "version": "5.0.1", 261 | "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", 262 | "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", 263 | "requires": { 264 | "dot-prop": "^5.2.0", 265 | "graceful-fs": "^4.1.2", 266 | "make-dir": "^3.0.0", 267 | "unique-string": "^2.0.0", 268 | "write-file-atomic": "^3.0.0", 269 | "xdg-basedir": "^4.0.0" 270 | } 271 | }, 272 | "crypto-random-string": { 273 | "version": "2.0.0", 274 | "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", 275 | "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" 276 | }, 277 | "debug": { 278 | "version": "3.2.7", 279 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 280 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 281 | "requires": { 282 | "ms": "^2.1.1" 283 | } 284 | }, 285 | "decompress-response": { 286 | "version": "3.3.0", 287 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", 288 | "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", 289 | "requires": { 290 | "mimic-response": "^1.0.0" 291 | } 292 | }, 293 | "deep-extend": { 294 | "version": "0.6.0", 295 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 296 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 297 | }, 298 | "defer-to-connect": { 299 | "version": "1.1.3", 300 | "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", 301 | "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" 302 | }, 303 | "dot-prop": { 304 | "version": "5.3.0", 305 | "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", 306 | "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", 307 | "requires": { 308 | "is-obj": "^2.0.0" 309 | } 310 | }, 311 | "duplexer3": { 312 | "version": "0.1.4", 313 | "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", 314 | "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" 315 | }, 316 | "emoji-regex": { 317 | "version": "8.0.0", 318 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 319 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 320 | }, 321 | "end-of-stream": { 322 | "version": "1.4.4", 323 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 324 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 325 | "requires": { 326 | "once": "^1.4.0" 327 | } 328 | }, 329 | "esbuild": { 330 | "version": "0.13.8", 331 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.8.tgz", 332 | "integrity": "sha512-A4af7G7YZLfG5OnARJRMtlpEsCkq/zHZQXewgPA864l9D6VjjbH1SuFYK/OSV6BtHwDGkdwyRrX0qQFLnMfUcw==", 333 | "requires": { 334 | "esbuild-android-arm64": "0.13.8", 335 | "esbuild-darwin-64": "0.13.8", 336 | "esbuild-darwin-arm64": "0.13.8", 337 | "esbuild-freebsd-64": "0.13.8", 338 | "esbuild-freebsd-arm64": "0.13.8", 339 | "esbuild-linux-32": "0.13.8", 340 | "esbuild-linux-64": "0.13.8", 341 | "esbuild-linux-arm": "0.13.8", 342 | "esbuild-linux-arm64": "0.13.8", 343 | "esbuild-linux-mips64le": "0.13.8", 344 | "esbuild-linux-ppc64le": "0.13.8", 345 | "esbuild-netbsd-64": "0.13.8", 346 | "esbuild-openbsd-64": "0.13.8", 347 | "esbuild-sunos-64": "0.13.8", 348 | "esbuild-windows-32": "0.13.8", 349 | "esbuild-windows-64": "0.13.8", 350 | "esbuild-windows-arm64": "0.13.8" 351 | } 352 | }, 353 | "esbuild-android-arm64": { 354 | "version": "0.13.8", 355 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.8.tgz", 356 | "integrity": "sha512-AilbChndywpk7CdKkNSZ9klxl+9MboLctXd9LwLo3b0dawmOF/i/t2U5d8LM6SbT1Xw36F8yngSUPrd8yPs2RA==", 357 | "optional": true 358 | }, 359 | "esbuild-darwin-64": { 360 | "version": "0.13.8", 361 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.8.tgz", 362 | "integrity": "sha512-b6sdiT84zV5LVaoF+UoMVGJzR/iE2vNUfUDfFQGrm4LBwM/PWXweKpuu6RD9mcyCq18cLxkP6w/LD/w9DtX3ng==", 363 | "optional": true 364 | }, 365 | "esbuild-darwin-arm64": { 366 | "version": "0.13.8", 367 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.8.tgz", 368 | "integrity": "sha512-R8YuPiiJayuJJRUBG4H0VwkEKo6AvhJs2m7Tl0JaIer3u1FHHXwGhMxjJDmK+kXwTFPriSysPvcobXC/UrrZCQ==", 369 | "optional": true 370 | }, 371 | "esbuild-freebsd-64": { 372 | "version": "0.13.8", 373 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.8.tgz", 374 | "integrity": "sha512-zBn6urrn8FnKC+YSgDxdof9jhPCeU8kR/qaamlV4gI8R3KUaUK162WYM7UyFVAlj9N0MyD3AtB+hltzu4cysTw==", 375 | "optional": true 376 | }, 377 | "esbuild-freebsd-arm64": { 378 | "version": "0.13.8", 379 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.8.tgz", 380 | "integrity": "sha512-pWW2slN7lGlkx0MOEBoUGwRX5UgSCLq3dy2c8RIOpiHtA87xAUpDBvZK10MykbT+aMfXc0NI2lu1X+6kI34xng==", 381 | "optional": true 382 | }, 383 | "esbuild-linux-32": { 384 | "version": "0.13.8", 385 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.8.tgz", 386 | "integrity": "sha512-T0I0ueeKVO/Is0CAeSEOG9s2jeNNb8jrrMwG9QBIm3UU18MRB60ERgkS2uV3fZ1vP2F8i3Z2e3Zju4lg9dhVmw==", 387 | "optional": true 388 | }, 389 | "esbuild-linux-64": { 390 | "version": "0.13.8", 391 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.8.tgz", 392 | "integrity": "sha512-Bm8SYmFtvfDCIu9sjKppFXzRXn2BVpuCinU1ChTuMtdKI/7aPpXIrkqBNOgPTOQO9AylJJc1Zw6EvtKORhn64w==", 393 | "optional": true 394 | }, 395 | "esbuild-linux-arm": { 396 | "version": "0.13.8", 397 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.8.tgz", 398 | "integrity": "sha512-4/HfcC40LJ4GPyboHA+db0jpFarTB628D1ifU+/5bunIgY+t6mHkJWyxWxAAE8wl/ZIuRYB9RJFdYpu1AXGPdg==", 399 | "optional": true 400 | }, 401 | "esbuild-linux-arm64": { 402 | "version": "0.13.8", 403 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.8.tgz", 404 | "integrity": "sha512-X4pWZ+SL+FJ09chWFgRNO3F+YtvAQRcWh0uxKqZSWKiWodAB20flsW/OWFYLXBKiVCTeoGMvENZS/GeVac7+tQ==", 405 | "optional": true 406 | }, 407 | "esbuild-linux-mips64le": { 408 | "version": "0.13.8", 409 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.8.tgz", 410 | "integrity": "sha512-o7e0D+sqHKT31v+mwFircJFjwSKVd2nbkHEn4l9xQ1hLR+Bv8rnt3HqlblY3+sBdlrOTGSwz0ReROlKUMJyldA==", 411 | "optional": true 412 | }, 413 | "esbuild-linux-ppc64le": { 414 | "version": "0.13.8", 415 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.8.tgz", 416 | "integrity": "sha512-eZSQ0ERsWkukJp2px/UWJHVNuy0lMoz/HZcRWAbB6reoaBw7S9vMzYNUnflfL3XA6WDs+dZn3ekHE4Y2uWLGig==", 417 | "optional": true 418 | }, 419 | "esbuild-netbsd-64": { 420 | "version": "0.13.8", 421 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.8.tgz", 422 | "integrity": "sha512-gZX4kP7gVvOrvX0ZwgHmbuHczQUwqYppxqtoyC7VNd80t5nBHOFXVhWo2Ad/Lms0E8b+wwgI/WjZFTCpUHOg9Q==", 423 | "optional": true 424 | }, 425 | "esbuild-openbsd-64": { 426 | "version": "0.13.8", 427 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.8.tgz", 428 | "integrity": "sha512-afzza308X4WmcebexbTzAgfEWt9MUkdTvwIa8xOu4CM2qGbl2LanqEl8/LUs8jh6Gqw6WsicEK52GPrS9wvkcw==", 429 | "optional": true 430 | }, 431 | "esbuild-sunos-64": { 432 | "version": "0.13.8", 433 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.8.tgz", 434 | "integrity": "sha512-mWPZibmBbuMKD+LDN23LGcOZ2EawMYBONMXXHmbuxeT0XxCNwadbCVwUQ/2p5Dp5Kvf6mhrlIffcnWOiCBpiVw==", 435 | "optional": true 436 | }, 437 | "esbuild-windows-32": { 438 | "version": "0.13.8", 439 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.8.tgz", 440 | "integrity": "sha512-QsZ1HnWIcnIEApETZWw8HlOhDSWqdZX2SylU7IzGxOYyVcX7QI06ety/aDcn437mwyO7Ph4RrbhB+2ntM8kX8A==", 441 | "optional": true 442 | }, 443 | "esbuild-windows-64": { 444 | "version": "0.13.8", 445 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.8.tgz", 446 | "integrity": "sha512-76Fb57B9eE/JmJi1QmUW0tRLQZfGo0it+JeYoCDTSlbTn7LV44ecOHIMJSSgZADUtRMWT9z0Kz186bnaB3amSg==", 447 | "optional": true 448 | }, 449 | "esbuild-windows-arm64": { 450 | "version": "0.13.8", 451 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.8.tgz", 452 | "integrity": "sha512-HW6Mtq5eTudllxY2YgT62MrVcn7oq2o8TAoAvDUhyiEmRmDY8tPwAhb1vxw5/cdkbukM3KdMYtksnUhF/ekWeg==", 453 | "optional": true 454 | }, 455 | "escape-goat": { 456 | "version": "2.1.1", 457 | "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", 458 | "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" 459 | }, 460 | "fast-glob": { 461 | "version": "3.2.7", 462 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", 463 | "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", 464 | "requires": { 465 | "@nodelib/fs.stat": "^2.0.2", 466 | "@nodelib/fs.walk": "^1.2.3", 467 | "glob-parent": "^5.1.2", 468 | "merge2": "^1.3.0", 469 | "micromatch": "^4.0.4" 470 | } 471 | }, 472 | "fastq": { 473 | "version": "1.13.0", 474 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", 475 | "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", 476 | "requires": { 477 | "reusify": "^1.0.4" 478 | } 479 | }, 480 | "fill-range": { 481 | "version": "7.0.1", 482 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 483 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 484 | "requires": { 485 | "to-regex-range": "^5.0.1" 486 | } 487 | }, 488 | "fsevents": { 489 | "version": "2.3.2", 490 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 491 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 492 | "optional": true 493 | }, 494 | "get-stream": { 495 | "version": "4.1.0", 496 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", 497 | "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", 498 | "requires": { 499 | "pump": "^3.0.0" 500 | } 501 | }, 502 | "glob-parent": { 503 | "version": "5.1.2", 504 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 505 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 506 | "requires": { 507 | "is-glob": "^4.0.1" 508 | } 509 | }, 510 | "global-dirs": { 511 | "version": "3.0.0", 512 | "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", 513 | "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", 514 | "requires": { 515 | "ini": "2.0.0" 516 | } 517 | }, 518 | "got": { 519 | "version": "9.6.0", 520 | "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", 521 | "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", 522 | "requires": { 523 | "@sindresorhus/is": "^0.14.0", 524 | "@szmarczak/http-timer": "^1.1.2", 525 | "cacheable-request": "^6.0.0", 526 | "decompress-response": "^3.3.0", 527 | "duplexer3": "^0.1.4", 528 | "get-stream": "^4.1.0", 529 | "lowercase-keys": "^1.0.1", 530 | "mimic-response": "^1.0.1", 531 | "p-cancelable": "^1.0.0", 532 | "to-readable-stream": "^1.0.0", 533 | "url-parse-lax": "^3.0.0" 534 | } 535 | }, 536 | "graceful-fs": { 537 | "version": "4.2.8", 538 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", 539 | "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" 540 | }, 541 | "has-flag": { 542 | "version": "3.0.0", 543 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 544 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 545 | }, 546 | "has-yarn": { 547 | "version": "2.1.0", 548 | "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", 549 | "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" 550 | }, 551 | "http-cache-semantics": { 552 | "version": "4.1.0", 553 | "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", 554 | "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" 555 | }, 556 | "husky": { 557 | "version": "7.0.2", 558 | "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.2.tgz", 559 | "integrity": "sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg==", 560 | "dev": true 561 | }, 562 | "ignore-by-default": { 563 | "version": "1.0.1", 564 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 565 | "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" 566 | }, 567 | "import-lazy": { 568 | "version": "2.1.0", 569 | "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", 570 | "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" 571 | }, 572 | "imurmurhash": { 573 | "version": "0.1.4", 574 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 575 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" 576 | }, 577 | "ini": { 578 | "version": "2.0.0", 579 | "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", 580 | "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==" 581 | }, 582 | "is-binary-path": { 583 | "version": "2.1.0", 584 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 585 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 586 | "requires": { 587 | "binary-extensions": "^2.0.0" 588 | } 589 | }, 590 | "is-ci": { 591 | "version": "2.0.0", 592 | "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", 593 | "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", 594 | "requires": { 595 | "ci-info": "^2.0.0" 596 | } 597 | }, 598 | "is-extglob": { 599 | "version": "2.1.1", 600 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 601 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" 602 | }, 603 | "is-fullwidth-code-point": { 604 | "version": "3.0.0", 605 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 606 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 607 | }, 608 | "is-glob": { 609 | "version": "4.0.3", 610 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 611 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 612 | "requires": { 613 | "is-extglob": "^2.1.1" 614 | } 615 | }, 616 | "is-installed-globally": { 617 | "version": "0.4.0", 618 | "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", 619 | "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", 620 | "requires": { 621 | "global-dirs": "^3.0.0", 622 | "is-path-inside": "^3.0.2" 623 | } 624 | }, 625 | "is-npm": { 626 | "version": "5.0.0", 627 | "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", 628 | "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==" 629 | }, 630 | "is-number": { 631 | "version": "7.0.0", 632 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 633 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 634 | }, 635 | "is-obj": { 636 | "version": "2.0.0", 637 | "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", 638 | "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" 639 | }, 640 | "is-path-inside": { 641 | "version": "3.0.3", 642 | "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", 643 | "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" 644 | }, 645 | "is-typedarray": { 646 | "version": "1.0.0", 647 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 648 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 649 | }, 650 | "is-yarn-global": { 651 | "version": "0.3.0", 652 | "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", 653 | "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" 654 | }, 655 | "json-buffer": { 656 | "version": "3.0.0", 657 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", 658 | "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" 659 | }, 660 | "keyv": { 661 | "version": "3.1.0", 662 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", 663 | "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", 664 | "requires": { 665 | "json-buffer": "3.0.0" 666 | } 667 | }, 668 | "latest-version": { 669 | "version": "5.1.0", 670 | "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", 671 | "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", 672 | "requires": { 673 | "package-json": "^6.3.0" 674 | } 675 | }, 676 | "lowercase-keys": { 677 | "version": "1.0.1", 678 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", 679 | "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" 680 | }, 681 | "lru-cache": { 682 | "version": "6.0.0", 683 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 684 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 685 | "requires": { 686 | "yallist": "^4.0.0" 687 | } 688 | }, 689 | "make-dir": { 690 | "version": "3.1.0", 691 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", 692 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", 693 | "requires": { 694 | "semver": "^6.0.0" 695 | }, 696 | "dependencies": { 697 | "semver": { 698 | "version": "6.3.0", 699 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 700 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" 701 | } 702 | } 703 | }, 704 | "merge2": { 705 | "version": "1.4.1", 706 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 707 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" 708 | }, 709 | "micromatch": { 710 | "version": "4.0.4", 711 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", 712 | "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", 713 | "requires": { 714 | "braces": "^3.0.1", 715 | "picomatch": "^2.2.3" 716 | } 717 | }, 718 | "mimic-response": { 719 | "version": "1.0.1", 720 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", 721 | "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" 722 | }, 723 | "minimatch": { 724 | "version": "3.0.4", 725 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 726 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 727 | "requires": { 728 | "brace-expansion": "^1.1.7" 729 | } 730 | }, 731 | "minimist": { 732 | "version": "1.2.5", 733 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 734 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 735 | }, 736 | "mkdirp": { 737 | "version": "1.0.4", 738 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", 739 | "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" 740 | }, 741 | "ms": { 742 | "version": "2.1.3", 743 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 744 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 745 | }, 746 | "nodemon": { 747 | "version": "2.0.14", 748 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.14.tgz", 749 | "integrity": "sha512-frcpDx+PviKEQRSYzwhckuO2zoHcBYLHI754RE9z5h1RGtrngerc04mLpQQCPWBkH/2ObrX7We9YiwVSYZpFJQ==", 750 | "requires": { 751 | "chokidar": "^3.2.2", 752 | "debug": "^3.2.6", 753 | "ignore-by-default": "^1.0.1", 754 | "minimatch": "^3.0.4", 755 | "pstree.remy": "^1.1.7", 756 | "semver": "^5.7.1", 757 | "supports-color": "^5.5.0", 758 | "touch": "^3.1.0", 759 | "undefsafe": "^2.0.3", 760 | "update-notifier": "^5.1.0" 761 | } 762 | }, 763 | "nopt": { 764 | "version": "1.0.10", 765 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 766 | "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", 767 | "requires": { 768 | "abbrev": "1" 769 | } 770 | }, 771 | "normalize-path": { 772 | "version": "3.0.0", 773 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 774 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" 775 | }, 776 | "normalize-url": { 777 | "version": "4.5.1", 778 | "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", 779 | "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" 780 | }, 781 | "once": { 782 | "version": "1.4.0", 783 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 784 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 785 | "requires": { 786 | "wrappy": "1" 787 | } 788 | }, 789 | "p-cancelable": { 790 | "version": "1.1.0", 791 | "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", 792 | "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" 793 | }, 794 | "package-json": { 795 | "version": "6.5.0", 796 | "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", 797 | "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", 798 | "requires": { 799 | "got": "^9.6.0", 800 | "registry-auth-token": "^4.0.0", 801 | "registry-url": "^5.0.0", 802 | "semver": "^6.2.0" 803 | }, 804 | "dependencies": { 805 | "semver": { 806 | "version": "6.3.0", 807 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 808 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" 809 | } 810 | } 811 | }, 812 | "path-browserify": { 813 | "version": "1.0.1", 814 | "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", 815 | "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" 816 | }, 817 | "picomatch": { 818 | "version": "2.3.0", 819 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", 820 | "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" 821 | }, 822 | "prepend-http": { 823 | "version": "2.0.0", 824 | "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", 825 | "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" 826 | }, 827 | "pstree.remy": { 828 | "version": "1.1.8", 829 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 830 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" 831 | }, 832 | "pump": { 833 | "version": "3.0.0", 834 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 835 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 836 | "requires": { 837 | "end-of-stream": "^1.1.0", 838 | "once": "^1.3.1" 839 | } 840 | }, 841 | "pupa": { 842 | "version": "2.1.1", 843 | "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", 844 | "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", 845 | "requires": { 846 | "escape-goat": "^2.0.0" 847 | } 848 | }, 849 | "queue-microtask": { 850 | "version": "1.2.3", 851 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 852 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" 853 | }, 854 | "rc": { 855 | "version": "1.2.8", 856 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 857 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 858 | "requires": { 859 | "deep-extend": "^0.6.0", 860 | "ini": "~1.3.0", 861 | "minimist": "^1.2.0", 862 | "strip-json-comments": "~2.0.1" 863 | }, 864 | "dependencies": { 865 | "ini": { 866 | "version": "1.3.8", 867 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 868 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" 869 | } 870 | } 871 | }, 872 | "readdirp": { 873 | "version": "3.6.0", 874 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 875 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 876 | "requires": { 877 | "picomatch": "^2.2.1" 878 | } 879 | }, 880 | "registry-auth-token": { 881 | "version": "4.2.1", 882 | "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", 883 | "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", 884 | "requires": { 885 | "rc": "^1.2.8" 886 | } 887 | }, 888 | "registry-url": { 889 | "version": "5.1.0", 890 | "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", 891 | "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", 892 | "requires": { 893 | "rc": "^1.2.8" 894 | } 895 | }, 896 | "responselike": { 897 | "version": "1.0.2", 898 | "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", 899 | "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", 900 | "requires": { 901 | "lowercase-keys": "^1.0.0" 902 | } 903 | }, 904 | "reusify": { 905 | "version": "1.0.4", 906 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 907 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" 908 | }, 909 | "run-parallel": { 910 | "version": "1.2.0", 911 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 912 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 913 | "requires": { 914 | "queue-microtask": "^1.2.2" 915 | } 916 | }, 917 | "semver": { 918 | "version": "5.7.1", 919 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 920 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 921 | }, 922 | "semver-diff": { 923 | "version": "3.1.1", 924 | "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", 925 | "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", 926 | "requires": { 927 | "semver": "^6.3.0" 928 | }, 929 | "dependencies": { 930 | "semver": { 931 | "version": "6.3.0", 932 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 933 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" 934 | } 935 | } 936 | }, 937 | "signal-exit": { 938 | "version": "3.0.5", 939 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", 940 | "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" 941 | }, 942 | "string-width": { 943 | "version": "4.2.3", 944 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 945 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 946 | "requires": { 947 | "emoji-regex": "^8.0.0", 948 | "is-fullwidth-code-point": "^3.0.0", 949 | "strip-ansi": "^6.0.1" 950 | } 951 | }, 952 | "strip-ansi": { 953 | "version": "6.0.1", 954 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 955 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 956 | "requires": { 957 | "ansi-regex": "^5.0.1" 958 | } 959 | }, 960 | "strip-json-comments": { 961 | "version": "2.0.1", 962 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 963 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 964 | }, 965 | "supports-color": { 966 | "version": "5.5.0", 967 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 968 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 969 | "requires": { 970 | "has-flag": "^3.0.0" 971 | } 972 | }, 973 | "to-readable-stream": { 974 | "version": "1.0.0", 975 | "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", 976 | "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" 977 | }, 978 | "to-regex-range": { 979 | "version": "5.0.1", 980 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 981 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 982 | "requires": { 983 | "is-number": "^7.0.0" 984 | } 985 | }, 986 | "touch": { 987 | "version": "3.1.0", 988 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", 989 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", 990 | "requires": { 991 | "nopt": "~1.0.10" 992 | } 993 | }, 994 | "ts-morph": { 995 | "version": "12.0.0", 996 | "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-12.0.0.tgz", 997 | "integrity": "sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA==", 998 | "requires": { 999 | "@ts-morph/common": "~0.11.0", 1000 | "code-block-writer": "^10.1.1" 1001 | } 1002 | }, 1003 | "tslib": { 1004 | "version": "2.3.1", 1005 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", 1006 | "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", 1007 | "dev": true 1008 | }, 1009 | "type-fest": { 1010 | "version": "0.20.2", 1011 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", 1012 | "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" 1013 | }, 1014 | "typedarray-to-buffer": { 1015 | "version": "3.1.5", 1016 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 1017 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 1018 | "requires": { 1019 | "is-typedarray": "^1.0.0" 1020 | } 1021 | }, 1022 | "typescript": { 1023 | "version": "4.4.4", 1024 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", 1025 | "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", 1026 | "dev": true 1027 | }, 1028 | "undefsafe": { 1029 | "version": "2.0.5", 1030 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 1031 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" 1032 | }, 1033 | "unique-string": { 1034 | "version": "2.0.0", 1035 | "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", 1036 | "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", 1037 | "requires": { 1038 | "crypto-random-string": "^2.0.0" 1039 | } 1040 | }, 1041 | "update-notifier": { 1042 | "version": "5.1.0", 1043 | "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", 1044 | "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", 1045 | "requires": { 1046 | "boxen": "^5.0.0", 1047 | "chalk": "^4.1.0", 1048 | "configstore": "^5.0.1", 1049 | "has-yarn": "^2.1.0", 1050 | "import-lazy": "^2.1.0", 1051 | "is-ci": "^2.0.0", 1052 | "is-installed-globally": "^0.4.0", 1053 | "is-npm": "^5.0.0", 1054 | "is-yarn-global": "^0.3.0", 1055 | "latest-version": "^5.1.0", 1056 | "pupa": "^2.1.1", 1057 | "semver": "^7.3.4", 1058 | "semver-diff": "^3.1.1", 1059 | "xdg-basedir": "^4.0.0" 1060 | }, 1061 | "dependencies": { 1062 | "semver": { 1063 | "version": "7.3.5", 1064 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 1065 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 1066 | "requires": { 1067 | "lru-cache": "^6.0.0" 1068 | } 1069 | } 1070 | } 1071 | }, 1072 | "url-parse-lax": { 1073 | "version": "3.0.0", 1074 | "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", 1075 | "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", 1076 | "requires": { 1077 | "prepend-http": "^2.0.0" 1078 | } 1079 | }, 1080 | "widest-line": { 1081 | "version": "3.1.0", 1082 | "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", 1083 | "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", 1084 | "requires": { 1085 | "string-width": "^4.0.0" 1086 | } 1087 | }, 1088 | "wrap-ansi": { 1089 | "version": "7.0.0", 1090 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1091 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1092 | "requires": { 1093 | "ansi-styles": "^4.0.0", 1094 | "string-width": "^4.1.0", 1095 | "strip-ansi": "^6.0.0" 1096 | } 1097 | }, 1098 | "wrappy": { 1099 | "version": "1.0.2", 1100 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1101 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 1102 | }, 1103 | "write-file-atomic": { 1104 | "version": "3.0.3", 1105 | "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", 1106 | "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", 1107 | "requires": { 1108 | "imurmurhash": "^0.1.4", 1109 | "is-typedarray": "^1.0.0", 1110 | "signal-exit": "^3.0.2", 1111 | "typedarray-to-buffer": "^3.1.5" 1112 | } 1113 | }, 1114 | "xdg-basedir": { 1115 | "version": "4.0.0", 1116 | "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", 1117 | "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" 1118 | }, 1119 | "yallist": { 1120 | "version": "4.0.0", 1121 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 1122 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 1123 | } 1124 | } 1125 | } 1126 | -------------------------------------------------------------------------------- /type-reduce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "scripts": { 14 | "test": "jest", 15 | "start": "nodemon -e ts --ignore 'cleaned.ts' --ignore 'dist/' --exec \"yarn build && yarn out\"", 16 | "build": "rm -rf ./dist && rm -rf ../dist && tsc && node ./esbuild.js && mkdir -p ../dist && mv dist/* ../dist/", 17 | "tsc": "tsc", 18 | "out": "node ./dist/index.js" 19 | }, 20 | "peerDependencies": {}, 21 | "prettier": { 22 | "printWidth": 80, 23 | "semi": true, 24 | "singleQuote": true, 25 | "trailingComma": "es5" 26 | }, 27 | "name": "type-reduce", 28 | "author": "Zack Radisic", 29 | "devDependencies": { 30 | "@swc/core": "^1.2.102", 31 | "@swc/jest": "^0.2.5", 32 | "@types/jest": "^27.0.2", 33 | "@types/node": "^16.11.1", 34 | "husky": "^7.0.2", 35 | "jest": "^27.3.1", 36 | "ts-jest": "^27.0.7", 37 | "typescript": "^4.4.4" 38 | }, 39 | "dependencies": { 40 | "@ts-morph/bootstrap": "^0.11.0", 41 | "esbuild": "^0.13.8", 42 | "nodemon": "^2.0.14", 43 | "ts-morph": "^12.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /type-reduce/src/__tests__/reduce.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { Project } from 'ts-morph'; 3 | import { GraphQLType, TypeReducer } from '../lib'; 4 | import { createReducer as createReducer_ } from '../index'; 5 | 6 | describe('TypeReducer', () => { 7 | describe('First pass', () => { 8 | it('Collects exported declarations', () => { 9 | const reducer = createReducer(` 10 | import { Input } from './prelude' 11 | 12 | export type User = { firstName: string; age: number } 13 | export type FindUserInput = Input<{ firstName: string; }> 14 | export type Query = { findUser: (args: { input: FindUserInput }) => Promise } 15 | `); 16 | 17 | reducer.collectTypeNames(); 18 | 19 | expect(reducer.graphQlTypes).toEqual({ 20 | User: GraphQLType.Type, 21 | FindUserInput: GraphQLType.Input, 22 | Query: GraphQLType.Type, 23 | }); 24 | }); 25 | 26 | it('Acknowledges non-exported types', () => { 27 | const reducer = createReducer( 28 | ` 29 | import { Input } from './prelude' 30 | import { Person } from './person' 31 | 32 | export type User = Person 33 | export type FindUserInput = Input<{ firstName: string; }> 34 | export type Query = { findUser: (args: { input: FindUserInput }) => Promise } 35 | `, 36 | { 37 | name: './person.ts', 38 | src: 'export type Person = { firstName: string, age: number }', 39 | } 40 | ); 41 | 42 | reducer.collectTypeNames(); 43 | 44 | expect(reducer.graphQlTypes).toEqual({ 45 | User: GraphQLType.Type, 46 | FindUserInput: GraphQLType.Input, 47 | Query: GraphQLType.Type, 48 | }); 49 | 50 | expect(reducer.acknowledgedTypes).toEqual({ 51 | Person: true, 52 | Input: true, 53 | }); 54 | }); 55 | }); 56 | 57 | describe('Type reduction', () => { 58 | // TODO: Errors on non-nullable unions 59 | // For some reason TS's type checker will do this, so 60 | // we manually add it back 61 | it("Doesn't drop nullable unions", () => { 62 | const reducer = createReducer(` 63 | export type User = { name: string | null } 64 | export type Query = { findUser: (args: { name: string }) => Promise } 65 | `); 66 | 67 | const [output] = reduceTypes(reducer); 68 | expect(output).toEqual(`type User = { name: string | null; } 69 | type Query = { findUser: (args: { name: string; }) => Promise; } 70 | `); 71 | }); 72 | 73 | describe('Utility type expansion', () => { 74 | it('Expands utility types in declarations', () => { 75 | const reducer = createReducer(` 76 | export type User = { name: string | null, id: number, age: number } 77 | export type Person = Omit 78 | `); 79 | 80 | const [output] = reduceTypes(reducer); 81 | expect(output) 82 | .toEqual(`type User = { name: string | null; id: number; age: number; } 83 | type Person = { name: string | null; id: number; } 84 | `); 85 | }); 86 | 87 | it('Expands utility types as input', () => { 88 | const reducer = createReducer(` 89 | export type User = { name: string | null, id: number, age: number } 90 | export type Query = { findUser: (args: Partial) => Promise } 91 | `); 92 | 93 | const [output] = reduceTypes(reducer); 94 | expect(output) 95 | .toEqual(`type User = { name: string | null; id: number; age: number; } 96 | type Query = { findUser: (args: { name?: string | null | undefined; id?: number | undefined; age?: number | undefined; }) => Promise; } 97 | `); 98 | }); 99 | 100 | it('Expands utility types in input arg', () => { 101 | const reducer = createReducer(` 102 | export type User = { name: string | null, id: number, age: number } 103 | export type Query = { findUser: (args: { user: Partial }) => Promise } 104 | `); 105 | 106 | const [output] = reduceTypes(reducer); 107 | expect(output) 108 | .toEqual(`type User = { name: string | null; id: number; age: number; } 109 | type Query = { findUser: (args: { user: { name?: string | null | undefined; id?: number | undefined; age?: number | undefined; }; }) => Promise; } 110 | `); 111 | }); 112 | }); 113 | }); 114 | }); 115 | 116 | function createReducer( 117 | source: string, 118 | ...other: { name: string; src: string }[] 119 | ) { 120 | const reducer = createReducer_({ 121 | code: source, 122 | additional: other, 123 | test: true, 124 | }); 125 | 126 | return reducer; 127 | } 128 | 129 | function reduceTypes( 130 | reducer: TypeReducer 131 | ): ReturnType { 132 | const [output, manifest] = reducer.generate(); 133 | const project = new Project(); 134 | const sourceFile = project.createSourceFile('./index.ts', output); 135 | sourceFile.formatText(); 136 | 137 | return [sourceFile.getFullText(), manifest]; 138 | } 139 | -------------------------------------------------------------------------------- /type-reduce/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Project, SourceFile } from 'ts-morph'; 2 | import { TypeReducer } from './lib'; 3 | 4 | const injectedTypes = ` 5 | type ___ExpandRecursively = T extends object ? T extends infer O ? { 6 | [K in keyof O]: ___ExpandRecursively; 7 | } : never : T; 8 | type ___Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; 9 | `; 10 | 11 | const testPrelude = 'export type Input> = T;'; 12 | 13 | type Opts = { 14 | tsconfigPath?: string; 15 | path?: string; 16 | code?: string; 17 | additional?: { name: string; src: string }[]; 18 | test?: boolean 19 | }; 20 | 21 | export const createReducer = ({ 22 | tsconfigPath, 23 | path, 24 | code, 25 | additional, 26 | test, 27 | }: Opts) => { 28 | const project = new Project(test ? undefined : { tsConfigFilePath: tsconfigPath }); 29 | 30 | additional?.forEach(({ name, src: code }) => 31 | project.createSourceFile(name, code) 32 | ); 33 | 34 | if (test) { 35 | project.createSourceFile('./prelude.ts', testPrelude); 36 | } 37 | 38 | // With strict on Typescript turns optional types into unions: 39 | // optionalType?: string -> optionalType?: string | undefined 40 | project.compilerOptions.set({ strict: false, strictNullChecks: true }); 41 | 42 | let sourceFile: SourceFile | undefined; 43 | if (path) { 44 | sourceFile = project.getSourceFile(path); 45 | if (!sourceFile) { 46 | throw new Error('Schema file not found'); 47 | } 48 | } else if (code) { 49 | sourceFile = project.createSourceFile('index.ts', code); 50 | } 51 | 52 | if (!sourceFile) throw new Error('No schema input file'); 53 | 54 | sourceFile.insertText(0, injectedTypes); 55 | 56 | const diagnostics = project.getPreEmitDiagnostics(); 57 | if (diagnostics.length) { 58 | diagnostics.forEach(d => console.error(d.compilerObject.messageText)) 59 | throw new Error('Aborting because of TSC errors'); 60 | } 61 | 62 | return new TypeReducer(project, sourceFile); 63 | }; 64 | 65 | export * from './types'; 66 | -------------------------------------------------------------------------------- /type-reduce/src/lib.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Node, 3 | FunctionTypeNode, 4 | ParameterDeclaration, 5 | Project, 6 | PropertySignature, 7 | SourceFile, 8 | ts, 9 | Type, 10 | TypeAliasDeclaration, 11 | TypeChecker, 12 | TypeLiteralNode, 13 | TypeReferenceNode, 14 | } from 'ts-morph'; 15 | 16 | const DefaultFormatFlags = 17 | ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias; 18 | 19 | export enum GraphQLType { 20 | Type, 21 | Input, 22 | } 23 | 24 | const parentless = (node: T): Omit => { 25 | const { parent: _, ...rest } = node; 26 | return rest; 27 | }; 28 | 29 | export class TypeReducer { 30 | project: Project; 31 | sourceFile: SourceFile; 32 | checker: TypeChecker; 33 | 34 | graphQlTypes: Record; 35 | acknowledgedTypes: Record; 36 | 37 | expanded: Record; 38 | finalExpansions: string[]; 39 | 40 | constructor(project: Project, sourceFile: SourceFile) { 41 | this.project = project; 42 | this.sourceFile = sourceFile; 43 | this.checker = project.getTypeChecker(); 44 | 45 | this.graphQlTypes = {}; 46 | this.acknowledgedTypes = {}; 47 | 48 | this.expanded = {}; 49 | this.finalExpansions = []; 50 | } 51 | 52 | generate(): [output: string, manifest: Record] { 53 | this.collectTypeNames(); 54 | this.generateReducedTypes(); 55 | 56 | return [this.finalExpansions.join('\n'), this.graphQlTypes]; 57 | } 58 | 59 | addTestPreludeTypes() { 60 | this.project.createSourceFile( 61 | './prelude.ts', 62 | 'export type Input> = T;' 63 | ); 64 | } 65 | 66 | addAdditionalFile(name: string, code: string) { 67 | this.project.createSourceFile(name, code) 68 | } 69 | 70 | // First pass, collect type names 71 | collectTypeNames() { 72 | const exported = this.sourceFile.getExportedDeclarations(); 73 | for (const [name, [decl]] of exported) { 74 | switch (decl.getKind()) { 75 | case ts.SyntaxKind.TypeAliasDeclaration: { 76 | const { type } = (decl as TypeAliasDeclaration).getStructure(); 77 | if ((type as string).indexOf('Input<') === 0) { 78 | this.graphQlTypes[name] = GraphQLType.Input; 79 | } else { 80 | this.graphQlTypes[name] = GraphQLType.Type; 81 | } 82 | break; 83 | } 84 | default: { 85 | break; 86 | } 87 | } 88 | } 89 | 90 | const imported = this.sourceFile.getImportDeclarations(); 91 | for (const decl of imported) { 92 | for (const imp of decl.getNamedImports()) { 93 | this.acknowledgedTypes[imp.getName()] = true; 94 | } 95 | } 96 | } 97 | 98 | generateReducedTypes() { 99 | const exported = this.sourceFile.getExportedDeclarations(); 100 | for (const [name, [decl]] of exported) { 101 | switch (decl.getKind()) { 102 | case ts.SyntaxKind.TypeAliasDeclaration: { 103 | this.visitTypeAliasNode(decl as TypeAliasDeclaration); 104 | break; 105 | } 106 | default: { 107 | break; 108 | } 109 | } 110 | } 111 | } 112 | 113 | visitTypeAliasNode(node: TypeAliasDeclaration) { 114 | const name = node.getName(); 115 | if (name === 'Query' || name === 'Mutation') { 116 | this.visitQueryOrMutationDecl(node); 117 | } 118 | 119 | switch (node.getTypeNode()?.getKind()) { 120 | // Resolving type references 121 | case ts.SyntaxKind.TypeReference: { 122 | const name = node.getTypeNode()?.getText() || ''; 123 | // Don't do anything if it's a graphql type 124 | if (this.graphQlTypes[name] !== undefined) { 125 | break; 126 | } 127 | if (this.acknowledgedTypes[name]) { 128 | this.expandNode(node, false, true); 129 | } 130 | // If we haven't visited this type, it probably means it's a utility type like 131 | // Partial, Omit, etc. and we want to expand it 132 | break; 133 | } 134 | } 135 | 136 | this.expandNode(node); 137 | 138 | const ty = node.getType().compilerType; 139 | this.finalExpansions.push( 140 | `type ${name} = ${this.checker.compilerObject.typeToString( 141 | ty, 142 | // For some reason setting this param to undefined will make 143 | // this fn omit `undefined` in union, e.g. string | undefined -> string 144 | node.compilerNode, 145 | ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias 146 | )}` 147 | ); 148 | } 149 | 150 | visitQueryOrMutationDecl(node: TypeAliasDeclaration) { 151 | const tyNode = node.getTypeNode() as TypeLiteralNode; 152 | for (const member of tyNode.getMembers()) { 153 | if (member.getKind() !== ts.SyntaxKind.PropertySignature) { 154 | throw new Error('Type `Query` or `Mutation` can only have members'); 155 | } 156 | 157 | const propSig = member as PropertySignature; 158 | const tyNode = propSig.getTypeNode(); 159 | 160 | if (tyNode?.getKind() !== ts.SyntaxKind.FunctionType) { 161 | throw new Error('Type `Query` or `Mutation` can only have members'); 162 | } 163 | const fn = tyNode as FunctionTypeNode; 164 | 165 | const params = fn.getParameters(); 166 | if (params.length === 1) { 167 | this.expandParam(params[0]); 168 | } else { 169 | for (const param of params) { 170 | this.expandNode(param, false); 171 | } 172 | } 173 | 174 | const ret = fn.getReturnTypeNode(); 175 | if (ret) { 176 | if (ret.getKind() !== ts.SyntaxKind.TypeReference) { 177 | throw new Error( 178 | '`Query` or `Mutation` resolvers must return a Promise' 179 | ); 180 | } 181 | 182 | const retNode = ret as TypeReferenceNode; 183 | if (retNode.getTypeName().getText() !== 'Promise') { 184 | throw new Error( 185 | '`Query` or `Mutation` resolvers must return a Promise' 186 | ); 187 | } 188 | 189 | const [inner] = retNode.getTypeArguments(); 190 | 191 | if (retNode.getType().isIntersection()) { 192 | fn.setReturnType( 193 | `Promise<___Expand<${this.typeToString(inner.getType(), inner)}>>` 194 | ); 195 | } else { 196 | fn.setReturnType( 197 | `Promise<${this.typeToString( 198 | inner.getType(), 199 | inner, 200 | ts.TypeFormatFlags.NoTruncation 201 | )}>` 202 | ); 203 | } 204 | } 205 | } 206 | } 207 | 208 | expandNode( 209 | node: { 210 | getType(): Type; 211 | getStructure(): any; 212 | set(obj: Record): any; 213 | }, 214 | inAutoExpandableCtx = true, 215 | forceExpansion = false, 216 | enclosingNode?: Node 217 | ) { 218 | const ty = node.getType(); 219 | if (ty.isIntersection() || forceExpansion) { 220 | const { type } = node.getStructure(); 221 | node.set({ type: `___Expand<${type}>` }); 222 | } 223 | if (!inAutoExpandableCtx) { 224 | node.set({ 225 | type: this.typeToString(node.getType(), enclosingNode || (node as any)), 226 | }); 227 | } 228 | } 229 | 230 | expandParam(param: ParameterDeclaration) { 231 | const node = param.getTypeNode(); 232 | 233 | if (node instanceof TypeReferenceNode) { 234 | const graphqlTy = this.graphQlTypes[node.getTypeName().print()]; 235 | if (graphqlTy) { 236 | switch (graphqlTy) { 237 | case GraphQLType.Input: { 238 | this.expandNode(param, false); 239 | break; 240 | } 241 | default: { 242 | throw new Error('Field arguments can only be inputs'); 243 | } 244 | } 245 | } else { 246 | // We have a type constructed from type utilities: e.g. Partial 247 | this.expandNode(param, false); 248 | } 249 | } else if (node instanceof TypeLiteralNode) { 250 | for (const prop of node.getProperties()) { 251 | this.expandProperty(prop); 252 | } 253 | } 254 | } 255 | 256 | expandProperty(propSig: PropertySignature) { 257 | const tyNode = propSig.getTypeNode(); 258 | if (tyNode instanceof TypeReferenceNode) { 259 | const graphQlTy = this.graphQlTypes[tyNode.getTypeName().print()]; 260 | if (graphQlTy !== undefined) { 261 | switch (graphQlTy) { 262 | case GraphQLType.Input: { 263 | return; 264 | } 265 | default: { 266 | throw new Error('Field arguments can only be inputs'); 267 | } 268 | } 269 | } 270 | 271 | this.expandNode(propSig, false, true); 272 | return; 273 | } 274 | 275 | propSig.set({ 276 | type: this.typeToString(propSig.getType(), propSig), 277 | }); 278 | } 279 | 280 | typeToString( 281 | ty: Type, 282 | node?: Node, 283 | flags: ts.TypeFormatFlags = ts.TypeFormatFlags.NoTruncation | 284 | ts.TypeFormatFlags.InTypeAlias 285 | ): string { 286 | switch (node?.getKind()) { 287 | case ts.SyntaxKind.TypeReference: { 288 | const name = (node as TypeReferenceNode).getTypeName().getText(); 289 | if (this.graphQlTypes[name] !== undefined) { 290 | return name; 291 | } 292 | return this.checker.compilerObject.typeToString( 293 | ty.compilerType, 294 | node?.compilerNode, 295 | flags 296 | ); 297 | } 298 | default: { 299 | const f = this.checker.compilerObject.typeToString( 300 | ty.compilerType, 301 | node?.compilerNode, 302 | flags 303 | ); 304 | return f; 305 | } 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /type-reduce/src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Marker type to indicate that the type passed in the generic 4 | * parameter should generate a GraphQL Input type. 5 | * 6 | * Example: 7 | * ``` 8 | * export type CreateUserInput = Input<{ 9 | * name: string 10 | * }> 11 | * ``` 12 | * This will generate the following GraphQL Schema: 13 | * ```graphql 14 | * input CreateUserInput { 15 | * name: String! 16 | * } 17 | * ``` 18 | */ 19 | export type Input> = T; -------------------------------------------------------------------------------- /type-reduce/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "emitDeclarationOnly": true, 6 | "downlevelIteration": true, 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // interop between ESM and CJS modules. Recommended by TS 25 | "esModuleInterop": true, 26 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 27 | "skipLibCheck": true, 28 | // error out if import and file system have a casing mismatch. Recommended by TS 29 | "forceConsistentCasingInFileNames": true, 30 | "outDir": "dist/" 31 | } 32 | } 33 | --------------------------------------------------------------------------------