From 3cf22d6816c34ee64c8c5666a5ca7df3f7c4e0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allen=20Zhang=20=28=E5=BC=A0=E6=B6=9B=29?= Date: Mon, 15 Jan 2024 20:17:46 +0800 Subject: [PATCH] feat: init --- .editorconfig | 11 + .env.example | 19 + .github/workflows/build-canyon-cli.yml | 46 ++ .github/workflows/npm-publish.yml | 30 + .github/workflows/publish-docker-image.yml | 30 + .github/workflows/test.yml | 23 + .gitignore | 35 ++ CODE_OF_CONDUCT.md | 132 +++++ Dockerfile | 14 + LICENSE | 21 + README.md | 47 ++ SECURITY.md | 27 + examples/vite-react-swc/.eslintrc.cjs | 18 + examples/vite-react-swc/.gitignore | 24 + examples/vite-react-swc/README.md | 30 + examples/vite-react-swc/index.html | 13 + examples/vite-react-swc/package.json | 29 + examples/vite-react-swc/public/vite.svg | 1 + examples/vite-react-swc/src/App.css | 42 ++ examples/vite-react-swc/src/App.tsx | 35 ++ examples/vite-react-swc/src/assets/react.svg | 1 + examples/vite-react-swc/src/index.css | 68 +++ examples/vite-react-swc/src/main.tsx | 10 + examples/vite-react-swc/src/vite-env.d.ts | 1 + examples/vite-react-swc/tsconfig.json | 25 + examples/vite-react-swc/tsconfig.node.json | 10 + examples/vite-react-swc/vite.config.ts | 11 + examples/vite-vue/.gitignore | 24 + examples/vite-vue/README.md | 18 + examples/vite-vue/index.html | 13 + examples/vite-vue/package.json | 21 + examples/vite-vue/public/vite.svg | 1 + examples/vite-vue/src/App.vue | 30 + examples/vite-vue/src/assets/vue.svg | 1 + .../vite-vue/src/components/HelloWorld.vue | 38 ++ examples/vite-vue/src/main.ts | 5 + examples/vite-vue/src/style.css | 79 +++ examples/vite-vue/src/vite-env.d.ts | 1 + examples/vite-vue/tsconfig.json | 25 + examples/vite-vue/tsconfig.node.json | 10 + examples/vite-vue/vite.config.ts | 12 + examples/webpack-babel-ts/.gitignore | 37 ++ examples/webpack-babel-ts/babel.config.js | 3 + examples/webpack-babel-ts/index.html | 14 + examples/webpack-babel-ts/package.json | 20 + examples/webpack-babel-ts/src/main.ts | 7 + examples/webpack-babel-ts/tsconfig.json | 12 + examples/webpack-babel-ts/webpack.config.js | 19 + package.json | 17 + packages/canyon-backend/.eslintrc.js | 25 + packages/canyon-backend/.gitignore | 35 ++ packages/canyon-backend/.prettierrc | 4 + packages/canyon-backend/README.md | 0 packages/canyon-backend/nest-cli.json | 8 + packages/canyon-backend/package.json | 81 +++ .../20240108141744_update/migration.sql | 88 +++ .../20240109115732_update/migration.sql | 36 ++ .../20240109122436_upda/migration.sql | 8 + .../20240110054935_update/migration.sql | 30 + .../prisma/migrations/migration_lock.toml | 3 + packages/canyon-backend/prisma/schema.prisma | 93 +++ .../src/adapter/coverage-data.adapter.ts | 24 + .../canyon-backend/src/app.controller.spec.ts | 22 + packages/canyon-backend/src/app.controller.ts | 4 + packages/canyon-backend/src/app.module.ts | 22 + packages/canyon-backend/src/app.service.ts | 8 + .../src/coverage/coverage.controller.ts | 41 ++ .../src/coverage/coverage.module.ts | 18 + .../src/coverage/dto/coverage-client.dto.ts | 58 ++ .../services/coverage-client.service.ts | 32 ++ .../retrieve-coverage-summary.service.ts | 24 + .../src/coverage/valids/is-valid-coverage.ts | 55 ++ packages/canyon-backend/src/main.ts | 17 + .../src/prisma/prisma.module.ts | 8 + .../src/prisma/prisma.service.ts | 19 + packages/canyon-backend/src/utils/diffline.ts | 182 ++++++ packages/canyon-backend/src/utils/utils.ts | 43 ++ packages/canyon-backend/test/app.e2e-spec.ts | 24 + packages/canyon-backend/test/jest-e2e.json | 9 + packages/canyon-backend/tsconfig.build.json | 4 + packages/canyon-backend/tsconfig.json | 21 + packages/canyon-cli/.eslintignore | 2 + packages/canyon-cli/.eslintrc.js | 18 + packages/canyon-cli/.gitignore | 79 +++ packages/canyon-cli/.prettierignore | 1 + packages/canyon-cli/.prettierrc | 14 + packages/canyon-cli/README.md | 7 + packages/canyon-cli/bin/canyon.ts | 33 ++ packages/canyon-cli/jest.config.js | 15 + packages/canyon-cli/package.json | 56 ++ packages/canyon-cli/src/ci_providers/index.ts | 14 + .../src/ci_providers/provider_gitlabci.ts | 83 +++ .../src/ci_providers/provider_jenkinsci.ts | 87 +++ .../src/ci_providers/provider_local.ts | 101 ++++ .../src/ci_providers/provider_template.ts | 154 +++++ packages/canyon-cli/src/helpers/cli.ts | 259 +++++++++ packages/canyon-cli/src/helpers/constants.ts | 3 + packages/canyon-cli/src/helpers/coveragepy.ts | 25 + packages/canyon-cli/src/helpers/files.ts | 369 ++++++++++++ packages/canyon-cli/src/helpers/fixes.ts | 85 +++ packages/canyon-cli/src/helpers/gcov.ts | 22 + packages/canyon-cli/src/helpers/git.ts | 38 ++ packages/canyon-cli/src/helpers/logger.ts | 64 +++ packages/canyon-cli/src/helpers/provider.ts | 72 +++ packages/canyon-cli/src/helpers/proxy.ts | 37 ++ packages/canyon-cli/src/helpers/swift.ts | 103 ++++ packages/canyon-cli/src/helpers/token.ts | 111 ++++ packages/canyon-cli/src/helpers/util.ts | 36 ++ packages/canyon-cli/src/helpers/validate.ts | 55 ++ packages/canyon-cli/src/helpers/web.ts | 251 ++++++++ packages/canyon-cli/src/helpers/xcode.ts | 63 ++ packages/canyon-cli/src/index.ts | 469 +++++++++++++++ packages/canyon-cli/src/types.ts | 90 +++ packages/canyon-cli/tsconfig.json | 18 + packages/canyon-data/.gitignore | 24 + packages/canyon-data/README.md | 0 packages/canyon-data/package.json | 30 + packages/canyon-data/src/coverage/index.ts | 12 + .../src/coverage/test/coverage.test.ts | 8 + .../test/mock-coverage-data-merged.json | 536 ++++++++++++++++++ .../src/coverage/test/mock-coverage-data.json | 536 ++++++++++++++++++ packages/canyon-data/src/index.ts | 2 + packages/canyon-data/src/summary/helpers.ts | 32 ++ packages/canyon-data/src/summary/index.ts | 133 +++++ packages/canyon-data/src/utils/line.ts | 39 ++ packages/canyon-data/src/utils/percent.ts | 9 + packages/canyon-data/tsconfig.json | 23 + packages/canyon-data/vite.config.ts | 16 + packages/canyon-platform/.eslintignore | 5 + packages/canyon-platform/.eslintrc | 75 +++ packages/canyon-platform/.gitignore | 28 + packages/canyon-platform/.prettierrc | 12 + packages/canyon-platform/README.md | 30 + packages/canyon-platform/codegen.ts | 17 + packages/canyon-platform/index.html | 13 + packages/canyon-platform/languages.json | 32 ++ packages/canyon-platform/locales/cn.json | 28 + packages/canyon-platform/locales/en.json | 28 + packages/canyon-platform/locales/ja.json | 28 + packages/canyon-platform/locales/ko.json | 28 + packages/canyon-platform/locales/tw.json | 28 + packages/canyon-platform/package.json | 55 ++ packages/canyon-platform/postcss.config.js | 6 + packages/canyon-platform/public/vite.svg | 1 + packages/canyon-platform/src/App.tsx | 20 + .../canyon-platform/src/ScrollBasedLayout.tsx | 68 +++ packages/canyon-platform/src/assets/logo.svg | 1 + packages/canyon-platform/src/assets/react.svg | 1 + .../src/components/app/footer.tsx | 47 ++ .../src/helpers/backend/GQLClient.ts | 12 + .../backend/gql/queries/GetProjects.graphql | 10 + .../helpers/backend/gql/queries/Me.graphql | 13 + .../src/helpers/backend/types/Email.ts | 16 + .../src/helpers/backend/types/TeamName.ts | 13 + packages/canyon-platform/src/i18n.ts | 47 ++ packages/canyon-platform/src/index.css | 16 + packages/canyon-platform/src/main.tsx | 31 + packages/canyon-platform/src/pages/index.tsx | 177 ++++++ .../index/projects/[id]/commits/[sha].tsx | 5 + .../src/pages/index/projects/[id]/index.tsx | 165 ++++++ .../src/pages/index/projects/index.tsx | 90 +++ .../src/pages/index/settings/index.tsx | 78 +++ .../canyon-platform/src/pages/welcome.tsx | 5 + packages/canyon-platform/src/vite-env.d.ts | 4 + packages/canyon-platform/tailwind.config.js | 14 + packages/canyon-platform/tsconfig.json | 25 + packages/canyon-platform/tsconfig.node.json | 10 + packages/canyon-platform/vite.config.ts | 30 + packages/canyon-report/.eslintrc | 69 +++ packages/canyon-report/.gitignore | 24 + packages/canyon-report/README.md | 9 + packages/canyon-report/demo/index.html | 18 + packages/canyon-report/index.html | 14 + packages/canyon-report/package.json | 38 ++ .../src/Report/components/Code.tsx | 42 ++ .../src/Report/components/IstanbulReport.tsx | 164 ++++++ .../src/Report/components/Line.tsx | 56 ++ .../src/Report/components/Mask.tsx | 162 ++++++ .../src/Report/components/Th.tsx | 91 +++ .../src/Report/components/Tr.tsx | 90 +++ packages/canyon-report/src/Report/index.ts | 5 + packages/canyon-report/src/Report/init.tsx | 34 ++ packages/canyon-report/src/Report/loadcss.ts | 527 +++++++++++++++++ packages/canyon-report/src/Report/types.ts | 44 ++ packages/canyon-report/src/helper.ts | 112 ++++ packages/canyon-report/src/index.ts | 5 + packages/canyon-report/src/main.tsx | 34 ++ packages/canyon-report/src/mock.ts | 535 +++++++++++++++++ packages/canyon-report/src/reset.css | 253 +++++++++ packages/canyon-report/tsconfig.json | 25 + packages/canyon-report/vite.config.ts | 26 + pnpm-workspace.yaml | 4 + screenshots/ci1.jpg | Bin 0 -> 1132448 bytes screenshots/codechange1.jpg | Bin 0 -> 609510 bytes screenshots/demo1.jpg | Bin 0 -> 547178 bytes screenshots/overview1.jpg | Bin 0 -> 800138 bytes screenshots/report1.jpg | Bin 0 -> 696885 bytes scripts/check.js | 19 + 198 files changed, 10152 insertions(+) create mode 100755 .editorconfig create mode 100755 .env.example create mode 100644 .github/workflows/build-canyon-cli.yml create mode 100755 .github/workflows/npm-publish.yml create mode 100755 .github/workflows/publish-docker-image.yml create mode 100755 .github/workflows/test.yml create mode 100755 .gitignore create mode 100755 CODE_OF_CONDUCT.md create mode 100755 Dockerfile create mode 100755 LICENSE create mode 100755 README.md create mode 100755 SECURITY.md create mode 100644 examples/vite-react-swc/.eslintrc.cjs create mode 100644 examples/vite-react-swc/.gitignore create mode 100644 examples/vite-react-swc/README.md create mode 100644 examples/vite-react-swc/index.html create mode 100644 examples/vite-react-swc/package.json create mode 100644 examples/vite-react-swc/public/vite.svg create mode 100644 examples/vite-react-swc/src/App.css create mode 100644 examples/vite-react-swc/src/App.tsx create mode 100644 examples/vite-react-swc/src/assets/react.svg create mode 100644 examples/vite-react-swc/src/index.css create mode 100644 examples/vite-react-swc/src/main.tsx create mode 100644 examples/vite-react-swc/src/vite-env.d.ts create mode 100644 examples/vite-react-swc/tsconfig.json create mode 100644 examples/vite-react-swc/tsconfig.node.json create mode 100644 examples/vite-react-swc/vite.config.ts create mode 100644 examples/vite-vue/.gitignore create mode 100644 examples/vite-vue/README.md create mode 100644 examples/vite-vue/index.html create mode 100644 examples/vite-vue/package.json create mode 100644 examples/vite-vue/public/vite.svg create mode 100644 examples/vite-vue/src/App.vue create mode 100644 examples/vite-vue/src/assets/vue.svg create mode 100644 examples/vite-vue/src/components/HelloWorld.vue create mode 100644 examples/vite-vue/src/main.ts create mode 100644 examples/vite-vue/src/style.css create mode 100644 examples/vite-vue/src/vite-env.d.ts create mode 100644 examples/vite-vue/tsconfig.json create mode 100644 examples/vite-vue/tsconfig.node.json create mode 100644 examples/vite-vue/vite.config.ts create mode 100755 examples/webpack-babel-ts/.gitignore create mode 100644 examples/webpack-babel-ts/babel.config.js create mode 100644 examples/webpack-babel-ts/index.html create mode 100644 examples/webpack-babel-ts/package.json create mode 100644 examples/webpack-babel-ts/src/main.ts create mode 100644 examples/webpack-babel-ts/tsconfig.json create mode 100644 examples/webpack-babel-ts/webpack.config.js create mode 100755 package.json create mode 100755 packages/canyon-backend/.eslintrc.js create mode 100755 packages/canyon-backend/.gitignore create mode 100755 packages/canyon-backend/.prettierrc create mode 100755 packages/canyon-backend/README.md create mode 100755 packages/canyon-backend/nest-cli.json create mode 100755 packages/canyon-backend/package.json create mode 100755 packages/canyon-backend/prisma/migrations/20240108141744_update/migration.sql create mode 100755 packages/canyon-backend/prisma/migrations/20240109115732_update/migration.sql create mode 100755 packages/canyon-backend/prisma/migrations/20240109122436_upda/migration.sql create mode 100755 packages/canyon-backend/prisma/migrations/20240110054935_update/migration.sql create mode 100755 packages/canyon-backend/prisma/migrations/migration_lock.toml create mode 100755 packages/canyon-backend/prisma/schema.prisma create mode 100755 packages/canyon-backend/src/adapter/coverage-data.adapter.ts create mode 100755 packages/canyon-backend/src/app.controller.spec.ts create mode 100755 packages/canyon-backend/src/app.controller.ts create mode 100755 packages/canyon-backend/src/app.module.ts create mode 100755 packages/canyon-backend/src/app.service.ts create mode 100755 packages/canyon-backend/src/coverage/coverage.controller.ts create mode 100755 packages/canyon-backend/src/coverage/coverage.module.ts create mode 100755 packages/canyon-backend/src/coverage/dto/coverage-client.dto.ts create mode 100755 packages/canyon-backend/src/coverage/services/coverage-client.service.ts create mode 100755 packages/canyon-backend/src/coverage/services/retrieve-coverage-summary.service.ts create mode 100755 packages/canyon-backend/src/coverage/valids/is-valid-coverage.ts create mode 100755 packages/canyon-backend/src/main.ts create mode 100755 packages/canyon-backend/src/prisma/prisma.module.ts create mode 100755 packages/canyon-backend/src/prisma/prisma.service.ts create mode 100755 packages/canyon-backend/src/utils/diffline.ts create mode 100755 packages/canyon-backend/src/utils/utils.ts create mode 100755 packages/canyon-backend/test/app.e2e-spec.ts create mode 100755 packages/canyon-backend/test/jest-e2e.json create mode 100755 packages/canyon-backend/tsconfig.build.json create mode 100755 packages/canyon-backend/tsconfig.json create mode 100755 packages/canyon-cli/.eslintignore create mode 100755 packages/canyon-cli/.eslintrc.js create mode 100755 packages/canyon-cli/.gitignore create mode 100755 packages/canyon-cli/.prettierignore create mode 100755 packages/canyon-cli/.prettierrc create mode 100755 packages/canyon-cli/README.md create mode 100755 packages/canyon-cli/bin/canyon.ts create mode 100755 packages/canyon-cli/jest.config.js create mode 100755 packages/canyon-cli/package.json create mode 100755 packages/canyon-cli/src/ci_providers/index.ts create mode 100755 packages/canyon-cli/src/ci_providers/provider_gitlabci.ts create mode 100755 packages/canyon-cli/src/ci_providers/provider_jenkinsci.ts create mode 100755 packages/canyon-cli/src/ci_providers/provider_local.ts create mode 100755 packages/canyon-cli/src/ci_providers/provider_template.ts create mode 100755 packages/canyon-cli/src/helpers/cli.ts create mode 100755 packages/canyon-cli/src/helpers/constants.ts create mode 100755 packages/canyon-cli/src/helpers/coveragepy.ts create mode 100755 packages/canyon-cli/src/helpers/files.ts create mode 100755 packages/canyon-cli/src/helpers/fixes.ts create mode 100755 packages/canyon-cli/src/helpers/gcov.ts create mode 100755 packages/canyon-cli/src/helpers/git.ts create mode 100755 packages/canyon-cli/src/helpers/logger.ts create mode 100755 packages/canyon-cli/src/helpers/provider.ts create mode 100755 packages/canyon-cli/src/helpers/proxy.ts create mode 100755 packages/canyon-cli/src/helpers/swift.ts create mode 100755 packages/canyon-cli/src/helpers/token.ts create mode 100755 packages/canyon-cli/src/helpers/util.ts create mode 100755 packages/canyon-cli/src/helpers/validate.ts create mode 100755 packages/canyon-cli/src/helpers/web.ts create mode 100755 packages/canyon-cli/src/helpers/xcode.ts create mode 100755 packages/canyon-cli/src/index.ts create mode 100755 packages/canyon-cli/src/types.ts create mode 100755 packages/canyon-cli/tsconfig.json create mode 100755 packages/canyon-data/.gitignore create mode 100755 packages/canyon-data/README.md create mode 100755 packages/canyon-data/package.json create mode 100755 packages/canyon-data/src/coverage/index.ts create mode 100755 packages/canyon-data/src/coverage/test/coverage.test.ts create mode 100755 packages/canyon-data/src/coverage/test/mock-coverage-data-merged.json create mode 100755 packages/canyon-data/src/coverage/test/mock-coverage-data.json create mode 100755 packages/canyon-data/src/index.ts create mode 100755 packages/canyon-data/src/summary/helpers.ts create mode 100755 packages/canyon-data/src/summary/index.ts create mode 100755 packages/canyon-data/src/utils/line.ts create mode 100755 packages/canyon-data/src/utils/percent.ts create mode 100755 packages/canyon-data/tsconfig.json create mode 100755 packages/canyon-data/vite.config.ts create mode 100755 packages/canyon-platform/.eslintignore create mode 100755 packages/canyon-platform/.eslintrc create mode 100755 packages/canyon-platform/.gitignore create mode 100755 packages/canyon-platform/.prettierrc create mode 100644 packages/canyon-platform/README.md create mode 100755 packages/canyon-platform/codegen.ts create mode 100644 packages/canyon-platform/index.html create mode 100644 packages/canyon-platform/languages.json create mode 100644 packages/canyon-platform/locales/cn.json create mode 100644 packages/canyon-platform/locales/en.json create mode 100644 packages/canyon-platform/locales/ja.json create mode 100644 packages/canyon-platform/locales/ko.json create mode 100644 packages/canyon-platform/locales/tw.json create mode 100644 packages/canyon-platform/package.json create mode 100755 packages/canyon-platform/postcss.config.js create mode 100644 packages/canyon-platform/public/vite.svg create mode 100644 packages/canyon-platform/src/App.tsx create mode 100644 packages/canyon-platform/src/ScrollBasedLayout.tsx create mode 100755 packages/canyon-platform/src/assets/logo.svg create mode 100644 packages/canyon-platform/src/assets/react.svg create mode 100644 packages/canyon-platform/src/components/app/footer.tsx create mode 100755 packages/canyon-platform/src/helpers/backend/GQLClient.ts create mode 100755 packages/canyon-platform/src/helpers/backend/gql/queries/GetProjects.graphql create mode 100755 packages/canyon-platform/src/helpers/backend/gql/queries/Me.graphql create mode 100755 packages/canyon-platform/src/helpers/backend/types/Email.ts create mode 100755 packages/canyon-platform/src/helpers/backend/types/TeamName.ts create mode 100644 packages/canyon-platform/src/i18n.ts create mode 100644 packages/canyon-platform/src/index.css create mode 100644 packages/canyon-platform/src/main.tsx create mode 100644 packages/canyon-platform/src/pages/index.tsx create mode 100755 packages/canyon-platform/src/pages/index/projects/[id]/commits/[sha].tsx create mode 100755 packages/canyon-platform/src/pages/index/projects/[id]/index.tsx create mode 100755 packages/canyon-platform/src/pages/index/projects/index.tsx create mode 100644 packages/canyon-platform/src/pages/index/settings/index.tsx create mode 100644 packages/canyon-platform/src/pages/welcome.tsx create mode 100644 packages/canyon-platform/src/vite-env.d.ts create mode 100755 packages/canyon-platform/tailwind.config.js create mode 100644 packages/canyon-platform/tsconfig.json create mode 100644 packages/canyon-platform/tsconfig.node.json create mode 100644 packages/canyon-platform/vite.config.ts create mode 100755 packages/canyon-report/.eslintrc create mode 100755 packages/canyon-report/.gitignore create mode 100755 packages/canyon-report/README.md create mode 100755 packages/canyon-report/demo/index.html create mode 100755 packages/canyon-report/index.html create mode 100755 packages/canyon-report/package.json create mode 100755 packages/canyon-report/src/Report/components/Code.tsx create mode 100755 packages/canyon-report/src/Report/components/IstanbulReport.tsx create mode 100755 packages/canyon-report/src/Report/components/Line.tsx create mode 100755 packages/canyon-report/src/Report/components/Mask.tsx create mode 100755 packages/canyon-report/src/Report/components/Th.tsx create mode 100755 packages/canyon-report/src/Report/components/Tr.tsx create mode 100755 packages/canyon-report/src/Report/index.ts create mode 100755 packages/canyon-report/src/Report/init.tsx create mode 100755 packages/canyon-report/src/Report/loadcss.ts create mode 100755 packages/canyon-report/src/Report/types.ts create mode 100755 packages/canyon-report/src/helper.ts create mode 100755 packages/canyon-report/src/index.ts create mode 100755 packages/canyon-report/src/main.tsx create mode 100755 packages/canyon-report/src/mock.ts create mode 100755 packages/canyon-report/src/reset.css create mode 100755 packages/canyon-report/tsconfig.json create mode 100755 packages/canyon-report/vite.config.ts create mode 100755 pnpm-workspace.yaml create mode 100755 screenshots/ci1.jpg create mode 100755 screenshots/codechange1.jpg create mode 100755 screenshots/demo1.jpg create mode 100755 screenshots/overview1.jpg create mode 100755 screenshots/report1.jpg create mode 100755 scripts/check.js diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 00000000..a1c0c006 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.env.example b/.env.example new file mode 100755 index 00000000..26bc4823 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +#-----------------------System Config------------------------------# +SYSTEM_QUESTION_LINK=https://github.com/canyon-project/canyon + +#-----------------------Backend Config------------------------------# +# Prisma Config +DATABASE_URL=postgress://canyon:canyon@localhost:5432/canyon + +# Gitlab Config +GITLAB_URL="***" +GITLAB_CLIENT_ID="***" +GITLAB_CLIENT_SECRET="***" +COVERAGE_DATA_URL=http://s3.xxx +REDIRECT_URI=http://localhost:3000/login + +APP_URI=http://localhost:3000 +UPLOAD_URL=http://localhost:3000 + + +PRIVATE_TOKEN=*** diff --git a/.github/workflows/build-canyon-cli.yml b/.github/workflows/build-canyon-cli.yml new file mode 100644 index 00000000..f6c1e710 --- /dev/null +++ b/.github/workflows/build-canyon-cli.yml @@ -0,0 +1,46 @@ +name: Node.js CI + +on: + push: + branches: [ "dev" ] + tags: + - "v*.*.*" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v2 + with: + version: 8 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '18.x' + - run: pnpm i + - run: pnpm --filter canyon-cli exec -- npm run build + - run: pnpm --filter canyon-cli exec -- npm run build-linux + - uses: actions/upload-artifact@v3 + with: + name: Build + path: packages/canyon-cli/out + + release: + runs-on: ubuntu-latest + permissions: + contents: write + + needs: build + + steps: + - uses: actions/download-artifact@v3 + with: + name: Build + path: packages/canyon-cli/out + - run: mv packages/canyon-cli/out build + - run: zip -r canyon-cli.zip build/ + - uses: ncipollo/release-action@v1 + with: + artifacts: "canyon-cli.zip" diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100755 index 00000000..45361826 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,30 @@ +name: Npm Publish + +on: + push: + branches: [ dev ] + +jobs: + install: + runs-on: ubuntu-latest + steps: + - uses: pnpm/action-setup@v2 + with: + version: 8 + + publish-npm: + needs: [install] + runs-on: ubuntu-latest + steps: + - uses: pnpm/action-setup@v2 + with: + version: 8 + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 18 + registry-url: https://registry.npmjs.org/ + - run: pnpm install + - run: pnpm --filter=canyon-cli publish -f --no-git-checks --access=public --filter + env: + NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}} diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml new file mode 100755 index 00000000..7b4a8168 --- /dev/null +++ b/.github/workflows/publish-docker-image.yml @@ -0,0 +1,30 @@ +name: push docker image + +on: + push: + branches: + - 'dev' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v5 + with: + platforms: linux/amd64, linux/arm64 + push: true + tags: zhangtao25/canyon:dev diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100755 index 00000000..2b265f71 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: ci + +on: + push: + branches: + - 'dev' + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v2 + with: + version: 8 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '20.x' + - run: pnpm install + - run: pnpm test diff --git a/.gitignore b/.gitignore new file mode 100755 index 00000000..6daa74da --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# IDE / Editor +.idea/* +!.idea/rcb-settings.xml + +# PNPM +.pnpm-store + + +pnpm-lock.yaml + +.env \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100755 index 00000000..661609dd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +wr_zhang25@163.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 00000000..cb77a0f5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:lts + +MAINTAINER wr_zhang25 + +RUN mkdir -p /app +COPY . /app/ +WORKDIR /app + +RUN npm install pnpm -g +RUN pnpm i +RUN pnpm run build + +EXPOSE 8080 +CMD ["node", "packages/canyon-backend/dist/main.js" ] diff --git a/LICENSE b/LICENSE new file mode 100755 index 00000000..bff8f57b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Canyon Platforms, Inc. and affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100755 index 00000000..9c1d8389 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Canyon [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/canyon-project/canyon/blob/main/LICENSE) [![build status](https://github.com/canyon-project/canyon/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/canyon-project/canyon/actions/workflows/ci.yml) + +👋 Canyon is a JavaScript code coverage solution + +![](./screenshots/overview1.jpg) + +### **Introduction** + +JavaScript 代码覆盖率解决方案。在前端工程构建阶段,将代码探针及工程信息插桩进产物中;在前端应用运行时,将内存中的覆盖率数据上报至服务端。支持覆盖率聚合、报告生成、新增行覆盖率。 + +主要用于端到端的UI测试用例覆盖率数据收集(也支持node或者UT覆盖率上报收集)。 + +### **Features** + +**Bundling:** 多种生态的bundling方案。 + +- `vite` - vite-plugin-istanbul +- `babel` - babel-plugin-istanbul +- `swc` - swc-coverage-instrument + +**React Native:** 支持React Native覆盖率数据收集。 + +**File:** 支持多种文件类型,例如js、jsx、ts、tsx。 + +**源码回溯:** 开启sourceMap选项来回溯源码覆盖率信息。 + +**CI** 提供覆盖率接口,方便CI工具集成。 + +**变更代码:** 通过配置想要对比的基线Commit Sha或者分支名,过滤筛选出变更代码文件的覆盖率以及计算出整体新增代码行覆盖率。 + +**Commit:** 使用oauth2登陆与github、gitlab等代码仓库链接,根据Commit Sha拉取代码进行覆盖率详情绘制。 + +**聚合:** 根据上报的报告ID进行实时聚合覆盖率数据聚合。 + +**报告组件:** 构建最小原生JavasScript的npm包,提供现代化前端报告水合方案以代替传统istanbul report。 + +**浏览器插件:** 提供浏览器插件,供开发人员实时检测应用覆盖率详情。 + +### Screenshots + +![报告](./screenshots/report1.jpg) + +![覆盖率](./screenshots/demo1.jpg) + +![新增行](./screenshots/codechange1.jpg) + +![配置](./screenshots/ci1.jpg) diff --git a/SECURITY.md b/SECURITY.md new file mode 100755 index 00000000..68b9ea83 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +This document outlines security procedures and general policies for the Canyon project. + +- [Security Policy](#security-policy) + - [Reporting a security vulnerability](#reporting-a-security-vulnerability) + - [Incident response process](#incident-response-process) + +## Reporting a security vulnerability + +Report security vulnerabilities by emailing the Canyon Support team at wr_zhang25@163.com. + +The primary security point of contact from Canyon Support team will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. + +**Do not create a GitHub issue ticket to report a security vulnerability.** + +The Canyon team and community take all security vulnerability reports in Canyon seriously. Thank you for improving the security of Canyon. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. + +Report security bugs in third-party modules to the person or team maintaining the module. + +## Incident response process + +In case an incident is discovered or reported, we will follow the following process to contain, respond, and remediate: + +1. Confirm the problem and determine the affected versions. +2. Audit code to find any potential similar problems. +3. Prepare fixes for all releases still under maintenance. These fixes will be deployed as fast as possible to production. diff --git a/examples/vite-react-swc/.eslintrc.cjs b/examples/vite-react-swc/.eslintrc.cjs new file mode 100644 index 00000000..d6c95379 --- /dev/null +++ b/examples/vite-react-swc/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/examples/vite-react-swc/.gitignore b/examples/vite-react-swc/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/examples/vite-react-swc/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/vite-react-swc/README.md b/examples/vite-react-swc/README.md new file mode 100644 index 00000000..0d6babed --- /dev/null +++ b/examples/vite-react-swc/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/examples/vite-react-swc/index.html b/examples/vite-react-swc/index.html new file mode 100644 index 00000000..e4b78eae --- /dev/null +++ b/examples/vite-react-swc/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/examples/vite-react-swc/package.json b/examples/vite-react-swc/package.json new file mode 100644 index 00000000..b91e087e --- /dev/null +++ b/examples/vite-react-swc/package.json @@ -0,0 +1,29 @@ +{ + "name": "vite-react-swc", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "swc-plugin-coverage-instrument": "^0.0.20" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} diff --git a/examples/vite-react-swc/public/vite.svg b/examples/vite-react-swc/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/examples/vite-react-swc/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/vite-react-swc/src/App.css b/examples/vite-react-swc/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/examples/vite-react-swc/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/examples/vite-react-swc/src/App.tsx b/examples/vite-react-swc/src/App.tsx new file mode 100644 index 00000000..afe48ac7 --- /dev/null +++ b/examples/vite-react-swc/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/examples/vite-react-swc/src/assets/react.svg b/examples/vite-react-swc/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/examples/vite-react-swc/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/vite-react-swc/src/index.css b/examples/vite-react-swc/src/index.css new file mode 100644 index 00000000..6119ad9a --- /dev/null +++ b/examples/vite-react-swc/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/vite-react-swc/src/main.tsx b/examples/vite-react-swc/src/main.tsx new file mode 100644 index 00000000..3d7150da --- /dev/null +++ b/examples/vite-react-swc/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/examples/vite-react-swc/src/vite-env.d.ts b/examples/vite-react-swc/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/vite-react-swc/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/vite-react-swc/tsconfig.json b/examples/vite-react-swc/tsconfig.json new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/examples/vite-react-swc/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/vite-react-swc/tsconfig.node.json b/examples/vite-react-swc/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/examples/vite-react-swc/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/vite-react-swc/vite.config.ts b/examples/vite-react-swc/vite.config.ts new file mode 100644 index 00000000..dce8fce5 --- /dev/null +++ b/examples/vite-react-swc/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react({ + plugins:[ + ["swc-plugin-coverage-instrument",{}] + ] + })], +}) diff --git a/examples/vite-vue/.gitignore b/examples/vite-vue/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/examples/vite-vue/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/vite-vue/README.md b/examples/vite-vue/README.md new file mode 100644 index 00000000..ef72fd52 --- /dev/null +++ b/examples/vite-vue/README.md @@ -0,0 +1,18 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/examples/vite-vue/package.json b/examples/vite-vue/package.json new file mode 100644 index 00000000..ae5a99d1 --- /dev/null +++ b/examples/vite-vue/package.json @@ -0,0 +1,21 @@ +{ + "name": "vite-vue", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.3.11" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.2", + "typescript": "^5.2.2", + "vite": "^5.0.8", + "vite-plugin-istanbul": "^5.0.0", + "vue-tsc": "^1.8.25" + } +} diff --git a/examples/vite-vue/public/vite.svg b/examples/vite-vue/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/examples/vite-vue/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/vite-vue/src/App.vue b/examples/vite-vue/src/App.vue new file mode 100644 index 00000000..bb666a8d --- /dev/null +++ b/examples/vite-vue/src/App.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/examples/vite-vue/src/assets/vue.svg b/examples/vite-vue/src/assets/vue.svg new file mode 100644 index 00000000..770e9d33 --- /dev/null +++ b/examples/vite-vue/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/vite-vue/src/components/HelloWorld.vue b/examples/vite-vue/src/components/HelloWorld.vue new file mode 100644 index 00000000..7b25f3f2 --- /dev/null +++ b/examples/vite-vue/src/components/HelloWorld.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/examples/vite-vue/src/main.ts b/examples/vite-vue/src/main.ts new file mode 100644 index 00000000..2425c0f7 --- /dev/null +++ b/examples/vite-vue/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/examples/vite-vue/src/style.css b/examples/vite-vue/src/style.css new file mode 100644 index 00000000..bb131d6b --- /dev/null +++ b/examples/vite-vue/src/style.css @@ -0,0 +1,79 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/vite-vue/src/vite-env.d.ts b/examples/vite-vue/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/vite-vue/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/vite-vue/tsconfig.json b/examples/vite-vue/tsconfig.json new file mode 100644 index 00000000..9e03e604 --- /dev/null +++ b/examples/vite-vue/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/vite-vue/tsconfig.node.json b/examples/vite-vue/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/examples/vite-vue/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/vite-vue/vite.config.ts b/examples/vite-vue/vite.config.ts new file mode 100644 index 00000000..b3dd936a --- /dev/null +++ b/examples/vite-vue/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import istanbul from 'vite-plugin-istanbul' +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + istanbul({ + forceBuildInstrument:true + }), + ], +}) diff --git a/examples/webpack-babel-ts/.gitignore b/examples/webpack-babel-ts/.gitignore new file mode 100755 index 00000000..484b52db --- /dev/null +++ b/examples/webpack-babel-ts/.gitignore @@ -0,0 +1,37 @@ +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# IDE / Editor +.idea/* +!.idea/rcb-settings.xml + +# PNPM +.pnpm-store + + +pnpm-lock.yaml + +.env + +build diff --git a/examples/webpack-babel-ts/babel.config.js b/examples/webpack-babel-ts/babel.config.js new file mode 100644 index 00000000..bb1d5d47 --- /dev/null +++ b/examples/webpack-babel-ts/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ['istanbul'] +}; diff --git a/examples/webpack-babel-ts/index.html b/examples/webpack-babel-ts/index.html new file mode 100644 index 00000000..055baa6f --- /dev/null +++ b/examples/webpack-babel-ts/index.html @@ -0,0 +1,14 @@ + + + + + + + Document + + + + + + diff --git a/examples/webpack-babel-ts/package.json b/examples/webpack-babel-ts/package.json new file mode 100644 index 00000000..939242d6 --- /dev/null +++ b/examples/webpack-babel-ts/package.json @@ -0,0 +1,20 @@ +{ + "name": "webpack-babel-ts", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build-ts": "tsc", + "build": "npm run build-ts && webpack --mode production" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "babel-loader": "^9.1.3", + "babel-plugin-istanbul": "^6.1.1", + "typescript": "^5.3.3", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4" + } +} diff --git a/examples/webpack-babel-ts/src/main.ts b/examples/webpack-babel-ts/src/main.ts new file mode 100644 index 00000000..d9dc06ff --- /dev/null +++ b/examples/webpack-babel-ts/src/main.ts @@ -0,0 +1,7 @@ +class Main { + sayHello() { + console.log('Hello World!'); + } +} + +new Main().sayHello(); diff --git a/examples/webpack-babel-ts/tsconfig.json b/examples/webpack-babel-ts/tsconfig.json new file mode 100644 index 00000000..9f77d8b7 --- /dev/null +++ b/examples/webpack-babel-ts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./build", + "sourceMap": true + } +} diff --git a/examples/webpack-babel-ts/webpack.config.js b/examples/webpack-babel-ts/webpack.config.js new file mode 100644 index 00000000..7fdc15c2 --- /dev/null +++ b/examples/webpack-babel-ts/webpack.config.js @@ -0,0 +1,19 @@ +const path = require('path'); + + +module.exports = { + entry: './build/main.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'bundle.js' + }, + module:{ + rules: [ + { + test: /\.(js|jsx)$/, + use:['babel-loader'], + exclude:'/node_modules/' + } + ] + } +}; diff --git a/package.json b/package.json new file mode 100755 index 00000000..eab9326a --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "canyon", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "test": "pnpm -r do-test", + "dev": "pnpm -r do-dev", + "build": "pnpm -r do-build", + "migrate": "npx prisma migrate dev --schema ./packages/canyon-backend/prisma/schema.prisma", + "rm": "find ./ -type d \\( -name \"dist\" -o -name \"node_modules\" \\) -exec rm -rf {} +", + "preinstall": "node ./scripts/check.js" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/canyon-backend/.eslintrc.js b/packages/canyon-backend/.eslintrc.js new file mode 100755 index 00000000..259de13c --- /dev/null +++ b/packages/canyon-backend/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/packages/canyon-backend/.gitignore b/packages/canyon-backend/.gitignore new file mode 100755 index 00000000..22f55adc --- /dev/null +++ b/packages/canyon-backend/.gitignore @@ -0,0 +1,35 @@ +# compiled output +/dist +/node_modules + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json \ No newline at end of file diff --git a/packages/canyon-backend/.prettierrc b/packages/canyon-backend/.prettierrc new file mode 100755 index 00000000..dcb72794 --- /dev/null +++ b/packages/canyon-backend/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/packages/canyon-backend/README.md b/packages/canyon-backend/README.md new file mode 100755 index 00000000..e69de29b diff --git a/packages/canyon-backend/nest-cli.json b/packages/canyon-backend/nest-cli.json new file mode 100755 index 00000000..f9aa683b --- /dev/null +++ b/packages/canyon-backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/packages/canyon-backend/package.json b/packages/canyon-backend/package.json new file mode 100755 index 00000000..9a091d23 --- /dev/null +++ b/packages/canyon-backend/package.json @@ -0,0 +1,81 @@ +{ + "name": "canyon-backend", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "preinstall": "prisma generate && nest build", + "migrate": "prisma migrate dev" + }, + "dependencies": { + "@canyon/data": "workspace:^", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.1.1", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/schedule": "^4.0.0", + "axios": "^1.6.5", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "diff": "^5.1.0", + "prisma": "^5.6.0", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@prisma/client": "^5.6.0", + "@types/express": "^4.17.17", + "@types/istanbul-lib-coverage": "^2.0.5", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/packages/canyon-backend/prisma/migrations/20240108141744_update/migration.sql b/packages/canyon-backend/prisma/migrations/20240108141744_update/migration.sql new file mode 100755 index 00000000..555895a2 --- /dev/null +++ b/packages/canyon-backend/prisma/migrations/20240108141744_update/migration.sql @@ -0,0 +1,88 @@ +-- CreateTable +CREATE TABLE "user" ( + "id" INTEGER NOT NULL, + "username" TEXT NOT NULL, + "password" TEXT NOT NULL, + "nickname" TEXT NOT NULL, + "avatar" TEXT NOT NULL, + "refresh_token" TEXT NOT NULL, + "access_token" TEXT NOT NULL, + "email" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "user_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "coverage" ( + "id" SERIAL NOT NULL, + "key" TEXT, + "commit_sha" TEXT NOT NULL, + "branch" TEXT NOT NULL, + "device" TEXT NOT NULL, + "compare_target" TEXT NOT NULL, + "project_id" TEXT NOT NULL, + "instrument_cwd" TEXT, + "reporter" INTEGER NOT NULL, + "report_id" TEXT NOT NULL, + "cov_type" TEXT NOT NULL, + "relation_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "coverage_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "project" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "path_with_namespace" TEXT NOT NULL, + "description" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "summary" ( + "id" SERIAL NOT NULL, + "total" INTEGER NOT NULL, + "covered" INTEGER NOT NULL, + "skipped" INTEGER NOT NULL, + "metric_type" TEXT NOT NULL, + "cov_type" TEXT NOT NULL, + "report_id" TEXT NOT NULL, + "commit_sha" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "summary_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "codechange" ( + "id" SERIAL NOT NULL, + "project_id" TEXT NOT NULL, + "compare_target" TEXT NOT NULL, + "commit_sha" TEXT NOT NULL, + "path" TEXT NOT NULL, + "additions" INTEGER[], + "deletions" INTEGER[], + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "codechange_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "task" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "status" TEXT NOT NULL, + "report_id" TEXT NOT NULL, + "commit_sha" TEXT NOT NULL, + "project_id" TEXT NOT NULL, + "result" JSONB NOT NULL, + + CONSTRAINT "task_pkey" PRIMARY KEY ("id") +); diff --git a/packages/canyon-backend/prisma/migrations/20240109115732_update/migration.sql b/packages/canyon-backend/prisma/migrations/20240109115732_update/migration.sql new file mode 100755 index 00000000..5abf8e6e --- /dev/null +++ b/packages/canyon-backend/prisma/migrations/20240109115732_update/migration.sql @@ -0,0 +1,36 @@ +/* + Warnings: + + - The primary key for the `codechange` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The primary key for the `coverage` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The primary key for the `summary` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The primary key for the `task` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- AlterTable +ALTER TABLE "codechange" DROP CONSTRAINT "codechange_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "codechange_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "codechange_id_seq"; + +-- AlterTable +ALTER TABLE "coverage" DROP CONSTRAINT "coverage_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "coverage_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "coverage_id_seq"; + +-- AlterTable +ALTER TABLE "summary" DROP CONSTRAINT "summary_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "summary_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "summary_id_seq"; + +-- AlterTable +ALTER TABLE "task" DROP CONSTRAINT "task_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "task_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "task_id_seq"; diff --git a/packages/canyon-backend/prisma/migrations/20240109122436_upda/migration.sql b/packages/canyon-backend/prisma/migrations/20240109122436_upda/migration.sql new file mode 100755 index 00000000..86bd6502 --- /dev/null +++ b/packages/canyon-backend/prisma/migrations/20240109122436_upda/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `ip` to the `coverage` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "coverage" ADD COLUMN "ip" TEXT NOT NULL; diff --git a/packages/canyon-backend/prisma/migrations/20240110054935_update/migration.sql b/packages/canyon-backend/prisma/migrations/20240110054935_update/migration.sql new file mode 100755 index 00000000..b62f2c16 --- /dev/null +++ b/packages/canyon-backend/prisma/migrations/20240110054935_update/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - You are about to drop the column `commit_sha` on the `codechange` table. All the data in the column will be lost. + - You are about to drop the column `commit_sha` on the `coverage` table. All the data in the column will be lost. + - You are about to drop the column `commit_sha` on the `summary` table. All the data in the column will be lost. + - You are about to drop the `task` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `sha` to the `codechange` table without a default value. This is not possible if the table is not empty. + - Added the required column `consumer` to the `coverage` table without a default value. This is not possible if the table is not empty. + - Added the required column `sha` to the `coverage` table without a default value. This is not possible if the table is not empty. + - Made the column `instrument_cwd` on table `coverage` required. This step will fail if there are existing NULL values in that column. + - Added the required column `sha` to the `summary` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "codechange" DROP COLUMN "commit_sha", +ADD COLUMN "sha" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "coverage" DROP COLUMN "commit_sha", +ADD COLUMN "consumer" INTEGER NOT NULL, +ADD COLUMN "sha" TEXT NOT NULL, +ALTER COLUMN "instrument_cwd" SET NOT NULL; + +-- AlterTable +ALTER TABLE "summary" DROP COLUMN "commit_sha", +ADD COLUMN "sha" TEXT NOT NULL; + +-- DropTable +DROP TABLE "task"; diff --git a/packages/canyon-backend/prisma/migrations/migration_lock.toml b/packages/canyon-backend/prisma/migrations/migration_lock.toml new file mode 100755 index 00000000..fbffa92c --- /dev/null +++ b/packages/canyon-backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/packages/canyon-backend/prisma/schema.prisma b/packages/canyon-backend/prisma/schema.prisma new file mode 100755 index 00000000..8bc3f279 --- /dev/null +++ b/packages/canyon-backend/prisma/schema.prisma @@ -0,0 +1,93 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" + binaryTargets = ["native", "debian-openssl-1.1.x"] +} + +model User { + id Int @id + username String + password String + nickname String + avatar String + refreshToken String @map("refresh_token") + accessToken String @map("access_token") + email String + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(3) + + @@map("user") +} + +model Coverage { + id String @id @default(cuid()) + key String? + sha String @map("sha") + branch String + device String + ip String + consumer Int // 1: 未消费, 2: 已消费 + compareTarget String @map("compare_target") + projectID String @map("project_id") + instrumentCwd String @map("instrument_cwd") + reporter Int + reportID String @map("report_id") + covType String @map("cov_type") + relationID String @map("relation_id") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(3) + + @@map("coverage") +} + +model Project { + id String @id + name String + pathWithNamespace String @map("path_with_namespace") + description String + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(3) + + @@map("project") +} + +model Summary { + id String @id @default(cuid()) + + total Int + covered Int + skipped Int + + metricType String @map("metric_type") + + covType String @map("cov_type") + + reportID String @map("report_id") + + sha String @map("sha") + + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(3) + + @@map("summary") +} + +model Codechange { + id String @id @default(cuid()) + + projectID String @map("project_id") + + compareTarget String @map("compare_target") + + sha String @map("sha") + + path String + + additions Int[] + + deletions Int[] + + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(3) + + @@map("codechange") +} diff --git a/packages/canyon-backend/src/adapter/coverage-data.adapter.ts b/packages/canyon-backend/src/adapter/coverage-data.adapter.ts new file mode 100755 index 00000000..f69418c2 --- /dev/null +++ b/packages/canyon-backend/src/adapter/coverage-data.adapter.ts @@ -0,0 +1,24 @@ +import axios from 'axios'; +import { CoverageMapData } from 'istanbul-lib-coverage'; +export async function getSpecificCoverageData( + coverageDataID: string, +): Promise { + const { data } = await axios.get( + `${process.env['COVERAGE_DATA_URL']}/coverage/${coverageDataID}`, + ); + return data; +} + +export async function createNewCoverageData( + coverageData: CoverageMapData, +): Promise<{ key: string }> { + return axios + .post(`${process.env['COVERAGE_DATA_URL']}/coverage`, coverageData) + .then(({ data }) => data); +} + +export function deleteSpecificCoverageData(coverageDataID: string) { + return axios + .delete(`${process.env['COVERAGE_DATA_URL']}/coverage/${coverageDataID}`) + .then(({ data }) => data); +} diff --git a/packages/canyon-backend/src/app.controller.spec.ts b/packages/canyon-backend/src/app.controller.spec.ts new file mode 100755 index 00000000..d22f3890 --- /dev/null +++ b/packages/canyon-backend/src/app.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; + +describe('AppController', () => { + let appController: AppController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [AppService], + }).compile(); + + appController = app.get(AppController); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(appController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/packages/canyon-backend/src/app.controller.ts b/packages/canyon-backend/src/app.controller.ts new file mode 100755 index 00000000..7f87f0c5 --- /dev/null +++ b/packages/canyon-backend/src/app.controller.ts @@ -0,0 +1,4 @@ +import { Body, Controller, Get, Post, Request } from '@nestjs/common'; + +@Controller() +export class AppController {} diff --git a/packages/canyon-backend/src/app.module.ts b/packages/canyon-backend/src/app.module.ts new file mode 100755 index 00000000..a5933a5a --- /dev/null +++ b/packages/canyon-backend/src/app.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { ConfigModule } from '@nestjs/config'; +import { PrismaModule } from './prisma/prisma.module'; +import { CoverageModule } from './coverage/coverage.module'; +// import { TasksModule } from './tasks/tasks.module'; +import { ScheduleModule } from '@nestjs/schedule'; +@Module({ + imports: [ + ScheduleModule.forRoot(), + ConfigModule.forRoot({ + envFilePath: './.[env]', + }), + PrismaModule, + CoverageModule, + // TasksModule, + ], + controllers: [AppController], + providers: [AppService], +}) +export class AppModule {} diff --git a/packages/canyon-backend/src/app.service.ts b/packages/canyon-backend/src/app.service.ts new file mode 100755 index 00000000..927d7cca --- /dev/null +++ b/packages/canyon-backend/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/packages/canyon-backend/src/coverage/coverage.controller.ts b/packages/canyon-backend/src/coverage/coverage.controller.ts new file mode 100755 index 00000000..ba512c8a --- /dev/null +++ b/packages/canyon-backend/src/coverage/coverage.controller.ts @@ -0,0 +1,41 @@ +import { Body, Controller, Get, Post, Request } from '@nestjs/common'; +// import { AppService } from './app.service'; +import { CoverageClientService } from './services/coverage-client.service'; +import { RetrieveCoverageSummaryService } from './services/retrieve-coverage-summary.service'; +import { CoverageClientDto } from './dto/coverage-client.dto'; +// import * as platform from 'platform' +// export function getPlatformInfo(str) { +// return platform.parse(str) +// } + +@Controller() +export class CoverageController { + constructor( + // private readonly appService: AppService, + private readonly coverageClientService: CoverageClientService, + private readonly retrieveCoverageSummaryService: RetrieveCoverageSummaryService, + ) {} + + @Post('coverage/client') + coverageClient( + @Body() coverageClientDto: CoverageClientDto, + @Request() req: any, + ): Promise { + return this.coverageClientService.invoke( + req?.user?.id || 1, + coverageClientDto, + req.headers['user-agent'], + req.ip, + ); + } + + @Get('coverage/summary') + coverageSummary(): Promise { + return this.retrieveCoverageSummaryService.invoke(); + } + + @Get('vi/health') + viHealth() { + return '230614ms'; + } +} diff --git a/packages/canyon-backend/src/coverage/coverage.module.ts b/packages/canyon-backend/src/coverage/coverage.module.ts new file mode 100755 index 00000000..95f432d5 --- /dev/null +++ b/packages/canyon-backend/src/coverage/coverage.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +// import { AppService } from './app.service'; +import { RetrieveCoverageSummaryService } from './services/retrieve-coverage-summary.service'; +import { CoverageClientService } from './services/coverage-client.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { CoverageController } from './coverage.controller'; + +@Module({ + imports: [], + controllers: [CoverageController], + providers: [ + // AppService, + PrismaService, + CoverageClientService, + RetrieveCoverageSummaryService, + ], +}) +export class CoverageModule {} diff --git a/packages/canyon-backend/src/coverage/dto/coverage-client.dto.ts b/packages/canyon-backend/src/coverage/dto/coverage-client.dto.ts new file mode 100755 index 00000000..9c89633a --- /dev/null +++ b/packages/canyon-backend/src/coverage/dto/coverage-client.dto.ts @@ -0,0 +1,58 @@ +import { + IsNotEmpty, + IsOptional, + IsString, + Matches, + MinLength, + Validate, +} from 'class-validator'; +import { IsValidCoverage } from '../valids/is-valid-coverage'; + +export class CoverageClientDto { + // git仓库相关 + @IsString() + @Matches(/^[a-f0-9]{40}$/i, { message: 'sha格式不正确' }) + @IsNotEmpty({ message: 'sha 不能为空' }) + sha: string; + + @IsString() + @MinLength(1, { message: 'branch长度最小为1' }) + @IsOptional({ message: 'branch 可以为空' }) + branch: string; + + @IsString() + @MinLength(1, { message: 'device长度最小为1' }) + @IsOptional({ message: 'device 可以为空' }) + device: string; + + // 允许为空,但是最小长度为1 + @IsString() + @MinLength(1, { message: 'compareTarget长度最小为1' }) + @IsOptional({ message: 'compareTarget可以为空' }) + compareTarget: string; + + // 允许为空 + @IsString() + @IsOptional({ message: 'key可以为空' }) + key: string; + + @IsString() + @IsNotEmpty({ message: 'projectID 不能为空' }) + projectID: string; + + // 单次 case 触发相关 + @IsString() + @MinLength(1, { message: 'reportID长度最小为1' }) + @IsOptional({ message: 'reportID 可以为空' }) + reportID: string; + + // istanbul覆盖率相关 + @IsString() + @MinLength(1, { message: 'reportID长度最小为1' }) + @IsNotEmpty({ message: 'instrumentCwd不能为空' }) + instrumentCwd: string; + + @IsNotEmpty({ message: 'coverage不能为空' }) + @Validate(IsValidCoverage) + coverage: any; +} diff --git a/packages/canyon-backend/src/coverage/services/coverage-client.service.ts b/packages/canyon-backend/src/coverage/services/coverage-client.service.ts new file mode 100755 index 00000000..bc1ba870 --- /dev/null +++ b/packages/canyon-backend/src/coverage/services/coverage-client.service.ts @@ -0,0 +1,32 @@ +import { PrismaService } from '../../prisma/prisma.service'; +import { CoverageClientDto } from '../dto/coverage-client.dto'; +import { Injectable } from '@nestjs/common'; +import { createNewCoverageData } from '../../adapter/coverage-data.adapter'; + +@Injectable() +export class CoverageClientService { + constructor(private readonly prisma: PrismaService) {} + async invoke(currentUser, coverageClientDto: CoverageClientDto, device, ip) { + const { key: coverageDataKey } = await createNewCoverageData( + coverageClientDto.coverage, + ); + return this.prisma.coverage.create({ + data: { + reportID: coverageClientDto.reportID, + sha: coverageClientDto.sha, + projectID: coverageClientDto.projectID, + covType: 'normal', + consumer: 1, + branch: coverageClientDto.branch, + key: coverageClientDto.key, + relationID: coverageDataKey, + device: device, + instrumentCwd: coverageClientDto.instrumentCwd, + compareTarget: coverageClientDto.compareTarget, + createdAt: new Date(), + reporter: 1, + ip: ip, + }, + }); + } +} diff --git a/packages/canyon-backend/src/coverage/services/retrieve-coverage-summary.service.ts b/packages/canyon-backend/src/coverage/services/retrieve-coverage-summary.service.ts new file mode 100755 index 00000000..eabaedf2 --- /dev/null +++ b/packages/canyon-backend/src/coverage/services/retrieve-coverage-summary.service.ts @@ -0,0 +1,24 @@ +export class RetrieveCoverageSummaryService { + async invoke() { + fetch('http://localhost:8080/coverage/client', { + method: 'post', + body: JSON.stringify({ + coverage: {}, + commitSha: '6fe4ef67c83fedc89085140c31a9537b450bc987', + projectID: '35883195', + instrumentCwd: + '/Users/xiaoen/Desktop/gitlab/canyon-project/canyon-demo', + reportID: '0018', + device: 'mac', + branch: 'dev', + compareTarget: 'main', + }), + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => { + console.log(res); + }); + return {}; + } +} diff --git a/packages/canyon-backend/src/coverage/valids/is-valid-coverage.ts b/packages/canyon-backend/src/coverage/valids/is-valid-coverage.ts new file mode 100755 index 00000000..384cd6a6 --- /dev/null +++ b/packages/canyon-backend/src/coverage/valids/is-valid-coverage.ts @@ -0,0 +1,55 @@ +import { + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +function isValidCoverage(coverage) { + // 检查是否是对象 + if (typeof coverage !== 'object' || coverage === null) { + return false; + } + // 检查是否有必须的属性 + const requiredProperties = [ + 'path', + 'statementMap', + 'fnMap', + 'branchMap', + 's', + 'f', + 'b', + ]; + for (const prop of requiredProperties) { + if (!(prop in coverage)) { + return false; + } + } + // 检查属性的类型和结构 + if ( + typeof coverage.path !== 'string' || + typeof coverage.statementMap !== 'object' || + typeof coverage.fnMap !== 'object' || + typeof coverage.branchMap !== 'object' || + typeof coverage.s !== 'object' || + typeof coverage.f !== 'object' || + typeof coverage.b !== 'object' + ) { + return false; + } + + // 如果所有检查通过,返回 true + return true; +} +@ValidatorConstraint({ name: 'isValidCoverage', async: false }) +export class IsValidCoverage implements ValidatorConstraintInterface { + validate(coverage: unknown) { + if (Object.keys(coverage).length === 0) { + return false; + } + return Object.values(coverage).every((item) => { + return isValidCoverage(item); + }); + } + + defaultMessage() { + return 'coverage格式不正确'; + } +} diff --git a/packages/canyon-backend/src/main.ts b/packages/canyon-backend/src/main.ts new file mode 100755 index 00000000..de37c5c4 --- /dev/null +++ b/packages/canyon-backend/src/main.ts @@ -0,0 +1,17 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { json } from 'express'; +import { ValidationPipe } from '@nestjs/common'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.useGlobalPipes(new ValidationPipe()); + app.use( + json({ + limit: '100mb', + }), + ); + app.enableCors(); + await app.listen(8080); +} +bootstrap(); diff --git a/packages/canyon-backend/src/prisma/prisma.module.ts b/packages/canyon-backend/src/prisma/prisma.module.ts new file mode 100755 index 00000000..953aa89d --- /dev/null +++ b/packages/canyon-backend/src/prisma/prisma.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common/decorators'; +import { PrismaService } from './prisma.service'; + +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/packages/canyon-backend/src/prisma/prisma.service.ts b/packages/canyon-backend/src/prisma/prisma.service.ts new file mode 100755 index 00000000..8c10940d --- /dev/null +++ b/packages/canyon-backend/src/prisma/prisma.service.ts @@ -0,0 +1,19 @@ +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ + constructor() { + super(); + } + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} diff --git a/packages/canyon-backend/src/utils/diffline.ts b/packages/canyon-backend/src/utils/diffline.ts new file mode 100755 index 00000000..c2535720 --- /dev/null +++ b/packages/canyon-backend/src/utils/diffline.ts @@ -0,0 +1,182 @@ +import * as Diff from 'diff'; +import { Change } from 'diff'; +interface DiffLine { + repoID: string; + baseCommitSha?: string; + compareCommitSha: string; + includesFileExtensions?: string[]; + gitlabUrl?: string; + token?: string; +} + +function calculateNewRows( + a: string, + b: string, +): { additions: number[]; deletions: number[] } { + const diffResult: Change[] = Diff.diffLines(a, b); + function generateArray(startValue: number, length: number) { + return Array.from({ length }, (_, index) => startValue - index).reverse(); + } + function sumToIndex(arr: number[], index: number) { + return arr.slice(0, index + 1).reduce((sum, value) => sum + value, 0); + } + const additionsDiffResult = diffResult.filter((i) => !i.removed); + const additions: any = []; + additionsDiffResult.forEach((i, index) => { + if (i.added) { + additions.push( + generateArray( + sumToIndex( + additionsDiffResult.map((i) => i.count || 0), + index, + ), + i.count || 0, + ), + ); + } + }); + + const deletionsDiffResult = diffResult.filter((i) => i.removed); + const deletions: any = []; + deletionsDiffResult.forEach((i, index) => { + if (i.removed) { + deletions.push( + generateArray( + sumToIndex( + deletionsDiffResult.map((i) => i.count || 0), + index, + ), + i.count || 0, + ), + ); + } + }); + return { + additions: additions.flat(Infinity), + deletions: deletions.flat(Infinity), + }; +} + +function getDecode(str: string) { + return decodeURIComponent( + atob(str) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join(''), + ); +} +export async function diffLine({ + repoID, + baseCommitSha = undefined, + compareCommitSha, + includesFileExtensions = ['ts', 'tsx', 'jsx', 'vue', 'js'], + gitlabUrl = 'https://gitlab.com', + token = 'default_token', +}: DiffLine): Promise< + { path: string; additions: number[]; deletions: number[] }[] +> { + const gitlabApiUrlFile = `${gitlabUrl}/api/v4/projects/${repoID}/repository/files`; + const gitlabApiUrlCommit = `${gitlabUrl}/api/v4/projects/${repoID}/repository/commits/${compareCommitSha}`; + + const gitlabApiUrlCommitResponse = await fetch(gitlabApiUrlCommit, { + headers: { + // Authorization: 'Bearer ' + token, // 在请求头中使用 GitLab API token + 'private-token': process.env.PRIVATE_TOKEN, + }, + }) + .then((res) => res.json()) + .then((data) => { + return { + parent_ids: data.parent_ids || [], + stats: data.stats, + }; + }); + + const result = []; + // 只关心 50000 行以内的更改 + if ( + gitlabApiUrlCommitResponse.parent_ids.length > 0 && + gitlabApiUrlCommitResponse.stats.additions < 50000 + ) { + // 声明realBaseCommitSha,如果baseCommitSha存在,则使用baseCommitSha,否则使用gitlabApiUrlCommitResponse.parent_ids[0] + const realBaseCommitSha = + baseCommitSha || gitlabApiUrlCommitResponse.parent_ids[0]; + const gitDiffs = await fetch( + `${gitlabUrl}/api/v4/projects/${repoID}/repository/compare?from=${realBaseCommitSha}&to=${compareCommitSha}`, + { + headers: { + // Authorization: 'Bearer ' + token, // 在请求头中使用 GitLab API token + 'private-token': process.env.PRIVATE_TOKEN, + }, + }, + ) + .then((res) => res.json()) + .then((response) => { + return (response.diffs || []).map( + ({ + old_path, + new_path, + a_mode, + b_mode, + new_file, + renamed_file, + deleted_file, + }) => { + return { + old_path, + new_path, + a_mode, + b_mode, + new_file, + renamed_file, + deleted_file, + }; + }, + ); + }); + + // const includesFileExtensions = /\.tsx?$|\.jsx?$|\.vue$|\.js$/i; + + const isMatchingExtension = ( + includesFileExtensions: string[], + pathname: string, + ) => includesFileExtensions.some((ext) => pathname.endsWith('.' + ext)); + + const gitDiffsFiltered = gitDiffs.filter((gitDiff) => + isMatchingExtension(includesFileExtensions, gitDiff.new_path), + ); + + for (let i = 0; i < gitDiffsFiltered.length; i++) { + const contents = await Promise.all( + [realBaseCommitSha, compareCommitSha].map((c) => { + return fetch( + `${gitlabApiUrlFile}/${encodeURIComponent( + gitDiffsFiltered[i].new_path, + )}?ref=${c}`, + { + headers: { + // Authorization: 'Bearer ' + token, // 在请求头中使用 GitLab API token + 'private-token': process.env.PRIVATE_TOKEN, + }, + method: 'GET', + }, + ) + .then((res) => res.json()) + .then((r) => { + return getDecode(r.content); + }) + .catch(() => { + return ''; + }); + }), + ); + result.push({ + path: gitDiffsFiltered[i].new_path, + ...calculateNewRows(contents[0], contents[1]), + }); + } + } + return result; +} diff --git a/packages/canyon-backend/src/utils/utils.ts b/packages/canyon-backend/src/utils/utils.ts new file mode 100755 index 00000000..fc20fec8 --- /dev/null +++ b/packages/canyon-backend/src/utils/utils.ts @@ -0,0 +1,43 @@ +export function percent(covered, total) { + let tmp; + if (total > 0) { + tmp = (1000 * 100 * covered) / total; + return Math.floor(tmp / 10) / 100; + } else { + return 100.0; + } +} + +export function removeNullKeys(obj) { + const newObj = {}; + for (const key in obj) { + if (obj[key] !== null) { + newObj[key] = obj[key]; + } + } + return newObj; +} + +export function deleteID(obj) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id: _, ...rest } = obj; + return rest; +} + +export function mapToLowerCamelCase(coverage): any { + return { + id: coverage.id, + sha: coverage.sha, + reportID: coverage.report_id, + relationID: coverage.relation_id, + covType: coverage.cov_type, + consumer: coverage.consumer, + branch: coverage.branch, + device: coverage.device, + ip: coverage.ip, + compareTarget: coverage.compare_target, + projectID: coverage.project_id, + instrumentCwd: coverage.instrument_cwd, + reporter: coverage.reporter, + }; +} diff --git a/packages/canyon-backend/test/app.e2e-spec.ts b/packages/canyon-backend/test/app.e2e-spec.ts new file mode 100755 index 00000000..50cda623 --- /dev/null +++ b/packages/canyon-backend/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/packages/canyon-backend/test/jest-e2e.json b/packages/canyon-backend/test/jest-e2e.json new file mode 100755 index 00000000..e9d912f3 --- /dev/null +++ b/packages/canyon-backend/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/packages/canyon-backend/tsconfig.build.json b/packages/canyon-backend/tsconfig.build.json new file mode 100755 index 00000000..64f86c6b --- /dev/null +++ b/packages/canyon-backend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/packages/canyon-backend/tsconfig.json b/packages/canyon-backend/tsconfig.json new file mode 100755 index 00000000..95f5641c --- /dev/null +++ b/packages/canyon-backend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false + } +} diff --git a/packages/canyon-cli/.eslintignore b/packages/canyon-cli/.eslintignore new file mode 100755 index 00000000..5aee1326 --- /dev/null +++ b/packages/canyon-cli/.eslintignore @@ -0,0 +1,2 @@ +test/providers/provider_template.test.js +dist/**/* diff --git a/packages/canyon-cli/.eslintrc.js b/packages/canyon-cli/.eslintrc.js new file mode 100755 index 00000000..cd151aa4 --- /dev/null +++ b/packages/canyon-cli/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parserOptions: { + ecmaVersion: 9, + }, + env: { + es6: true, + jest: true, + node: true, + 'shared-node-browser': true, + }, + rules: { + '@typescript-eslint/no-var-requires': 1, + }, +} diff --git a/packages/canyon-cli/.gitignore b/packages/canyon-cli/.gitignore new file mode 100755 index 00000000..79d0ed22 --- /dev/null +++ b/packages/canyon-cli/.gitignore @@ -0,0 +1,79 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +# pkg output +out/ +dist/ + +canyon +canyon-macos +canyon.exe +.DS_Store +.idea/* +.dccache +.coverage +# File genrated as part of XCode tests +coverage-report-test.json + +*.coverage.txt + +pnpm-lock.yaml diff --git a/packages/canyon-cli/.prettierignore b/packages/canyon-cli/.prettierignore new file mode 100755 index 00000000..3488f7fe --- /dev/null +++ b/packages/canyon-cli/.prettierignore @@ -0,0 +1 @@ +npm-shrinkwrap.json diff --git a/packages/canyon-cli/.prettierrc b/packages/canyon-cli/.prettierrc new file mode 100755 index 00000000..7ef21353 --- /dev/null +++ b/packages/canyon-cli/.prettierrc @@ -0,0 +1,14 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "printWidth": 80, + "proseWrap": "preserve", + "requirePragma": false, + "semi": false, + "singleQuote": true, + "trailingComma": "all" +} diff --git a/packages/canyon-cli/README.md b/packages/canyon-cli/README.md new file mode 100755 index 00000000..e4cc0e5c --- /dev/null +++ b/packages/canyon-cli/README.md @@ -0,0 +1,7 @@ +debug + +node ~/Desktop/github/canyon-project/canyon/packages/canyon-cli/dist/bin/canyon.js --url http://localhost:8080 + +git tag -a v0.0.5 -m "Version 0.0.5" + +git push origin v0.0.5 diff --git a/packages/canyon-cli/bin/canyon.ts b/packages/canyon-cli/bin/canyon.ts new file mode 100755 index 00000000..1d2eef8b --- /dev/null +++ b/packages/canyon-cli/bin/canyon.ts @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +import { logError, main, verbose } from '../src' +import { addArguments } from '../src/helpers/cli' + +var argv = require('yargs') // eslint-disable-line + +argv.usage('Usage: $0 [options]') + +addArguments(argv) + +argv.version().help('help').alias('help', 'h').argv + +const realArgs = argv.argv + +const start = Date.now() + +verbose(`Start of uploader: ${start}...`, realArgs.verbose) +main(realArgs) + .then(() => { + const end = Date.now() + verbose(`End of uploader: ${end - start} milliseconds`, realArgs.verbose) + }) + .catch(error => { + if (error instanceof Error) { + logError(`There was an error running the uploader: ${error.message}`) + verbose(`The error stack is: ${error.stack}`, realArgs.verbose) + } + + const end = Date.now() + verbose(`End of uploader: ${end - start} milliseconds`, realArgs.verbose) + process.exit(realArgs.nonZero ? -1 : 0) + }) diff --git a/packages/canyon-cli/jest.config.js b/packages/canyon-cli/jest.config.js new file mode 100755 index 00000000..2d334fad --- /dev/null +++ b/packages/canyon-cli/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + preset: 'ts-jest', + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/ci_providers/provider_template.ts', + '!**/node_modules/**', + '!**/vendor/**', + ], + coverageReporters: ['text', 'cobertura', 'html'], + setupFilesAfterEnv: ['/test/test_helpers.ts'], + reporters: ['jest-spec-reporter'], + testPathIgnorePatterns: ['/dist/'], + modulePathIgnorePatterns: ['/dist'], +} diff --git a/packages/canyon-cli/package.json b/packages/canyon-cli/package.json new file mode 100755 index 00000000..ae401a9e --- /dev/null +++ b/packages/canyon-cli/package.json @@ -0,0 +1,56 @@ +{ + "name": "canyon-cli", + "version": "0.7.1-beta.3", + "description": "Canyon CLI", + "bin": { + "canyon": "dist/bin/canyon.js" + }, + "scripts": { + "lint": "eslint \"src/**/*.ts\"", + "test": "npm run lint && npm run build && jest --runInBand", + "test:e2e": "jest test/e2e/output.test.ts", + "build:clean": "rm -rf dist", + "build": "tsc --build", + "build-linux": "pkg . --targets linuxstatic --output out/canyon-linux", + "type-check": "tsc --noEmit", + "type-check:watch": "npm run type-check -- --watch", + "release": "standard-version --sign", + "prepublish": "npm run build" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "fast-glob": "3.3.1", + "js-yaml": "4.1.0", + "snake-case": "3.0.4", + "undici": "5.26.3", + "validator": "13.11.0", + "yargs": "17.7.2" + }, + "devDependencies": { + "@types/jest": "29.5.5", + "@types/js-yaml": "4.0.7", + "@types/mock-fs": "4.13.2", + "@types/node": "20.8.6", + "@types/validator": "13.11.3", + "@types/yargs": "17.0.28", + "@typescript-eslint/eslint-plugin": "6.8.0", + "@typescript-eslint/parser": "6.8.0", + "eslint": "8.51.0", + "eslint-config-prettier": "9.0.0", + "jest": "29.7.0", + "jest-spec-reporter": "1.0.19", + "mock-fs": "5.2.0", + "pkg": "5.8.1", + "prettier": "3.0.3", + "standard-version": "9.5.0", + "testdouble": "3.19.0", + "testdouble-jest": "2.0.0", + "ts-jest": "29.1.1", + "typescript": "5.2.2" + }, + "volta": { + "node": "18.18.2" + } +} diff --git a/packages/canyon-cli/src/ci_providers/index.ts b/packages/canyon-cli/src/ci_providers/index.ts new file mode 100755 index 00000000..7418da78 --- /dev/null +++ b/packages/canyon-cli/src/ci_providers/index.ts @@ -0,0 +1,14 @@ +import { IProvider } from '../types' + +import * as providerGitLabci from './provider_gitlabci' +import * as providerJenkinsci from './provider_jenkinsci' +import * as providerLocal from './provider_local' + +// Please make sure provider_local is last +const providerList: IProvider[] = [ + providerGitLabci, + providerJenkinsci, + providerLocal, +] + +export default providerList diff --git a/packages/canyon-cli/src/ci_providers/provider_gitlabci.ts b/packages/canyon-cli/src/ci_providers/provider_gitlabci.ts new file mode 100755 index 00000000..0ee82035 --- /dev/null +++ b/packages/canyon-cli/src/ci_providers/provider_gitlabci.ts @@ -0,0 +1,83 @@ +import { IServiceParams, UploaderEnvs, UploaderInputs } from '../types' + +import { parseSlugFromRemoteAddr } from '../helpers/git' + +export function detect(envs: UploaderEnvs): boolean { + return Boolean(envs.GITLAB_CI) +} + +function _getBuild(inputs: UploaderInputs): string { + const { args, envs } = inputs + return args.build || envs.CI_BUILD_ID || envs.CI_JOB_ID || '' +} + +function _getBuildURL(): string { + return '' +} + +function _getBranch(inputs: UploaderInputs): string { + const { args, envs } = inputs + return args.branch || envs.CI_BUILD_REF_NAME || envs.CI_COMMIT_REF_NAME || '' +} + +function _getJob(): string { + return '' +} + +function _getPR(inputs: UploaderInputs): string { + const { args } = inputs + return args.pr || '' +} + +function _getService(): string { + return 'gitlab' +} + +export function getServiceName(): string { + return 'GitLab CI' +} + +function _getSHA(inputs: UploaderInputs): string { + const { args, envs } = inputs + return args.sha || envs.CI_MERGE_REQUEST_SOURCE_BRANCH_SHA || envs.CI_BUILD_REF || envs.CI_COMMIT_SHA || '' +} + +function _getSlug(inputs: UploaderInputs): string { + const { args, envs } = inputs + if (args.slug !== '') return args.slug + const remoteAddr = envs.CI_BUILD_REPO || envs.CI_REPOSITORY_URL || '' + return ( + envs.CI_PROJECT_PATH || + parseSlugFromRemoteAddr(remoteAddr) || + '' + ) +} + +export async function getServiceParams(inputs: UploaderInputs): Promise { + return { + branch: _getBranch(inputs), + build: _getBuild(inputs), + buildURL: _getBuildURL(), + commit: _getSHA(inputs), + job: _getJob(), + pr: _getPR(inputs), + service: _getService(), + slug: _getSlug(inputs), + } +} + +export function getEnvVarNames(): string[] { + return [ + 'CI_BUILD_ID', + 'CI_BUILD_REF', + 'CI_BUILD_REF_NAME', + 'CI_BUILD_REPO', + 'CI_COMMIT_REF_NAME', + 'CI_COMMIT_SHA', + 'CI_JOB_ID', + 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', + 'CI_PROJECT_PATH', + 'CI_REPOSITORY_URL', + 'GITLAB_CI', + ] +} diff --git a/packages/canyon-cli/src/ci_providers/provider_jenkinsci.ts b/packages/canyon-cli/src/ci_providers/provider_jenkinsci.ts new file mode 100755 index 00000000..4f4d442a --- /dev/null +++ b/packages/canyon-cli/src/ci_providers/provider_jenkinsci.ts @@ -0,0 +1,87 @@ +import { parseSlugFromRemoteAddr } from '../helpers/git' +import { IServiceParams, UploaderEnvs, UploaderInputs } from '../types' + +export function detect(envs: UploaderEnvs): boolean { + return Boolean(envs.JENKINS_URL) +} + +function _getBuild(inputs: UploaderInputs): string { + const { args, envs } = inputs + return args.build || envs.BUILD_NUMBER || '' +} + +function _getBuildURL(inputs: UploaderInputs): string { + const { envs } = inputs + return envs.BUILD_URL ? (envs.BUILD_URL) : '' +} + +function _getBranch(inputs: UploaderInputs): string { + const { args, envs } = inputs + return ( + args.branch || + envs.ghprbSourceBranch || + envs.CHANGE_BRANCH || + envs.GIT_BRANCH || + envs.BRANCH_NAME || + '' + ) +} + +function _getJob() { + return '' +} + +function _getPR(inputs: UploaderInputs): string { + const { args, envs } = inputs + return args.pr || envs.ghprbPullId || envs.CHANGE_ID || '' +} + +function _getService(): string { + return 'jenkins' +} + +export function getServiceName(): string { + return 'Jenkins CI' +} + +function _getSHA(inputs: UploaderInputs): string { + const { args, envs } = inputs + // Note that the value of GIT_COMMIT may not be accurate if Jenkins + // is merging `master` in to the working branch first. In these cases + // there is no envvar representing the actual submitted commit + return args.sha || envs.ghprbActualCommit || envs.GIT_COMMIT || '' +} + +function _getSlug(inputs: UploaderInputs): string { + const { args } = inputs + if (args.slug !== '') return args.slug + return parseSlugFromRemoteAddr('') || '' +} + +export async function getServiceParams(inputs: UploaderInputs): Promise { + return { + branch: _getBranch(inputs), + build: _getBuild(inputs), + buildURL: _getBuildURL(inputs), + commit: _getSHA(inputs), + job: _getJob(), + pr: _getPR(inputs), + service: _getService(), + slug: _getSlug(inputs), + } +} + +export function getEnvVarNames(): string[] { + return [ + 'BRANCH_NAME', + 'BUILD_NUMBER', + 'BUILD_URL', + 'CHANGE_ID', + 'GIT_BRANCH', + 'GIT_COMMIT', + 'JENKINS_URL', + 'ghprbActualCommit', + 'ghprbPullId', + 'ghprbSourceBranch', + ] +} diff --git a/packages/canyon-cli/src/ci_providers/provider_local.ts b/packages/canyon-cli/src/ci_providers/provider_local.ts new file mode 100755 index 00000000..1b356b8a --- /dev/null +++ b/packages/canyon-cli/src/ci_providers/provider_local.ts @@ -0,0 +1,101 @@ +import { parseSlug } from '../helpers/git' +import { isProgramInstalled, runExternalProgram } from '../helpers/util' +import { IServiceParams, UploaderInputs } from '../types' + +// This provider requires git to be installed +export function detect(): boolean { + return isProgramInstalled('git') +} + +function _getBuild(inputs: UploaderInputs): string { + const { args } = inputs + return args.build || '' +} + +function _getBuildURL(): string { + return '' +} + +function _getBranch(inputs: UploaderInputs): string { + const { args, envs } = inputs + const branch = args.branch || envs.GIT_BRANCH || envs.BRANCH_NAME || '' + if (branch !== '') { + return branch + } + try { + const branchName = runExternalProgram('git', ['rev-parse', '--abbrev-ref', 'HEAD']) + return branchName + } catch (error) { + throw new Error( + `There was an error getting the branch name from git: ${error}`, + ) + } +} + +function _getJob(): string { + return '' +} + +function _getPR(inputs: UploaderInputs): string { + const { args } = inputs + return args.pr || '' +} + +// This is the value that gets passed to the Canyon uploader +function _getService(): string { + return '' +} + +// This is the name that gets printed +export function getServiceName(): string { + return 'Local' +} + +function _getSHA(inputs: UploaderInputs) { + const { args, envs } = inputs + const sha = args.sha || envs.GIT_COMMIT || '' + if (sha !== '') { + return sha + } + try { + const sha = runExternalProgram('git', ['rev-parse', 'HEAD']) + return sha + } catch (error) { + throw new Error(`There was an error getting the commit SHA from git: ${error}`) + } +} + +function _getSlug(inputs: UploaderInputs): string { + const { args } = inputs + if (args.slug) { + return args.slug + } + try { + const slug = runExternalProgram('git', ['config', '--get', 'remote.origin.url']) + return parseSlug(slug) + } catch (error) { + throw new Error(`There was an error getting the slug from git: ${error}`) + } +} + +export async function getServiceParams(inputs: UploaderInputs): Promise { + return { + branch: _getBranch(inputs), + build: _getBuild(inputs), + buildURL: _getBuildURL(), + commit: _getSHA(inputs), + job: _getJob(), + pr: _getPR(inputs), + service: _getService(), + slug: _getSlug(inputs), + } +} + +export function getEnvVarNames(): string[] { + return [ + 'BRANCH_NAME', + 'CI', + 'GIT_BRANCH', + 'GIT_COMMIT', + ] +} diff --git a/packages/canyon-cli/src/ci_providers/provider_template.ts b/packages/canyon-cli/src/ci_providers/provider_template.ts new file mode 100755 index 00000000..2a190143 --- /dev/null +++ b/packages/canyon-cli/src/ci_providers/provider_template.ts @@ -0,0 +1,154 @@ +import { IServiceParams, UploaderEnvs, UploaderInputs } from '../types' + +/** + * Detects if this CI provider is being used + * + * @param {*} envs an object of enviromental variable key/value pairs + * @returns boolean + */ + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function detect(envs: UploaderEnvs): boolean { + return false +} + +/** + * Determine the build number, based on args and envs + * + * @param {args: {}, envs: {}} inputs an object of arguments and enviromental variable key/value pairs + * @returns {string} + */ +function _getBuild(inputs: UploaderInputs): string { + const { args } = inputs + return args.build || '' +} + +/** + * Determine the build URL for use in the Canyon UI + * + * @param {args: {}, envs: {}} inputs an object of arguments and enviromental variable key/value pairs + * @returns {string} + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function _getBuildURL(inputs: UploaderInputs): string { + return '' +} + +/** + * Determine the branch of the repository, based on args and envs + * + * @param {args: {}, envs: {}} inputs an object of arguments and enviromental variable key/value pairs + * @returns {string} + */ +function _getBranch(inputs: UploaderInputs): string { + const { args } = inputs + try { + return args.branch || '' + } catch (error) { + throw new Error( + `There was an error getting the branch name from git: ${error}`, + ) + } +} + +/** + * Determine the job number, based on args or envs + * + * @param {*} envs an object of enviromental variable key/value pairs + * @returns {string} + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function _getJob(envs: UploaderEnvs): string { + return '' +} + +/** + * Determine the PR number, based on args and envs + * + * @param {args: {}, envs: {}} inputs an object of arguments and enviromental variable key/value pairs + * @returns {string} + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function _getPR(inputs: UploaderInputs): string { + const { args } = inputs + try { + return args.pr || '' + } catch (error) { + throw new Error(`There was an error getting the pr number: ${error}`) + } +} + +/** + * The CI service name that gets sent to the Canyon uploader as part of the query string + * + * @returns {string} + */ +function _getService(): string { + return '' +} + +/** + * The CI Service name that gets displayed when running the uploader + * + * @returns + */ +export function getServiceName(): string { + return '' +} +/** + * Determine the commit SHA that is being uploaded, based on args or envs + * + * @param {args: {}, envs: {}} inputs an object of arguments and enviromental variable key/value pairs + * @returns {string} + */ +function _getSHA(inputs: UploaderInputs): string { + const { args } = inputs + try { + return args.sha || '' + } catch (error) { + throw new Error( + `There was an error getting the commit SHA from git: ${error}`, + ) + } +} +/** + * Determine the slug (org/repo) based on args or envs + * + * @param {args: {}, envs: {}} inputs an object of arguments and enviromental variable key/value pairs + * @returns {string} + */ +function _getSlug(inputs: UploaderInputs): string { + const { args } = inputs + try { + return args.slug || '' + } catch (error) { + throw new Error(`There was an error getting the slug from git: ${error}`) + } +} +/** + * Generates and return the serviceParams object + * + * @param {args: {}, envs: {}} inputs an object of arguments and enviromental variable key/value pairs + * @returns {{ branch: string, build: string, buildURL: string, commit: string, job: string, pr: string, service: string, slug: string }} + */ +export async function getServiceParams(inputs: UploaderInputs): Promise { + return { + branch: _getBranch(inputs), + build: _getBuild(inputs), + buildURL: _getBuildURL(inputs), + commit: _getSHA(inputs), + job: _getJob(inputs.envs), + pr: _getPR(inputs), + service: _getService(), + slug: _getSlug(inputs), + } +} + +/** + * Returns all the environment variables used by the provider + * + * @returns [{string}] + */ +export function getEnvVarNames(): string[] { + return [] +} diff --git a/packages/canyon-cli/src/helpers/cli.ts b/packages/canyon-cli/src/helpers/cli.ts new file mode 100755 index 00000000..fb8ea7df --- /dev/null +++ b/packages/canyon-cli/src/helpers/cli.ts @@ -0,0 +1,259 @@ +export interface ICLIArgument { + name: string + alias?: string + type?: string + default?: string | boolean + description: string +} + +const args: ICLIArgument[] = [ + { + alias: 'B', + name: 'branch', + type: 'string', + description: 'Specify the branch manually', + }, + { + alias: 'b', + name: 'build', + type: 'number', + description: 'Specify the build number manually', + }, + { + alias: 'c', + name: 'clean', + type: 'boolean', + default: false, + description: 'Move discovered coverage reports to the trash', + }, + { + alias: 'C', + name: 'sha', + type: 'string', + description: 'Specify the commit SHA manually', + }, + { + alias: 'CL', + name: 'changelog', + type: 'boolean', + default: false, + description: 'Display a link for the current changelog' + }, + { + alias: 'd', + name: 'dryRun', + type: 'boolean', + default: false, + description: "Don't upload files to Canyon", + }, + { + alias: 'e', + name: 'env', + description: 'Specify environment variables to be included with this build.\nAlso accepting environment variables: CANYON_ENV=VAR,VAR2', + }, + { + alias: 'f', + name: 'file', + type: 'string', + description: 'Target file(s) to upload', + }, + { + name: 'preventSymbolicLinks', + type: 'boolean', + description: 'Specifies whether to prevent following of symbolic links. Defaults to false (symbolic links are followed).' + }, + { + name: 'fullReport', + type: 'string', + description: 'Specify the path to a previously uploaded Canyon report' + }, + { + alias: 'F', + name: 'flags', + type: 'string', + default: '', + description: 'Flag the upload to group coverage metrics', + }, + { + alias: 'g', + name: 'gcov', + type: 'boolean', + default: false, + description: 'Run with gcov support', + }, + { + alias: 'ga', + name: 'gcovArgs', + type: 'string', + description: 'Extra arguments to pass to gcov', + }, + { + alias: 'gi', + name: 'gcovIgnore', + type: 'string', + description: 'Paths to ignore during gcov gathering', + }, + { + alias: 'gI', + name: 'gcovInclude', + type: 'string', + description: 'Paths to include during gcov gathering', + }, + { + alias: 'gx', + name: 'gcovExecutable', + type: 'string', + description: "gcov executable to run. Defaults to 'gcov'", + }, + { + alias: 'i', + name: 'networkFilter', + type: 'string', + description: 'Specify a filter on the files listed in the network section of the Canyon report. Useful for upload-specific path fixing', + }, + { + alias: 'k', + name: 'networkPrefix', + type: 'string', + description: 'Specify a prefix on files listed in the network section of the Canyon report. Useful to help resolve path fixing', + }, + { + alias: 'n', + name: 'name', + type: 'string', + default: '', + description: 'Custom defined name of the upload. Visible in Canyon UI', + }, + { + alias: 'N', + name: 'parent', + type: 'string', + description: "The commit SHA of the parent for which you are uploading coverage. If not present, the parent will be determined using the API of your repository provider. When using the repository provider's API, the parent is determined via finding the closest ancestor to the commit.", + }, + { + alias: 'P', + name: 'pr', + type: 'number', + description: 'Specify the pull request number manually', + }, + { + alias: 'Q', + name: 'source', + type: 'string', + default: '', + description: `Used internally by Canyon, this argument helps track + wrappers of the uploader (e.g. GitHub Action, CircleCI Orb)`, + }, + { + alias: 'R', + name: 'rootDir', + description: 'Specify the project root directory when not in a git repo', + }, + { + alias: 'r', + name: 'slug', + type: 'string', + default: '', + description: 'Specify the slug manually', + }, + { + alias: 's', + name: 'dir', + type: 'string', + description: 'Directory to search for coverage reports.\nAlready searches project root and current working directory', + }, + { + alias: 'T', + name: 'tag', + type: 'string', + default: '', + description: 'Specify the git tag', + }, + { + alias: 't', + name: 'token', + type: 'string', + default: '', + description: 'Canyon upload token', + }, + { + alias: 'U', + name: 'upstream', + type: 'string', + default: '', + description: 'The upstream http proxy server to connect through', + }, + { + alias: 'u', + name: 'url', + type: 'string', + description: 'Change the upload host (Enterprise use)', + default: 'https://canyon.io', + }, + { + alias: 'v', + name: 'verbose', + type: 'boolean', + description: 'Run with verbose logging', + }, + { + alias: 'X', + name: 'feature', + type: 'string', + description: `Toggle functionalities. Separate multiple ones by comma: -X network,search + -X fixes Enable file fixes to ignore common lines from coverage (e.g. blank lines or empty brackets) + -X network Disable uploading the file network + -X search Disable searching for coverage files`, + }, + { + alias: 'xc', + name: 'xcode', + type: 'boolean', + default: false, + description: '[Deprecating, please use xs] Run with xcode support', + }, + { + alias: 'xp', + name: 'xcodeArchivePath', + type: 'string', + description: '[Deprecating, please use xs] Specify the xcode archive path. Likely specified as the -resultBundlePath and should end in .xcresult', + }, + { + alias: 'xs', + name: 'swift', + type: 'boolean', + default: false, + description: 'Run with swift support', + }, + { + alias: 'xsp', + name: 'swiftProject', + type: 'string', + default: '', + description: 'Specify the swift project' + }, + { + alias: 'xc', + name: 'xcode', + type: 'boolean', + default: false, + description: 'Run with xcode support', + }, + { + alias: 'Z', + name: 'nonZero', + type: 'boolean', + default: false, + description: 'Should errors exit with a non-zero (default: false)', + }, +] + +export interface IYargsObject { + option: (arg0: string, arg1: ICLIArgument) => void +} + +export function addArguments(yargsInstance: IYargsObject): void { + args.forEach(arg => { + yargsInstance.option(arg.name, arg) + }) +} diff --git a/packages/canyon-cli/src/helpers/constants.ts b/packages/canyon-cli/src/helpers/constants.ts new file mode 100755 index 00000000..e4b59ffa --- /dev/null +++ b/packages/canyon-cli/src/helpers/constants.ts @@ -0,0 +1,3 @@ +export const DEFAULT_UPLOAD_HOST = 'https://canyon.io' + +export const SPAWNPROCESSBUFFERSIZE = 1_048_576 * 100 // 100 MiB diff --git a/packages/canyon-cli/src/helpers/coveragepy.ts b/packages/canyon-cli/src/helpers/coveragepy.ts new file mode 100755 index 00000000..dc24073a --- /dev/null +++ b/packages/canyon-cli/src/helpers/coveragepy.ts @@ -0,0 +1,25 @@ +import glob from 'fast-glob' + +import { isProgramInstalled, runExternalProgram } from "./util" +import { info } from './logger' + +export async function generateCoveragePyFile(projectRoot: string, overrideFiles: string[]): Promise { + if (!isProgramInstalled('coverage')) { + return 'coveragepy is not installed' + } + + if (overrideFiles.length > 0) { + return `Skipping coveragepy, files already specified` + } + + const dotCoverage = await glob( + ['.coverage', '.coverage.*'], + {cwd: projectRoot, dot: true, onlyFiles: true}, + ) + if (dotCoverage.length == 0) { + return 'Skipping coveragepy, no .coverage file found.' + } + + info('Running coverage xml...') + return runExternalProgram('coverage', ['xml']); +} diff --git a/packages/canyon-cli/src/helpers/files.ts b/packages/canyon-cli/src/helpers/files.ts new file mode 100755 index 00000000..768dbbec --- /dev/null +++ b/packages/canyon-cli/src/helpers/files.ts @@ -0,0 +1,369 @@ +import { spawnSync } from 'child_process' +import glob from 'fast-glob' +import fs from 'fs' +import { readFile } from 'fs/promises' +import { posix as path } from 'path' +import { UploaderArgs } from '../types' +import { info, logError, UploadLogger } from './logger' +import { runExternalProgram } from './util' +// import micromatch from "../vendor/micromatch/index.js"; +import { SPAWNPROCESSBUFFERSIZE } from './constants' + +export const MARKER_NETWORK_END = '\n<<<<<< network\n' +export const MARKER_FILE_END = '<<<<<< EOF\n' +export const MARKER_ENV_END = '<<<<<< ENV\n' + +const globstar = (pattern: string) => `**/${pattern}` + +/** + * + * @param {string} projectRoot + * @param {Object} args + * @returns {Promise} + */ +export async function getFileListing( + projectRoot: string, + args: UploaderArgs, +): Promise { + return getAllFiles(projectRoot, projectRoot, args).join('\n') +} + +export function manualBlocklist(): string[] { + // TODO: honor the .gitignore file instead of a hard-coded list + return [ + '.DS_Store', + '.circleci', + '.git', + '.gitignore', + '.nvmrc', + '.nyc_output', + 'bower_components', + 'jspm_packages', + 'node_modules', + 'vendor', + ] +} + +function globBlocklist(): string[] { + // TODO: honor the .gitignore file instead of a hard-coded list + return [ + '__pycache__', + 'node_modules/**/*', + 'vendor', + '.circleci', + '.git', + '.gitignore', + '.nvmrc', + '.nyc_output', + '.tox', + '*.am', + '*.bash', + '*.bat', + '*.bw', + '*.cfg', + '*.class', + '*.cmake', + '*.cmake', + '*.conf', + '*.coverage', + '*.cp', + '*.cpp', + '*.crt', + '*.css', + '*.csv', + '*.csv', + '*.data', + '*.db', + '*.dox', + '*.ec', + '*.ec', + '*.egg', + '*.egg-info', + '*.el', + '*.env', + '*.erb', + '*.exe', + '*.ftl', + '*.gif', + '*.go', + '*.gradle', + '*.gz', + '*.h', + '*.html', + '*.in', + '*.jade', + '*.jar*', + '*.jpeg', + '*.jpg', + '*.js', + '*.less', + '*.log', + '*.m4', + '*.mak*', + '*.map', + '*.marker', + '*.md', + '*.o', + '*.p12', + '*.pem', + '*.png', + '*.pom*', + '*.profdata', + '*.proto', + '*.ps1', + '*.pth', + '*.py', + '*.pyc', + '*.pyo', + '*.rb', + '*.rsp', + '*.rst', + '*.ru', + '*.sbt', + '*.scss', + '*.scss', + '*.serialized', + '*.sh', + '*.snapshot', + '*.sql', + '*.svg', + '*.tar.tz', + '*.template', + '*.ts', + '*.whl', + '*.xcconfig', + '*.xcoverage.*', + '*/classycle/report.xml', + '*codecov.yml', + '*~', + '.*coveragerc', + '.coverage*', + 'codecov.SHA256SUM', + 'codecov.SHA256SUM.sig', + 'coverage-summary.json', + 'createdFiles.lst', + 'fullLocaleNames.lst', + 'include.lst', + 'inputFiles.lst', + 'phpunit-code-coverage.xml', + 'phpunit-coverage.xml', + 'remapInstanbul.coverage*.json', + 'scoverage.measurements.*', + 'test-result-*-codecoverage.json', + 'test_*_coverage.txt', + 'testrunner-coverage*', + '*.*js', + '.yarn', + '*.zip', + ] +} + +export function coverageFilePatterns(): string[] { + return [ + '*coverage*.*', + 'nosetests.xml', + 'jacoco*.xml', + 'clover.xml', + 'report.xml', + '*.codecov.!(exe)', + 'codecov.!(exe)', + '*cobertura.xml', + 'excoveralls.json', + 'luacov.report.out', + 'coverage-final.json', + 'naxsi.info', + 'lcov.info', + 'lcov.dat', + '*.lcov', + '*.clover', + 'cover.out', + 'gcov.info', + '*.gcov', + '*.lst', + 'test_cov.xml', + ] +} + +const EMPTY_STRING = '' as const + +const isNegated = (path: string) => path.startsWith('!') + +/** + * + * @param {string} projectRoot + * @param {string[]} coverageFilePatterns + * @returns {Promise} + */ +export async function getCoverageFiles( + projectRoot: string, + coverageFilePatterns: string[], + followSymbolicLinks: boolean = true, +): Promise { + const globstar = (pattern: string) => `**/${pattern}` + + return glob(coverageFilePatterns.map((pattern: string) => { + const parts = [] + + if (isNegated(pattern)) { + parts.push('!') + parts.push(globstar(pattern.substr(1))) + } else { + parts.push(globstar(pattern)) + } + + return parts.join(EMPTY_STRING) + }), { + cwd: projectRoot, + dot: true, + followSymbolicLinks, + ignore: getBlocklist(), + suppressErrors: true, + }) +} + +export function fetchGitRoot(): string { + const currentWorkingDirectory = process.cwd() + try { + const gitRoot = runExternalProgram('git', ['rev-parse', '--show-toplevel']) + return (gitRoot != "" ? gitRoot : currentWorkingDirectory) + } catch (error) { + info(`Error fetching git root. Defaulting to ${currentWorkingDirectory}. Please try using the -R flag. ${error}`) + return currentWorkingDirectory + } +} + +/** + * + * @param {string} projectRoot Root of the project + * @param {string} dirPath Directory to search in + * @param {Object} args + * @returns {string[]} + */ +export function getAllFiles( + projectRoot: string, + dirPath: string, + args: UploaderArgs, +): string[] { + UploadLogger.verbose(`Searching for files in ${dirPath}`) + + const { stdout, status, error } = spawnSync( + 'git', + ['-C', dirPath, 'ls-files'], + { encoding: 'utf8', maxBuffer: SPAWNPROCESSBUFFERSIZE }, + ) + + let files = [] + if (error instanceof Error || status !== 0) { + files = glob + .sync(['**/*', '**/.[!.]*'], { + cwd: dirPath, + ignore: manualBlocklist().map(globstar), + suppressErrors: true, + }) + } else { + files = stdout.split(/[\r\n]+/) + } + + if (args.networkFilter) { + files = files.filter(file => file.startsWith(String(args.networkFilter))) + } + + if (args.networkPrefix) { + files = files.map(file => String(args.networkPrefix) + file) + } + + return files +} + +/** + * + * @param {string} projectRoot + * @param {string} filePath + * @returns {string} + */ +export async function readCoverageFile( + projectRoot: string, + filePath: string, +): Promise { + return readFile(getFilePath(projectRoot, filePath), { + encoding: 'utf-8', + }).catch(err => { + throw new Error(`There was an error reading the coverage file: ${err}`) + }) +} + +/** + * + * @param {string} projectRoot + * @param {string} filePath + * @returns boolean + */ +export function fileExists(projectRoot: string, filePath: string): boolean { + return fs.existsSync(getFilePath(projectRoot, filePath)) +} + +/** + * + * @param {string} filePath + * @returns string + */ +export function fileHeader(filePath: string): string { + return `# path=${filePath}\n` +} + +/** + * + * @param {string} projectRoot + * @param {string} filePath + * @returns {string} + */ +export function getFilePath(projectRoot: string, filePath: string): string { + if ( + filePath.startsWith('./') || + filePath.startsWith('/') || + filePath.startsWith('.\\') || + filePath.startsWith('.\\') || + /^[A-Z]:\\\S*/.test(filePath) // This line is here to handle Windows drive letter absolute paths such as "C:\" + ) { + return filePath + } + if (projectRoot === '.') { + return path.join('.', filePath) + } + return path.join(projectRoot, filePath) +} + +/** + * + * @param {string} projectRoot + * @param {string} filePath + */ +export function removeFile(projectRoot: string, filePath: string): void { + fs.unlink(getFilePath(projectRoot, filePath), err => { + if (err) { + logError(`Error removing ${filePath} coverage file`) + } + }) +} +export function getBlocklist(): string[] { + return [...manualBlocklist(), ...globBlocklist()].map(globstar) +} + +export function filterFilesAgainstBlockList(paths: string[], ignoreGlobs: string[]): string[] { + return paths +} + +export function cleanCoverageFilePaths(projectRoot: string, paths: string[]): string[] { + UploadLogger.verbose(`Preparing to clean the following coverage paths: ${paths.toString()}`) + const coverageFilePaths = [... new Set(paths.filter(file => { + return fileExists(projectRoot, file) + }))] + + if (coverageFilePaths.length === 0) { + logError(`None of the following appear to exist as files: ${paths.toString()}`) + throw new Error('Error while cleaning paths. No paths matched existing files!') + } + + return coverageFilePaths +} + diff --git a/packages/canyon-cli/src/helpers/fixes.ts b/packages/canyon-cli/src/helpers/fixes.ts new file mode 100755 index 00000000..c70fbc11 --- /dev/null +++ b/packages/canyon-cli/src/helpers/fixes.ts @@ -0,0 +1,85 @@ +import fs from 'fs' +import readline from 'readline' + +import { getAllFiles } from './files' +import { UploadLogger } from './logger' + +export const FIXES_HEADER = '# path=fixes\n' + +export async function generateFixes(projectRoot: string): Promise { + // Fake out the UploaderArgs as they are not needed + const allFiles = await getAllFiles(projectRoot, projectRoot, { + flags: '', + slug: '', + upstream: '', + }) + + const allAdjustments: string[] = [] + const EMPTYLINE = /^\s*$/mg + // { or } + const SYNTAXBRACKET = /^\s*[{}]\s*(\/\/.*)?$/m + // [ or ] + const SYNTAXLIST = /^\s*[[\]]\s*(\/\/.*)?$/m + // // + const SYNTAXCOMMENT = /^\s*\/\/.*$/m + // /* or */ + const SYNTAXBLOCK = /^\s*(\/\*|\*\/).*$/m + // func { + const SYNTAXGOFUNC = /^\s*func.*\{\s*$/mg + + for (const file of allFiles) { + let lineAdjustments: string[] = [] + + if ( + file.match(/\.c$/) || + file.match(/\.cpp$/) || + file.match(/\.h$/) || + file.match(/\.hpp$/) || + file.match(/\.m$/) || + file.match(/\.swift$/) || + file.match(/\.vala$/) + ) { + lineAdjustments = await getMatchedLines(file, [EMPTYLINE, SYNTAXBRACKET]) + } else if ( + file.match(/\.php$/) + ) { + lineAdjustments = await getMatchedLines(file, [SYNTAXBRACKET, SYNTAXLIST]) + } else if ( + file.match(/\.go$/) + ) { + lineAdjustments = await getMatchedLines(file, [EMPTYLINE, SYNTAXCOMMENT, SYNTAXBLOCK, SYNTAXBRACKET, SYNTAXGOFUNC]) + } else if ( + file.match(/\.kt$/) + ) { + lineAdjustments = await getMatchedLines(file, [SYNTAXBRACKET, SYNTAXCOMMENT]) + } + + if (lineAdjustments.length > 0) { + UploadLogger.verbose(`Matched file ${file} for adjustments: ${lineAdjustments.join(',')}`) + allAdjustments.push(`${file}:${lineAdjustments.join(',')}\n`) + } + } + return allAdjustments.join('') +} + +async function getMatchedLines(file: string, matchers: RegExp[]): Promise { + const fileStream = fs.createReadStream(file) + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity + }); + + const matchedLines: string[] = [] + let lineNumber = 1 + + for await (const line of rl) { + for (const matcher of matchers) { + if (line.match(matcher)) { + matchedLines.push(lineNumber.toString()) + break + } + } + lineNumber++ + } + return matchedLines +} diff --git a/packages/canyon-cli/src/helpers/gcov.ts b/packages/canyon-cli/src/helpers/gcov.ts new file mode 100755 index 00000000..5eed1930 --- /dev/null +++ b/packages/canyon-cli/src/helpers/gcov.ts @@ -0,0 +1,22 @@ +import glob from 'fast-glob' + +import { manualBlocklist } from '../../src/helpers/files' +import { isProgramInstalled, runExternalProgram } from "./util" + +export async function generateGcovCoverageFiles(projectRoot: string, include: string[] = [], ignore: string[] = [], gcovArgs: string[] = [], gcovExecutable = 'gcov'): Promise { + if (!isProgramInstalled(gcovExecutable)) { + throw new Error(`${gcovExecutable} is not installed, cannot process files`) + } + + const globstar = (pattern: string) => `**/${pattern}` + const gcovInclude = ['*.gcno', ...include].map(globstar) + const gcovIgnore = [...manualBlocklist(), ...ignore].map(globstar) + const files = await glob(gcovInclude, {cwd: projectRoot, dot: true, ignore: gcovIgnore, onlyFiles: true}) + if (!files.length) { + throw new Error('No gcov files found') + } + if (gcovExecutable === 'gcov') { + gcovArgs.unshift('-pb') + } + return runExternalProgram(gcovExecutable, [...gcovArgs, ...files]); +} diff --git a/packages/canyon-cli/src/helpers/git.ts b/packages/canyon-cli/src/helpers/git.ts new file mode 100755 index 00000000..9b4acff2 --- /dev/null +++ b/packages/canyon-cli/src/helpers/git.ts @@ -0,0 +1,38 @@ +import { runExternalProgram } from './util' + +export function parseSlug(slug: string): string { + // origin https://github.com/torvalds/linux.git (fetch) + // git@github.com: canyon / uploader.git + if (typeof slug !== 'string') { + return '' + } + + if (slug.match('http:') || slug.match('https:') || slug.match('ssh:')) { + // Type is http(s) or ssh + const phaseOne = slug.split('//')[1]?.replace('.git', '') || '' + const phaseTwo = phaseOne?.split('/') || '' + const cleanSlug = phaseTwo.length > 2 ? `${phaseTwo[1]}/${phaseTwo[2]}` : '' + return cleanSlug + } else if (slug.match('@')) { + // Type is git + const cleanSlug = slug.split(':')[1]?.replace('.git', '') + return cleanSlug || '' + } + throw new Error(`Unable to parse slug URL: ${slug}`) +} + +export function parseSlugFromRemoteAddr(remoteAddr?: string): string { + let slug = '' + if (!remoteAddr) { + remoteAddr = ( + runExternalProgram('git', ['config', '--get', 'remote.origin.url']) || '' + ) + } + if (remoteAddr) { + slug = parseSlug(remoteAddr) + } + if (slug === '/') { + slug = '' + } + return slug +} diff --git a/packages/canyon-cli/src/helpers/logger.ts b/packages/canyon-cli/src/helpers/logger.ts new file mode 100755 index 00000000..a744df1d --- /dev/null +++ b/packages/canyon-cli/src/helpers/logger.ts @@ -0,0 +1,64 @@ +/** + * We really only need three log levels + * * Error + * * Info + * * Verbose + */ + +function _getTimestamp() { + return new Date().toISOString() +} + +/** + * + * @param {string} message - message to log + * @param {boolean} shouldVerbose - value of the verbose flag + * @return void + */ +export function verbose(message: string, shouldVerbose: boolean): void { + if (shouldVerbose === true) { + console.debug(`[${_getTimestamp()}] ['verbose'] ${message}`) + } +} + +/** + * + * @param {string} message - message to log + * @return void + */ +export function logError(message: string): void { + console.error(`[${_getTimestamp()}] ['error'] ${message}`) +} + +/** + * + * @param {string} message - message to log + * @return void + */ +export function info(message: string): void { + console.log(`[${_getTimestamp()}] ['info'] ${message}`) +} + +export class UploadLogger { + private static _instance: UploadLogger + logLevel = 'info' + + private constructor() { + // Intentionally empty + } + + static getInstance(): UploadLogger { + if (!UploadLogger._instance) { + UploadLogger._instance = new UploadLogger() + } + return UploadLogger._instance; + } + + static setLogLevel(level: string) { + UploadLogger.getInstance().logLevel = level + } + + static verbose(message: string) { + verbose(message, UploadLogger.getInstance().logLevel === 'verbose') + } +} diff --git a/packages/canyon-cli/src/helpers/provider.ts b/packages/canyon-cli/src/helpers/provider.ts new file mode 100755 index 00000000..c19dbc0a --- /dev/null +++ b/packages/canyon-cli/src/helpers/provider.ts @@ -0,0 +1,72 @@ +import providers from '../ci_providers' +import { info, logError, UploadLogger } from '../helpers/logger' +import { IServiceParams, UploaderInputs } from '../types' + +export async function detectProvider( + inputs: UploaderInputs, + hasToken = false, +): Promise> { + const { args } = inputs + let serviceParams: Partial | undefined + + // check if we have a complete set of manual overrides (slug, SHA) + if (args.sha && (args.slug || hasToken)) { + // We have the needed args for a manual override + info(`Using manual override from args.`) + serviceParams = { + commit: args.sha, + ...(hasToken ? {} : { slug: args.slug }), + } + } else { + serviceParams = undefined + } + + // loop though all providers + try { + const serviceParams = await walkProviders(inputs) + return { ...serviceParams, ...serviceParams } + } catch (error) { + // if fails, display message explaining failure, and explaining that SHA and slug need to be set as args + if (typeof serviceParams !== 'undefined') { + logError(`Error detecting repos setting using git: ${error}`) + } else { + throw new Error( + '\nUnable to detect SHA and slug, please specify them manually.\nSee the help for more details.', + ) + } + } + return serviceParams +} + +export async function walkProviders(inputs: UploaderInputs): Promise { + for (const provider of providers) { + if (provider.detect(inputs.envs)) { + info(`Detected ${provider.getServiceName()} as the CI provider.`) + UploadLogger.verbose('-> Using the following env variables:') + for (const envVarName of provider.getEnvVarNames()) { + UploadLogger.verbose(` ${envVarName}: ${inputs.envs[envVarName]}`) + } + return await provider.getServiceParams(inputs) + } + } + throw new Error(`Unable to detect provider.`) +} + +export function setSlug( + slugArg: string | undefined, + orgEnv: string | undefined, + repoEnv: string | undefined, +): string { + if (typeof slugArg !== "undefined" && slugArg !== '') { + return slugArg + } + if ( + typeof orgEnv !== 'undefined' && + typeof repoEnv !== 'undefined' && + orgEnv !== '' && + repoEnv !== '' + ) { + return `${orgEnv}/${repoEnv}` + } + return '' +} diff --git a/packages/canyon-cli/src/helpers/proxy.ts b/packages/canyon-cli/src/helpers/proxy.ts new file mode 100755 index 00000000..356f0b33 --- /dev/null +++ b/packages/canyon-cli/src/helpers/proxy.ts @@ -0,0 +1,37 @@ +import { ProxyAgent } from 'undici'; +import { UploaderArgs, UploaderEnvs } from '../types.js'; +import { logError } from './logger' + +export function getBasicAuthToken(username: string, password: string): string { + const authString = Buffer.from(`${username}:${password}`).toString('base64') + return `Basic ${authString}` +} + +export function removeUrlAuth(url: string | URL): string { + const noAuthUrl = new URL(url) + noAuthUrl.username = '' + noAuthUrl.password = '' + return noAuthUrl.href +} + +export function addProxyIfNeeded(envs: UploaderEnvs, args: UploaderArgs): ProxyAgent | undefined { + if (!args.upstream) { + return undefined + } + + // https://github.com/nodejs/undici/blob/main/docs/api/ProxyAgent.md#example---basic-proxy-request-with-authentication + try { + const proxyUrl = new URL(args.upstream) + if (proxyUrl.username && proxyUrl.password) { + return new ProxyAgent({ + uri: removeUrlAuth(proxyUrl), + token: getBasicAuthToken(proxyUrl.username, proxyUrl.password), + }) + } + return new ProxyAgent({ uri: args.upstream }) + } catch (err) { + logError(`Couldn't set upstream proxy: ${err}`) + } + + return undefined +} diff --git a/packages/canyon-cli/src/helpers/swift.ts b/packages/canyon-cli/src/helpers/swift.ts new file mode 100755 index 00000000..20856114 --- /dev/null +++ b/packages/canyon-cli/src/helpers/swift.ts @@ -0,0 +1,103 @@ +import fs from 'fs' +import * as fsPromise from 'fs/promises' +import glob from 'fast-glob' +import os from 'os' +import path from 'path' + +import { info, UploadLogger } from '../helpers/logger' +import { isProgramInstalled, runExternalProgram } from "./util" + +export async function generateSwiftCoverageFiles(project: string): Promise { + if (!isProgramInstalled('xcrun')) { + throw new Error('xcrun is not installed, cannot process files') + } + info('==> Processing Xcode reports via llvm-cov...') + + const derivedDataDir = `${os.homedir()}/Library/Developer/Xcode/DerivedData` + UploadLogger.verbose(` DerivedData folder: ${derivedDataDir}`) + + if (project == "") { + info(" hint: Speed up Swift processing by using use -J 'AppName'") + } + + const profDataFiles = await glob.sync(['**/*.profdata'], { + cwd: derivedDataDir, + absolute: true, + onlyFiles: true, + }) + + if (profDataFiles.length == 0) { + info(' -> No swift coverage found.') + } else { + info(`Found ${profDataFiles.length} profdata files:`) + } + + for (const profDataFile of profDataFiles) { + info(` ${profDataFile}`) + } + + let outputFiles: string[] = [] + for (const profDataFile of profDataFiles) { + const projOutputFiles = await convertSwiftFile(profDataFile, project) + for (const projOutputFile of projOutputFiles) { + info(`${profDataFile} ${projOutputFile}`) + } + outputFiles = outputFiles.concat(projOutputFiles) + } + return outputFiles +} + +async function convertSwiftFile(profDataFile: string, project: string): Promise { + UploadLogger.verbose(`Starting conversion of ${profDataFile}`) + let dirName = path.dirname(profDataFile) + const BUILD = 'Build' + if (profDataFile.includes(BUILD)) { + dirName = dirName.substr(0, dirName.indexOf(BUILD) + (BUILD.length)) + } + + const outputFiles: string[] = [] + + for (const fileType of ['app', 'framework', 'xctest']) { + const reportDirs = await glob.sync([`**/*.${fileType}`], { + cwd: dirName, + absolute: true, + onlyFiles: false + }) + + if (reportDirs.length == 0) { + continue + } + + for (const reportDir of reportDirs) { + const proj = path.basename(reportDir, `.${fileType}`) + + if (project != "" && proj != project) { + UploadLogger.verbose(` Skipping ${proj} as it does not match project ${project}`) + continue + } + info(` + Building reports for ${proj} ${fileType}`) + UploadLogger.verbose(` Reports sourced from ${reportDir}`) + + let dest = path.join(reportDir, proj) + if (!fs.existsSync(dest)) { + dest = path.join(reportDir, 'Contents', 'MacOS', proj) + } + + const outputFile = `${proj.replace(/\s/g,'')}.${fileType}.coverage.txt` + try { + await fsPromise.writeFile( + outputFile, + runExternalProgram( + 'xcrun', + ['llvm-cov', 'show', '-instr-profile', profDataFile, dest] + ) + ) + info(` Coverage report written to ${outputFile}`) + outputFiles.push(outputFile) + } catch (error) { + info(` Could not write coverage report to ${outputFile}: ${error}`) + } + } + } + return outputFiles +} diff --git a/packages/canyon-cli/src/helpers/token.ts b/packages/canyon-cli/src/helpers/token.ts new file mode 100755 index 00000000..8f3caccf --- /dev/null +++ b/packages/canyon-cli/src/helpers/token.ts @@ -0,0 +1,111 @@ +import fs from 'fs' +import yaml from 'js-yaml' +import path from 'path' +import { UploaderInputs } from '../types' +import { DEFAULT_UPLOAD_HOST } from './constants' +import { info, logError, UploadLogger } from './logger' +import { validateToken } from './validate' + +/** + * + * @param {object} inputs + * @param {string} projectRoot + * @returns string + */ +export function getToken(inputs: UploaderInputs, projectRoot: string): string { + const { args, envs } = inputs + const options = [ + [args.token, 'arguments'], + [envs.CANYON_TOKEN, 'environment variables'], + [getTokenFromYaml(projectRoot), 'Canyon yaml config'], + ] + + for (const [token, source] of options) { + if (token) { + info(`-> Token found by ${source}`) + // If this is self-hosted (-u is set), do not validate + // This is because self-hosted can use a global upload token + if (args.url !== DEFAULT_UPLOAD_HOST) { + UploadLogger.verbose('Self-hosted install detected due to -u flag') + info(`-> Token set by ${source}`) + return token + } + if (validateToken(token) !== true) { + throw new Error( + `Token found by ${source} with length ${token?.length} did not pass validation`, + ) + } + + return token + } + } + + return '' +} + +interface ICanyonYAML { + canyon?: { + token?: string + } + canyon_token?: string +} + +// eslint-disable-next-line @typescript-eslint/ban-types +function yamlParse(input: object | string | number): ICanyonYAML { + let yaml: ICanyonYAML + if (typeof input === 'string') { + yaml = JSON.parse(input) + } else if (typeof input === 'number') { + yaml = JSON.parse(input.toString()) + } else { + yaml = input + } + return yaml +} + +export function getTokenFromYaml( + projectRoot: string, +): string { + const dirNames = ['', '.github', 'dev'] + + const yamlNames = [ + '.canyon.yaml', + '.canyon.yml', + 'canyon.yaml', + 'canyon.yml', + ] + + for (const dir of dirNames) { + for (const name of yamlNames) { + const filePath = path.join(projectRoot, dir, name) + + try { + if (fs.existsSync(filePath)) { + const fileContents = fs.readFileSync(filePath, { + encoding: 'utf-8', + }) + const yamlConfig: ICanyonYAML = yamlParse( + new Object(yaml.load(fileContents, { json: true }) || {}, + )) + if ( + yamlConfig['canyon'] && + yamlConfig['canyon']['token'] && + validateToken(yamlConfig['canyon']['token']) + ) { + return yamlConfig['canyon']['token'] + } + + if (yamlConfig['canyon_token']) { + logError( + `'canyon_token' is a deprecated field. Please switch to 'canyon.token' ` + + '', + ) + } + } + } catch (err) { + UploadLogger.verbose(`Error searching for upload token in ${filePath}: ${err}`) + } + } + } + return '' +} diff --git a/packages/canyon-cli/src/helpers/util.ts b/packages/canyon-cli/src/helpers/util.ts new file mode 100755 index 00000000..f9fd9f66 --- /dev/null +++ b/packages/canyon-cli/src/helpers/util.ts @@ -0,0 +1,36 @@ +import childprocess from 'child_process' +import { SPAWNPROCESSBUFFERSIZE } from './constants' + + +export function isProgramInstalled(programName: string): boolean { + return !childprocess.spawnSync(programName).error +} + +export function runExternalProgram( + programName: string, + optionalArguments: string[] = [], +): string { + const result = childprocess.spawnSync( + programName, + optionalArguments, + { maxBuffer: SPAWNPROCESSBUFFERSIZE }, + ) + if (result.error) { + throw new Error(`Error running external program: ${result.error}`) + } + return result.stdout.toString().trim() +} + +export function isSetAndNotEmpty(val: string | undefined): boolean { + return typeof val !== 'undefined' && val !== '' +} + +export function argAsArray(args?: T | T[]): T[] { + const result: T[] = [] + if (typeof args === "undefined") { + return result + } + return result.concat(args) +} + +export function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)); } diff --git a/packages/canyon-cli/src/helpers/validate.ts b/packages/canyon-cli/src/helpers/validate.ts new file mode 100755 index 00000000..a4d2dba0 --- /dev/null +++ b/packages/canyon-cli/src/helpers/validate.ts @@ -0,0 +1,55 @@ +import validator from 'validator' + +/** + * + * @param {string} token + * @returns boolean + */ +export function validateToken(token: string): boolean { + // TODO: this should be refactored to check against format and length + return validator.isAlphanumeric(token) || validator.isUUID(token) +} + +export function validateURL(url: string): boolean { + return validator.isURL(url, { require_protocol: true }) +} + +export function isValidFlag(flag: string): boolean { + // eslint-disable-next-line no-useless-escape + const mask = /^[\w\.\-]{1,45}$/ + return flag.length === 0 || mask.test(flag) +} + +export function validateFlags(flags: string[]): void { + const invalidFlags = flags.filter(flag => isValidFlag(flag) !== true) + + if (invalidFlags.length > 0) { + throw new Error( + `Flags must consist only of alphanumeric characters, '_', '-', or '.' and not exceed 45 characters. Received ${flags}`, + ) + } +} + +/** + * Validate that a SHA is the correct length and content + * @param {string} commitSHA + * @param {number} requestedLength + * @returns {boolean} + */ +const GIT_SHA_LENGTH = 40 + +export function validateSHA( + commitSHA: string, + requestedLength = GIT_SHA_LENGTH, +): boolean { + return ( + commitSHA.length === requestedLength && validator.isAlphanumeric(commitSHA) + ) +} + +export function checkSlug(slug: string): boolean { + if (slug !== '' && !slug.match(/\//)) { + return false + } + return true +} diff --git a/packages/canyon-cli/src/helpers/web.ts b/packages/canyon-cli/src/helpers/web.ts new file mode 100755 index 00000000..7243d6f8 --- /dev/null +++ b/packages/canyon-cli/src/helpers/web.ts @@ -0,0 +1,251 @@ +import dns from 'node:dns' +import { snakeCase } from 'snake-case' +import { request, setGlobalDispatcher, errors, Dispatcher } from 'undici' + +import { version } from '../../package.json' +import { + IRequestHeaders, + IServiceParams, + PostResults, + PutResults, + UploaderArgs, + UploaderEnvs, + UploaderInputs, +} from '../types' +import { UploadLogger, info, logError } from './logger' +import { addProxyIfNeeded } from './proxy' +import { sleep } from './util' + + +const maxRetries = 4 +const baseBackoffDelayMs = 1000 // Adjust this value based on your needs. + +/** + * + * @param {Object} inputs + * @param {NodeJS.ProcessEnv} inputs.envs + * @param {Object} serviceParams + * @returns Object + */ +export function populateBuildParams( + inputs: UploaderInputs, + serviceParams: Partial, +): Partial { + const { args, envs } = inputs + serviceParams.name = args.name || envs.CANYON_NAME || '' + serviceParams.tag = args.tag || '' + + if (typeof args.flags === 'string') { + serviceParams.flags = args.flags + } else { + serviceParams.flags = args.flags.join(',') + } + + serviceParams.parent = args.parent || '' + return serviceParams +} + +export function getPackage(source: string): string { + if (source) { + return `${source}-canyon-cli-${version}` + } else { + return `canyon-cli-${version}` + } +} + +async function requestWithRetry( + url: string, + options: Dispatcher.RequestOptions, + retryCount = 0, +): Promise { + try { + const response = await request(url, options) + return response + } catch (error: unknown) { + if ( + ( + (error instanceof errors.UndiciError && error.code == 'ECONNRESET') || + (error instanceof errors.UndiciError && error.code == 'ETIMEDOUT') || + error instanceof errors.ConnectTimeoutError || + error instanceof errors.SocketError + ) && retryCount < maxRetries + ) { + const backoffDelay = baseBackoffDelayMs * 2 ** retryCount + await sleep(backoffDelay) + UploadLogger.verbose('Request to Canyon failed. Retrying...') + logError(`Request error: ${error.message}`) + return requestWithRetry(url, options, retryCount + 1) + } + throw error + } +} + +export async function uploadToCanyonPUT( + putAndResultUrlPair: PostResults, + uploadFile: string | Buffer, + envs: UploaderEnvs, + args: UploaderArgs, +): Promise { + info('Uploading...') + + const requestHeaders = generateRequestHeadersPUT( + putAndResultUrlPair.putURL, + uploadFile, + envs, + args, + ) + if (requestHeaders.agent) { + setGlobalDispatcher(requestHeaders.agent) + } + dns.setDefaultResultOrder('ipv4first') + const response = await requestWithRetry( + requestHeaders.url.origin, + requestHeaders.options, + ) + + if (response.statusCode !== 200) { + const data = await response.body.text() + throw new Error( + `There was an error fetching the storage URL during PUT: ${response.statusCode} - ${data}`, + ) + } + + return { status: 'processing', resultURL: putAndResultUrlPair.resultURL } +} + +export async function uploadToCanyonPOST( + postURL: URL, + token: string, + query: string, + source: string, + envs: UploaderEnvs, + args: UploaderArgs, +): Promise { + const requestHeaders = generateRequestHeadersPOST( + postURL, + token, + query, + source, + envs, + args, + ) + if (requestHeaders.agent) { + setGlobalDispatcher(requestHeaders.agent) + } + dns.setDefaultResultOrder('ipv4first') + + const response = await requestWithRetry( + requestHeaders.url.origin, + requestHeaders.options, + ) + + if (response.statusCode !== 200) { + const data = await response.body.text() + throw new Error( + `There was an error fetching the storage URL during POST: ${response.statusCode} - ${data}`, + ) + } + + return await response.body.text() +} +/** + * + * @param {Object} queryParams + * @returns {string} + */ +export function generateQuery(queryParams: Partial): string { + return new URLSearchParams( + Object.entries(queryParams).map(([key, value]) => [snakeCase(key), value]), + ).toString() +} + +export function parsePOSTResults(putAndResultUrlPair: string): PostResults { + info(putAndResultUrlPair) + + // JS for [[:graph:]] https://www.regular-expressions.info/posixbrackets.html + const re = /([\x21-\x7E]+)[\r\n]?/gm + + const matches = putAndResultUrlPair.match(re) + + if (matches === null) { + throw new Error( + `Parsing results from POST failed: (${putAndResultUrlPair})`, + ) + } + + if (matches?.length !== 2) { + throw new Error( + `Incorrect number of urls when parsing results from POST: ${matches.length}`, + ) + } + + if (matches[0] === undefined || matches[1] === undefined) { + throw new Error( + `Invalid URLs received when parsing results from POST: ${matches[0]},${matches[1]}`, + ) + } + const resultURL = new URL(matches[0].trimEnd()) + const putURL = new URL(matches[1]) + // This match may have trailing 0x0A and 0x0D that must be trimmed + + return { putURL, resultURL } +} + +export function displayChangelog(): void { + // info(`The change log for this version (v${version}) can be found at`) + // info(`https://github.com/canyon/uploader/blob/v${version}/CHANGELOG.md`) +} + +export function generateRequestHeadersPOST( + postURL: URL, + token: string, + query: string, + source: string, + envs: UploaderEnvs, + args: UploaderArgs, +): IRequestHeaders { + const url = new URL( + `upload/v4?package=${getPackage(source)}&token=${token}&${query}`, + postURL, + ) + + const headers = { + 'X-Upload-Token': token, + 'X-Reduced-Redundancy': 'false', + } + + return { + agent: addProxyIfNeeded(envs, args), + url: url, + options: { + headers, + method: 'POST', + origin: postURL, + path: `${url.pathname}${url.search}`, + }, + } +} + +export function generateRequestHeadersPUT( + uploadURL: URL, + uploadFile: string | Buffer, + envs: UploaderEnvs, + args: UploaderArgs, +): IRequestHeaders { + const headers = { + 'Content-Type': 'text/plain', + 'Content-Encoding': 'gzip', + } + + return { + agent: addProxyIfNeeded(envs, args), + url: uploadURL, + options: { + body: uploadFile, + headers, + method: 'PUT', + origin: uploadURL, + path: `${uploadURL.pathname}${uploadURL.search}`, + }, + } +} diff --git a/packages/canyon-cli/src/helpers/xcode.ts b/packages/canyon-cli/src/helpers/xcode.ts new file mode 100755 index 00000000..5188ce47 --- /dev/null +++ b/packages/canyon-cli/src/helpers/xcode.ts @@ -0,0 +1,63 @@ +import fs from 'fs/promises' + +import { info, UploadLogger } from '../helpers/logger' +import { + XcodeCoverageFileReport, + XcodeCoverageReport, +} from '../types' +import { isProgramInstalled, runExternalProgram } from "./util" + +export async function generateXcodeCoverageFiles(archivePath: string): Promise { + if (!isProgramInstalled('xcrun')) { + throw new Error('xcrun is not installed, cannot process files') + } + info('Running xcode coversion...') + + const coverage: XcodeCoverageReport = {} + const report = { coverage: coverage } + + getFileList(archivePath).forEach(repoFilePath => { + UploadLogger.verbose(`Converting ${repoFilePath}...`) + const coverageInfo = getCoverageInfo(archivePath, repoFilePath) + const coverageJson = convertCoverage(coverageInfo) + report.coverage[repoFilePath] = coverageJson + }) + + let pathFilename = archivePath.split('/').pop() + if (pathFilename) { + pathFilename = pathFilename.split('.xcresult')[0] + } + const filename = `./coverage-report-${pathFilename}.json` + UploadLogger.verbose(`Writing coverage to ${filename}`) + await fs.writeFile(filename, JSON.stringify(report)) + return filename +} + +function getFileList(archivePath: string): string[] { + const fileList = runExternalProgram('xcrun', ['xccov', 'view', '--file-list', '--archive', archivePath]); + return fileList.split('\n').filter(i => i !== '') +} + +function getCoverageInfo(archivePath: string, filePath: string): string { + return runExternalProgram('xcrun', ['xccov', 'view', '--archive', archivePath, '--file', filePath]) +} + +function convertCoverage(coverageInfo: string): XcodeCoverageFileReport { + const coverageInfoArr = coverageInfo.split('\n') + const obj: XcodeCoverageFileReport = {} + coverageInfoArr.forEach(line => { + const [lineNum, lineInfo] = line.split(':') + if (lineNum && Number.isInteger(Number(lineNum))) { + const lineHits = lineInfo?.trimStart().split(' ')[0]?.trim() + if (typeof lineHits !== 'string') { + return + } + if (lineHits === '*') { + obj[String(lineNum.trim())] = null + } else { + obj[String(lineNum.trim())] = lineHits + } + } + }) + return obj +} diff --git a/packages/canyon-cli/src/index.ts b/packages/canyon-cli/src/index.ts new file mode 100755 index 00000000..7b2ec791 --- /dev/null +++ b/packages/canyon-cli/src/index.ts @@ -0,0 +1,469 @@ +import { UploaderArgs, UploaderInputs } from './types' + +import fs from 'fs' +import zlib from 'zlib' +import { version } from '../package.json' +import { detectProvider } from './helpers/provider' +import * as webHelpers from './helpers/web' +import { info, UploadLogger } from './helpers/logger' +import { getToken } from './helpers/token' +import { + cleanCoverageFilePaths, + coverageFilePatterns, + fetchGitRoot, + fileHeader, + filterFilesAgainstBlockList, + getBlocklist, + getCoverageFiles, + getFileListing, + getFilePath, + MARKER_ENV_END, + MARKER_FILE_END, + MARKER_NETWORK_END, + readCoverageFile, + removeFile, +} from './helpers/files' +import { generateCoveragePyFile } from './helpers/coveragepy' +import { generateFixes, FIXES_HEADER } from './helpers/fixes' +import { generateGcovCoverageFiles } from './helpers/gcov' +import { generateSwiftCoverageFiles } from './helpers/swift' +import { generateXcodeCoverageFiles } from './helpers/xcode' +import { argAsArray } from './helpers/util' +import { checkSlug, validateFlags } from './helpers/validate' + +/** + * + * @param {string} uploadHost + * @param {string} token + * @param {string} query + * @param {string} uploadFile + * @param {string} source + */ +function dryRun( + uploadHost: string, + token: string, + query: string, + uploadFile: string, + source: string, +) { + info('==> Dumping upload file (no upload)') + info( + `${uploadHost}/upload/v4?package=${webHelpers.getPackage( + source, + )}&token=${token}&${query}`, + ) + info(uploadFile) +} + +/** + * + * @param {Object} args + * @param {string} args.build Specify the build number manually + * @param {string} args.branch Specify the branch manually + * @param {string} args.dir Directory to search for coverage reports. + * @param {string} args.env Specify environment variables to be included with this build + * @param {string} args.sha Specify the commit SHA manually + * @param {string} args.file Target file(s) to upload + * @param {string} args.flags Flag the upload to group coverage metrics + * @param {string} args.name Custom defined name of the upload. Visible in Canyon UI + * @param {string} args.networkFilter Specify a filter on the files listed in the network section of the Canyon report. Useful for upload-specific path fixing + * @param {string} args.networkPrefix Specify a prefix on files listed in the network section of the Canyon report. Useful to help resolve path fixing + * @param {string} args.parent The commit SHA of the parent for which you are uploading coverage. + * @param {string} args.pr Specify the pull request number manually + * @param {string} args.token Canyon upload token + * @param {string} args.tag Specify the git tag + * @param {boolean} args.swift Specify whether to use swift conversion + * @param {string} args.swiftProject Specific swift project to convert + * @param {boolean} args.verbose Run with verbose logging + * @param {string} args.rootDir Specify the project root directory when not in a git repo + * @param {boolean} args.nonZero Should errors exit with a non-zero (default: false) + * @param {boolean} args.dryRun Don't upload files to Canyon + * @param {string} args.slug Specify the slug manually + * @param {string} args.url Change the upload host (Enterprise use) + * @param {boolean} args.clean Move discovered coverage reports to the trash + * @param {string} args.feature Toggle features + * @param {string} args.source Track wrappers of the uploader + */ +export async function main( + args: UploaderArgs, +): Promise> { + + if (args.verbose) { + UploadLogger.setLogLevel('verbose') + } + + // Did user asking for changelog? + if (args.changelog) { + webHelpers.displayChangelog() + return + } + + /* + Step 1: validate and sanitize inputs + Step 2: detect if we are in a git repo + Step 3: sanitize and set token + Step 4: get network (file listing) + Step 5: select coverage files (search or specify) + Step 6: generate upload file + Step 7: determine CI provider + Step 8: either upload or dry-run + */ + + // #region == Step 1: validate and sanitize inputs + // TODO: clean and sanitize envs and args + const envs = process.env + // args + const inputs: UploaderInputs = { args, envs } + + let uploadHost: string + if (args.url) { + uploadHost = args.url + } else { + uploadHost = 'https://canyon.io' + } + + info(generateHeader(getVersion())) + + let flags: string[] + if (typeof args.flags === 'object') { + flags = [...args.flags] + } else { + flags = String(args.flags || '').split(',') + } + + validateFlags(flags) + + // #endregion + // #region == Step 2: detect if we are in a git repo + const projectRoot = args.rootDir || fetchGitRoot() + if (projectRoot === '') { + info( + '=> No git repo detected. Please use the -R flag if the below detected directory is not correct.', + ) + } + + info(`=> Project root located at: ${projectRoot}`) + + // #endregion + // #region == Step 3: sanitize and set token + const token = await getToken(inputs, projectRoot) + if (token === '') { + info('-> No token specified or token is empty') + } + + // #endregion + // #region == Step 4: get network + const uploadFileChunks: Buffer[] = [] + + if (!args.fullReport) { + if (!args.feature || args.feature.split(',').includes('network') === false) { + UploadLogger.verbose('Start of network processing...') + let fileListing = '' + try { + fileListing = await getFileListing(projectRoot, args) + } catch (error) { + throw new Error(`Error getting file listing: ${error}`) + } + + uploadFileChunks.push(Buffer.from(fileListing)) + uploadFileChunks.push(Buffer.from(MARKER_NETWORK_END)) + } + + // #endregion + // #region == Step 5: select coverage files (search or specify) + + let requestedPaths: string[] = [] + + // Look for files + + if (args.gcov) { + const gcovInclude: string[] = argAsArray(args.gcovInclude) + const gcovIgnore: string[] = argAsArray(args.gcovIgnore) + const gcovArgs: string[] = argAsArray(args.gcovArgs) + const gcovExecutable: string = args.gcovExecutable || 'gcov' + + UploadLogger.verbose(`Running ${gcovExecutable}...`) + const gcovLogs = await generateGcovCoverageFiles(projectRoot, gcovInclude, gcovIgnore, gcovArgs, gcovExecutable) + UploadLogger.verbose(`${gcovLogs}`) + } + + if (args.swift) { + await generateSwiftCoverageFiles(args.swiftProject || '') + } + + if (args.xcode) { + if (!args.xcodeArchivePath) { + throw new Error('Please specify xcodeArchivePath to run the Canyon uploader with xcode support') + } else { + const xcodeArchivePath: string = args.xcodeArchivePath + const xcodeLogs = await generateXcodeCoverageFiles(xcodeArchivePath) + UploadLogger.verbose(`${xcodeLogs}`) + } + } + + let coverageFilePaths: string[] = [] + if (args.file !== undefined) { + if (typeof args.file === 'string') { + requestedPaths = args.file.split(',') + } else { + requestedPaths = args.file // Already an array + } + + requestedPaths = requestedPaths.filter((path) => { + return Boolean(path) || info('Warning: Skipping an empty path passed to `-f`') + }) + } + + try { + const coveragePyLogs = await generateCoveragePyFile(projectRoot, requestedPaths) + UploadLogger.verbose(`${coveragePyLogs}`) + } catch (error) { + UploadLogger.verbose(`Skipping coveragepy conversion: ${error}`) + } + + coverageFilePaths = requestedPaths + + + if (!args.feature || args.feature.split(',').includes('search') === false) { + info('Searching for coverage files...') + const isNegated = (path: string) => path.startsWith('!') + coverageFilePaths = coverageFilePaths.concat(await getCoverageFiles( + args.dir || projectRoot, + (() => { + const numOfNegatedPaths = coverageFilePaths.filter(isNegated).length + + if (coverageFilePaths.length > numOfNegatedPaths) { + return coverageFilePaths + } else { + return coverageFilePaths.concat(coverageFilePatterns()) + } + })(), + !args.preventSymbolicLinks, + )) + + // Generate what the file listing would be after the blocklist is applied + + let coverageFilePathsAfterFilter = coverageFilePaths + + if (coverageFilePaths.length > 0) { + coverageFilePathsAfterFilter = filterFilesAgainstBlockList(coverageFilePaths, getBlocklist()) + } + + + + + // If args.file was passed, emit warning for 'filtered' filess + + if (requestedPaths.length > 0) { + if (coverageFilePathsAfterFilter.length !== requestedPaths.length) { + info('Warning: Some files passed via the -f flag would normally be excluded from search.') + // info('If Canyon encounters issues processing your reports, please review https://docs.canyon.com/docs/supported-report-formats') + } + } else { + // Overwrite coverageFilePaths with coverageFilePathsAfterFilter + info('Warning: Some files located via search were excluded from upload.') + // info('If Canyon did not locate your files, please review https://docs.canyon.com/docs/supported-report-formats') + + coverageFilePaths = coverageFilePathsAfterFilter + } + + } + + let coverageFilePathsThatExist: string[] = [] + + if (coverageFilePaths.length > 0) { + coverageFilePathsThatExist = cleanCoverageFilePaths(args.dir || projectRoot, coverageFilePaths) + } + + if (coverageFilePathsThatExist.length > 0) { + info(`=> Found ${coverageFilePathsThatExist.length} possible coverage files:\n ` + + coverageFilePathsThatExist.join('\n ')) + } else { + const noFilesError = args.file ? + 'No coverage files found, exiting.' : + 'No coverage files located, please try use `-f`, or change the project root with `-R`' + throw new Error(noFilesError) + } + + UploadLogger.verbose('End of network processing') + // #endregion + // #region == Step 6: generate upload file + // TODO: capture envs + + // Get coverage report contents + let coverageFileAdded = false + for (const coverageFile of coverageFilePathsThatExist) { + let fileContents + try { + info(`Processing ${getFilePath(args.dir || projectRoot, coverageFile)}...`), + (fileContents = await readCoverageFile( + args.dir || projectRoot, + coverageFile, + )) + } catch (err) { + info(`Could not read coverage file (${coverageFile}): ${err}`) + continue + } + + uploadFileChunks.push(Buffer.from(fileHeader(coverageFile))) + uploadFileChunks.push(Buffer.from(fileContents)) + uploadFileChunks.push(Buffer.from(MARKER_FILE_END)) + coverageFileAdded = true + } + if (!coverageFileAdded) { + throw new Error( 'No coverage files could be found to upload, exiting.') + } + + // Environment variables + if (args.env || envs.CANYON_ENV) { + const environmentVars = args.env || envs.CANYON_ENV || '' + const vars = environmentVars + .split(',') + .filter(Boolean) + .map(evar => `${evar}=${process.env[evar] || ''}\n`) + .join('') + uploadFileChunks.push(Buffer.from(vars)) + uploadFileChunks.push(Buffer.from(MARKER_ENV_END)) + } + + // Fixes + if (args.feature && args.feature.split(',').includes('fixes') === true) { + info('Generating file fixes...') + const fixes = await generateFixes(projectRoot) + uploadFileChunks.push(Buffer.from(FIXES_HEADER)) + uploadFileChunks.push(Buffer.from(fixes)) + uploadFileChunks.push(Buffer.from(MARKER_FILE_END)) + info('Finished generating file fixes') + } + + // Cleanup + if (args.clean) { + for (const coverageFile of coverageFilePathsThatExist) { + removeFile(args.dir || projectRoot, coverageFile) + } + } + } else { + const fullPath = getFilePath(args.dir || projectRoot, args.fullReport) + if (!fs.existsSync(fullPath)) { + throw new Error(`Error uploading to Canyon: Path to ${args.fullReport} does not exist and no coverage report could be uploaded`) + } + uploadFileChunks.push(fs.readFileSync(fullPath)) + + // Cleanup + if (args.clean) { + removeFile(args.dir || projectRoot, args.fullReport) + } + } + + const uploadFile = Buffer.concat(uploadFileChunks) + const gzippedFile = zlib.gzipSync(uploadFile) + + // #endregion + // #region == Step 7: determine CI provider + + const hasToken = token !== '' + + const serviceParams = await detectProvider(inputs, hasToken) + + // #endregion + // #region == Step 8: either upload or dry-run + + const buildParams = webHelpers.populateBuildParams(inputs, serviceParams) + + UploadLogger.verbose('Using the following upload parameters:') + for (const parameter in buildParams) { + UploadLogger.verbose(`${parameter}`) + } + + if (!hasToken) { + if (!buildParams.slug) { + throw new Error( + 'Slug must be set if a token is not passed. Consider passing a slug via `-r`', + ) + } else { + const validSlug = checkSlug(buildParams.slug) + if (!validSlug) { + throw new Error( + `Slug must follow the format of "/". We detected "${buildParams.slug}"`, + ) + } + } + } + + const query = webHelpers.generateQuery({ + ...buildParams, + instrumentCwd: projectRoot, + }) + + if (args.dryRun) { + dryRun(uploadHost, token, query, uploadFile.toString(), args.source || '') + return + } + + info( + `Pinging Canyon: ${uploadHost}/upload/v4?package=${webHelpers.getPackage( + args.source || '', + )}&token=*******&${query}`, + ) + UploadLogger.verbose(`Passed token was ${token.length} characters long`) + try { + UploadLogger.verbose( + `${uploadHost}/upload/v4?package=${webHelpers.getPackage( + args.source || '', + )}&${query} + Content-Type: 'text/plain' + Content-Encoding: 'gzip' + X-Reduced-Redundancy: 'false'` + ) + + const postURL = new URL(uploadHost) + + const putAndResultUrlPair = await webHelpers.uploadToCanyonPOST( + postURL, + token, + query, + args.source || '', + envs, + args, + ) + + const postResults = webHelpers.parsePOSTResults(putAndResultUrlPair) + + UploadLogger.verbose(`Returned upload url: ${postResults.putURL}`) + + const statusAndResultPair = await webHelpers.uploadToCanyonPUT( + postResults, + gzippedFile, + envs, + args, + ) + info(JSON.stringify(statusAndResultPair)) + return {resultURL: statusAndResultPair.resultURL.href, status: statusAndResultPair.status } + } catch (error) { + throw new Error(`Error uploading to ${uploadHost}: ${error}`) + } + // #endregion +} + +/** + * + * @param {string} version + * @returns {string} + */ +export function generateHeader(version: string): string { + return ` + ____ + / ___|__ _ _ __ _ _ ___ _ __ + | | / _\` | '_ \\| | | |/ _ \\| '_ \\ + | |__| (_| | | | | |_| | (_) | | | | + \\____\\__,_|_| |_|\\__, |\\___/|_| |_| + |___/ + + Canyon report uploader ${version}` +} + +export function getVersion(): string { + return version +} + +export { logError, info, verbose } from './helpers/logger' diff --git a/packages/canyon-cli/src/types.ts b/packages/canyon-cli/src/types.ts new file mode 100755 index 00000000..26c02d1b --- /dev/null +++ b/packages/canyon-cli/src/types.ts @@ -0,0 +1,90 @@ +import { Dispatcher, ProxyAgent } from "undici"; + +export interface UploaderArgs { + branch?: string // Specify the branch manually + build?: string // Specify the build number manually + changelog?: string // Displays the changelog and exits + clean?: string // Move discovered coverage reports to the trash + dir?: string // Directory to search for coverage reports. + dryRun?: string // Don't upload files to Canyon + env?: string // Specify environment variables to be included with this build + feature?: string // Toggle features + file?: string | string[] // Target file(s) to upload + flags: string | string[] // Flag the upload to group coverage metrics + fullReport?: string // Specify the path to a previously uploaded Canyon report + gcov?: string // Run with gcov support + gcovArgs?: string | string[] // Extra arguments to pass to gcov + gcovIgnore?: string | string[] // Paths to ignore during gcov gathering + gcovInclude?: string | string[] // Paths to include during gcov gathering + gcovExecutable?: string // gcov executable to run. + name?: string // Custom defined name of the upload. Visible in Canyon UI + networkFilter?: string // Specify a prefix on the files listed in the network section of the Canyon report. Useful for upload-specific path fixing + networkPrefix?: string // Specify a prefix on files listed in the network section of the Canyon report. Useful to help resolve path fixing + nonZero?: string // Should errors exit with a non-zero (default: false) + parent?: string // The commit SHA of the parent for which you are uploading coverage. + pr?: string // Specify the pull request number manually + preventSymbolicLinks?: string // Specifies whether to prevent following of symoblic links + rootDir?: string // Specify the project root directory when not in a git repo + sha?: string // Specify the commit SHA manually + slug: string // Specify the slug manually + source?: string // Track wrappers of the uploader + swift?: string // Run with swift support + swiftProject?: string // Specify the swift project + tag?: string // Specify the git tag + token?: string // Canyon upload token + upstream: string // Upstream proxy to connect to + url?: string // Change the upload host (Enterprise use) + verbose?: string // Run with verbose logging + xcode?: string // Run with xcode support + xcodeArchivePath?: string // Specify the xcode archive path. Likely specified as the -resultBundlePath and should end in .xcresult +} + +export type UploaderEnvs = NodeJS.Dict + +export interface UploaderInputs { + envs: UploaderEnvs + args: UploaderArgs +} + +export interface IProvider { + detect: (arg0: UploaderEnvs) => boolean + getServiceName: () => string + getServiceParams: (arg0: UploaderInputs) => Promise + getEnvVarNames: () => string[] +} + +export interface IServiceParams { + branch: string + build: string + buildURL: string + commit: string + job: string + pr: string | '' + service: string + slug: string + name?: string + tag?: string + flags?: string + parent?: string + project?: string + server_uri?: string +} + +export interface IRequestHeaders { + agent?: ProxyAgent + url: URL + options: Dispatcher.RequestOptions +} + +export interface PostResults { + putURL: URL + resultURL: URL +} + +export interface PutResults { + status: string + resultURL: URL +} + +export type XcodeCoverageFileReport = Record +export type XcodeCoverageReport = Record diff --git a/packages/canyon-cli/tsconfig.json b/packages/canyon-cli/tsconfig.json new file mode 100755 index 00000000..cf45c4ef --- /dev/null +++ b/packages/canyon-cli/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESnext", + "module": "commonjs", + "declaration": true, + "allowJs": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "./", + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "noUncheckedIndexedAccess": true + }, + "exclude": ["dist/*", "test/*"] +} diff --git a/packages/canyon-data/.gitignore b/packages/canyon-data/.gitignore new file mode 100755 index 00000000..a547bf36 --- /dev/null +++ b/packages/canyon-data/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/canyon-data/README.md b/packages/canyon-data/README.md new file mode 100755 index 00000000..e69de29b diff --git a/packages/canyon-data/package.json b/packages/canyon-data/package.json new file mode 100755 index 00000000..015ae412 --- /dev/null +++ b/packages/canyon-data/package.json @@ -0,0 +1,30 @@ +{ + "name": "@canyon/data", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "dist/canyon-data.cjs", + "module": "dist/canyon-data.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/*" + ], + "scripts": { + "build": "vite build", + "prepare": "vite build", + "do-test": "vitest run" + }, + "devDependencies": { + "@types/istanbul-lib-coverage": "^2.0.5", + "@types/istanbul-lib-source-maps": "^4.0.4", + "@types/node": "^20.3.1", + "typescript": "^5.0.2", + "vite": "^4.4.5", + "vite-plugin-dts": "^3.5.3", + "vitest": "^0.34.5" + }, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-source-maps": "^4.0.1" + } +} diff --git a/packages/canyon-data/src/coverage/index.ts b/packages/canyon-data/src/coverage/index.ts new file mode 100755 index 00000000..66a99f7f --- /dev/null +++ b/packages/canyon-data/src/coverage/index.ts @@ -0,0 +1,12 @@ +import libCoverage, {CoverageMapData} from "istanbul-lib-coverage"; +/** + * 合并两个覆盖率数据 + * @param first 第一个覆盖率数据 + * @param second 第二个覆盖率数据 + * @returns 合并过后的覆盖率数据 + */ +export function mergeCoverageMap(first:CoverageMapData, second:CoverageMapData) { + const map = libCoverage.createCoverageMap(JSON.parse(JSON.stringify(first))); + map.merge(second); + return JSON.parse(JSON.stringify(map.toJSON())); +} diff --git a/packages/canyon-data/src/coverage/test/coverage.test.ts b/packages/canyon-data/src/coverage/test/coverage.test.ts new file mode 100755 index 00000000..ae48d21b --- /dev/null +++ b/packages/canyon-data/src/coverage/test/coverage.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from 'vitest' +import mockCoverageData from './mock-coverage-data.json' +import mockCoverageDataMerged from './mock-coverage-data-merged.json' + +import {mergeCoverageMap} from "../index.ts"; +test('测试mergeCoverageMap方法', () => { + expect(mergeCoverageMap(mockCoverageData,mockCoverageData)).toMatchObject(mockCoverageDataMerged) +}) diff --git a/packages/canyon-data/src/coverage/test/mock-coverage-data-merged.json b/packages/canyon-data/src/coverage/test/mock-coverage-data-merged.json new file mode 100755 index 00000000..f37b983e --- /dev/null +++ b/packages/canyon-data/src/coverage/test/mock-coverage-data-merged.json @@ -0,0 +1,536 @@ +{ + "/builds/canyon/canyon-demo/src/pages/Welcome.tsx": { + "path": "/builds/canyon/canyon-demo/src/pages/Welcome.tsx", + "statementMap": { + "0": { + "start": { + "line": 3, + "column": 16 + }, + "end": { + "line": 10, + "column": 1 + } + }, + "1": { + "start": { + "line": 4, + "column": 17 + }, + "end": { + "line": 4, + "column": 30 + } + }, + "2": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 9, + "column": 6 + } + }, + "3": { + "start": { + "line": 7, + "column": 8 + }, + "end": { + "line": 7, + "column": 20 + } + } + }, + "fnMap": { + "0": { + "name": "(anonymous_0)", + "decl": { + "start": { + "line": 3, + "column": 16 + }, + "end": { + "line": 3, + "column": 17 + } + }, + "loc": { + "start": { + "line": 3, + "column": 22 + }, + "end": { + "line": 10, + "column": 1 + } + }, + "line": 3 + }, + "1": { + "name": "(anonymous_1)", + "decl": { + "start": { + "line": 6, + "column": 21 + }, + "end": { + "line": 6, + "column": 22 + } + }, + "loc": { + "start": { + "line": 6, + "column": 25 + }, + "end": { + "line": 8, + "column": 5 + } + }, + "line": 6 + } + }, + "branchMap": {}, + "s": { + "0": 2, + "1": 0, + "2": 0, + "3": 0 + }, + "f": { + "0": 0, + "1": 0 + }, + "b": {}, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "44a06ed36306fbbfd189234db0541dd6d0488999" + }, + "/builds/canyon/canyon-demo/src/pages/Home.tsx": { + "path": "/builds/canyon/canyon-demo/src/pages/Home.tsx", + "statementMap": { + "0": { + "start": { + "line": 4, + "column": 30 + }, + "end": { + "line": 4, + "column": 41 + } + }, + "1": { + "start": { + "line": 6, + "column": 4 + }, + "end": { + "line": 6, + "column": 27 + } + }, + "2": { + "start": { + "line": 8, + "column": 8 + }, + "end": { + "line": 13, + "column": 9 + } + }, + "3": { + "start": { + "line": 9, + "column": 12 + }, + "end": { + "line": 9, + "column": 39 + } + }, + "4": { + "start": { + "line": 11, + "column": 12 + }, + "end": { + "line": 11, + "column": 37 + } + }, + "5": { + "start": { + "line": 12, + "column": 12 + }, + "end": { + "line": 12, + "column": 31 + } + }, + "6": { + "start": { + "line": 15, + "column": 4 + }, + "end": { + "line": 35, + "column": 5 + } + }, + "7": { + "start": { + "line": 18, + "column": 64 + }, + "end": { + "line": 18, + "column": 94 + } + }, + "8": { + "start": { + "line": 18, + "column": 84 + }, + "end": { + "line": 18, + "column": 93 + } + }, + "9": { + "start": { + "line": 24, + "column": 20 + }, + "end": { + "line": 24, + "column": 53 + } + }, + "10": { + "start": { + "line": 29, + "column": 20 + }, + "end": { + "line": 29, + "column": 43 + } + } + }, + "fnMap": { + "0": { + "name": "Home", + "decl": { + "start": { + "line": 3, + "column": 9 + }, + "end": { + "line": 3, + "column": 13 + } + }, + "loc": { + "start": { + "line": 3, + "column": 16 + }, + "end": { + "line": 36, + "column": 1 + } + }, + "line": 3 + }, + "1": { + "name": "tips", + "decl": { + "start": { + "line": 7, + "column": 13 + }, + "end": { + "line": 7, + "column": 17 + } + }, + "loc": { + "start": { + "line": 7, + "column": 28 + }, + "end": { + "line": 14, + "column": 5 + } + }, + "line": 7 + }, + "2": { + "name": "(anonymous_2)", + "decl": { + "start": { + "line": 18, + "column": 58 + }, + "end": { + "line": 18, + "column": 59 + } + }, + "loc": { + "start": { + "line": 18, + "column": 64 + }, + "end": { + "line": 18, + "column": 94 + } + }, + "line": 18 + }, + "3": { + "name": "(anonymous_3)", + "decl": { + "start": { + "line": 18, + "column": 73 + }, + "end": { + "line": 18, + "column": 74 + } + }, + "loc": { + "start": { + "line": 18, + "column": 84 + }, + "end": { + "line": 18, + "column": 93 + } + }, + "line": 18 + }, + "4": { + "name": "(anonymous_4)", + "decl": { + "start": { + "line": 23, + "column": 45 + }, + "end": { + "line": 23, + "column": 46 + } + }, + "loc": { + "start": { + "line": 23, + "column": 49 + }, + "end": { + "line": 25, + "column": 17 + } + }, + "line": 23 + }, + "5": { + "name": "(anonymous_5)", + "decl": { + "start": { + "line": 28, + "column": 46 + }, + "end": { + "line": 28, + "column": 47 + } + }, + "loc": { + "start": { + "line": 28, + "column": 50 + }, + "end": { + "line": 30, + "column": 17 + } + }, + "line": 28 + } + }, + "branchMap": { + "0": { + "loc": { + "start": { + "line": 8, + "column": 8 + }, + "end": { + "line": 13, + "column": 9 + } + }, + "type": "if", + "locations": [ + { + "start": { + "line": 8, + "column": 8 + }, + "end": { + "line": 13, + "column": 9 + } + }, + { + "start": { + "line": 10, + "column": 15 + }, + "end": { + "line": 13, + "column": 9 + } + } + ], + "line": 8 + } + }, + "s": { + "0": 2, + "1": 2, + "2": 2, + "3": 0, + "4": 2, + "5": 2, + "6": 2, + "7": 0, + "8": 0, + "9": 0, + "10": 0 + }, + "f": { + "0": 2, + "1": 2, + "2": 0, + "3": 0, + "4": 0, + "5": 0 + }, + "b": { + "0": [ + 0, + 2 + ] + }, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "94acebb40f91fa6aab2be22d5f32d9b020aefe85" + }, + "/builds/canyon/canyon-demo/src/routers/index.tsx": { + "path": "/builds/canyon/canyon-demo/src/routers/index.tsx", + "statementMap": {}, + "fnMap": {}, + "branchMap": {}, + "s": {}, + "f": {}, + "b": {}, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "331bbffdd38304ac26edcd0f159b967d23f5bdcd" + }, + "/builds/canyon/canyon-demo/src/App.tsx": { + "path": "/builds/canyon/canyon-demo/src/App.tsx", + "statementMap": { + "0": { + "start": { + "line": 7, + "column": 26 + }, + "end": { + "line": 7, + "column": 49 + } + }, + "1": { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 12, + "column": 3 + } + } + }, + "fnMap": { + "0": { + "name": "App", + "decl": { + "start": { + "line": 6, + "column": 9 + }, + "end": { + "line": 6, + "column": 12 + } + }, + "loc": { + "start": { + "line": 6, + "column": 15 + }, + "end": { + "line": 13, + "column": 1 + } + }, + "line": 6 + } + }, + "branchMap": {}, + "s": { + "0": 2, + "1": 2 + }, + "f": { + "0": 2 + }, + "b": {}, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "1454065cc7fb2c09d5dcdbb82b046335a719923d" + }, + "/builds/canyon/canyon-demo/src/main.tsx": { + "path": "/builds/canyon/canyon-demo/src/main.tsx", + "statementMap": { + "0": { + "start": { + "line": 7, + "column": 0 + }, + "end": { + "line": 12, + "column": 1 + } + } + }, + "fnMap": {}, + "branchMap": {}, + "s": { + "0": 2 + }, + "f": {}, + "b": {}, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "2ca7e398b7e7ea1ee9480ada2f5111a10d111593" + } +} diff --git a/packages/canyon-data/src/coverage/test/mock-coverage-data.json b/packages/canyon-data/src/coverage/test/mock-coverage-data.json new file mode 100755 index 00000000..bfcdff24 --- /dev/null +++ b/packages/canyon-data/src/coverage/test/mock-coverage-data.json @@ -0,0 +1,536 @@ +{ + "/builds/canyon/canyon-demo/src/pages/Welcome.tsx": { + "path": "/builds/canyon/canyon-demo/src/pages/Welcome.tsx", + "statementMap": { + "0": { + "start": { + "line": 3, + "column": 16 + }, + "end": { + "line": 10, + "column": 1 + } + }, + "1": { + "start": { + "line": 4, + "column": 17 + }, + "end": { + "line": 4, + "column": 30 + } + }, + "2": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 9, + "column": 6 + } + }, + "3": { + "start": { + "line": 7, + "column": 8 + }, + "end": { + "line": 7, + "column": 20 + } + } + }, + "fnMap": { + "0": { + "name": "(anonymous_0)", + "decl": { + "start": { + "line": 3, + "column": 16 + }, + "end": { + "line": 3, + "column": 17 + } + }, + "loc": { + "start": { + "line": 3, + "column": 22 + }, + "end": { + "line": 10, + "column": 1 + } + }, + "line": 3 + }, + "1": { + "name": "(anonymous_1)", + "decl": { + "start": { + "line": 6, + "column": 21 + }, + "end": { + "line": 6, + "column": 22 + } + }, + "loc": { + "start": { + "line": 6, + "column": 25 + }, + "end": { + "line": 8, + "column": 5 + } + }, + "line": 6 + } + }, + "branchMap": {}, + "s": { + "0": 1, + "1": 0, + "2": 0, + "3": 0 + }, + "f": { + "0": 0, + "1": 0 + }, + "b": {}, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "44a06ed36306fbbfd189234db0541dd6d0488999" + }, + "/builds/canyon/canyon-demo/src/pages/Home.tsx": { + "path": "/builds/canyon/canyon-demo/src/pages/Home.tsx", + "statementMap": { + "0": { + "start": { + "line": 4, + "column": 30 + }, + "end": { + "line": 4, + "column": 41 + } + }, + "1": { + "start": { + "line": 6, + "column": 4 + }, + "end": { + "line": 6, + "column": 27 + } + }, + "2": { + "start": { + "line": 8, + "column": 8 + }, + "end": { + "line": 13, + "column": 9 + } + }, + "3": { + "start": { + "line": 9, + "column": 12 + }, + "end": { + "line": 9, + "column": 39 + } + }, + "4": { + "start": { + "line": 11, + "column": 12 + }, + "end": { + "line": 11, + "column": 37 + } + }, + "5": { + "start": { + "line": 12, + "column": 12 + }, + "end": { + "line": 12, + "column": 31 + } + }, + "6": { + "start": { + "line": 15, + "column": 4 + }, + "end": { + "line": 35, + "column": 5 + } + }, + "7": { + "start": { + "line": 18, + "column": 64 + }, + "end": { + "line": 18, + "column": 94 + } + }, + "8": { + "start": { + "line": 18, + "column": 84 + }, + "end": { + "line": 18, + "column": 93 + } + }, + "9": { + "start": { + "line": 24, + "column": 20 + }, + "end": { + "line": 24, + "column": 53 + } + }, + "10": { + "start": { + "line": 29, + "column": 20 + }, + "end": { + "line": 29, + "column": 43 + } + } + }, + "fnMap": { + "0": { + "name": "Home", + "decl": { + "start": { + "line": 3, + "column": 9 + }, + "end": { + "line": 3, + "column": 13 + } + }, + "loc": { + "start": { + "line": 3, + "column": 16 + }, + "end": { + "line": 36, + "column": 1 + } + }, + "line": 3 + }, + "1": { + "name": "tips", + "decl": { + "start": { + "line": 7, + "column": 13 + }, + "end": { + "line": 7, + "column": 17 + } + }, + "loc": { + "start": { + "line": 7, + "column": 28 + }, + "end": { + "line": 14, + "column": 5 + } + }, + "line": 7 + }, + "2": { + "name": "(anonymous_2)", + "decl": { + "start": { + "line": 18, + "column": 58 + }, + "end": { + "line": 18, + "column": 59 + } + }, + "loc": { + "start": { + "line": 18, + "column": 64 + }, + "end": { + "line": 18, + "column": 94 + } + }, + "line": 18 + }, + "3": { + "name": "(anonymous_3)", + "decl": { + "start": { + "line": 18, + "column": 73 + }, + "end": { + "line": 18, + "column": 74 + } + }, + "loc": { + "start": { + "line": 18, + "column": 84 + }, + "end": { + "line": 18, + "column": 93 + } + }, + "line": 18 + }, + "4": { + "name": "(anonymous_4)", + "decl": { + "start": { + "line": 23, + "column": 45 + }, + "end": { + "line": 23, + "column": 46 + } + }, + "loc": { + "start": { + "line": 23, + "column": 49 + }, + "end": { + "line": 25, + "column": 17 + } + }, + "line": 23 + }, + "5": { + "name": "(anonymous_5)", + "decl": { + "start": { + "line": 28, + "column": 46 + }, + "end": { + "line": 28, + "column": 47 + } + }, + "loc": { + "start": { + "line": 28, + "column": 50 + }, + "end": { + "line": 30, + "column": 17 + } + }, + "line": 28 + } + }, + "branchMap": { + "0": { + "loc": { + "start": { + "line": 8, + "column": 8 + }, + "end": { + "line": 13, + "column": 9 + } + }, + "type": "if", + "locations": [ + { + "start": { + "line": 8, + "column": 8 + }, + "end": { + "line": 13, + "column": 9 + } + }, + { + "start": { + "line": 10, + "column": 15 + }, + "end": { + "line": 13, + "column": 9 + } + } + ], + "line": 8 + } + }, + "s": { + "0": 1, + "1": 1, + "2": 1, + "3": 0, + "4": 1, + "5": 1, + "6": 1, + "7": 0, + "8": 0, + "9": 0, + "10": 0 + }, + "f": { + "0": 1, + "1": 1, + "2": 0, + "3": 0, + "4": 0, + "5": 0 + }, + "b": { + "0": [ + 0, + 1 + ] + }, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "94acebb40f91fa6aab2be22d5f32d9b020aefe85" + }, + "/builds/canyon/canyon-demo/src/routers/index.tsx": { + "path": "/builds/canyon/canyon-demo/src/routers/index.tsx", + "statementMap": {}, + "fnMap": {}, + "branchMap": {}, + "s": {}, + "f": {}, + "b": {}, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "331bbffdd38304ac26edcd0f159b967d23f5bdcd" + }, + "/builds/canyon/canyon-demo/src/App.tsx": { + "path": "/builds/canyon/canyon-demo/src/App.tsx", + "statementMap": { + "0": { + "start": { + "line": 7, + "column": 26 + }, + "end": { + "line": 7, + "column": 49 + } + }, + "1": { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 12, + "column": 3 + } + } + }, + "fnMap": { + "0": { + "name": "App", + "decl": { + "start": { + "line": 6, + "column": 9 + }, + "end": { + "line": 6, + "column": 12 + } + }, + "loc": { + "start": { + "line": 6, + "column": 15 + }, + "end": { + "line": 13, + "column": 1 + } + }, + "line": 6 + } + }, + "branchMap": {}, + "s": { + "0": 1, + "1": 1 + }, + "f": { + "0": 1 + }, + "b": {}, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "1454065cc7fb2c09d5dcdbb82b046335a719923d" + }, + "/builds/canyon/canyon-demo/src/main.tsx": { + "path": "/builds/canyon/canyon-demo/src/main.tsx", + "statementMap": { + "0": { + "start": { + "line": 7, + "column": 0 + }, + "end": { + "line": 12, + "column": 1 + } + } + }, + "fnMap": {}, + "branchMap": {}, + "s": { + "0": 1 + }, + "f": {}, + "b": {}, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "2ca7e398b7e7ea1ee9480ada2f5111a10d111593" + } +} diff --git a/packages/canyon-data/src/index.ts b/packages/canyon-data/src/index.ts new file mode 100755 index 00000000..cff738fa --- /dev/null +++ b/packages/canyon-data/src/index.ts @@ -0,0 +1,2 @@ +export * from "./coverage" +export * from "./summary" diff --git a/packages/canyon-data/src/summary/helpers.ts b/packages/canyon-data/src/summary/helpers.ts new file mode 100755 index 00000000..0257b0fb --- /dev/null +++ b/packages/canyon-data/src/summary/helpers.ts @@ -0,0 +1,32 @@ +export const emptySummary = { + functions:{ + covered: 0, + total: 0, + skipped: 0, + pct: 0, + }, + statements:{ + covered: 0, + total: 0, + skipped: 0, + pct: 0, + }, + branches:{ + covered: 0, + total: 0, + skipped: 0, + pct: 0, + }, + lines:{ + covered: 0, + total: 0, + skipped: 0, + pct: 0, + }, + newlines:{ + covered: 0, + total: 0, + skipped: 0, + pct: 0, + }, +} diff --git a/packages/canyon-data/src/summary/index.ts b/packages/canyon-data/src/summary/index.ts new file mode 100755 index 00000000..67c6304b --- /dev/null +++ b/packages/canyon-data/src/summary/index.ts @@ -0,0 +1,133 @@ +import {percent} from "../utils/percent.ts"; +import libCoverage, {CoverageMapData, CoverageSummaryData, Totals} from "istanbul-lib-coverage"; +import {calculateNewLineCoverageForSingleFile} from "../utils/line.ts"; +import {emptySummary} from "./helpers.ts"; +export interface CodeChange{ + path:string + additions:number[] +} +export interface CoverageSummaryDataMap { + [key: string]: CoverageSummaryData&{newlines:Totals}; +} + +/** + * 合并两个概要数据 + * @param first 第一个概要数据 + * @param second 第二个概要数据 + * @returns 合并过后的概要数据 + */ +export function mergeSummary(first:any,second:any):any { + const ret = JSON.parse(JSON.stringify(first)); + const keys = [ + 'lines', + 'statements', + 'branches', + 'functions', + 'branchesTrue', + 'newlines' + ]; + keys.forEach(key => { + if (second[key]) { + ret[key].total += second[key].total; + ret[key].covered += second[key].covered; + ret[key].skipped += second[key].skipped; + ret[key].pct = percent(ret[key].covered, ret[key].total); + } + }); + + return ret; +} + + +export const genSummaryMapByCoverageMap = (coverageMapData: CoverageMapData,codeChanges?:CodeChange[]):CoverageSummaryDataMap => { + const summaryMap: any = {}; + const m = libCoverage.createCoverageMap(coverageMapData); + m.files().forEach(function (f) { + const fc = m.fileCoverageFor(f), + s = fc.toSummary(); + summaryMap[f] = { + ...s.data, + newlines:calculateNewLineCoverageForSingleFile(fc.data,codeChanges?.find(c=>`~/${c.path}`===f)?.additions||[]) + }; + }); + return JSON.parse(JSON.stringify(summaryMap)); +} + +export const getSummaryByPath = ( + path: string, + summary: CoverageSummaryDataMap, +) => { + let summaryObj = JSON.parse(JSON.stringify(emptySummary)); + const filterSummary = Object.keys(summary).reduce((pre: any, cur) => { + if (cur.indexOf(path) === 0) { + pre[cur] = summary[cur]; + } + return pre; + }, {}); + + Object.keys(filterSummary).forEach((item) => { + summaryObj = mergeSummary(summaryObj,filterSummary[item]); + }); + return JSON.parse(JSON.stringify(summaryObj)); +}; + +// summary 是总的 +export const genSummaryTreeItem = ( + path: string, + summary: CoverageSummaryDataMap, +):{ + path: string; + summary: CoverageSummaryData; + children: { + path: string; + summary: CoverageSummaryData; + }[]; +} => { + function check(item: string, path: string) { + if (path === '~') { + return true; + } + return item.includes(path); + } + + // 如果是文件 + if (Object.keys(summary).find((item) => item === path)) { + return { + path, + summary: getSummaryByPath(path, summary), + children: [], + }; + } + // 如果是文件夹 + const fileLists: string[] = []; + const folderLists: string[] = []; + + Object.keys(summary).forEach((item) => { + const newpath = path === '' ? item : item.replace(path + '/', ''); + if (check(item, path) && !newpath.includes('/')) { + fileLists.push(item); + } + if (check(item, path) && newpath.includes('/')) { + folderLists.push((path === '' ? '' : path + '/') + newpath.split('/')[0]); + } + }); + + return { + path, + summary: getSummaryByPath(path, summary), + children: [ + ...[...new Set(fileLists)].map((item) => { + return { + path: item, + summary: getSummaryByPath(item, summary), + }; + }), + ...[...new Set(folderLists)].map((item) => { + return { + path: item, + summary: getSummaryByPath(item, summary), + }; + }), + ], + }; +}; diff --git a/packages/canyon-data/src/utils/line.ts b/packages/canyon-data/src/utils/line.ts new file mode 100755 index 00000000..17df53cf --- /dev/null +++ b/packages/canyon-data/src/utils/line.ts @@ -0,0 +1,39 @@ +import {FileCoverageData, Range} from "istanbul-lib-coverage"; + +/** + * returns computed line coverage from statement coverage. + * This is a map of hits keyed by line number in the source. + */ +function getLineCoverage(statementMap:{ [key: string]: Range },s:{ [key: string]: number }) { + const statements = s; + const lineMap = Object.create(null); + + Object.entries(statements).forEach(([st, count]) => { + if (!statementMap[st]) { + return; + } + const { line } = statementMap[st].start; + const prevVal = lineMap[line]; + if (prevVal === undefined || prevVal < count) { + lineMap[line] = count; + } + }); + return lineMap; +} + + +export function calculateNewLineCoverageForSingleFile(coverage:FileCoverageData, newLine:number[]) { + const lineStats = getLineCoverage(coverage.statementMap,coverage.s); + const rows:[string,unknown][] = []; + Object.entries(lineStats).forEach(([lineNumber, count]) => { + if (newLine.includes(Number(lineNumber))) { + rows.push([lineNumber, count]); + } + }); + return { + total: newLine.length, + covered: newLine.length - rows.filter((i) => !i[1]).length, + skipped: 0, + pct:0 + }; +} diff --git a/packages/canyon-data/src/utils/percent.ts b/packages/canyon-data/src/utils/percent.ts new file mode 100755 index 00000000..21b076af --- /dev/null +++ b/packages/canyon-data/src/utils/percent.ts @@ -0,0 +1,9 @@ +export function percent(covered:number, total:number) { + let tmp; + if (total > 0) { + tmp = (1000 * 100 * covered) / total; + return Math.floor(tmp / 10) / 100; + } else { + return 100.0; + } +}; diff --git a/packages/canyon-data/tsconfig.json b/packages/canyon-data/tsconfig.json new file mode 100755 index 00000000..75abdef2 --- /dev/null +++ b/packages/canyon-data/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/packages/canyon-data/vite.config.ts b/packages/canyon-data/vite.config.ts new file mode 100755 index 00000000..0950a2da --- /dev/null +++ b/packages/canyon-data/vite.config.ts @@ -0,0 +1,16 @@ +import { resolve } from "path" +import { defineConfig } from "vite" +import dts from 'vite-plugin-dts' + +export default defineConfig({ + plugins:[dts()], + build: { + outDir: "./dist", + emptyOutDir: true, + lib: { + entry: resolve(__dirname, "src/index.ts"), + fileName: "canyon-data", + formats: ["es", "cjs"], + }, + }, +}) diff --git a/packages/canyon-platform/.eslintignore b/packages/canyon-platform/.eslintignore new file mode 100755 index 00000000..9af69a5f --- /dev/null +++ b/packages/canyon-platform/.eslintignore @@ -0,0 +1,5 @@ +node_modules +server.js +index.html +Dockerfile +dist diff --git a/packages/canyon-platform/.eslintrc b/packages/canyon-platform/.eslintrc new file mode 100755 index 00000000..8377f3b9 --- /dev/null +++ b/packages/canyon-platform/.eslintrc @@ -0,0 +1,75 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:jsx-a11y/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:import/typescript", + "plugin:react/jsx-runtime", + "plugin:prettier/recommended", + "prettier" + ], + "env": { + "browser": true, + "node": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2021, + "sourceType": "module" + }, + "plugins": [ + "react", + "@typescript-eslint", + "import", + "simple-import-sort", + "jsx-a11y", + "react-hooks", + "prettier" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", + "linebreak-style": "off", + "comma-dangle": [0, "always-multiline"], + "quotes": [0, "double"], + "semi": [0, "always"], + "space-before-function-paren": [0, "never"], + "multiline-ternary": "off", + "camelcase": "off", + "prettier/prettier": [ + "warn", + { + "endOfLine": "auto", + "singleQuote": true + } + ], + "react/prop-types": "off", + "react/display-name": "off", + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "react/react-in-jsx-scope": "off", + "jsx-a11y/anchor-is-valid": "off", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/no-static-element-interactions": "off", + "react/no-unknown-property": ["error", { "ignore": ["css"] }], + "jsx-a11y/no-autofocus": "off" + }, + "ignorePatterns": ["**/*.css","**/*.md"] +} diff --git a/packages/canyon-platform/.gitignore b/packages/canyon-platform/.gitignore new file mode 100755 index 00000000..c25eeb54 --- /dev/null +++ b/packages/canyon-platform/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +pnpm-lock.yaml + +src/helpers/backend/gen \ No newline at end of file diff --git a/packages/canyon-platform/.prettierrc b/packages/canyon-platform/.prettierrc new file mode 100755 index 00000000..e1018d0d --- /dev/null +++ b/packages/canyon-platform/.prettierrc @@ -0,0 +1,12 @@ +{ + "jsxSingleQuote": true, + "semi": true, + "tabWidth": 2, + "printWidth": 100, + "bracketSameLine": false, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "auto", + "singleQuote": true, + "trailingComma": "all" +} diff --git a/packages/canyon-platform/README.md b/packages/canyon-platform/README.md new file mode 100644 index 00000000..0d6babed --- /dev/null +++ b/packages/canyon-platform/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/packages/canyon-platform/codegen.ts b/packages/canyon-platform/codegen.ts new file mode 100755 index 00000000..10e168bb --- /dev/null +++ b/packages/canyon-platform/codegen.ts @@ -0,0 +1,17 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { type CodegenConfig } from '@graphql-codegen/cli'; + +const config: CodegenConfig = { + schema: '/Users/zhangtao/github.com/canyon-project/canyon/packages/canyon-backend/*.gql', + documents: ['src/**/*.graphql'], + generates: { + './src/helpers/backend/gen/': { + preset: 'client', + presetConfig: { + persistedDocuments: 'string', + }, + }, + }, +}; + +export default config; diff --git a/packages/canyon-platform/index.html b/packages/canyon-platform/index.html new file mode 100644 index 00000000..e4b78eae --- /dev/null +++ b/packages/canyon-platform/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/packages/canyon-platform/languages.json b/packages/canyon-platform/languages.json new file mode 100644 index 00000000..579ac8fc --- /dev/null +++ b/packages/canyon-platform/languages.json @@ -0,0 +1,32 @@ +[ + { + "code": "en", + "file": "en.json", + "iso": "en-US", + "name": "English" + }, + { + "code": "cn", + "file": "cn.json", + "iso": "zh-CN", + "name": "简体中文" + }, + { + "code": "tw", + "file": "tw.json", + "iso": "zh-TW", + "name": "繁體中文" + }, + { + "code": "ja", + "file": "ja.json", + "iso": "ja-JA", + "name": "日本語" + }, + { + "code": "ko", + "file": "ko.json", + "iso": "ko-KO", + "name": "한국어" + } +] \ No newline at end of file diff --git a/packages/canyon-platform/locales/cn.json b/packages/canyon-platform/locales/cn.json new file mode 100644 index 00000000..b8caf1a9 --- /dev/null +++ b/packages/canyon-platform/locales/cn.json @@ -0,0 +1,28 @@ +{ + "menus": { + "projects": "项目", + "settings": "设置" + }, + "projects": { + "desc": "查看与您的账户相关的所有项目", + "table": { + "name": "名称", + "description": "描述", + "created_at": "创建时间", + "updated_at": "更新时间", + "option": "操作", + "delete": "删除", + "detail": "详情" + } + }, + "settings": { + "desc": "设置您的账户信息", + "form": { + "name": "名称", + "email": "邮箱", + "password": "密码", + "password_confirmation": "确认密码", + "submit": "提交" + } + } +} \ No newline at end of file diff --git a/packages/canyon-platform/locales/en.json b/packages/canyon-platform/locales/en.json new file mode 100644 index 00000000..5485880b --- /dev/null +++ b/packages/canyon-platform/locales/en.json @@ -0,0 +1,28 @@ +{ + "menus": { + "projects": "Projects", + "settings": "Settings" + }, + "projects": { + "desc": "View all projects related to your account", + "table": { + "name": "Name", + "description": "Description", + "created_at": "Created At", + "updated_at": "Updated At", + "option": "Options", + "delete": "Delete", + "detail": "Details" + } + }, + "settings": { + "desc": "Configure your account information", + "form": { + "name": "Name", + "email": "Email", + "password": "Password", + "password_confirmation": "Confirm Password", + "submit": "Submit" + } + } +} diff --git a/packages/canyon-platform/locales/ja.json b/packages/canyon-platform/locales/ja.json new file mode 100644 index 00000000..da56e67f --- /dev/null +++ b/packages/canyon-platform/locales/ja.json @@ -0,0 +1,28 @@ +{ + "menus": { + "projects": "プロジェクト", + "settings": "設定" + }, + "projects": { + "desc": "アカウントに関連するすべてのプロジェクトを表示します", + "table": { + "name": "名前", + "description": "説明", + "created_at": "作成日時", + "updated_at": "更新日時", + "option": "オプション", + "delete": "削除", + "detail": "詳細" + } + }, + "settings": { + "desc": "アカウント情報を設定します", + "form": { + "name": "名前", + "email": "メール", + "password": "パスワード", + "password_confirmation": "パスワード確認", + "submit": "送信" + } + } +} diff --git a/packages/canyon-platform/locales/ko.json b/packages/canyon-platform/locales/ko.json new file mode 100644 index 00000000..ce31eaa0 --- /dev/null +++ b/packages/canyon-platform/locales/ko.json @@ -0,0 +1,28 @@ +{ + "menus": { + "projects": "프로젝트", + "settings": "설정" + }, + "projects": { + "desc": "귀하의 계정과 관련된 모든 프로젝트 보기", + "table": { + "name": "이름", + "description": "설명", + "created_at": "생성 일시", + "updated_at": "업데이트 일시", + "option": "옵션", + "delete": "삭제", + "detail": "세부 정보" + } + }, + "settings": { + "desc": "계정 정보 구성", + "form": { + "name": "이름", + "email": "이메일", + "password": "비밀번호", + "password_confirmation": "비밀번호 확인", + "submit": "제출" + } + } +} diff --git a/packages/canyon-platform/locales/tw.json b/packages/canyon-platform/locales/tw.json new file mode 100644 index 00000000..80a53c3d --- /dev/null +++ b/packages/canyon-platform/locales/tw.json @@ -0,0 +1,28 @@ +{ + "menus": { + "projects": "項目", + "settings": "設置" + }, + "projects": { + "desc": "查看與您的帳戶相關的所有項目", + "table": { + "name": "名稱", + "description": "描述", + "created_at": "創建時間", + "updated_at": "更新時間", + "option": "操作", + "delete": "刪除", + "detail": "詳情" + } + }, + "settings": { + "desc": "設置您的帳戶信息", + "form": { + "name": "名稱", + "email": "郵箱", + "password": "密碼", + "password_confirmation": "確認密碼", + "submit": "提交" + } + } +} diff --git a/packages/canyon-platform/package.json b/packages/canyon-platform/package.json new file mode 100644 index 00000000..9f49d054 --- /dev/null +++ b/packages/canyon-platform/package.json @@ -0,0 +1,55 @@ +{ + "name": "canyon-platform", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "codegen": "graphql-codegen --config codegen.ts" + }, + "dependencies": { + "@ant-design/icons": "^5.2.6", + "@apollo/client": "^3.8.9", + "@graphql-typed-document-node/core": "^3.2.0", + "antd": "^5.13.0", + "copy-to-clipboard": "^3.3.3", + "echarts": "^5.4.3", + "echarts-for-react": "^3.0.2", + "graphql": "^16.8.1", + "i18next": "^23.7.16", + "i18next-browser-languagedetector": "^7.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.0.0" + }, + "devDependencies": { + "@graphql-codegen/cli": "^5.0.0", + "@graphql-codegen/client-preset": "^4.1.0", + "@types/react": "^18.2.47", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^6.18.1", + "@typescript-eslint/parser": "^6.18.1", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.16", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "eslint-plugin-simple-import-sort": "^10.0.0", + "postcss": "^8.4.33", + "prettier": "^3.2.2", + "react-router-dom": "^6.21.2", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", + "vite": "^5.0.11", + "vite-plugin-pages": "^0.32.0", + "vite-plugin-svgr": "^4.2.0" + } +} diff --git a/packages/canyon-platform/postcss.config.js b/packages/canyon-platform/postcss.config.js new file mode 100755 index 00000000..2e7af2b7 --- /dev/null +++ b/packages/canyon-platform/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/packages/canyon-platform/public/vite.svg b/packages/canyon-platform/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/packages/canyon-platform/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/canyon-platform/src/App.tsx b/packages/canyon-platform/src/App.tsx new file mode 100644 index 00000000..715a380d --- /dev/null +++ b/packages/canyon-platform/src/App.tsx @@ -0,0 +1,20 @@ +import { ConfigProvider } from 'antd'; +import { useRoutes } from 'react-router-dom'; + +import routes from '~react-pages'; + +const App = () => { + return ( + + {useRoutes(routes)} + + ); +}; + +export default App; diff --git a/packages/canyon-platform/src/ScrollBasedLayout.tsx b/packages/canyon-platform/src/ScrollBasedLayout.tsx new file mode 100644 index 00000000..5c4ba435 --- /dev/null +++ b/packages/canyon-platform/src/ScrollBasedLayout.tsx @@ -0,0 +1,68 @@ +import {FC, ReactNode, useEffect, useState} from "react"; +import {theme} from "antd"; +const { useToken } = theme; + +const ScrollBasedLayout: FC<{ + sideBar: ReactNode, + mainContent: ReactNode, + footer: ReactNode, +}> = ({ + sideBar, + mainContent, + footer, + }) => { + const {token} = useToken(); + const [isScrolled, setIsScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + const scrollY = window.scrollY || document.documentElement.scrollTop; + const footer = document.getElementById('footer'); + + console.log(scrollY, 'scrollY', window.innerHeight) + // 检查滚动是否超过100vh + setIsScrolled(scrollY + window.innerHeight > footer.offsetTop); + }; + + // 添加滚动事件监听器 + window.addEventListener('scroll', handleScroll); + + // 在组件卸载时移除监听器,以防止内存泄漏 + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); // 仅在组件挂载和卸载时运行 + + return
+
+
+ +
+ +
+ +
+ {sideBar} +
+
+ +
+ {mainContent} +
+
+
+ {footer} +
+
+} + +export default ScrollBasedLayout \ No newline at end of file diff --git a/packages/canyon-platform/src/assets/logo.svg b/packages/canyon-platform/src/assets/logo.svg new file mode 100755 index 00000000..092881cd --- /dev/null +++ b/packages/canyon-platform/src/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/canyon-platform/src/assets/react.svg b/packages/canyon-platform/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/packages/canyon-platform/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/canyon-platform/src/components/app/footer.tsx b/packages/canyon-platform/src/components/app/footer.tsx new file mode 100644 index 00000000..93f390cc --- /dev/null +++ b/packages/canyon-platform/src/components/app/footer.tsx @@ -0,0 +1,47 @@ +import { Typography } from 'antd'; +const { Text } = Typography; +const AppFooter = () => { + const lists = [ + { + title: 'Product', + children: [ + { + label: 'Expo on GitHub', + value: 'Expo on GitHub', + }, + { + label: 'Expo CLI on GitHub', + value: 'Expo CLI on GitHub', + }, + ], + }, + ]; + const lists2 = [lists[0], lists[0], lists[0], lists[0]]; + return ( +
+ +
+ ); +}; + +export default AppFooter; diff --git a/packages/canyon-platform/src/helpers/backend/GQLClient.ts b/packages/canyon-platform/src/helpers/backend/GQLClient.ts new file mode 100755 index 00000000..2f517cd2 --- /dev/null +++ b/packages/canyon-platform/src/helpers/backend/GQLClient.ts @@ -0,0 +1,12 @@ +/** + * A wrapper type for defining errors possible in a GQL operation + */ +export type GQLError = + | { + type: 'network_error'; + error: Error; + } + | { + type: 'gql_error'; + error: T; + }; diff --git a/packages/canyon-platform/src/helpers/backend/gql/queries/GetProjects.graphql b/packages/canyon-platform/src/helpers/backend/gql/queries/GetProjects.graphql new file mode 100755 index 00000000..d6c3cb22 --- /dev/null +++ b/packages/canyon-platform/src/helpers/backend/gql/queries/GetProjects.graphql @@ -0,0 +1,10 @@ +query GetProjects { + getProjects { + id + name + pathWithNamespace + description + reportTimes + lastReportTime + } +} diff --git a/packages/canyon-platform/src/helpers/backend/gql/queries/Me.graphql b/packages/canyon-platform/src/helpers/backend/gql/queries/Me.graphql new file mode 100755 index 00000000..063a7301 --- /dev/null +++ b/packages/canyon-platform/src/helpers/backend/gql/queries/Me.graphql @@ -0,0 +1,13 @@ +query Me { + me { + id + username + password + nickname + avatar + refreshToken + accessToken + email + createdAt + } +} diff --git a/packages/canyon-platform/src/helpers/backend/types/Email.ts b/packages/canyon-platform/src/helpers/backend/types/Email.ts new file mode 100755 index 00000000..0a4f9a3e --- /dev/null +++ b/packages/canyon-platform/src/helpers/backend/types/Email.ts @@ -0,0 +1,16 @@ +import * as t from 'io-ts'; + +const emailRegex = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + +interface EmailBrand { + readonly Email: unique symbol; +} + +export const EmailCodec = t.brand( + t.string, + (x): x is t.Branded => emailRegex.test(x), + 'Email' +); + +export type Email = t.TypeOf; diff --git a/packages/canyon-platform/src/helpers/backend/types/TeamName.ts b/packages/canyon-platform/src/helpers/backend/types/TeamName.ts new file mode 100755 index 00000000..c864e702 --- /dev/null +++ b/packages/canyon-platform/src/helpers/backend/types/TeamName.ts @@ -0,0 +1,13 @@ +import * as t from 'io-ts'; + +interface TeamNameBrand { + readonly TeamName: unique symbol; +} + +export const TeamNameCodec = t.brand( + t.string, + (x): x is t.Branded => x.trim().length >= 6, + 'TeamName' +); + +export type TeamName = t.TypeOf; diff --git a/packages/canyon-platform/src/i18n.ts b/packages/canyon-platform/src/i18n.ts new file mode 100644 index 00000000..126f8bce --- /dev/null +++ b/packages/canyon-platform/src/i18n.ts @@ -0,0 +1,47 @@ +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; + +import languages from '../languages.json'; +import cn from '../locales/cn.json'; +import en from '../locales/en.json'; +import ja from '../locales/ja.json'; +import ko from '../locales/ko.json'; +import tw from '../locales/tw.json'; +const getIos = (code: string, languages: { code: string; iso: string }[]) => + languages.find((item: { code: string }) => item.code === code)?.iso || 'en-US'; +i18n + // 检测用户当前使用的语言 + // 文档: https://github.com/i18next/i18next-browser-languageDetector + .use(LanguageDetector) + // 注入 react-i18next 实例 + .use(initReactI18next) + // 初始化 i18next + // 配置参数的文档: https://www.i18next.com/overview/configuration-options + .init({ + debug: true, + fallbackLng: 'en', + interpolation: { + escapeValue: false, + }, + resources: { + en: { + translation: en, + }, + cn: { + translation: cn, + }, + tw: { + translation: tw, + }, + ja: { + translation: ja, + }, + ko: { + translation: ko, + }, + }, + lng: localStorage.getItem('language') || navigator.language, + }); + +export default i18n; diff --git a/packages/canyon-platform/src/index.css b/packages/canyon-platform/src/index.css new file mode 100644 index 00000000..e2c0b34d --- /dev/null +++ b/packages/canyon-platform/src/index.css @@ -0,0 +1,16 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + + +.ant-menu-light.ant-menu-root.ant-menu-vertical{ + border-inline-end:none !important; +} + +a{ + color: #287DFA; +} +a:hover{ + color: #287DFA; + opacity: .8; +} diff --git a/packages/canyon-platform/src/main.tsx b/packages/canyon-platform/src/main.tsx new file mode 100644 index 00000000..500157b3 --- /dev/null +++ b/packages/canyon-platform/src/main.tsx @@ -0,0 +1,31 @@ +import './i18n.ts'; +import 'antd/dist/reset.css'; +import './index.css'; + +import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache } from '@apollo/client'; +import ReactDOM from 'react-dom/client'; +import { HashRouter } from 'react-router-dom'; + +import App from './App.tsx'; + +// 创建一个http link来发送GraphQL请求 +const httpLink = createHttpLink({ + uri: '/graphql', // 你的GraphQL API的URL + headers: { + Authorization: `Bearer ` + (localStorage.getItem('token') || ''), + }, +}); + +// 创建Apollo Client实例 +const client = new ApolloClient({ + link: httpLink, // 将error link和http link组合起来 + cache: new InMemoryCache(), +}); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/packages/canyon-platform/src/pages/index.tsx b/packages/canyon-platform/src/pages/index.tsx new file mode 100644 index 00000000..878b39f6 --- /dev/null +++ b/packages/canyon-platform/src/pages/index.tsx @@ -0,0 +1,177 @@ +import Icon, { + AppstoreOutlined, + FolderOutlined, + LogoutOutlined, + MailOutlined, + MoreOutlined, + SettingOutlined, +} from '@ant-design/icons'; +import { + Avatar, + ConfigProvider, + Divider, + Dropdown, + Menu, + MenuProps, + theme, + Typography, +} from 'antd'; +import { useEffect, useMemo, useState } from 'react'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; +// import { Outlet, useLocation, useNavigate } from 'react-router'; +// import DeployIcon from '~icons/grommet-icons/deploy'; +const { useToken } = theme; +const { Text } = Typography; +import { useTranslation } from 'react-i18next'; + +import logoSvg from '../assets/logo.svg'; +import AppFooter from '../components/app/footer.tsx'; +import ScrollBasedLayout from '../ScrollBasedLayout.tsx'; + +type MenuItem = Required['items'][number]; + +function getItem( + label: React.ReactNode, + key: React.Key, + icon?: React.ReactNode, + children?: MenuItem[], +): MenuItem { + return { + key, + icon, + children, + label, + } as MenuItem; +} + +function Index() { + const { t } = useTranslation(); + const items: MenuProps['items'] = [ + { + label: t('menus.projects'), + key: 'projects', + icon: , + }, + { + label: t('menus.settings'), + key: 'settings', + icon: , + // icon: , + }, + ]; + useEffect(() => { + // console.log(localStorage.getItem('user')) + if (localStorage.getItem('user') === null) { + // nav('/login'); + } + }, []); + const loc = useLocation(); + const nav = useNavigate(); + const selectedKey = useMemo(() => { + if (loc.pathname === '/') { + return 'deployments'; + } else { + return loc.pathname.replace('/', ''); + } + }, [loc.pathname]); + const [count, setCount] = useState(0); + const { token } = useToken(); + const meData = (() => { + try { + console.log(JSON.parse(localStorage.getItem('user') || '{}')); + return JSON.parse(localStorage.getItem('user') || '{}'); + } catch (e) { + return { + username: 'tzh', + email: 'ssss', + }; + } + })(); + const dropdownItems = [ + // getItem('个人设置', 'settings', ), + // { + // key: '3', + // type: 'divider', + // }, + getItem('Logout', 'logout', ), + ]; + const dropdownClick = ({ key }: any) => { + if (key === 'logout') { + localStorage.clear(); + window.location.href = '/login'; + } + if (key === 'settings') { + // window.location.href = '/settings'; + // nav(`/settings`); + } + }; + return ( + < + > + +
+
+ + Canyon +
+
+ + {/**/} + + { + if (selectInfo.key === 'projects') { + nav('/projects'); + } else { + nav(selectInfo.key); + } + }} + selectedKeys={[selectedKey]} + // selectedKeys={['mail']} + items={items} + className={''} + style={{ flex: '1' }} + /> + + +
+ +
+ {meData?.username || 'zhangtao25'} + {meData?.email || 'wr_zhang25@163.com'} +
+ + + {/**/} +
+
+ + } + mainContent={ +
+
+ +
+
+ } + footer={} + /> + {/**/} + + ); +} + +export default Index; diff --git a/packages/canyon-platform/src/pages/index/projects/[id]/commits/[sha].tsx b/packages/canyon-platform/src/pages/index/projects/[id]/commits/[sha].tsx new file mode 100755 index 00000000..8b0fd391 --- /dev/null +++ b/packages/canyon-platform/src/pages/index/projects/[id]/commits/[sha].tsx @@ -0,0 +1,5 @@ +const ProjectCommitPage = () => { + return
ProjectCommitPage
+} + +export default ProjectCommitPage \ No newline at end of file diff --git a/packages/canyon-platform/src/pages/index/projects/[id]/index.tsx b/packages/canyon-platform/src/pages/index/projects/[id]/index.tsx new file mode 100755 index 00000000..d4331e8c --- /dev/null +++ b/packages/canyon-platform/src/pages/index/projects/[id]/index.tsx @@ -0,0 +1,165 @@ +import {Divider, Input, Table} from "antd"; +import {useState} from "react"; +import ReactECharts from 'echarts-for-react'; +const dataSource = [ + { + commitSha: '1', + branch: '192.168.123.1', + message: 'Running', + address: '38%(2C)', + test1: '15%(4G)', + }, + { + commitSha: '2', + branch: '192.168.123.1', + message: 'Running', + address: '38%(2C)', + test1: '15%(4G)', + }, +]; +const columns = [ + { + title: 'Commit Sha', + dataIndex: 'commitSha', + render(_: any) { + return ( + { + // nav(`/project/${pam.id}/commits/${_}`); + }} + > + {_} + {/*{_.slice(0, 7)}*/} + + ); + }, + }, + { + title: 'Branch', + dataIndex: 'branch', + }, + { + title: 'Message', + dataIndex: 'message', + render(_: any) { + return ( +
+ {_} +
+ ); + }, + }, + { + title: 'Overall Coverage', + dataIndex: 'statements', + render(_: any): JSX.Element { + return {_}%; + }, + }, + { + title: 'New Lines Coverage', + dataIndex: 'newlines', + render(_: any): JSX.Element { + return {_}%; + }, + }, + { + title: 'Report Times', + dataIndex: 'times', + }, + { + title: 'Operation' + }, +]; + +const option = { + grid: { + top: '30px', + left: '30px', + right: '10px', + bottom: '20px', + }, + tooltip: { + trigger: 'axis', + }, + legend: { + x: 'right', + data: ['Overall Coverage', 'New Lines Coverage'], + }, + xAxis: { + type: 'category', + data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + }, + yAxis: { + type: 'value' + }, + series: [ + { + data: [150, 230, 224, 218, 135, 147, 260], + type: 'line' + }, + { + data: [250, 130, 124, 118, 235, 247, 160], + type: 'line' + } + ] +}; +const ProjectOverviewPage = () => { + const [searchValue, setSearchValue] = useState(''); + return
+

{'canyon/canyon-demo'}

+ +

Overview

+ +
+
+ {([{label:'name',value:'zt'},{label:'name',value:'zt'},{label:'name',value:'zt'},{label:'name',value:'zt'},]).map((item, index) => { + return ( +
+
{item.label}
+
{item.value}
+
+ ); + })} +
+ +
+

Trends in coverage the last 10 commits

+ +
+
+ +

Records

+ { + setSearchValue(value); + }} + style={{width: '700px', marginBottom: '10px'}} + /> + {/*div*/} + + + +} + +export default ProjectOverviewPage \ No newline at end of file diff --git a/packages/canyon-platform/src/pages/index/projects/index.tsx b/packages/canyon-platform/src/pages/index/projects/index.tsx new file mode 100755 index 00000000..29ce010e --- /dev/null +++ b/packages/canyon-platform/src/pages/index/projects/index.tsx @@ -0,0 +1,90 @@ +import { Button, Card, Descriptions, DescriptionsProps, Divider, Table, Typography } from 'antd'; +// import {ProjectFilled} from "@ant-design/icons"; +const { Text } = Typography; +import Icon, { FileAddOutlined, FolderOutlined, PlusOutlined } from '@ant-design/icons'; +import { useQuery } from '@apollo/client'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import MessageSvg from '../../../assets/icons/project.svg'; +import { GetProjectsDocument } from '../../../helpers/backend/gen/graphql.ts'; +const ProjectPage = () => { + const { t } = useTranslation(); + const nav = useNavigate(); + const columns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + }, + { + title: t('projects.table.name'), + dataIndex: 'name', + key: 'name', + }, + { + title: t('projects.table.description'), + dataIndex: 'description', + render: (text: string) => { + return 8; + }, + }, + // { + // title: t('projects.table.test1'), + // dataIndex: 'test1', + // key: 'test1', + // render: (text: string) => { + // return 8; + // }, + // }, + { + title: t('projects.table.option'), + key: 'operation', + render: () => ( +
{ + nav('/projects/1'); + }} + > + {t('projects.table.detail')} + + {t('projects.table.detail')} +
+ ), + }, + ]; + + const { data: projectsData, loading, refetch } = useQuery(GetProjectsDocument, {}); + useEffect(() => { + refetch(); + }, []); + + return ( +
+
+
+

+ + {t('menus.projects')} +

+ {t('projects.desc')} +
+
+ {/*暂时隐藏*/} + +
+
+ +
+ +
+ + + + ); +}; + +export default ProjectPage; diff --git a/packages/canyon-platform/src/pages/index/settings/index.tsx b/packages/canyon-platform/src/pages/index/settings/index.tsx new file mode 100644 index 00000000..16f39dfc --- /dev/null +++ b/packages/canyon-platform/src/pages/index/settings/index.tsx @@ -0,0 +1,78 @@ +import { FolderOutlined, SearchOutlined, SettingFilled, SettingOutlined } from '@ant-design/icons'; +import { Alert, Card, Input, message, Select } from 'antd'; +import TextArea from 'antd/es/input/TextArea'; +import copy from 'copy-to-clipboard'; +import { useTranslation } from 'react-i18next'; + +import languages from '../../../../languages.json'; +const Settings = () => { + const { t } = useTranslation(); + return ( +
+
+

+ + {t('menus.settings')} +

+
+ + +
+
Language
+ +
+