Repository: Akryum/awesomejs.dev Branch: dev Commit: e4bd536cf0a9 Files: 238 Total size: 327.2 KB Directory structure: gitextract_sbyilq6x/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── lint.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── lerna.json ├── now.json ├── package.json ├── packages/ │ ├── backend/ │ │ ├── .gitignore │ │ ├── .nodepack/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── app-migration-plugin-versions.json │ │ │ └── app-migration-records.json │ │ ├── README.md │ │ ├── apollo.config.js │ │ ├── config/ │ │ │ ├── algolia.ts │ │ │ ├── apollo.ts │ │ │ ├── cookie.ts │ │ │ ├── cors.ts │ │ │ ├── db.ts │ │ │ └── github.ts │ │ ├── gql-codegen.yml │ │ ├── nodepack.config.js │ │ ├── package.json │ │ ├── schema-fragment-matcher.js │ │ ├── src/ │ │ │ ├── const/ │ │ │ │ └── error-codes.ts │ │ │ ├── context/ │ │ │ │ ├── algolia.ts │ │ │ │ ├── fauna.ts │ │ │ │ ├── github.ts │ │ │ │ └── npm.ts │ │ │ ├── context.d.ts │ │ │ ├── generated/ │ │ │ │ ├── config.d.ts │ │ │ │ ├── context.d.ts │ │ │ │ └── schema.ts │ │ │ ├── index.ts │ │ │ ├── passport/ │ │ │ │ ├── github.ts │ │ │ │ ├── index.ts │ │ │ │ └── util.ts │ │ │ ├── routes/ │ │ │ │ └── user.ts │ │ │ ├── schema/ │ │ │ │ ├── admin.ts │ │ │ │ ├── auth.ts │ │ │ │ ├── package/ │ │ │ │ │ ├── bookmark.ts │ │ │ │ │ ├── edit-project-types.ts │ │ │ │ │ ├── edit.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── package-interface/ │ │ │ │ │ ├── data-source.ts │ │ │ │ │ ├── db-types.ts │ │ │ │ │ ├── edit-project-types.ts │ │ │ │ │ ├── edit.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── insight.ts │ │ │ │ │ ├── package-metadata.ts │ │ │ │ │ └── releases.ts │ │ │ │ ├── package-proposal/ │ │ │ │ │ ├── approve.ts │ │ │ │ │ ├── db-types.ts │ │ │ │ │ ├── edit-project-types.ts │ │ │ │ │ ├── edit.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── propose.ts │ │ │ │ │ ├── remove.ts │ │ │ │ │ └── upvote.ts │ │ │ │ ├── project-type/ │ │ │ │ │ ├── bookmarks.ts │ │ │ │ │ ├── db-types.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── scalar/ │ │ │ │ │ ├── data.ts │ │ │ │ │ └── json.ts │ │ │ │ ├── team/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── team-access.ts │ │ │ │ └── user/ │ │ │ │ ├── db-types.ts │ │ │ │ └── index.ts │ │ │ ├── shim-ejs.d.ts │ │ │ └── util/ │ │ │ ├── fauna.ts │ │ │ ├── github.ts │ │ │ ├── metadata.ts │ │ │ ├── package-index.ts │ │ │ ├── readme.ts │ │ │ ├── tag-map.ts │ │ │ └── tags.ts │ │ ├── tsconfig.json │ │ └── tslint.json │ ├── frontend/ │ │ ├── .browserslistrc │ │ ├── .editorconfig │ │ ├── .eslintignore │ │ ├── .eslintrc.js │ │ ├── .gitignore │ │ ├── README.md │ │ ├── apollo.config.js │ │ ├── babel.config.js │ │ ├── cypress.json │ │ ├── jsconfig.json │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── public/ │ │ │ ├── index.html │ │ │ └── robots.txt │ │ ├── src/ │ │ │ ├── assets/ │ │ │ │ └── styles/ │ │ │ │ ├── components/ │ │ │ │ │ ├── document.postcss │ │ │ │ │ ├── grids.postcss │ │ │ │ │ ├── markdown.postcss │ │ │ │ │ ├── multi-select.postcss │ │ │ │ │ └── popper.postcss │ │ │ │ ├── main.postcss │ │ │ │ ├── tailwind.postcss │ │ │ │ └── transitions.postcss │ │ │ ├── cache.js │ │ │ ├── components/ │ │ │ │ ├── App.vue │ │ │ │ ├── BaseButton.vue │ │ │ │ ├── BasePopper.vue │ │ │ │ ├── EmptyMessage.vue │ │ │ │ ├── ErrorMessage.vue │ │ │ │ ├── LoadingIndicator.vue │ │ │ │ ├── PageTitle.vue │ │ │ │ ├── PopupModal.vue │ │ │ │ ├── RouteTab.vue │ │ │ │ ├── SubmitAnimation.vue │ │ │ │ ├── ToastNotification.vue │ │ │ │ ├── about/ │ │ │ │ │ ├── Contributing.vue │ │ │ │ │ └── Privacy.vue │ │ │ │ ├── admin/ │ │ │ │ │ ├── AdminDashboard.vue │ │ │ │ │ ├── AdminHome.vue │ │ │ │ │ ├── AdminTeamCreate.vue │ │ │ │ │ ├── AdminTeamEditForm.vue │ │ │ │ │ ├── AdminTeamView.vue │ │ │ │ │ ├── AdminTeams.vue │ │ │ │ │ ├── UserMultiSelect.vue │ │ │ │ │ └── fragments.js │ │ │ │ ├── app/ │ │ │ │ │ ├── AppFooter.vue │ │ │ │ │ ├── AppGlobalLoading.vue │ │ │ │ │ ├── AppHeader.vue │ │ │ │ │ ├── AppHeaderLogo.vue │ │ │ │ │ ├── AppHome.vue │ │ │ │ │ ├── AppServiceWorkerManager.vue │ │ │ │ │ └── AppSponsors.vue │ │ │ │ ├── chart/ │ │ │ │ │ └── DotChart.vue │ │ │ │ ├── pkg/ │ │ │ │ │ ├── NoPackageSelected.vue │ │ │ │ │ ├── PackageAddButton.vue │ │ │ │ │ ├── PackageAddWizard.vue │ │ │ │ │ ├── PackageAdded.vue │ │ │ │ │ ├── PackageBookmarkButton.vue │ │ │ │ │ ├── PackageCount.vue │ │ │ │ │ ├── PackageDataSource.vue │ │ │ │ │ ├── PackageDownloadsCount.vue │ │ │ │ │ ├── PackageEditForm.vue │ │ │ │ │ ├── PackageEditProjectTypesForm.vue │ │ │ │ │ ├── PackageGeneralInfo.vue │ │ │ │ │ ├── PackageInsightNpmDownloads.vue │ │ │ │ │ ├── PackageInstallButton.vue │ │ │ │ │ ├── PackageInstallationManager.vue │ │ │ │ │ ├── PackageLinks.vue │ │ │ │ │ ├── PackageList.vue │ │ │ │ │ ├── PackageListItem.vue │ │ │ │ │ ├── PackageLogo.vue │ │ │ │ │ ├── PackageProposalApproveButton.vue │ │ │ │ │ ├── PackageProposalList.vue │ │ │ │ │ ├── PackageProposalRemoveButton.vue │ │ │ │ │ ├── PackageProposalTabEdit.vue │ │ │ │ │ ├── PackageProposalTabGeneral.vue │ │ │ │ │ ├── PackageProposalUpvoteButton.vue │ │ │ │ │ ├── PackageProposalView.vue │ │ │ │ │ ├── PackageReadme.vue │ │ │ │ │ ├── PackageRelease.vue │ │ │ │ │ ├── PackageReleaseAsset.vue │ │ │ │ │ ├── PackageReleaseCount.vue │ │ │ │ │ ├── PackageShareButton.vue │ │ │ │ │ ├── PackageShareModal.vue │ │ │ │ │ ├── PackageTabDataSources.vue │ │ │ │ │ ├── PackageTabEdit.vue │ │ │ │ │ ├── PackageTabGeneral.vue │ │ │ │ │ ├── PackageTabInsight.vue │ │ │ │ │ ├── PackageTabReleases.vue │ │ │ │ │ ├── PackageTag.vue │ │ │ │ │ ├── PackageTags.vue │ │ │ │ │ ├── PackageView.vue │ │ │ │ │ ├── PackageViewLayout.vue │ │ │ │ │ └── fragments.js │ │ │ │ ├── project-type/ │ │ │ │ │ ├── ProjectTypeAllTags.vue │ │ │ │ │ ├── ProjectTypeBookmarkButton.vue │ │ │ │ │ ├── ProjectTypeMultiSelect.vue │ │ │ │ │ ├── ProjectTypePackageProposalsButton.vue │ │ │ │ │ ├── ProjectTypePackageProposalsView.vue │ │ │ │ │ ├── ProjectTypeSelect.vue │ │ │ │ │ ├── ProjectTypeView.vue │ │ │ │ │ ├── ProjectTypes.vue │ │ │ │ │ ├── ProjectTypesGrid.vue │ │ │ │ │ ├── ProjectTypesItem.vue │ │ │ │ │ └── fragments.js │ │ │ │ ├── search/ │ │ │ │ │ ├── SearchOverlay.vue │ │ │ │ │ └── SearchOverlayAsyncState.vue │ │ │ │ └── user/ │ │ │ │ ├── LoginView.vue │ │ │ │ ├── NoBookmarkPackageSelected.vue │ │ │ │ ├── UserCheckSignedIn.vue │ │ │ │ ├── UserDashboard.vue │ │ │ │ ├── UserMenu.vue │ │ │ │ ├── UserTabBookmarks.vue │ │ │ │ ├── UserTabHome.vue │ │ │ │ ├── UserTeams.vue │ │ │ │ ├── fragments.js │ │ │ │ └── useCurrentUser.js │ │ │ ├── components.js │ │ │ ├── index.d.ts │ │ │ ├── main.js │ │ │ ├── plugins.js │ │ │ ├── registerServiceWorker.js │ │ │ ├── router.js │ │ │ ├── util/ │ │ │ │ ├── algolia-npm.js │ │ │ │ ├── algolia.js │ │ │ │ ├── emoji.js │ │ │ │ ├── env.js │ │ │ │ ├── error.js │ │ │ │ ├── favicon.js │ │ │ │ ├── installation.js │ │ │ │ ├── lock-scroll.js │ │ │ │ ├── proposal.js │ │ │ │ ├── qrcode.js │ │ │ │ ├── responsive.js │ │ │ │ ├── router.js │ │ │ │ ├── scroll-behavior.js │ │ │ │ ├── scroll.js │ │ │ │ ├── service-worker.js │ │ │ │ ├── share.js │ │ │ │ └── tags.js │ │ │ └── vue-apollo.js │ │ ├── tailwind.config.js │ │ ├── tests/ │ │ │ └── e2e/ │ │ │ ├── .eslintrc.js │ │ │ ├── plugins/ │ │ │ │ └── index.js │ │ │ ├── specs/ │ │ │ │ └── test.js │ │ │ └── support/ │ │ │ ├── commands.js │ │ │ └── index.js │ │ └── vue.config.js │ └── shared-utils/ │ ├── package.json │ ├── tags.d.ts │ ├── tags.js │ ├── tags.ts │ └── tsconfig.json ├── postcss.config.js ├── res/ │ └── thumbnail.xcf └── workspace.code-workspace ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: [Akryum] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Install deps run: yarn install --pure-lockfile - name: Linting files... run: npx lerna run lint -- --no-fix ================================================ FILE: .gitignore ================================================ node_modules/ .DS_Store *.log packages/test dist /.env /*.sh ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers 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, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at guillaume@moonreach.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Guillaume Chau 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. ================================================ FILE: README.md ================================================ # awesomejs.dev Find awesome packages for the framework you are using ## Sponsors [![sponsors logos](https://guillaume-chau.info/sponsors.png)](https://guillaume-chau.info/sponsors) ================================================ FILE: lerna.json ================================================ { "npmClient": "yarn", "useWorkspaces": true, "version": "0.1.0", "packages": [ "packages/*" ] } ================================================ FILE: now.json ================================================ { "version": 2, "regions": [ "all" ], "builds": [ { "src": "packages/frontend/package.json", "use": "@now/static-build", "config": { "distDir": "dist" } }, { "src": "packages/backend/dist/app.js", "use": "@now/node", "config": { "includeFiles": [ "packages/backend/dist/**" ] } } ], "routes": [ { "src": "/", "dest": "/packages/frontend/index.html" }, { "src": "/api", "dest": "/packages/backend/dist/app.js" }, { "src": "/api/(.*)", "dest": "/packages/backend/dist/app.js" }, { "src": "/.well-known/apollo/server-health", "headers": { "cache-control": "s-maxage=0" }, "dest": "/packages/backend/dist/app.js" }, { "src": "^/service-worker.js", "headers": { "cache-control": "s-maxage=0" }, "dest": "/packages/frontend/service-worker.js" }, { "src": "/(.*\\.(ico|js|json|css|svg|png|jpg|txt))", "headers": { "cache-control": "s-maxage=31536000" }, "dest": "/packages/frontend/$1" }, { "src": "/(.*)", "dest": "/packages/frontend/index.html" } ], "env": { "PORT": "4040", "BASE_API_PATH": "/api", "CLIENT_BASE_URL": "", "DB_SECRET": "@awesomejs_db_secret", "GITHUB_AUTH": "@awesomejs_github_auth", "COOKIE_SECRET": "@awesomejs_cookie_secret", "ALGOLIA_ID": "@awesomejs_algolia_id", "ALGOLIA_KEY": "@awesomejs_algolia_key", "APOLLO_ENGINE_KEY": "@awesomejs_apollo_engine_key", "APOLLO_ENGINE_SCHEMA_TAG": "prod", "OAUTH_GITHUB_ID": "@awesomejs_oauth_github_id", "OAUTH_GITHUB_SECRET": "@awesomejs_oauth_github_secret", "OAUTH_GITHUB_CALLBACK": "https://awesomejs.dev/api/auth/github/callback" }, "build": { "env": { "DB_SECRET": "@awesomejs_db_secret", "GITHUB_AUTH": "@awesomejs_github_auth", "ALGOLIA_ID": "@awesomejs_algolia_id", "ALGOLIA_KEY": "@awesomejs_algolia_key", "VUE_APP_ALGOLIA_ID": "@awesomejs_algolia_id", "VUE_APP_ALGOLIA_KEY": "@awesomejs_algolia_key", "VUE_APP_GRAPHQL_HTTP": "/api/graphql", "VUE_APP_API_BASE": "/api" } }, "github": { "autoAlias": false } } ================================================ FILE: package.json ================================================ { "name": "awesomejs.dev", "version": "1.0.0", "description": "Find awesome packages for the framework you are using", "private": true, "workspaces": [ "packages/*" ], "scripts": { "build": "lerna run build" }, "repository": { "type": "git", "url": "git+https://github.com/Akryum/awesomejs.dev.git" }, "author": "Guillaume Chau ", "license": "MIT", "bugs": { "url": "https://github.com/Akryum/awesomejs.dev/issues" }, "homepage": "https://github.com/Akryum/awesomejs.dev#readme", "devDependencies": { "lerna": "^3.16.4" } } ================================================ FILE: packages/backend/.gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw* ================================================ FILE: packages/backend/.nodepack/.gitignore ================================================ /temp /env-migration-records.json /config.json ================================================ FILE: packages/backend/.nodepack/README.md ================================================ # Nodepack internal config files Add this folder to version control. Modify at yourn own risk! ================================================ FILE: packages/backend/.nodepack/app-migration-plugin-versions.json ================================================ { "@nodepack/service": "0.7.0", "@nodepack/plugin-apollo": "0.7.0", "@nodepack/plugin-express": "0.7.0", "@nodepack/plugin-typescript": "0.7.0", "@nodepack/plugin-babel": "0.7.0", "@nodepack/plugin-passport": "0.7.0" } ================================================ FILE: packages/backend/.nodepack/app-migration-records.json ================================================ [ { "id": "defaultTemplate", "pluginId": "@nodepack/service", "pluginVersion": "0.4.11", "options": {}, "date": "2019-09-30T17:11:45.061Z" }, { "id": "defaultTemplate", "pluginId": "@nodepack/plugin-typescript", "pluginVersion": "0.4.5", "options": { "tslint": true }, "date": "2019-09-30T17:11:45.074Z" }, { "id": "configPath", "pluginId": "@nodepack/plugin-typescript", "pluginVersion": "0.4.5", "options": {}, "date": "2019-09-30T17:11:45.300Z" }, { "id": "defaultRoutes", "pluginId": "@nodepack/plugin-express", "pluginVersion": "0.4.7", "options": {}, "date": "2019-09-30T17:11:45.326Z" }, { "id": "defaultPackage", "pluginId": "@nodepack/plugin-apollo", "pluginVersion": "0.4.7", "options": {}, "date": "2019-09-30T17:11:45.334Z" }, { "id": "defaultSchema", "pluginId": "@nodepack/plugin-apollo", "pluginVersion": "0.4.7", "options": {}, "date": "2019-09-30T17:11:45.343Z" }, { "id": "contextTypePath", "pluginId": "@nodepack/plugin-typescript", "pluginVersion": "0.6.0", "options": {}, "date": "2019-11-05T13:08:27.307Z" }, { "id": "skipLibCheck", "pluginId": "@nodepack/plugin-typescript", "pluginVersion": "0.7.0", "options": {}, "date": "2019-11-05T15:53:31.756Z" } ] ================================================ FILE: packages/backend/README.md ================================================ # awesomejs-backend ## Project setup ``` yarn install ``` ### Env Create a `.env.local` file next to this `README.md` with the necessary secret variables: ``` DB_SECRET=xxxxxxxxxxxxxxxxxxxxx GITHUB_AUTH=xxxxxxxxxxxxxxxxxxxxx COOKIE_SECRET=xxxxxxxxxxxxxxxxxxxxx ALGOLIA_ID=xxxxxxxxxxxxxxxxxxxxx ALGOLIA_KEY=xxxxxxxxxxxxxxxxxxxxx OAUTH_GITHUB_ID=xxxxxxxxxxxxxxxxxxxxx OAUTH_GITHUB_SECRET=xxxxxxxxxxxxxxxxxxxxx OAUTH_GITHUB_CALLBACK=http://localhost:4040/auth/github/callback ``` - `DB_SECRET` must be a valid FaunaDB secret - `GITHUB_AUTH` mush be a valid GitHub personal access token - `COOKIE_SECRET` must be a random string, ideally pretty long - `ALGOLIA_ID` is the Algolia application ID - `ALGOLIA_KEY` is the Algolia Admin API key - `OAUTH_GITHUB_ID` is the OAuth Client ID - `OAUTH_GITHUB_SECRET` is the OAuth Secret ### Customize configuration See [Configuration Reference](https://github.com/Akryum/nodepack). ================================================ FILE: packages/backend/apollo.config.js ================================================ const path = require('path') // Load .env files const { loadEnv } = require('vue-cli-plugin-apollo/utils/load-env') const env = loadEnv([ path.resolve(__dirname, '.env'), path.resolve(__dirname, '.env.local') ]) module.exports = { service: { endpoint: { url: 'http://localhost:4040/graphql' } }, engine: { apiKey: env.VUE_APP_APOLLO_ENGINE_KEY } } ================================================ FILE: packages/backend/config/algolia.ts ================================================ export default { id: process.env.ALGOLIA_ID, key: process.env.ALGOLIA_KEY, } ================================================ FILE: packages/backend/config/apollo.ts ================================================ import { ApolloConfig } from '@nodepack/plugin-apollo' import chalk from 'chalk' const basePath = process.env.BASE_API_PATH || '' export default { path: `${basePath}/graphql`, subscriptionsPath: `${basePath}/subscriptions`, playground: `${basePath}/playground`, apolloServerOptions: { formatError: (error) => { console.log(chalk.red('Error'), error.stack || error) if (error.extensions.exception) { console.log(chalk.red('Related exception:'), error.extensions.exception) } return error }, engine: { apiKey: process.env.APOLLO_ENGINE_KEY, schemaTag: process.env.APOLLO_ENGINE_SCHEMA_TAG, }, }, } as ApolloConfig ================================================ FILE: packages/backend/config/cookie.ts ================================================ import { CookieOptions } from 'express' export default { secret: process.env.COOKIE_SECRET, domain: process.env.COOKIE_DOMAIN, } as CookieOptions ================================================ FILE: packages/backend/config/cors.ts ================================================ import { CorsOptions } from 'cors' export default { origin: process.env.CLIENT_BASE_URL, methods: ['OPTIONS', 'POST'], credentials: true, } as CorsOptions ================================================ FILE: packages/backend/config/db.ts ================================================ export default { secret: process.env.DB_SECRET, } ================================================ FILE: packages/backend/config/github.ts ================================================ import { Options } from '@octokit/rest' export default { auth: process.env.GITHUB_AUTH, } as Options ================================================ FILE: packages/backend/gql-codegen.yml ================================================ schema: http://localhost:4040/graphql generates: src/generated/schema.ts: plugins: - typescript - typescript-resolvers config: contextType: '@/context#Context' mappers: PackageInterface: '@/schema/package-interface/db-types#DBPackageInterface' PackageProposal: '@/schema/package-proposal/db-types#DBPackageProposal' ProjectType: '@/schema/project-type/db-types#DBProjectType' User: '@/schema/user/db-types#DBUser' UserAccount: '@/schema/user/db-types#DBUserAccount' optionalResolveType: true schema-fragment-matcher.js: - fragment-matcher ================================================ FILE: packages/backend/nodepack.config.js ================================================ /** @type {import('@nodepack/service').ProjectOptions} */ module.exports = { minify: false, externals: true, } ================================================ FILE: packages/backend/package.json ================================================ { "name": "backend", "version": "0.1.0", "private": true, "scripts": { "dev": "nodepack-service dev", "build": "nodepack-service build --no-preInstall --no-maintenance", "lint": "nodepack-service lint", "now-build": "yarn run build", "start": "node -r dotenv/config ./dist/app.js dotenv_config_path=.env.local", "schema-gen": "graphql-codegen --config gql-codegen.yml" }, "dependencies": { "@awesomejs/shared-utils": "^1.0.0", "@graphql-codegen/typescript-resolvers": "^1.9.1", "@nodepack/app": "^0.8.0", "@nodepack/app-context": "^0.8.0", "@octokit/rest": "16.30.1", "algoliasearch": "^3.35.0", "faunadb": "^2.8.1", "graphql": "^14.4.2", "graphql-tag": "^2.10.1", "graphql-type-json": "^0.3.1", "morgan": "^1.9.1", "ms": "^2.1.2", "node-fetch": "^2.6.0", "npm-registry-fetch": "^4.0.1", "p-memoize": "^3.1.0", "passport-github2": "^0.1.11" }, "devDependencies": { "@graphql-codegen/cli": "^1.9.1", "@graphql-codegen/fragment-matcher": "^1.9.1", "@graphql-codegen/typescript": "^1.9.1", "@nodepack/plugin-apollo": "^0.8.0", "@nodepack/plugin-babel": "^0.8.0", "@nodepack/plugin-express": "^0.8.0", "@nodepack/plugin-passport": "^0.8.0", "@nodepack/plugin-typescript": "^0.8.0", "@nodepack/service": "^0.8.0", "@types/algoliasearch": "^3.34.3", "@types/graphql-type-json": "^0.3.2", "@types/morgan": "^1.7.37", "@types/ms": "^0.7.31", "@types/node-fetch": "^2", "@types/npm-registry-fetch": "^4.0.1", "@types/passport-github2": "^1.2.4" }, "engines": { "node": ">=10" } } ================================================ FILE: packages/backend/schema-fragment-matcher.js ================================================ export default { "__schema": { "types": [ { "kind": "INTERFACE", "name": "PackageInterface", "possibleTypes": [ { "name": "PackageProposal" }, { "name": "Package" } ] } ] } } ================================================ FILE: packages/backend/src/const/error-codes.ts ================================================ export enum ErrorCode { ERROR_GUEST = 'guest', ERROR_UNAUTHORIZED = 'unauthorized', ERROR_VALIDATION = 'validation', } ================================================ FILE: packages/backend/src/context/algolia.ts ================================================ import Algolia, { Client as AlgoliaClient } from 'algoliasearch' import { onCreate, addProp } from '@nodepack/app-context' import { Context } from '@/context' onCreate((ctx: Context) => { addProp(ctx, 'algolia', () => Algolia(ctx.config.algolia.id, ctx.config.algolia.key)) }) export default interface AlgoliaContext { algolia: AlgoliaClient } ================================================ FILE: packages/backend/src/context/fauna.ts ================================================ import { onCreate, addProp } from '@nodepack/app-context' import { Client as FaunaClient } from 'faunadb' import { Context } from '../context' onCreate((context: Context) => { addProp(context, 'db', () => new FaunaClient(context.config.db)) }) export default interface FaunaContext { db: FaunaClient } ================================================ FILE: packages/backend/src/context/github.ts ================================================ import { onCreate, addProp } from '@nodepack/app-context' import { Context } from '../context' import Octokit from '@octokit/rest' onCreate(async (context: Context) => { addProp(context, 'github', () => new Octokit(context.config.github)) }) export default interface GitHubContext { github: Octokit } ================================================ FILE: packages/backend/src/context/npm.ts ================================================ import { onCreate, addProp } from '@nodepack/app-context' import { Context } from '../context' import npmFetch, { Options } from 'npm-registry-fetch' import mem from 'p-memoize' import ms from 'ms' type NpmFunction = (url: string, opts?: Options) => Promise onCreate(async (context: Context) => { addProp(context, 'npm', () => mem(npmFetch.json, { maxAge: ms('1s'), })) addProp(context, 'npmApi', () => mem((url: string, opts: any = undefined) => npmFetch.json(`https://api.npmjs.org/${url}`, opts), { maxAge: ms('1s'), })) }) export default interface NpmContext { npm: NpmFunction npmApi: NpmFunction } ================================================ FILE: packages/backend/src/context.d.ts ================================================ import BaseContext from '@context' import { PassportUser } from '@nodepack/plugin-passport' import { values } from 'faunadb' import { User as BaseUser, UserAccount } from '@/generated/schema' import { DBUserAccount, DBUser } from './schema/user/db-types' export interface User extends PassportUser, BaseUser, DBUser { } export interface Context extends BaseContext { user: User } ================================================ FILE: packages/backend/src/generated/config.d.ts ================================================ // This file is auto-generated by @nodepack/plugin-typescript /* eslint-disable */ /* tslint:disable */ import ConfigAlgolia from '@config/algolia' import ConfigApollo from '@config/apollo' import ConfigCookie from '@config/cookie' import ConfigCors from '@config/cors' import ConfigDb from '@config/db' import ConfigGithub from '@config/github' export default interface BaseConfig { algolia: typeof ConfigAlgolia apollo: typeof ConfigApollo cookie: typeof ConfigCookie cors: typeof ConfigCors db: typeof ConfigDb github: typeof ConfigGithub } export type Config = BaseConfig ================================================ FILE: packages/backend/src/generated/context.d.ts ================================================ // This file is auto-generated by @nodepack/plugin-typescript /* eslint-disable */ /* tslint:disable */ // Config import ProjectConfigBase from './config' // Plugins import NodepackPluginApollo from '@nodepack/plugin-apollo/types/context' import NodepackPluginExpress from '@nodepack/plugin-express/types/context' import NodepackPluginPassport from '@nodepack/plugin-passport/types/context' // Project context files import Algolia from '@/context/algolia' import Fauna from '@/context/fauna' import Github from '@/context/github' import Npm from '@/context/npm' interface ContextBase { config: ProjectConfigBase } export type Context = ContextBase & NodepackPluginApollo & NodepackPluginExpress & NodepackPluginPassport & Algolia & Fauna & Github & Npm export default Context ================================================ FILE: packages/backend/src/generated/schema.ts ================================================ import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; import { DBPackageInterface } from '@/schema/package-interface/db-types'; import { DBPackageProposal } from '@/schema/package-proposal/db-types'; import { DBProjectType } from '@/schema/project-type/db-types'; import { DBUser, DBUserAccount } from '@/schema/user/db-types'; import { Context } from '@/context'; export type Maybe = T | null; export type Omit = Pick>; export type RequireFields = { [X in Exclude]?: T[X] } & { [P in K]-?: NonNullable }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string, String: string, Boolean: boolean, Int: number, Float: number, JSON: any, Date: any, }; export type ApprovePackageProposalInput = { proposalId: Scalars['ID'], }; export type CreateTeamInput = { name: Scalars['String'], projectTypeIds: Array, userIds: Array, }; export type DataSourcesInput = { github?: Maybe, npm?: Maybe, }; export type DownloadsPoint = { __typename?: 'DownloadsPoint', downloads: Scalars['Int'], day: Scalars['Date'], }; export type EditPackageInfoInput = { common: EditPackageInterfaceInput, }; export type EditPackageInterfaceInput = { id: Scalars['ID'], info?: Maybe, dataSources?: Maybe, }; export type EditPackageProjectTypesInput = { packageId: Scalars['ID'], projectTypeIds: Array, }; export type EditPackageProposalInfoInput = { common: EditPackageInterfaceInput, }; export type EditTeamInput = { id: Scalars['ID'], name: Scalars['String'], projectTypeIds: Array, userIds: Array, }; export type GithubDataSourceInput = { owner: Scalars['String'], repo: Scalars['String'], }; export type Mutation = { __typename?: 'Mutation', togglePackageBookmark?: Maybe, indexPackages?: Maybe, indexPackage?: Maybe, resetProjectTypeTagCounters?: Maybe, approvePackageProposal?: Maybe, editPackageProposalProjectTypes?: Maybe, editPackageProposalInfo?: Maybe, proposePackage?: Maybe, removePackageProposal: Scalars['Boolean'], togglePackageProposalUpvote?: Maybe, editPackageProjectTypes?: Maybe, editPackageInfo?: Maybe, toggleProjectTypeBookmark?: Maybe, createTeam?: Maybe, editTeam?: Maybe, }; export type MutationTogglePackageBookmarkArgs = { input: TogglePackageBookmarkInput }; export type MutationIndexPackageArgs = { id: Scalars['ID'] }; export type MutationApprovePackageProposalArgs = { input: ApprovePackageProposalInput }; export type MutationEditPackageProposalProjectTypesArgs = { input: EditPackageProjectTypesInput }; export type MutationEditPackageProposalInfoArgs = { input: EditPackageProposalInfoInput }; export type MutationProposePackageArgs = { input: ProposePackageInput }; export type MutationRemovePackageProposalArgs = { input: RemovePackageProposalInput }; export type MutationTogglePackageProposalUpvoteArgs = { input: TogglePackageProposalUpvoteInput }; export type MutationEditPackageProjectTypesArgs = { input: EditPackageProjectTypesInput }; export type MutationEditPackageInfoArgs = { input: EditPackageInfoInput }; export type MutationToggleProjectTypeBookmarkArgs = { input: ToggleProjectTypeBookmarkInput }; export type MutationCreateTeamArgs = { input: CreateTeamInput }; export type MutationEditTeamArgs = { input: EditTeamInput }; export type NpmDataSourceInput = { name: Scalars['String'], }; export type Package = PackageInterface & { __typename?: 'Package', id: Scalars['ID'], name: Scalars['String'], projectTypes: Array, info: PackageInfo, dataSources: Array, insight: PackageInsight, stars?: Maybe, repo?: Maybe, defaultLogo?: Maybe, maintainers: Array, homepage?: Maybe, license?: Maybe, description?: Maybe, readme?: Maybe, releases: Array, releaseCount?: Maybe, tagCount?: Maybe, bookmarked?: Maybe, }; export type PackageDataSource = { __typename?: 'PackageDataSource', type: Scalars['String'], data?: Maybe, }; export type PackageInfo = { __typename?: 'PackageInfo', tags: Array, }; export type PackageInfoInput = { tags: Array, }; export type PackageInsight = { __typename?: 'PackageInsight', npm?: Maybe, }; export type PackageInterface = { id: Scalars['ID'], name: Scalars['String'], projectTypes: Array, info: PackageInfo, dataSources: Array, insight: PackageInsight, stars?: Maybe, repo?: Maybe, defaultLogo?: Maybe, maintainers: Array, homepage?: Maybe, license?: Maybe, description?: Maybe, readme?: Maybe, releases: Array, releaseCount?: Maybe, tagCount?: Maybe, }; export type PackageMaintainer = { __typename?: 'PackageMaintainer', name?: Maybe, email?: Maybe, avatar?: Maybe, }; export type PackageNpmInsight = { __typename?: 'PackageNpmInsight', downloads: Scalars['Int'], downloadsPoints: Array, }; export type PackageNpmInsightDownloadsArgs = { range: PackageNpmInsightDownloadsRange }; export type PackageNpmInsightDownloadsPointsArgs = { range: PackageNpmInsightDownloadsRange }; export enum PackageNpmInsightDownloadsRange { Day = 'day', Week = 'week', Month = 'month' } export type PackageProposal = PackageInterface & { __typename?: 'PackageProposal', id: Scalars['ID'], name: Scalars['String'], projectTypes: Array, info: PackageInfo, dataSources: Array, insight: PackageInsight, stars?: Maybe, repo?: Maybe, defaultLogo?: Maybe, maintainers: Array, homepage?: Maybe, license?: Maybe, description?: Maybe, readme?: Maybe, releases: Array, releaseCount?: Maybe, tagCount?: Maybe, user?: Maybe, upvotes: Scalars['Int'], upvoted: Scalars['Boolean'], }; export type PackageRelease = { __typename?: 'PackageRelease', id: Scalars['ID'], date?: Maybe, title?: Maybe, tagName?: Maybe, description?: Maybe, prerelease?: Maybe, assets: Array, }; export type PackageReleaseAsset = { __typename?: 'PackageReleaseAsset', name: Scalars['String'], downloadUrl: Scalars['String'], size: Scalars['Int'], }; export type PackagesPage = { __typename?: 'PackagesPage', items: Array, after?: Maybe, }; export type ProjectType = { __typename?: 'ProjectType', id: Scalars['ID'], name: Scalars['String'], slug: Scalars['String'], logo: Scalars['String'], popularTags: Array, tags: Array, inTeam: Scalars['Boolean'], packageProposals: Array, packageProposalCount: Scalars['Int'], packages: PackagesPage, bookmarked?: Maybe, }; export type ProjectTypePackagesArgs = { tags?: Maybe>, after?: Maybe }; export type ProposePackageInput = { projectTypeId: Scalars['ID'], packageName: Scalars['String'], tags: Array, }; export type Query = { __typename?: 'Query', currentUser?: Maybe, allUsers: Array, packageProposal?: Maybe, packageProposalByName?: Maybe, package?: Maybe, packageByName?: Maybe, projectTypes: Array, projectType?: Maybe, projectTypeBySlug?: Maybe, team?: Maybe, allTeams: Array, }; export type QueryPackageProposalArgs = { id: Scalars['ID'] }; export type QueryPackageProposalByNameArgs = { name: Scalars['String'] }; export type QueryPackageArgs = { id: Scalars['ID'] }; export type QueryPackageByNameArgs = { name: Scalars['String'] }; export type QueryProjectTypeArgs = { id: Scalars['ID'] }; export type QueryProjectTypeBySlugArgs = { slug: Scalars['String'] }; export type QueryTeamArgs = { id: Scalars['ID'] }; export type RemovePackageProposalInput = { id: Scalars['ID'], }; export type Tag = { __typename?: 'Tag', id: Scalars['ID'], count: Scalars['Int'], }; export type Team = { __typename?: 'Team', id: Scalars['ID'], name: Scalars['String'], projectTypes: Array, users: Array, }; export type TogglePackageBookmarkInput = { packageId: Scalars['ID'], }; export type TogglePackageProposalUpvoteInput = { proposalId: Scalars['ID'], }; export type ToggleProjectTypeBookmarkInput = { projectTypeId: Scalars['ID'], }; export type User = { __typename?: 'User', id: Scalars['ID'], nickname: Scalars['String'], email: Scalars['String'], accounts: Array, avatar?: Maybe, admin?: Maybe, teams: Array, bookmarkedPackages: Array, }; export type UserAccount = { __typename?: 'UserAccount', id: Scalars['ID'], provider: Scalars['String'], profileId: Scalars['ID'], nickname?: Maybe, profileUrl?: Maybe, }; export type ResolverTypeWrapper = Promise | T; export type ResolverFn = ( parent: TParent, args: TArgs, context: TContext, info: GraphQLResolveInfo ) => Promise | TResult; export type StitchingResolver = { fragment: string; resolve: ResolverFn; }; export type Resolver = | ResolverFn | StitchingResolver; export type SubscriptionSubscribeFn = ( parent: TParent, args: TArgs, context: TContext, info: GraphQLResolveInfo ) => AsyncIterator | Promise>; export type SubscriptionResolveFn = ( parent: TParent, args: TArgs, context: TContext, info: GraphQLResolveInfo ) => TResult | Promise; export interface SubscriptionSubscriberObject { subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; resolve?: SubscriptionResolveFn; } export interface SubscriptionResolverObject { subscribe: SubscriptionSubscribeFn; resolve: SubscriptionResolveFn; } export type SubscriptionObject = | SubscriptionSubscriberObject | SubscriptionResolverObject; export type SubscriptionResolver = | ((...args: any[]) => SubscriptionObject) | SubscriptionObject; export type TypeResolveFn = ( parent: TParent, context: TContext, info: GraphQLResolveInfo ) => Maybe; export type NextResolverFn = () => Promise; export type DirectiveResolverFn = ( next: NextResolverFn, parent: TParent, args: TArgs, context: TContext, info: GraphQLResolveInfo ) => TResult | Promise; /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = { Query: ResolverTypeWrapper<{}>, User: ResolverTypeWrapper, ID: ResolverTypeWrapper, String: ResolverTypeWrapper, UserAccount: ResolverTypeWrapper, Boolean: ResolverTypeWrapper, Team: ResolverTypeWrapper & { projectTypes: Array, users: Array }>, ProjectType: ResolverTypeWrapper, Tag: ResolverTypeWrapper, Int: ResolverTypeWrapper, PackageProposal: ResolverTypeWrapper, PackageInterface: ResolverTypeWrapper, PackageInfo: ResolverTypeWrapper, PackageDataSource: ResolverTypeWrapper, JSON: ResolverTypeWrapper, PackageInsight: ResolverTypeWrapper, PackageNpmInsight: ResolverTypeWrapper, PackageNpmInsightDownloadsRange: PackageNpmInsightDownloadsRange, DownloadsPoint: ResolverTypeWrapper, Date: ResolverTypeWrapper, PackageMaintainer: ResolverTypeWrapper, PackageRelease: ResolverTypeWrapper, PackageReleaseAsset: ResolverTypeWrapper, PackagesPage: ResolverTypeWrapper & { items: Array }>, Package: ResolverTypeWrapper & { projectTypes: Array }>, Mutation: ResolverTypeWrapper<{}>, TogglePackageBookmarkInput: TogglePackageBookmarkInput, ApprovePackageProposalInput: ApprovePackageProposalInput, EditPackageProjectTypesInput: EditPackageProjectTypesInput, EditPackageProposalInfoInput: EditPackageProposalInfoInput, EditPackageInterfaceInput: EditPackageInterfaceInput, PackageInfoInput: PackageInfoInput, DataSourcesInput: DataSourcesInput, GithubDataSourceInput: GithubDataSourceInput, NpmDataSourceInput: NpmDataSourceInput, ProposePackageInput: ProposePackageInput, RemovePackageProposalInput: RemovePackageProposalInput, TogglePackageProposalUpvoteInput: TogglePackageProposalUpvoteInput, EditPackageInfoInput: EditPackageInfoInput, ToggleProjectTypeBookmarkInput: ToggleProjectTypeBookmarkInput, CreateTeamInput: CreateTeamInput, EditTeamInput: EditTeamInput, }; /** Mapping between all available schema types and the resolvers parents */ export type ResolversParentTypes = { Query: {}, User: DBUser, ID: Scalars['ID'], String: Scalars['String'], UserAccount: DBUserAccount, Boolean: Scalars['Boolean'], Team: Omit & { projectTypes: Array, users: Array }, ProjectType: DBProjectType, Tag: Tag, Int: Scalars['Int'], PackageProposal: DBPackageProposal, PackageInterface: DBPackageInterface, PackageInfo: PackageInfo, PackageDataSource: PackageDataSource, JSON: Scalars['JSON'], PackageInsight: PackageInsight, PackageNpmInsight: PackageNpmInsight, PackageNpmInsightDownloadsRange: PackageNpmInsightDownloadsRange, DownloadsPoint: DownloadsPoint, Date: Scalars['Date'], PackageMaintainer: PackageMaintainer, PackageRelease: PackageRelease, PackageReleaseAsset: PackageReleaseAsset, PackagesPage: Omit & { items: Array }, Package: Omit & { projectTypes: Array }, Mutation: {}, TogglePackageBookmarkInput: TogglePackageBookmarkInput, ApprovePackageProposalInput: ApprovePackageProposalInput, EditPackageProjectTypesInput: EditPackageProjectTypesInput, EditPackageProposalInfoInput: EditPackageProposalInfoInput, EditPackageInterfaceInput: EditPackageInterfaceInput, PackageInfoInput: PackageInfoInput, DataSourcesInput: DataSourcesInput, GithubDataSourceInput: GithubDataSourceInput, NpmDataSourceInput: NpmDataSourceInput, ProposePackageInput: ProposePackageInput, RemovePackageProposalInput: RemovePackageProposalInput, TogglePackageProposalUpvoteInput: TogglePackageProposalUpvoteInput, EditPackageInfoInput: EditPackageInfoInput, ToggleProjectTypeBookmarkInput: ToggleProjectTypeBookmarkInput, CreateTeamInput: CreateTeamInput, EditTeamInput: EditTeamInput, }; export type AdminDirectiveResolver = DirectiveResolverFn; export type AuthDirectiveResolver = DirectiveResolverFn; export interface DateScalarConfig extends GraphQLScalarTypeConfig { name: 'Date' } export type DownloadsPointResolvers = { downloads?: Resolver, day?: Resolver, }; export interface JsonScalarConfig extends GraphQLScalarTypeConfig { name: 'JSON' } export type MutationResolvers = { togglePackageBookmark?: Resolver, ParentType, ContextType, RequireFields>, indexPackages?: Resolver, ParentType, ContextType>, indexPackage?: Resolver, ParentType, ContextType, RequireFields>, resetProjectTypeTagCounters?: Resolver, ParentType, ContextType>, approvePackageProposal?: Resolver, ParentType, ContextType, RequireFields>, editPackageProposalProjectTypes?: Resolver, ParentType, ContextType, RequireFields>, editPackageProposalInfo?: Resolver, ParentType, ContextType, RequireFields>, proposePackage?: Resolver, ParentType, ContextType, RequireFields>, removePackageProposal?: Resolver>, togglePackageProposalUpvote?: Resolver, ParentType, ContextType, RequireFields>, editPackageProjectTypes?: Resolver, ParentType, ContextType, RequireFields>, editPackageInfo?: Resolver, ParentType, ContextType, RequireFields>, toggleProjectTypeBookmark?: Resolver, ParentType, ContextType, RequireFields>, createTeam?: Resolver, ParentType, ContextType, RequireFields>, editTeam?: Resolver, ParentType, ContextType, RequireFields>, }; export type PackageResolvers = { id?: Resolver, name?: Resolver, projectTypes?: Resolver, ParentType, ContextType>, info?: Resolver, dataSources?: Resolver, ParentType, ContextType>, insight?: Resolver, stars?: Resolver, ParentType, ContextType>, repo?: Resolver, ParentType, ContextType>, defaultLogo?: Resolver, ParentType, ContextType>, maintainers?: Resolver, ParentType, ContextType>, homepage?: Resolver, ParentType, ContextType>, license?: Resolver, ParentType, ContextType>, description?: Resolver, ParentType, ContextType>, readme?: Resolver, ParentType, ContextType>, releases?: Resolver, ParentType, ContextType>, releaseCount?: Resolver, ParentType, ContextType>, tagCount?: Resolver, ParentType, ContextType>, bookmarked?: Resolver, ParentType, ContextType>, }; export type PackageDataSourceResolvers = { type?: Resolver, data?: Resolver, ParentType, ContextType>, }; export type PackageInfoResolvers = { tags?: Resolver, ParentType, ContextType>, }; export type PackageInsightResolvers = { npm?: Resolver, ParentType, ContextType>, }; export type PackageInterfaceResolvers = { __resolveType?: TypeResolveFn<'PackageProposal' | 'Package', ParentType, ContextType>, id?: Resolver, name?: Resolver, projectTypes?: Resolver, ParentType, ContextType>, info?: Resolver, dataSources?: Resolver, ParentType, ContextType>, insight?: Resolver, stars?: Resolver, ParentType, ContextType>, repo?: Resolver, ParentType, ContextType>, defaultLogo?: Resolver, ParentType, ContextType>, maintainers?: Resolver, ParentType, ContextType>, homepage?: Resolver, ParentType, ContextType>, license?: Resolver, ParentType, ContextType>, description?: Resolver, ParentType, ContextType>, readme?: Resolver, ParentType, ContextType>, releases?: Resolver, ParentType, ContextType>, releaseCount?: Resolver, ParentType, ContextType>, tagCount?: Resolver, ParentType, ContextType>, }; export type PackageMaintainerResolvers = { name?: Resolver, ParentType, ContextType>, email?: Resolver, ParentType, ContextType>, avatar?: Resolver, ParentType, ContextType>, }; export type PackageNpmInsightResolvers = { downloads?: Resolver>, downloadsPoints?: Resolver, ParentType, ContextType, RequireFields>, }; export type PackageProposalResolvers = { id?: Resolver, name?: Resolver, projectTypes?: Resolver, ParentType, ContextType>, info?: Resolver, dataSources?: Resolver, ParentType, ContextType>, insight?: Resolver, stars?: Resolver, ParentType, ContextType>, repo?: Resolver, ParentType, ContextType>, defaultLogo?: Resolver, ParentType, ContextType>, maintainers?: Resolver, ParentType, ContextType>, homepage?: Resolver, ParentType, ContextType>, license?: Resolver, ParentType, ContextType>, description?: Resolver, ParentType, ContextType>, readme?: Resolver, ParentType, ContextType>, releases?: Resolver, ParentType, ContextType>, releaseCount?: Resolver, ParentType, ContextType>, tagCount?: Resolver, ParentType, ContextType>, user?: Resolver, ParentType, ContextType>, upvotes?: Resolver, upvoted?: Resolver, }; export type PackageReleaseResolvers = { id?: Resolver, date?: Resolver, ParentType, ContextType>, title?: Resolver, ParentType, ContextType>, tagName?: Resolver, ParentType, ContextType>, description?: Resolver, ParentType, ContextType>, prerelease?: Resolver, ParentType, ContextType>, assets?: Resolver, ParentType, ContextType>, }; export type PackageReleaseAssetResolvers = { name?: Resolver, downloadUrl?: Resolver, size?: Resolver, }; export type PackagesPageResolvers = { items?: Resolver, ParentType, ContextType>, after?: Resolver, ParentType, ContextType>, }; export type ProjectTypeResolvers = { id?: Resolver, name?: Resolver, slug?: Resolver, logo?: Resolver, popularTags?: Resolver, ParentType, ContextType>, tags?: Resolver, ParentType, ContextType>, inTeam?: Resolver, packageProposals?: Resolver, ParentType, ContextType>, packageProposalCount?: Resolver, packages?: Resolver>, bookmarked?: Resolver, ParentType, ContextType>, }; export type QueryResolvers = { currentUser?: Resolver, ParentType, ContextType>, allUsers?: Resolver, ParentType, ContextType>, packageProposal?: Resolver, ParentType, ContextType, RequireFields>, packageProposalByName?: Resolver, ParentType, ContextType, RequireFields>, package?: Resolver, ParentType, ContextType, RequireFields>, packageByName?: Resolver, ParentType, ContextType, RequireFields>, projectTypes?: Resolver, ParentType, ContextType>, projectType?: Resolver, ParentType, ContextType, RequireFields>, projectTypeBySlug?: Resolver, ParentType, ContextType, RequireFields>, team?: Resolver, ParentType, ContextType, RequireFields>, allTeams?: Resolver, ParentType, ContextType>, }; export type TagResolvers = { id?: Resolver, count?: Resolver, }; export type TeamResolvers = { id?: Resolver, name?: Resolver, projectTypes?: Resolver, ParentType, ContextType>, users?: Resolver, ParentType, ContextType>, }; export type UserResolvers = { id?: Resolver, nickname?: Resolver, email?: Resolver, accounts?: Resolver, ParentType, ContextType>, avatar?: Resolver, ParentType, ContextType>, admin?: Resolver, ParentType, ContextType>, teams?: Resolver, ParentType, ContextType>, bookmarkedPackages?: Resolver, ParentType, ContextType>, }; export type UserAccountResolvers = { id?: Resolver, provider?: Resolver, profileId?: Resolver, nickname?: Resolver, ParentType, ContextType>, profileUrl?: Resolver, ParentType, ContextType>, }; export type Resolvers = { Date?: GraphQLScalarType, DownloadsPoint?: DownloadsPointResolvers, JSON?: GraphQLScalarType, Mutation?: MutationResolvers, Package?: PackageResolvers, PackageDataSource?: PackageDataSourceResolvers, PackageInfo?: PackageInfoResolvers, PackageInsight?: PackageInsightResolvers, PackageInterface?: PackageInterfaceResolvers, PackageMaintainer?: PackageMaintainerResolvers, PackageNpmInsight?: PackageNpmInsightResolvers, PackageProposal?: PackageProposalResolvers, PackageRelease?: PackageReleaseResolvers, PackageReleaseAsset?: PackageReleaseAssetResolvers, PackagesPage?: PackagesPageResolvers, ProjectType?: ProjectTypeResolvers, Query?: QueryResolvers, Tag?: TagResolvers, Team?: TeamResolvers, User?: UserResolvers, UserAccount?: UserAccountResolvers, }; /** * @deprecated * Use "Resolvers" root object instead. If you wish to get "IResolvers", add "typesPrefix: I" to your config. */ export type IResolvers = Resolvers; export type DirectiveResolvers = { admin?: AdminDirectiveResolver, auth?: AuthDirectiveResolver, }; /** * @deprecated * Use "DirectiveResolvers" root object instead. If you wish to get "IDirectiveResolvers", add "typesPrefix: I" to your config. */ export type IDirectiveResolvers = DirectiveResolvers; ================================================ FILE: packages/backend/src/index.ts ================================================ import './passport' import { bootstrap, printReady } from '@nodepack/app' import { Context } from './context' import morgan from 'morgan' import { hook } from '@nodepack/app-context' import { spawn } from 'child_process' if (process.env.NODEPACK_ENV === 'development') { hook('expressCreate', (ctx: Context) => { // Express logs ctx.express.use(morgan('dev')) ctx.express.use(morgan(':method :url req-cookie: :req[cookie] res-set-cookie: :res[set-cookie]')) }) } bootstrap(() => { printReady() }) // Auto-generate shcema code hook('apolloListen', () => { spawn('yarn', ['schema-gen'], { cwd: process.cwd(), stdio: ['inherit', 'inherit', 'inherit'], }) }) ================================================ FILE: packages/backend/src/passport/github.ts ================================================ import { use } from './util' import { Strategy } from 'passport-github2' use( 'github', (verify) => new Strategy({ clientID: process.env.OAUTH_GITHUB_ID, clientSecret: process.env.OAUTH_GITHUB_SECRET, callbackURL: process.env.OAUTH_GITHUB_CALLBACK, }, verify), { scope: ['user:email'], }, ) ================================================ FILE: packages/backend/src/passport/index.ts ================================================ import './github' ================================================ FILE: packages/backend/src/passport/util.ts ================================================ import { VerifyCallback, Strategy, VerifyFunction } from 'passport-oauth2' import { query as q, values } from 'faunadb' import { useStrategy, deserializeUser } from '@nodepack/plugin-passport' import { hook } from '@nodepack/app-context' import { Context } from '@/context' import passport, { AuthenticateOptions } from 'passport' import fetch from 'node-fetch' import { mapDocument } from '@/util/fauna' export interface UserAccount { provider: string profileId: string userRef: values.Ref nickname?: string profileUrl?: string accessToken?: string refreshToken?: string } export interface OAuthProfile { id: string username?: string displayName?: string email?: string emails?: Array<{ value: string }> photos?: Array<{ value: string }> profileUrl?: string } const basePath = process.env.BASE_API_PATH || '' const clientBaseUrl = process.env.CLIENT_BASE_URL || '' export function use ( provider: string, factory: (verify: VerifyFunction) => Strategy, authenticateOptions: AuthenticateOptions, ) { hook('expressCreate', (ctx: Context) => { const strategy = factory(verifyOAuth(provider, ctx)) useStrategy(strategy, (context: Context) => { context.express.get( `${basePath}/auth/${provider}`, passport.authenticate(provider, authenticateOptions), ) context.express.get( `${basePath}/auth/${provider}/callback`, passport.authenticate(provider, { failureRedirect: `${clientBaseUrl}/login?error=1`, }), (req, res) => { res.redirect(`${clientBaseUrl}/`) }, ) }) }) } deserializeUser(async (passportCtx, { serialized }) => { try { const ctx = passportCtx as Context const doc = await ctx.db.query>( q.Get(q.Ref(q.Collection('Users'), serialized)), ) return mapDocument(doc) } catch (e) { console.error(e) return null } }) export function verifyOAuth (provider: string, ctx: Context) { return async (accessToken: string, refreshToken: string, profile: OAuthProfile, done: VerifyCallback) => { try { let user: values.Document let account: values.Document = await ctx.db.query( q.Let( { ref: q.Match(q.Index('useraccounts_by_provider_and_profileid'), provider, profile.id), }, q.If( q.Exists(q.Var('ref')), q.Get(q.Var('ref')), null, ), ), ) if (!account) { // Create User let email: string if (!profile.email && (!profile.emails || !profile.emails.length)) { // If the user doesn't have any public email // we need to fetch the private ones const data = await fetch(`https://api.github.com/user/emails`, { headers: { authorization: `token ${accessToken}`, }, }) const json = await data.json() email = json.find((item: any) => item.primary).email } else { email = profile.email || profile.emails[0].value } const avatar = profile.photos.length ? profile.photos[0].value : null const nickname = profile.displayName || profile.username user = await ctx.db.query( q.Create( q.Collection('Users'), { data: { email, avatar, nickname, }, }, ), ) account = await ctx.db.query( q.Create( q.Collection('UserAccounts'), { data: { provider, profileId: profile.id, userRef: user.ref, nickname, profileUrl: profile.profileUrl, accessToken, refreshToken, } as UserAccount, }, ), ) } else { user = await ctx.db.query( q.Get(account.data.userRef), ) } done(null, user ? mapDocument(user) : null) } catch (err) { console.error(err) done(err) } } } ================================================ FILE: packages/backend/src/routes/user.ts ================================================ import { ExpressContext } from '@nodepack/plugin-express' const basePath = process.env.BASE_API_PATH || '' const clientBaseUrl = process.env.CLIENT_BASE_URL || '' export default function ({ express: app }: ExpressContext) { app.use(`${basePath}/auth/logout`, (req, res) => { req.logout() res.redirect(`${clientBaseUrl}/`) }) } ================================================ FILE: packages/backend/src/schema/admin.ts ================================================ import { SchemaDirectiveVisitor } from 'graphql-tools' import { defaultFieldResolver, GraphQLField } from 'graphql' import gql from 'graphql-tag' import { ErrorCode } from '@/const/error-codes' import { query as q } from 'faunadb' import { getIndexObject, indexPackage } from '../util/package-index' import { Resolvers } from '@/generated/schema' import { ApolloError } from '@nodepack/plugin-apollo' export const typeDefs = gql` """ Field requiring authenticated user. Otherwise, a 'unauthorized' error is thrown. """ directive @admin on FIELD_DEFINITION extend type Mutation { indexPackages: Boolean @admin @auth indexPackage (id: ID!): Boolean @admin @auth resetProjectTypeTagCounters: Boolean @admin @auth } ` export const schemaDirectives = { admin: class AuthDirective extends SchemaDirectiveVisitor { public visitFieldDefinition (field: GraphQLField) { const { resolve = defaultFieldResolver } = field field.resolve = (root, args, context, info) => { if (!context.user.admin) { throw new ApolloError('Access denied', ErrorCode.ERROR_UNAUTHORIZED) } return resolve(root, args, context, info) } } }, } export const resolvers: Resolvers = { Mutation: { // Used to index all packages by admin indexPackages: async (root, args, ctx) => { const { data: allPackages } = await ctx.db.query( q.Map( q.Map( q.Paginate(q.Match(q.Index('all_packages')), { size: 100000 }), q.Lambda('ref', q.Get(q.Var('ref'))), ), q.Lambda('doc', q.Merge( q.Var('doc'), { projectType: q.Get(q.Ref( q.Collection('ProjectTypes'), q.Select(['data', 'projectTypeId'], q.Var('doc')), )), }, ), ), ), ) const index = ctx.algolia.initIndex('packages') await index.addObjects( await Promise.all(allPackages.map((doc: any) => getIndexObject( ctx, doc, ))), ) return true }, indexPackage: async (root, { id }, ctx) => { const pkg: any = await ctx.db.query( q.Let({ doc: q.Get(q.Ref( q.Collection('Packages'), id, )), }, q.Merge( q.Var('doc'), { projectType: q.Get(q.Ref( q.Collection('ProjectTypes'), q.Select(['data', 'projectTypeId'], q.Var('doc')), )), }, )), ) await indexPackage(ctx, pkg) return true }, resetProjectTypeTagCounters: async (root, args, ctx) => { const projectTypeMap = new Map>() const { data: packages } = await ctx.db.query(q.Map( q.Paginate(q.Match(q.Index('all_packages')), { size: 100000 }), q.Lambda('ref', q.Get(q.Var('ref'))), )) for (const pkg of packages) { let counters = projectTypeMap.get(pkg.data.projectTypeId) if (!counters) { counters = new Map() projectTypeMap.set(pkg.data.projectTypeId, counters) } for (const tag of pkg.data.info.tags) { let count = counters.get(tag) if (!count) { count = 0 } count++ counters.set(tag, count) } } await ctx.db.query( q.Do( // Reset counters to empty objects q.Foreach( q.Paginate(q.Match(q.Index('all_projecttypes')), { size: 100000 }), q.Lambda('ref', q.Do( q.Update(q.Var('ref'), { data: { tagMap: null } }), q.Update(q.Var('ref'), { data: { tagMap: {} } }), )), ), // Set counts ...Array.from(projectTypeMap.keys()).map((id) => q.Update( q.Ref(q.Collection('ProjectTypes'), id), { data: { tagMap: Array.from(projectTypeMap.get(id).keys()).reduce((map, key) => { map[key] = projectTypeMap.get(id).get(key) return map }, {} as { [key: string]: number }), }, }, ), ), ), ) return true }, }, } ================================================ FILE: packages/backend/src/schema/auth.ts ================================================ import { SchemaDirectiveVisitor } from 'graphql-tools' import { defaultFieldResolver, GraphQLField } from 'graphql' import gql from 'graphql-tag' import { ApolloError } from '@nodepack/plugin-apollo' import { ErrorCode } from '@/const/error-codes' export const typeDefs = gql` """ Field requiring authenticated user. Otherwise, a 'unauthorized' error is thrown. """ directive @auth on FIELD_DEFINITION ` export const schemaDirectives = { auth: class AuthDirective extends SchemaDirectiveVisitor { public visitFieldDefinition (field: GraphQLField) { const { resolve = defaultFieldResolver } = field field.resolve = (root, args, context, info) => { if (!context.user) { throw new ApolloError('Not logged in', ErrorCode.ERROR_GUEST) } return resolve(root, args, context, info) } } }, } ================================================ FILE: packages/backend/src/schema/package/bookmark.ts ================================================ import gql from 'graphql-tag' import { Resolvers } from '@/generated/schema' import { query as q, values } from 'faunadb' import { mapDocument, mapDocuments } from '@/util/fauna' export const typeDefs = gql` extend type Package { bookmarked: Boolean } extend type User { bookmarkedPackages: [Package!]! } type Mutation { togglePackageBookmark (input: TogglePackageBookmarkInput!): Package @auth } input TogglePackageBookmarkInput { packageId: ID! } ` export const resolvers: Resolvers = { Package: { bookmarked: async (pkg, args, ctx) => { if (!ctx.user) { return false } return !!await ctx.db.query( q.Exists(q.Match( q.Index('packagebookmarks_by_package_and_user'), q.Ref(q.Collection('Users'), ctx.user.id), q.Ref(q.Collection('Packages'), pkg.id), )), ) }, }, User: { bookmarkedPackages: async (user, args, ctx) => { const { data } = await ctx.db.query( q.Map( q.Paginate(q.Match(q.Index('packagebookmarks_by_userref'), q.Ref(q.Collection('Users'), user.id))), q.Lambda(['ref'], q.Get(q.Select(['data', 'packageRef'], q.Get(q.Var('ref'))))), ), ) return mapDocuments(data) }, }, Mutation: { togglePackageBookmark: async (root, { input }, ctx) => { const ref = q.Ref(q.Collection('Packages'), input.packageId) const userRef = q.Ref(q.Collection('Users'), ctx.user.id) const pkg: values.Document = await ctx.db.query( q.Get(ref), ) const match = q.Match( q.Index('packagebookmarks_by_package_and_user'), userRef, ref, ) if (await ctx.db.query(q.Exists(match))) { await ctx.db.query( q.Delete(q.Select(['ref'], q.Get(match))), ) } else { await ctx.db.query( q.Create( q.Collection('PackageBookmarks'), { data: { packageRef: pkg.ref, userRef, }, }, ), ) } return mapDocument(pkg) }, }, } ================================================ FILE: packages/backend/src/schema/package/edit-project-types.ts ================================================ import gql from 'graphql-tag' import { Resolvers } from '@/generated/schema' import { query as q, values } from 'faunadb' import { editPackageProjectTypes } from '../package-interface/edit-project-types' import { updatePackageIndex } from '@/util/package-index' import { updateProjectTypeTags } from '@/util/tag-map' import { mapDocument } from '@/util/fauna' import { checkPackageTeamAccess } from '../team/team-access' export const typeDefs = gql` extend type Mutation { editPackageProjectTypes (input: EditPackageProjectTypesInput!): Package @auth } ` export const resolvers: Resolvers = { Mutation: { editPackageProjectTypes: async (root, { input }, ctx) => { const ref = q.Ref(q.Collection('Packages'), input.packageId) await checkPackageTeamAccess(ctx, ref) const oldDoc = await ctx.db.query>(q.Get(ref)) const doc = await editPackageProjectTypes(ref, input.projectTypeIds, ctx) // Update tags // Dedupe project type refs const projectTypeIds = Array.from(new Set([ ...oldDoc.data.projectTypes.map((r: values.Ref) => r.id), ...doc.data.projectTypes.map((r: values.Ref) => r.id), ])) const projectTypeRefs = projectTypeIds.map((id) => q.Ref(q.Collection('ProjectTypes'), id)) for (const projectTypeRef of projectTypeRefs) { await updateProjectTypeTags(projectTypeRef, ctx) } // Update index await updatePackageIndex(ctx, doc) return mapDocument(doc) }, }, } ================================================ FILE: packages/backend/src/schema/package/edit.ts ================================================ import gql from 'graphql-tag' import { Resolvers } from '@/generated/schema' import { query as q } from 'faunadb' import { updateProjectTypeTags } from '@/util/tag-map' import { updatePackageIndex } from '@/util/package-index' import { editPackageCommon } from '../package-interface/edit' import { mapDocument } from '@/util/fauna' import { checkPackageTeamAccess } from '../team/team-access' export const typeDefs = gql` extend type Mutation { editPackageInfo (input: EditPackageInfoInput!): Package @auth } input EditPackageInfoInput { common: EditPackageInterfaceInput! } ` export const resolvers: Resolvers = { Mutation: { editPackageInfo: async (root, { input }, ctx) => { const ref = q.Ref(q.Collection('Packages'), input.common.id) await checkPackageTeamAccess(ctx, ref) const pkg = await editPackageCommon(ref, input.common, ctx) // Update tags for (const projectTypeRef of pkg.data.projectTypes) { await updateProjectTypeTags(projectTypeRef, ctx) } // Update search index await updatePackageIndex(ctx, pkg) return mapDocument(pkg) }, }, } ================================================ FILE: packages/backend/src/schema/package/index.ts ================================================ import gql from 'graphql-tag' import { query as q, values, Expr } from 'faunadb' import { Resolvers } from '@/generated/schema' import { mapDocuments, mapDocument } from '@/util/fauna' export const typeDefs = gql` extend type ProjectType { packages (tags: [String!] = null, after: JSON = null): PackagesPage! } type PackagesPage { items: [Package!]! after: JSON } extend type Query { package (id: ID!): Package packageByName (name: String!): Package } ` export const resolvers: Resolvers = { ProjectType: { packages: async (projectType, input, ctx) => { const projectTypeRef = q.Ref(q.Collection('ProjectTypes'), projectType.id) const { data, after } = await ctx.db.query( q.Map( q.Paginate( input.tags && input.tags.length ? q.Join( q.Intersection( q.Match(q.Index('packages_by_project_type'), projectTypeRef), q.Union( ...input.tags.map((tag: string) => q.Match(q.Index('packages_by_tag'), tag)), ), ), q.Index('packages_by_ref_sort_by_stars_desc'), ) : q.Match(q.Index('packages_by_project_type_sort_by_stars_desc'), projectTypeRef), { size: 12, after: input.after ? new Expr(input.after) : null }, ), q.Lambda(['stars', 'ref'], q.Get(q.Var('ref'))), ), ) return { items: mapDocuments(data), after, } }, }, Query: { package: async (root, { id }, ctx) => { const doc = await ctx.db.query>( q.Get(q.Ref(q.Collection('Packages'), id)), ) if (doc.data) { return mapDocument(doc) } }, packageByName: async (root, { name } , ctx) => { try { const doc = await ctx.db.query>( q.Get(q.Match(q.Index('packages_by_name'), name)), ) return mapDocument(doc) } catch (e) { // Nothing } }, }, } ================================================ FILE: packages/backend/src/schema/package-interface/data-source.ts ================================================ import gql from 'graphql-tag' import { Resolvers } from '@/generated/schema' export const typeDefs = gql` extend interface PackageInterface { dataSources: [PackageDataSource!]! } extend type Package { dataSources: [PackageDataSource!]! } extend type PackageProposal { dataSources: [PackageDataSource!]! } type PackageDataSource { type: String! data: JSON } input DataSourcesInput { github: GithubDataSourceInput npm: NpmDataSourceInput } input GithubDataSourceInput { owner: String! repo: String! } input NpmDataSourceInput { name: String! } ` export interface NpmDataSource { name: string } export interface GithubDataSource { owner: string repo: string } export interface PackageDataSources { npm: NpmDataSource github: GithubDataSource } export const resolvers: Resolvers = { PackageInterface: { dataSources: async (pkg) => { return Object.keys(pkg.dataSources || {}).map((key) => ({ type: key, // @ts-ignore data: pkg.dataSources[key], })) }, }, } ================================================ FILE: packages/backend/src/schema/package-interface/db-types.ts ================================================ import { PackageInterface } from '@/generated/schema' import { PackageDataSources, GithubDataSource } from './data-source' import { values } from 'faunadb' export interface PackageMetadata { version: number ts: number data: T } export interface PackageMetadatas { npm?: PackageMetadata github?: PackageMetadata } export interface DBPackageInterface extends Omit { ref: values.Ref dataSources: PackageDataSources metadata: PackageMetadatas /** * @deprecated */ github: GithubDataSource } ================================================ FILE: packages/backend/src/schema/package-interface/edit-project-types.ts ================================================ import gql from 'graphql-tag' import { Context } from '@/context' import { Expr, query as q, values } from 'faunadb' import { ApolloError } from '@nodepack/plugin-apollo' import { ErrorCode } from '@/const/error-codes' export const typeDefs = gql` input EditPackageProjectTypesInput { packageId: ID! projectTypeIds: [ID!]! } ` export async function editPackageProjectTypes ( ref: Expr, projectTypeIds: string[], ctx: Context, ) { if (!projectTypeIds.length) { throw new ApolloError('Select at least one project type', ErrorCode.ERROR_VALIDATION) } // Update data const item = await ctx.db.query>( q.Update(ref, { data: { projectTypes: projectTypeIds.map((id) => q.Ref(q.Collection('ProjectTypes'), id)), }, }), ) return item } ================================================ FILE: packages/backend/src/schema/package-interface/edit.ts ================================================ import gql from 'graphql-tag' import { Context } from '@/context' import { sanitizeTags } from '@/util/tags' import { query as q, Expr, values } from 'faunadb' import { EditPackageInterfaceInput } from '@/generated/schema' export const typeDefs = gql` input EditPackageInterfaceInput { id: ID! info: PackageInfoInput dataSources: DataSourcesInput } ` export async function editPackageCommon ( ref: Expr, input: EditPackageInterfaceInput, ctx: Context, ) { // Process tags input.info.tags = sanitizeTags(input.info.tags) // Update data const item = await ctx.db.query>( q.Update(ref, { data: { ...input.info ? { info: input.info } : {}, ...input.dataSources ? { dataSources: input.dataSources, metadata: { npm: null, github: null, }, } : {}, }, }), ) return item } ================================================ FILE: packages/backend/src/schema/package-interface/index.ts ================================================ import gql from 'graphql-tag' import { Resolvers } from '@/generated/schema' import { query as q } from 'faunadb' import { DBProjectType } from '../project-type/db-types' import { mapDocuments } from '@/util/fauna' export const typeDefs = gql` interface PackageInterface { id: ID! name: String! projectTypes: [ProjectType!]! info: PackageInfo! } type Package implements PackageInterface { id: ID! name: String! projectTypes: [ProjectType!]! info: PackageInfo! } type PackageProposal implements PackageInterface { id: ID! name: String! projectTypes: [ProjectType!]! info: PackageInfo! } type PackageMaintainer { name: String email: String avatar: String } type PackageInfo { tags: [String!]! } input PackageInfoInput { tags: [String!]! } ` export const resolvers: Resolvers = { PackageInterface: { __resolveType: (pkg: any) => { if (pkg.ref.collection.id === 'Packages') { return 'Package' } if (pkg.ref.collection.id === 'PackageProposals') { return 'PackageProposal' } return null }, projectTypes: async (pkg, input, ctx) => { const list = await ctx.db.query( q.Map( pkg.projectTypes, q.Lambda(['ref'], q.Get(q.Var('ref'))), ), ) return mapDocuments(list) as DBProjectType[] }, }, } ================================================ FILE: packages/backend/src/schema/package-interface/insight.ts ================================================ import gql from 'graphql-tag' import { Resolvers } from '@/generated/schema' export const typeDefs = gql` extend interface PackageInterface { insight: PackageInsight! } extend type Package { insight: PackageInsight! } extend type PackageProposal { insight: PackageInsight! } type PackageInsight { npm: PackageNpmInsight } type PackageNpmInsight { downloads (range: PackageNpmInsightDownloadsRange!): Int! downloadsPoints (range: PackageNpmInsightDownloadsRange!): [DownloadsPoint!]! } type DownloadsPoint { downloads: Int! day: Date! } enum PackageNpmInsightDownloadsRange { day week month } ` export const resolvers: Resolvers = { PackageInterface: { insight: (pkg) => pkg as any, }, PackageInsight: { npm: (pkg: any) => pkg.dataSources.npm ? pkg : null, }, PackageNpmInsight: { downloads: async (pkg: any, { range }, ctx) => { const data = await ctx.npmApi(`/downloads/point/last-${range}/${encodeURIComponent(pkg.dataSources.npm.name)}`) return data.downloads }, downloadsPoints: async (pkg: any, { range }, ctx) => { const data = await ctx.npmApi(`/downloads/range/last-${range}/${encodeURIComponent(pkg.dataSources.npm.name)}`) return data.downloads }, }, } ================================================ FILE: packages/backend/src/schema/package-interface/package-metadata.ts ================================================ import { Resolvers } from '@/generated/schema' import { getNpmMetadata, getGithubMetadata } from '@/util/metadata' import { getReadme } from '@/util/readme' import gql from 'graphql-tag' export const typeDefs = gql` extend interface PackageInterface { stars: Int repo: String defaultLogo: String maintainers: [PackageMaintainer!]! homepage: String license: String description: String readme: String } extend type Package { stars: Int repo: String defaultLogo: String maintainers: [PackageMaintainer!]! homepage: String license: String description: String readme: String } extend type PackageProposal { stars: Int repo: String defaultLogo: String maintainers: [PackageMaintainer!]! homepage: String license: String description: String readme: String } ` export const resolvers: Resolvers = { PackageInterface: { stars: async (pkg, args, ctx) => (await getGithubMetadata(pkg, ctx)).stars, repo: async (pkg, args, ctx) => (await getGithubMetadata(pkg, ctx)).htmlUrl, defaultLogo: async (pkg, args, ctx) => { if (pkg.dataSources.npm) { const data = await getNpmMetadata(pkg, ctx) if (data?.logo) { return data.logo } else if (data?.awesomejs?.logo) { return data.awesomejs.logo } } return (await getGithubMetadata(pkg, ctx)).owner?.avatar }, maintainers: async (pkg, args, ctx) => (await getNpmMetadata(pkg, ctx)).maintainers || [], homepage: async (pkg, args, ctx) => (await getNpmMetadata(pkg, ctx)).homepage, license: async (pkg, args, ctx) => (await getNpmMetadata(pkg, ctx)).license, description: async (pkg, args, ctx) => (await getGithubMetadata(pkg, ctx)).description || (await getNpmMetadata(pkg, ctx)).description, readme: async (pkg, args, ctx) => getReadme(pkg, ctx), }, } ================================================ FILE: packages/backend/src/schema/package-interface/releases.ts ================================================ import { getGithubDataSource } from '@/util/metadata' import gql from 'graphql-tag' import { Resolvers } from '@/generated/schema' import { getPageTotalCount } from '@/util/github' export const typeDefs = gql` type PackageRelease { id: ID! date: Date title: String tagName: String description: String prerelease: Boolean assets: [PackageReleaseAsset!]! } type PackageReleaseAsset { name: String! downloadUrl: String! size: Int! } extend interface PackageInterface { releases: [PackageRelease!]! releaseCount: Int tagCount: Int } extend type Package { releases: [PackageRelease!]! releaseCount: Int tagCount: Int } extend type PackageProposal { releases: [PackageRelease!]! releaseCount: Int tagCount: Int } ` export const resolvers: Resolvers = { PackageInterface: { releases: async (pkg, args, ctx) => { const { owner, repo } = await getGithubDataSource(pkg, ctx) if (repo) { const { data } = await ctx.github.repos.listReleases({ owner, repo, headers: { accept: 'application/vnd.github.3.html', }, }) return data.filter((i) => !i.draft).map((item) => ({ id: item.id.toString(), date: item.published_at, title: item.name || item.tag_name, tagName: item.tag_name, // @ts-ignore description: item.body_html, prerelease: item.prerelease, assets: item.assets.map((asset) => ({ name: asset.name, downloadUrl: asset.browser_download_url, size: asset.size, })), })) } return [] }, releaseCount: async (pkg, args, ctx) => { const { owner, repo } = await getGithubDataSource(pkg, ctx) if (repo) { const result = await ctx.github.repos.listReleases({ owner, repo, per_page: 1, }) return getPageTotalCount(result) } }, tagCount: async (pkg, args, ctx) => { const { owner, repo } = await getGithubDataSource(pkg, ctx) if (repo) { const result = await ctx.github.repos.listTags({ owner, repo, per_page: 1, }) return getPageTotalCount(result) } }, }, } ================================================ FILE: packages/backend/src/schema/package-proposal/approve.ts ================================================ import gql from 'graphql-tag' import { query as q, Expr, values } from 'faunadb' import { indexPackage } from '@/util/package-index' import { Resolvers } from '@/generated/schema' import { mapDocument } from '@/util/fauna' import { checkPackageTeamAccess } from '../team/team-access' export const typeDefs = gql` extend type Mutation { approvePackageProposal (input: ApprovePackageProposalInput!): Package @auth } input ApprovePackageProposalInput { proposalId: ID! } ` export const resolvers: Resolvers = { Mutation: { approvePackageProposal: async (root, { input }, ctx) => { const ref = q.Ref(q.Collection('PackageProposals'), input.proposalId) await checkPackageTeamAccess(ctx, ref) const pkgProposal: any = await ctx.db.query( q.Get(ref), ) // Update tag maps const projectTypes = await ctx.db.query>>(q.Map( pkgProposal.data.projectTypes, q.Lambda(['ref'], q.Get(q.Var('ref'))), )) const projectTypeUpdates: Expr[] = [] for (const projectType of projectTypes) { const tagMap = projectType.data.tagMap for (const tag of pkgProposal.data.info.tags) { tagMap[tag] = tagMap[tag] || 0 tagMap[tag]++ } projectTypeUpdates.push(q.Update( projectType.ref, { data: { tagMap, }, }, )) } const pkg: any = await ctx.db.query( q.Do( q.Delete(pkgProposal.ref), ...projectTypeUpdates, q.Create( q.Collection('Packages'), { data: { name: pkgProposal.data.name, projectTypes: projectTypes.map((pt) => pt.ref), info: pkgProposal.data.info, dataSources: pkgProposal.data.dataSources, metadata: pkgProposal.data.metadata, }, }, ), ), ) await indexPackage(ctx, pkg) return mapDocument(pkg) }, }, } ================================================ FILE: packages/backend/src/schema/package-proposal/db-types.ts ================================================ import { values } from 'faunadb' import { PackageProposal } from '@/generated/schema' export interface DBPackageProposal extends PackageProposal { projectTypeRef: values.Ref userRef: values.Ref } ================================================ FILE: packages/backend/src/schema/package-proposal/edit-project-types.ts ================================================ import gql from 'graphql-tag' import { Resolvers } from '@/generated/schema' import { query as q } from 'faunadb' import { editPackageProjectTypes } from '../package-interface/edit-project-types' import { mapDocument } from '@/util/fauna' import { checkPackageTeamAccess } from '../team/team-access' export const typeDefs = gql` extend type Mutation { editPackageProposalProjectTypes (input: EditPackageProjectTypesInput!): PackageProposal @auth } ` export const resolvers: Resolvers = { Mutation: { editPackageProposalProjectTypes: async (root, { input }, ctx) => { const ref = q.Ref(q.Collection('PackageProposals'), input.packageId) await checkPackageTeamAccess(ctx, ref) const doc = await editPackageProjectTypes(ref, input.projectTypeIds, ctx) return mapDocument(doc) }, }, } ================================================ FILE: packages/backend/src/schema/package-proposal/edit.ts ================================================ import gql from 'graphql-tag' import { query as q } from 'faunadb' import { Resolvers } from '@/generated/schema' import { editPackageCommon } from '../package-interface/edit' import { mapDocument } from '@/util/fauna' import { checkPackageTeamAccess } from '../team/team-access' export const typeDefs = gql` extend type Mutation { editPackageProposalInfo (input: EditPackageProposalInfoInput!): PackageProposal @auth } input EditPackageProposalInfoInput { common: EditPackageInterfaceInput! } ` export const resolvers: Resolvers = { Mutation: { editPackageProposalInfo: async (root, { input }, ctx) => { const ref = q.Ref(q.Collection('PackageProposals'), input.common.id) await checkPackageTeamAccess(ctx, ref) const item = await editPackageCommon(ref, input.common, ctx) return mapDocument(item) }, }, } ================================================ FILE: packages/backend/src/schema/package-proposal/index.ts ================================================ import gql from 'graphql-tag' import { query as q, values } from 'faunadb' import { Resolvers } from '@/generated/schema' import { mapDocument, mapDocuments } from '@/util/fauna' export const typeDefs = gql` extend type PackageProposal { user: User } extend type ProjectType { packageProposals: [PackageProposal!]! packageProposalCount: Int! } extend type Query { packageProposal (id: ID!): PackageProposal packageProposalByName (name: String!): PackageProposal } ` export const resolvers: Resolvers = { PackageProposal: { user: async (proposal, args, ctx) => { try { const user = await ctx.db.query>( q.Get(proposal.userRef), ) return mapDocument(user) } catch (e) { // Nothing } }, }, ProjectType: { packageProposals: async (projectType, args, ctx) => { const { data } = await ctx.db.query( q.Map( q.Paginate( q.Match( q.Index('packageproposals_by_projecttypes_sort_by_upvote'), q.Ref(q.Collection('ProjectTypes'), projectType.id), ), ), q.Lambda(['upvotes', 'ref'], q.Get(q.Var('ref'))), ), ) return mapDocuments(data) }, packageProposalCount: async (projectType, args, ctx) => { const { data } = await ctx.db.query( q.Count( q.Paginate( q.Match( q.Index('packageproposals_by_projecttypes_sort_by_upvote'), q.Ref(q.Collection('ProjectTypes'), projectType.id), ), ), ), ) return data[0] }, }, Query: { packageProposal: async (root, { id }, ctx) => { const doc = await ctx.db.query>( q.Get(q.Ref(q.Collection('PackageProposals'), id)), ) return mapDocument(doc) }, packageProposalByName: async (root, { name }, ctx) => { try { const doc = await ctx.db.query>( q.Get(q.Match(q.Index('packageproposal_by_name'), name)), ) return mapDocument(doc) } catch (e) { // Nothing } }, }, } ================================================ FILE: packages/backend/src/schema/package-proposal/propose.ts ================================================ import gql from 'graphql-tag' import { query as q, values } from 'faunadb' import { sanitizeTags } from '@/util/tags' import { ApolloError } from '@nodepack/plugin-apollo' import { Resolvers } from '@/generated/schema' import { mapDocument } from '@/util/fauna' export const typeDefs = gql` extend type Mutation { proposePackage (input: ProposePackageInput!): PackageProposal @auth } input ProposePackageInput { projectTypeId: ID! packageName: String! tags: [String!]! } ` export const resolvers: Resolvers = { Mutation: { proposePackage: async (root, { input }, ctx) => { input.tags = sanitizeTags(input.tags) if (await ctx.db.query( q.Exists(q.Match(q.Index('packageproposal_by_name'), input.packageName)), )) { throw new ApolloError('Package proposal already exists') } if (await ctx.db.query( q.Exists(q.Match(q.Index('packages_by_name'), input.packageName)), )) { throw new ApolloError('Package was already added') } // Npm check try { await ctx.npm(`/${encodeURIComponent(input.packageName)}`) } catch (e) { throw new ApolloError(`Package not found on npm`) } const doc = await ctx.db.query>( q.Create( q.Collection('PackageProposals'), { data: { name: input.packageName, projectTypes: [ q.Ref(q.Collection('ProjectTypes'), input.projectTypeId), ], userRef: q.Ref(q.Collection('Users'), ctx.user.id), upvotes: 0, info: { tags: input.tags, }, dataSources: { npm: { name: input.packageName, }, }, }, }, ), ) return mapDocument(doc) }, }, } ================================================ FILE: packages/backend/src/schema/package-proposal/remove.ts ================================================ import gql from 'graphql-tag' import { Resolvers } from '@/generated/schema' import { query as q } from 'faunadb' import { checkPackageTeamAccess } from '../team/team-access' export const typeDefs = gql` input RemovePackageProposalInput { id: ID! } extend type Mutation { removePackageProposal (input: RemovePackageProposalInput!): Boolean! } ` export const resolvers: Resolvers = { Mutation: { removePackageProposal: async (root, { input }, ctx) => { const ref = q.Ref(q.Collection('PackageProposals'), input.id) await checkPackageTeamAccess(ctx, ref) await ctx.db.query(q.Delete(ref)) return true }, }, } ================================================ FILE: packages/backend/src/schema/package-proposal/upvote.ts ================================================ import gql from 'graphql-tag' import { query as q, values } from 'faunadb' import { Resolvers } from '@/generated/schema' import { mapDocument } from '@/util/fauna' export const typeDefs = gql` extend type PackageProposal { upvotes: Int! upvoted: Boolean! } extend type Mutation { togglePackageProposalUpvote (input: TogglePackageProposalUpvoteInput!): PackageProposal @auth } input TogglePackageProposalUpvoteInput { proposalId: ID! } ` export const resolvers: Resolvers = { PackageProposal: { upvoted: async (pkg, args, ctx) => { if (!ctx.user) { return false } return !!await ctx.db.query( q.Exists(q.Match( q.Index('packageproposalupvotes_by_proposal_and_user'), q.Ref(q.Collection('Users'), ctx.user.id), q.Ref(q.Collection('PackageProposals'), pkg.id), )), ) }, }, Mutation: { togglePackageProposalUpvote: async (root, { input }, ctx) => { const ref = q.Ref(q.Collection('PackageProposals'), input.proposalId) const userRef = q.Ref(q.Collection('Users'), ctx.user.id) const match = q.Match( q.Index('packageproposalupvotes_by_proposal_and_user'), userRef, ref, ) const doc = await ctx.db.query>( q.Get(ref), ) if (await ctx.db.query(q.Exists(match))) { await ctx.db.query( q.Do( q.Delete(q.Select(['ref'], q.Get(match))), q.Update(ref, { data: { upvotes: --doc.data.upvotes, }, }), ), ) } else { await ctx.db.query( q.Do( q.Create( q.Collection('PackageProposalUpvotes'), { data: { proposalRef: ref, userRef, }, }, ), q.Update(ref, { data: { upvotes: ++doc.data.upvotes, }, }), ), ) } return mapDocument(doc) }, }, } ================================================ FILE: packages/backend/src/schema/project-type/bookmarks.ts ================================================ import gql from 'graphql-tag' import { query as q, values } from 'faunadb' import { Resolvers } from '@/generated/schema' import { mapDocument } from '@/util/fauna' export const typeDefs = gql` extend type ProjectType { bookmarked: Boolean } extend type Mutation { toggleProjectTypeBookmark (input: ToggleProjectTypeBookmarkInput!): ProjectType @auth } input ToggleProjectTypeBookmarkInput { projectTypeId: ID! } ` export const resolvers: Resolvers = { ProjectType: { bookmarked: (projectType, args, ctx) => { return ctx.user && ctx.user.projectTypeBookmarks && ctx.user.projectTypeBookmarks.includes(projectType.id) }, }, Mutation: { toggleProjectTypeBookmark: async (root, { input: { projectTypeId } }, ctx) => { let bookmarks = ctx.user.projectTypeBookmarks if (!bookmarks) { bookmarks = [projectTypeId] } else { const index = bookmarks.indexOf(projectTypeId) if (index === -1) { bookmarks.push(projectTypeId) } else { bookmarks.splice(index, 1) } } const doc = await ctx.db.query>( q.Do( q.Update( q.Ref(q.Collection('Users'), ctx.user.id), { data: { projectTypeBookmarks: bookmarks, }, }, ), q.Get(q.Ref(q.Collection('ProjectTypes'), projectTypeId)), ), ) if (doc.data) { return mapDocument(doc) } }, }, } ================================================ FILE: packages/backend/src/schema/project-type/db-types.ts ================================================ import { ProjectType } from '@/generated/schema' export interface DBProjectType extends ProjectType { tagMap: { [key: string]: number } } ================================================ FILE: packages/backend/src/schema/project-type/index.ts ================================================ import gql from 'graphql-tag' import { query as q, values } from 'faunadb' import { Resolvers } from '@/generated/schema' import { isSpecialTag } from '@awesomejs/shared-utils/tags' import { DBProjectType } from './db-types' import { mapDocument, mapDocuments } from '@/util/fauna' import { hasTeamAccess } from '../team/team-access' export const typeDefs = gql` type ProjectType { id: ID! name: String! slug: String! logo: String! popularTags: [Tag!]! tags: [Tag!]! inTeam: Boolean! } type Tag { id: ID! count: Int! } extend type Query { projectTypes: [ProjectType!]! projectType (id: ID!): ProjectType projectTypeBySlug (slug: String!): ProjectType } ` function getSortedTags (projectType: DBProjectType) { return Object.keys(projectType.tagMap).filter( (key) => key !== projectType.name.toLowerCase(), ).map((key) => ({ id: key, count: projectType.tagMap[key], })).sort( (a, b) => { if (isSpecialTag(a.id)) { return -1 } if (isSpecialTag(b.id)) { return 1 } return b.count - a.count }, ) } export const resolvers: Resolvers = { ProjectType: { popularTags: (projectType) => { return getSortedTags(projectType).slice(0, 8) }, tags: (projectType) => { return getSortedTags(projectType) }, inTeam: (projectType, args, ctx) => hasTeamAccess(ctx, projectType.id), }, Query: { projectTypes: async (root, args, ctx) => { const { data } = await ctx.db.query( q.Map( q.Paginate( q.Match(q.Index('projecttypes_sort_by_name_asc')), ), q.Lambda(['name', 'ref'], q.Get(q.Var('ref'))), ), ) return mapDocuments(data) }, projectType: async (root, { id }, ctx) => { const doc = await ctx.db.query>( q.Get(q.Ref(q.Collection('ProjectTypes'), id)), ) return mapDocument(doc) }, projectTypeBySlug: async (root, { slug }, ctx) => { const doc = await ctx.db.query>( q.Get(q.Match(q.Index('projecttypes_by_slug'), slug)), ) return mapDocument(doc) }, }, } ================================================ FILE: packages/backend/src/schema/scalar/data.ts ================================================ import gql from 'graphql-tag' import { GraphQLScalarType, Kind } from 'graphql' export const typeDefs = gql` scalar Date ` export const resolvers = { Date: new GraphQLScalarType({ name: 'Date', description: 'Date timestamp. It\'s serialized as a time number in ms, for example `1550923964562`.', parseValue (value) { return new Date(value) }, serialize (value: Date) { return new Date(value).getTime() }, parseLiteral (ast) { if (ast.kind === Kind.INT) { return new Date(ast.value) } return null }, }), } ================================================ FILE: packages/backend/src/schema/scalar/json.ts ================================================ import gql from 'graphql-tag' import GraphQLJSON from 'graphql-type-json' export const typeDefs = gql` scalar JSON ` export const resolvers = { JSON: GraphQLJSON, } ================================================ FILE: packages/backend/src/schema/team/index.ts ================================================ import gql from 'graphql-tag' import { Resolvers, CreateTeamInput, EditTeamInput } from '@/generated/schema' import { query as q, values } from 'faunadb' import { mapDocuments, mapDocument } from '@/util/fauna' import { ApolloError } from '@nodepack/plugin-apollo' import { ErrorCode } from '@/const/error-codes' export const typeDefs = gql` type Team { id: ID! name: String! projectTypes: [ProjectType!]! users: [User!]! } extend type Query { team (id: ID!): Team allTeams: [Team!]! @admin @auth } input CreateTeamInput { name: String! projectTypeIds: [ID!]! userIds: [ID!]! } input EditTeamInput { id: ID! name: String! projectTypeIds: [ID!]! userIds: [ID!]! } extend type Mutation { createTeam (input: CreateTeamInput!): Team @admin @auth editTeam (input: EditTeamInput!): Team @admin @auth } ` function validateInput ( input: CreateTeamInput | EditTeamInput, ) { if (!input.name) { throw new ApolloError('Team name is required', ErrorCode.ERROR_VALIDATION) } if (!input.projectTypeIds.length) { throw new ApolloError('Select at least one project type', ErrorCode.ERROR_VALIDATION) } } export const resolvers: Resolvers = { Team: { projectTypes: async (team, args, ctx) => { return mapDocuments(await ctx.db.query>>(q.Map( team.projectTypes, q.Lambda(['ref'], q.Get(q.Var('ref'))), ))) }, users: async (team, args, ctx) => { return mapDocuments(await ctx.db.query>>(q.Map( team.users, q.Lambda(['ref'], q.Get(q.Var('ref'))), ))) }, }, Query: { team: async (root, { id }, ctx) => { const doc = await ctx.db.query>(q.Get(q.Ref(q.Collection('teams'), id))) return mapDocument(doc) }, allTeams: async (root, args, ctx) => { const { data } = await ctx.db.query(q.Map( q.Paginate(q.Match(q.Index('all_teams')), { size: 1000 }), q.Lambda(['ref'], q.Get(q.Var('ref'))), )) return mapDocuments(data) }, }, Mutation: { createTeam: async (root, { input }, ctx) => { validateInput(input) const doc = await ctx.db.query>(q.Create(q.Collection('teams'), { data: { name: input.name, projectTypes: input.projectTypeIds.map((id) => q.Ref(q.Collection('ProjectTypes'), id)), users: input.userIds.map((id) => q.Ref(q.Collection('Users'), id)), }, })) return mapDocument(doc) }, editTeam: async (root, { input }, ctx) => { validateInput(input) const doc = await ctx.db.query>(q.Update(q.Ref(q.Collection('teams'), input.id), { data: { name: input.name, projectTypes: input.projectTypeIds.map((id) => q.Ref(q.Collection('ProjectTypes'), id)), users: input.userIds.map((id) => q.Ref(q.Collection('Users'), id)), }, })) return mapDocument(doc) }, }, } ================================================ FILE: packages/backend/src/schema/team/team-access.ts ================================================ import { User, Context } from '@/context' import { query as q, values } from 'faunadb' import { ApolloError } from '@nodepack/plugin-apollo' import { ErrorCode } from '@/const/error-codes' export async function loadTeams ( user: User, ctx: Context, ) { if (!user.teamsProjectTypes) { const { data } = await ctx.db.query(q.Map( q.Paginate(q.Match(q.Index('teams_by_user'), user.ref), { size: 1000 }), q.Lambda(['ref'], q.Select(['data', 'projectTypes'], q.Get(q.Var('ref')))), )) const teamsProjectTypes = [] for (const projectTypes of data) { teamsProjectTypes.push(...projectTypes) } user.teamsProjectTypes = teamsProjectTypes } } export async function hasTeamAccess ( ctx: Context, projectTypeId: string, user: User = ctx.user, ): Promise { if (!ctx.user) { return false } if (ctx.user.admin) { return true } await loadTeams(user, ctx) return user.teamsProjectTypes.some((ref) => ref.id === projectTypeId) } export async function checkTeamAccess ( ctx: Context, projectTypeId: string, user: User = ctx.user, ) { if (!await hasTeamAccess(ctx, projectTypeId, user)) { throw new ApolloError('Unauthorized', ErrorCode.ERROR_UNAUTHORIZED) } } export async function hasPackageTeamAccess ( ctx: Context, pkgRef: any, user: User = ctx.user, ) { if (!ctx.user) { return false } if (ctx.user.admin) { return true } const doc = await ctx.db.query>(q.Get(pkgRef)) for (const ptRef of doc.data.projectTypes) { if (await hasTeamAccess(ctx, ptRef.id, user)) { return true } } return false } export async function checkPackageTeamAccess ( ctx: Context, pkgRef: any, user: User = ctx.user, ) { if (!await hasPackageTeamAccess(ctx, pkgRef, user)) { throw new ApolloError('Unauthorized', ErrorCode.ERROR_UNAUTHORIZED) } } ================================================ FILE: packages/backend/src/schema/user/db-types.ts ================================================ import { User, UserAccount } from '@/generated/schema' import { values } from 'faunadb' export interface DBUserAccount extends UserAccount { userRef: values.Ref } export interface DBUser extends Omit { ref: values.Ref accounts: DBUserAccount[] projectTypeBookmarks?: string[] teamsProjectTypes: values.Ref[] } ================================================ FILE: packages/backend/src/schema/user/index.ts ================================================ import gql from 'graphql-tag' import { Resolvers } from '@/generated/schema' import { ApolloError } from '@nodepack/plugin-apollo' import { ErrorCode } from '@/const/error-codes' import { query as q } from 'faunadb' import { mapDocuments } from '@/util/fauna' export const typeDefs = gql` type User { id: ID! nickname: String! email: String! accounts: [UserAccount!]! avatar: String admin: Boolean teams: [Team!]! } type UserAccount { id: ID! provider: String! profileId: ID! nickname: String profileUrl: String } type Query { currentUser: User allUsers: [User!]! @admin @auth } ` export const resolvers: Resolvers = { User: { accounts: (user, args, ctx) => { if (user !== ctx.user as any) { throw new ApolloError('Unauthorized', ErrorCode.ERROR_UNAUTHORIZED) } return user.accounts }, teams: async (user, args, ctx) => { const { data } = await ctx.db.query(q.Map( q.Paginate(q.Match(q.Index('teams_by_user'), user.ref), { size: 1000 }), q.Lambda(['ref'], q.Get(q.Var('ref'))), )) return mapDocuments(data) }, }, Query: { // @ts-ignore currentUser: (root, args, ctx) => ctx.user, allUsers: async (root, args, ctx) => { const { data } = await ctx.db.query(q.Map( q.Paginate(q.Match(q.Index('all_users')), { size: 10000 }), q.Lambda(['ref'], q.Get(q.Var('ref'))), )) return mapDocuments(data) }, }, } ================================================ FILE: packages/backend/src/shim-ejs.d.ts ================================================ declare module '*.ejs' { export default function render (data?: any | null): string } ================================================ FILE: packages/backend/src/util/fauna.ts ================================================ import { values } from 'faunadb' export function mapDocument (document: values.Document) { return { id: document.ref.id, ref: document.ref, ts: document.ts, ...document.data, } } export function mapDocuments (documents: Array>) { return documents.map(mapDocument) } ================================================ FILE: packages/backend/src/util/github.ts ================================================ import Octokit from '@octokit/rest' export function getPageTotalCount (result: Octokit.Response) { if (!result.headers.link) { return 0 } const links = result.headers.link.split(',').map( (text) => text.split(';').map((part) => part.trim()), ) const last = links.find((link) => link[1] === 'rel="last"') if (last) { const [, count] = /page=(\d+)>/.exec(last[0]) return parseInt(count, 10) } else if (Array.isArray(result.data)) { return result.data.length } return 0 } ================================================ FILE: packages/backend/src/util/metadata.ts ================================================ import mem from 'p-memoize' import ms from 'ms' import { Context } from '@/context' import { query as q, values } from 'faunadb' import { DBPackageInterface } from '@/schema/package-interface/db-types' const METADATA_MAX_AGE = ms('6h') const ALGOLIA_INDEX: { [key: string]: string } = { Packages: 'packages', PackageProposals: null, } export async function updateMetadata ( ctx: Context, ref: values.Ref, type: string, data: any, version: number, additionalData: any = {}, ) { const result = { version, ts: Date.now(), data, } await ctx.db.query( q.Update( ref, { data: { metadata: { [type]: result, }, ...additionalData, }, }, ), ) return result } const NPM_METADATA_VERSION = 6 export const getNpmMetadata = mem(async (pkg: DBPackageInterface, ctx: Context): Promise => { try { let result = pkg.metadata?.npm if (!result || result.version !== NPM_METADATA_VERSION || Date.now() - result.ts > METADATA_MAX_AGE) { let npmName: string if (pkg.dataSources?.npm) { npmName = pkg.dataSources.npm.name } else { npmName = pkg.name } const data = await ctx.npm(`/${encodeURIComponent(npmName)}`) console.log('REQUEST npm', npmName) // Add new data props to be saved here // and increment NPM_METADATA_VERSION result = await updateMetadata(ctx, pkg.ref, 'npm', { maintainers: data.maintainers, repository: data.repository, homepage: data.homepage, license: data.license, description: data.description, logo: data.logo, awesomejs: data.awesomejs, }, NPM_METADATA_VERSION, { dataSources: { npm: { name: npmName, }, }, }) } return result.data } catch (e) { console.error(e) } return { maintainers: [], } }, { maxAge: ms('1s'), cacheKey: (pkg) => pkg.id, }) const GITHUB_METADATA_VERSION = 7 export const getGithubDataSource = async (pkg: DBPackageInterface, ctx: Context) => { let owner: string let repo: string if (pkg.dataSources?.github) { owner = pkg.dataSources.github.owner repo = pkg.dataSources.github.repo } else if (pkg.github) { // @TODO legacy owner = pkg.github.owner repo = pkg.github.repo await ctx.db.query( q.Update( pkg.ref, { data: { github: null, dataSources: { github: { owner, repo, }, }, }, }, ), ) console.log('Migrated from `github` to `dataSources.github`.') } else { const npmData = await getNpmMetadata(pkg, ctx) let githubUrl if (npmData.repository?.type === 'git' && npmData.repository?.url.includes('github.com')) { githubUrl = npmData.repository.url } else if (npmData.bugs?.url.includes('github.com')) { githubUrl = npmData.bugs.url } else if (npmData.homepage?.includes('github.com')) { githubUrl = npmData.homepage } if (githubUrl) { const [, o, r] = /github\.com\/([a-z0-9_-]+)\/([a-z0-9_-]+)/i.exec(githubUrl) owner = o repo = r await ctx.db.query( q.Update( pkg.ref, { data: { dataSources: { github: { owner, repo, }, }, }, }, ), ) } } return { owner, repo, } } export const getGithubMetadata = mem(async (pkg: DBPackageInterface, ctx: Context): Promise => { try { let result = pkg.metadata?.github if (!result || result.version !== GITHUB_METADATA_VERSION || Date.now() - result.ts > METADATA_MAX_AGE) { let data const { owner, repo } = await getGithubDataSource(pkg, ctx) if (!repo) { return {} } const { data: githubData } = await ctx.github.repos.get({ owner, repo, }) console.log('REQUEST github', pkg.name) // Add new data props to be saved here // and increment GITHUB_METADATA_VERSION data = { slug: { owner, repo, }, stars: githubData.stargazers_count, htmlUrl: githubData.html_url, owner: { avatar: githubData.owner.avatar_url, }, description: githubData.description, defaultBranch: githubData.default_branch, } const algoliaIndex = ALGOLIA_INDEX[pkg.ref.collection.id] if (algoliaIndex) { const index = ctx.algolia.initIndex(algoliaIndex) await index.partialUpdateObject({ objectID: pkg.id, stars: githubData.stargazers_count, defaultLogo: githubData.owner.avatar_url, ...(githubData.description ? { description: githubData.description, } : {}), }) } result = await updateMetadata(ctx, pkg.ref, 'github', data, GITHUB_METADATA_VERSION) } return result.data } catch (e) { console.error(e) return { slug: {}, owner: {}, } } }, { maxAge: ms('1s'), cacheKey: (pkg) => pkg.id, }) ================================================ FILE: packages/backend/src/util/package-index.ts ================================================ import { Context } from '@/context' import { query as q } from 'faunadb' export async function getIndexObject ( ctx: Context, pkg: any, ) { const npmData = await ctx.npm(`/${encodeURIComponent(pkg.data.name)}`) let githubData if (pkg.data.metadata.github) { const { owner, repo } = pkg.data.metadata.github.data.slug const { data } = await ctx.github.repos.get({ owner, repo, }) githubData = data } else { githubData = { owner: {}, } } const projectType = await ctx.db.query(q.Get(pkg.data.projectTypes[0])) const projectTypes = await ctx.db.query(q.Map( pkg.data.projectTypes, q.Lambda(['ref'], q.Get(q.Var('ref'))), )) return { objectID: pkg.ref.id, _tags: pkg.data.info.tags || [], name: pkg.data.name, description: githubData.description || npmData.description, keywords: npmData.keywords, license: npmData.license, maintainers: npmData.maintainers, stars: githubData.stargazers_count || 0, defaultLogo: githubData.owner.avatar_url, projectType: { id: projectType.ref.id, name: projectType.data.name, slug: projectType.data.slug, logo: projectType.data.logo, }, projectTypes: projectTypes.map((pt) => ({ id: pt.ref.id, name: pt.data.name, slug: pt.data.slug, logo: pt.data.logo, })), } } export async function indexPackage ( ctx: Context, pkg: any, ) { const index = ctx.algolia.initIndex('packages') return index.addObject(await getIndexObject(ctx, pkg)) } export async function updatePackageIndex ( ctx: Context, pkg: any, ) { const index = ctx.algolia.initIndex('packages') return index.saveObject(await getIndexObject(ctx, pkg)) } ================================================ FILE: packages/backend/src/util/readme.ts ================================================ import { Context } from '@/context' import { getNpmMetadata, getGithubMetadata } from './metadata' import { GithubDataSource } from '@/schema/package-interface/data-source' export async function getFileContent ( githubDatasource: GithubDataSource, path: string, ctx: Context, ) { const { data }: { data: string } = await ctx.github.repos.getContents({ owner: githubDatasource.owner, repo: githubDatasource.repo, headers: { accept: 'application/vnd.github.3.html', }, path, }) as any return data } export async function getReadmeContent ( githubDatasource: GithubDataSource, ctx: Context, ) { const { data }: { data: string } = await ctx.github.repos.getReadme({ owner: githubDatasource.owner, repo: githubDatasource.repo, headers: { accept: 'application/vnd.github.3.html', }, }) as any return data } export async function getReadme ( pkg: any, ctx: Context, ): Promise { if (pkg.dataSources.github) { const npmMetadata = pkg.dataSources.npm ? await getNpmMetadata(pkg, ctx) : null let data: string if (npmMetadata?.repository?.directory) { data = await getFileContent(pkg.dataSources.github, `${npmMetadata.repository.directory}/README.md`, ctx) } if (!data) { data = await getReadmeContent(pkg.dataSources.github, ctx) } const githubMetadata = await getGithubMetadata(pkg, ctx) data = processReadme(pkg.dataSources.github, data, githubMetadata.defaultBranch) return data } } export function processReadme ( githubDatasource: GithubDataSource, text: string, defaultBranch: string, ) { // Fix image urls text = text.replace(/src="([^"]+)/gi, (result, group1) => { if (group1.startsWith('http')) { return result } else if (group1.startsWith('/')) { return `src="https://github.com/${ encodeURIComponent(githubDatasource.owner) }/${ encodeURIComponent(githubDatasource.repo) }/raw/${defaultBranch}${group1}` } else { return `src="https://raw.githubusercontent.com/${ encodeURIComponent(githubDatasource.owner) }/${ encodeURIComponent(githubDatasource.repo) }/${defaultBranch}/${group1}${group1.endsWith('svg') ? '?sanitize=true' : ''}` } }) // Fix image sizes text = text.replace(/(width|height)="(\d+)"/gi, (result, group1, group2) => `${result} style="${group1}:${group2}px"`) // Links text = text.replace(/href="(.*)"/gi, (result, group1) => { if (group1.startsWith('#')) { return `href="#user-content-${group1.substr(1)}"` } else { return `${result} target="_blank"` } }) return text } ================================================ FILE: packages/backend/src/util/tag-map.ts ================================================ import { Context } from '@/context' import { query as q, values, Expr } from 'faunadb' export async function updateProjectTypeTags (projectTypeRef: values.Ref | Expr, ctx: Context) { const { data: packages } = await ctx.db.query(q.Map( q.Paginate(q.Match(q.Index('packages_by_project_type'), projectTypeRef), { size: 100000 }), q.Lambda('ref', q.Get(q.Var('ref'))), )) const counters = new Map() for (const pkg of packages) { for (const tag of pkg.data.info.tags) { let count = counters.get(tag) if (!count) { count = 0 } count++ counters.set(tag, count) } } await ctx.db.query( q.Do( // Reset counters to empty objects q.Update(projectTypeRef, { data: { tagMap: null } }), q.Update(projectTypeRef, { data: { tagMap: Array.from(counters.keys()).reduce((map, key) => { map[key] = counters.get(key) return map }, {} as { [key: string]: number }) } }), ), ) } ================================================ FILE: packages/backend/src/util/tags.ts ================================================ export function sanitizeTags (tags: string[]) { return Array.from(new Set(tags.map((t) => t.trim()).filter((t) => t.length))) } ================================================ FILE: packages/backend/tsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": [ "src/*" ], "@config/*": [ "config/*" ], "@context": [ "src/generated/context.d.ts" ] }, "lib": [ "dom", "esnext" ], "types": [ "webpack-env" ], "target": "es5", "module": "esnext", "importHelpers": true, "moduleResolution": "node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "sourceMap": true, "noUnusedLocals": true, "skipLibCheck": true, // Strict "noImplicitAny": true, "noImplicitThis": true, "alwaysStrict": true, "strictBindCallApply": true, "strictFunctionTypes": true, // May break autocomplete "strictNullChecks": false }, "exclude": [ "node_modules" ], "include": [ "src/**/*.ts", "tests/**/*.ts" ] } ================================================ FILE: packages/backend/tslint.json ================================================ { "defaultSeverity": "warning", "extends": [ "tslint:recommended" ], "linterOptions": { "exclude": [ "node_modules/**", "src/generated/**" ] }, "rules": { "quotemark": [true, "single"], "indent": [true, "spaces", 2], "interface-name": false, "ordered-imports": false, "object-literal-sort-keys": false, "no-consecutive-blank-lines": false, "semicolon": [true, "never"], "space-before-function-paren": [true, "always"], "no-console": false, "trailing-comma": [true, {"multiline": "always", "singleline": "never"}] } } ================================================ FILE: packages/frontend/.browserslistrc ================================================ > 1% last 2 versions ================================================ FILE: packages/frontend/.editorconfig ================================================ [*.{js,jsx,ts,tsx,vue}] indent_style = space indent_size = 2 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: packages/frontend/.eslintignore ================================================ node_modules/ ================================================ FILE: packages/frontend/.eslintrc.js ================================================ module.exports = { root: true, env: { node: true, }, 'extends': [ 'plugin:vue/recommended', '@vue/standard', ], rules: { 'no-console': process.env.NODE_ENV === 'production' ? ['error', { 'allow': ['error'] }] : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'comma-dangle': ['error', 'always-multiline'], }, parserOptions: { parser: 'babel-eslint', }, } ================================================ FILE: packages/frontend/.gitignore ================================================ .DS_Store node_modules /dist /tests/e2e/videos/ /tests/e2e/screenshots/ # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: packages/frontend/README.md ================================================ # awesomejs-frontend ## Project setup ``` yarn install ``` ### Compiles and hot-reloads for development ``` yarn run dev ``` ### Compiles and minifies for production ``` yarn run build ``` ### Run your tests ``` yarn run test ``` ### Lints and fixes files ``` yarn run lint ``` ### Run your end-to-end tests ``` yarn run test:e2e ``` ### Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). ================================================ FILE: packages/frontend/apollo.config.js ================================================ const path = require('path') // Load .env files const { loadEnv } = require('vue-cli-plugin-apollo/utils/load-env') const env = loadEnv([ path.resolve(__dirname, '.env'), path.resolve(__dirname, '.env.local'), ]) module.exports = { client: { service: { name: 'awesomejs', url: 'http://localhost:4040/graphql', }, includes: ['src/**/*.{js,jsx,ts,tsx,vue,gql}'], }, engine: { apiKey: env.VUE_APP_APOLLO_ENGINE_KEY, }, } ================================================ FILE: packages/frontend/babel.config.js ================================================ module.exports = { presets: [ '@vue/app', ], } ================================================ FILE: packages/frontend/cypress.json ================================================ { "pluginsFile": "tests/e2e/plugins/index.js" } ================================================ FILE: packages/frontend/jsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], }, }, "include": [ "./src/**/*", ], } ================================================ FILE: packages/frontend/package.json ================================================ { "name": "frontend", "version": "0.1.0", "private": true, "scripts": { "dev": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "test:e2e": "vue-cli-service test:e2e" }, "dependencies": { "@awesomejs/shared-utils": "^1.0.0", "@vue/apollo-composable": "4.0.0-alpha.4", "@vue/apollo-util": "4.0.0-alpha.2", "@vue/composition-api": "^0.3.4", "algoliasearch": "^3.35.0", "core-js": "^3.2.1", "d3": "^5.15.0", "emoji-toolkit": "^5.0.5", "focus-trap": "^5.0.2", "focus-trap-vue": "^0.0.4", "focus-visible": "^5.0.2", "github-syntax-dark": "^0.5.0", "lodash": "^4.17.15", "luxon": "^1.21.3", "millify": "^3.1.2", "qrcode": "^1.4.2", "register-service-worker": "^1.6.2", "v-tooltip": "^3.0.0-alpha.10", "vue": "^2.6.10", "vue-focus": "^2.1.0", "vue-global-events": "^1.1.2", "vue-meta": "^2.3.1", "vue-multiselect": "^2.1.6", "vue-router": "^3.0.3" }, "devDependencies": { "@fullhuman/postcss-purgecss": "^1.2.0", "@ky-is/vue-cli-plugin-tailwind": "^2.0.0", "@types/luxon": "^1.21.0", "@vue/cli-plugin-babel": "^4.0.0-rc.7", "@vue/cli-plugin-e2e-cypress": "^4.0.0-rc.7", "@vue/cli-plugin-eslint": "^4.0.0-rc.7", "@vue/cli-plugin-pwa": "^4.0.0-rc.7", "@vue/cli-service": "^4.0.0-rc.7", "@vue/eslint-config-standard": "^4.0.0", "babel-eslint": "^10.0.1", "eslint": "^5.16.0", "eslint-plugin-vue": "^5.0.0", "graphql-tag": "^2.9.0", "postcss-nested": "^4.1.2", "postcss-preset-env": "^6.6.0", "tailwindcss": "^1.0.1", "vue-cli-plugin-apollo": "^0.21.0", "vue-template-compiler": "^2.6.10" }, "engines": { "node": ">=10" } } ================================================ FILE: packages/frontend/postcss.config.js ================================================ const IN_PRODUCTION = process.env.NODE_ENV === 'production' module.exports = { plugins: [ require('postcss-preset-env')({ stage: 0 }), require('tailwindcss')(), IN_PRODUCTION && require('@fullhuman/postcss-purgecss')({ content: [ `./public/**/*.html`, `./src/**/*.vue` ], defaultExtractor (content) { const contentWithoutStyleBlocks = content.replace(//gi, '') return contentWithoutStyleBlocks.match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || [] }, whitelist: [ // 'a', ], whitelistPatterns: [ /-(leave|enter|appear)(|-(to|from|active))$/, /^(?!(|.*?:)cursor-move).+-move$/, /^router-link(|-exact)-active$/, /^v-popper/, ], }), require('autoprefixer')(), require('postcss-nested')(), ], } ================================================ FILE: packages/frontend/public/index.html ================================================ Awesome JS
================================================ FILE: packages/frontend/public/robots.txt ================================================ User-agent: * Disallow: ================================================ FILE: packages/frontend/src/assets/styles/components/document.postcss ================================================ .about-document { max-width: 768px; @apply px-12 mx-auto my-32; h1 .text-lg { @apply text-3xl; } h2 { @apply text-2xl mt-24 mb-4 text-yellow-400; } h3 { @apply text-xl mt-12 mb-4 text-yellow-500; } p { @apply mt-4 mb-6; } @media (max-width: 640px) { @apply px-4 my-12; h2 { @apply mt-12; } } } ================================================ FILE: packages/frontend/src/assets/styles/components/grids.postcss ================================================ .project-types-grid { display: grid; /* @TODO use theme values */ grid-template-columns: repeat(auto-fit, 128px); grid-gap: 48px; justify-items: center; @media (max-width: 1023px) { grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); grid-gap: 16px; } } ================================================ FILE: packages/frontend/src/assets/styles/components/markdown.postcss ================================================ /* purgecss start ignore */ .markdown { @apply overflow-hidden; img { @apply inline-block; &.ally-bg { @apply bg-gray-200 rounded p-2; &.avatar { @apply p-0 rounded-lg; } } &[align="left"] { @apply mr-5; } &[align="right"] { @apply ml-5; } } a { @apply text-purple-500; &:hover { @apply text-purple-400; } } h1 { @apply text-3xl mt-8 mb-4 text-gray-600; @media (max-width: 640px) { @apply text-2xl; } } h2 { @apply text-2xl; @media (max-width: 640px) { @apply text-xl; } } h3 { @apply text-xl; @media (max-width: 640px) { @apply text-lg; } } h1, h2 { @apply pb-2 border-b border-gray-800; } h2, h3 { @apply mt-8 mb-3 text-gray-500; } h4, h5, h6 { @apply text-lg mt-6; @media (max-width: 640px) { @apply text-base; } } h1, h2, h3, h4, h5, h6 { @apply flex flex-wrap items-center whitespace-pre-wrap font-bold; &:first-child { @apply mt-0; } &[align="center"] { @apply justify-center; } &[align="right"] { @apply justify-end; } &:hover { .anchor { @apply visible; } } } p { @apply my-4; } table { border-collapse: collapse; } tr { &:nth-child(2n) { @apply bg-gray-850; } } td, th { @apply border border-gray-800 p-2; } hr { @apply border-gray-800; } ul { @apply list-disc; } ol { @apply list-decimal; } ul, ol { @apply my-2 pl-2; } li { @apply ml-6; } pre { @apply text-white bg-gray-850 rounded p-4 my-4 overflow-x-auto; font-size: 14px; } code { @apply text-white bg-gray-850 rounded px-2 py-1 break-words; } blockquote { @apply pl-4 my-4 border-l-4 border-purple-700 text-gray-500; p { &:first-child { @apply mt-0; } &:last-child { @apply mb-0; } } } .markdown-body { @apply p-0; } .anchor { @apply invisible text-gray-600 pr-1 -ml-5 h-6 flex items-center; svg { @apply fill-current; } } .pl-md { @apply text-red-300 bg-red-900 py-1; } .pl-mi1 { @apply text-green-300 bg-green-900 py-1; } } /* purgecss end ignore */ ================================================ FILE: packages/frontend/src/assets/styles/components/multi-select.postcss ================================================ /* purgecss start ignore */ fieldset[disabled] .multiselect { pointer-events: none; } .multiselect__spinner { position: absolute; right: 1px; top: 1px; width: 48px; height: 35px; background: #fff; display: block; &:before, &:after { position: absolute; content: ""; top: 50%; left: 50%; margin: -8px 0 0 -8px; width: 16px; height: 16px; border-radius: 100%; border-color: #41b883 transparent transparent; border-style: solid; border-width: 2px; box-shadow: 0 0 0 1px transparent; } &:before { animation: spinning 2.4s cubic-bezier(0.41, 0.26, 0.2, 0.62); animation-iteration-count: infinite; } &:after { animation: spinning 2.4s cubic-bezier(0.51, 0.09, 0.21, 0.8); animation-iteration-count: infinite; } } .multiselect__loading-enter-active, .multiselect__loading-leave-active { transition: opacity 0.4s ease-in-out; opacity: 1; } .multiselect__loading-enter, .multiselect__loading-leave-active { opacity: 0; } .multiselect, .multiselect__input, .multiselect__single { font-family: inherit; font-size: 16px; touch-action: manipulation; } .multiselect { box-sizing: content-box; display: block; position: relative; width: 100%; min-height: 40px; text-align: left; * { box-sizing: border-box; } &:focus { outline: none; } } .multiselect--disabled { background: #ededed; pointer-events: none; opacity: 0.6; } .multiselect--active { z-index: 50; } .multiselect--active:not(.multiselect--above) .multiselect__current, .multiselect--active:not(.multiselect--above) .multiselect__input, .multiselect--active:not(.multiselect--above) .multiselect__tags { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .multiselect--active .multiselect__select { transform: rotateZ(180deg); } .multiselect--above.multiselect--active .multiselect__current, .multiselect--above.multiselect--active .multiselect__input, .multiselect--above.multiselect--active .multiselect__tags { border-top-left-radius: 0; border-top-right-radius: 0; } .multiselect__input, .multiselect__single { position: relative; display: inline-block; min-height: 20px; line-height: 20px; border: none; padding-left: 12px; padding-top: 2px; background: none; width: calc(100%); transition: border 0.1s ease; box-sizing: border-box; margin-bottom: 8px; vertical-align: top; } .multiselect__input::placeholder { @apply text-gray-300; } .multiselect__tag ~ .multiselect__input, .multiselect__tag ~ .multiselect__single { width: auto; } .multiselect__input:hover, .multiselect__single:hover { border-color: #cfcfcf; } .multiselect__input:focus, .multiselect__single:focus { border-color: #a8a8a8; outline: none; } .multiselect__single { padding-left: 5px; margin-bottom: 8px; } .multiselect__tags-wrap { display: inline; } .multiselect__tags { min-height: 56px; @apply block bg-gray-800 rounded pl-4 pt-4 pb-1; &:hover { @apply bg-gray-700; } } .multiselect__tag { padding: 4px 26px 4px 10px; margin-right: 10px; line-height: 1; margin-bottom: 5px; @apply truncate max-w-full rounded text-white bg-blue-700 relative inline-block; } .multiselect__tag-icon { margin-left: 7px; right: 0; top: 0; bottom: 0; font-weight: 700; font-style: initial; width: 22px; line-height: 22px; transition: all 0.2s ease; @apply rounded text-center absolute cursor-pointer; &:after { content: "×"; font-size: 14px; @apply text-blue-300; } &:focus, &:hover { @apply bg-blue-600; &:after { @apply text-white; } } } .multiselect__current { line-height: 16px; min-height: 40px; box-sizing: border-box; display: block; overflow: hidden; padding: 8px 12px 0; padding-right: 30px; white-space: nowrap; margin: 0; text-decoration: none; border-radius: 5px; border: 1px solid #e8e8e8; cursor: pointer; } .multiselect__select { line-height: 16px; display: block; position: absolute; box-sizing: border-box; width: 40px; height: 52px; right: 1px; top: 1px; padding: 4px 8px; margin: 0; text-decoration: none; text-align: center; cursor: pointer; transition: transform 0.2s ease; &:before { position: relative; right: 0; top: 65%; color: #999; margin-top: 4px; border-style: solid; border-width: 5px 5px 0 5px; border-color: #999999 transparent transparent transparent; content: ""; } } .multiselect__placeholder { color: #adadad; display: inline-block; margin-bottom: 10px; padding-left: 12px; } .multiselect--active .multiselect__placeholder { display: none; } .multiselect__content-wrapper { @apply bg-gray-900 w-full border-gray-800 border z-50 overflow-auto block absolute; max-height: 240px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; -webkit-overflow-scrolling: touch; } .multiselect__content { list-style: none; display: inline-block; padding: 0; margin: 0; min-width: 100%; vertical-align: top; } .multiselect--above .multiselect__content-wrapper { bottom: 100%; border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-top-left-radius: 5px; border-top-right-radius: 5px; border-bottom: none; border-top: 1px solid theme('colors.gray.800'); } .multiselect__content::webkit-scrollbar { display: none; } .multiselect__element { display: block; } .multiselect__option { display: block; padding: 12px; min-height: 40px; line-height: 16px; text-decoration: none; text-transform: none; vertical-align: middle; position: relative; cursor: pointer; white-space: nowrap; &:after { top: 0; right: 0; position: absolute; line-height: 40px; padding-right: 12px; padding-left: 20px; font-size: 13px; } } .multiselect__option--highlight { @apply bg-gray-800; outline: none; &:after { content: attr(data-select); } } .multiselect__option--selected { @apply bg-blue-500 text-white font-bold; &:after { content: attr(data-selected); @apply text-blue-300; } &.multiselect__option--highlight { @apply bg-red-700 text-white; &:after { content: attr(data-deselect); @apply text-red-100; } } } .multiselect--disabled .multiselect__current, .multiselect--disabled .multiselect__select { background: #ededed; color: #a6a6a6; } .multiselect__option--disabled { background: #ededed !important; color: #a6a6a6 !important; cursor: text; pointer-events: none; } .multiselect__option--group { background: #ededed; color: #35495e; &.multiselect__option--highlight { background: #35495e; color: #fff; &:after { background: #35495e; } } } .multiselect__option--disabled.multiselect__option--highlight { background: #dedede; } .multiselect__option--group-selected.multiselect__option--highlight { background: #ff6a6a; color: #fff; &:after { background: #ff6a6a; content: attr(data-deselect); color: #fff; } } .multiselect-enter-active, .multiselect-leave-active { transition: all 0.15s ease; } .multiselect-enter, .multiselect-leave-active { opacity: 0; } .multiselect__strong { margin-bottom: 8px; line-height: 20px; display: inline-block; vertical-align: top; } *[dir="rtl"] .multiselect { text-align: right; } *[dir="rtl"] .multiselect__select { right: auto; left: 1px; } *[dir="rtl"] .multiselect__tags { padding: 8px 8px 0px 40px; } *[dir="rtl"] .multiselect__content { text-align: right; } *[dir="rtl"] .multiselect__option:after { right: auto; left: 0; } *[dir="rtl"] .multiselect__clear { right: auto; left: 12px; } *[dir="rtl"] .multiselect__spinner { right: auto; left: 1px; } @keyframes spinning { from { transform: rotate(0); } to { transform: rotate(2turn); } } /* purgecss end ignore */ ================================================ FILE: packages/frontend/src/assets/styles/components/popper.postcss ================================================ /* purgecss start ignore */ .v-popper { .v-popper__trigger { /* Fix height */ line-height: 0; } } .v-popper__popper { &.v-popper--theme-tooltip { .v-popper__inner { @apply bg-white text-gray-900; } .v-popper__arrow { @apply border-white; } } &.v-popper--theme-dropdown { .v-popper__inner { @apply p-0 bg-gray-900; color: inherit; box-shadow: 0 42px 64px rgba(0, 0, 0, .3), 0 0 12px rgba(0, 0, 0, .3); } .v-popper__arrow { @apply border-gray-900; } .v-popper__wrapper { &.animate { animation: dropdown-animation .15s cubic-bezier(0, 1, .5, 1); } } } &.v-popper--theme-yellow-arrow { .v-popper__arrow { @apply border-yellow-900; } } } @keyframes dropdown-animation { 0% { transform: scale(.6); } 100% { transform: none; } } /* App backdrop when popper is open */ #app { &::before { @apply fixed inset-0 bg-black pointer-events-none opacity-0; content: ''; z-index: 50; transition: opacity .15s linear; } } .v-popper { transition: z-index 0s 1s; z-index: 1; } body.popper-open { #app { &::before { opacity: .5; } } .v-popper { transition: none; &.v-popper--open { z-index: 51; } } } /* purgecss end ignore */ ================================================ FILE: packages/frontend/src/assets/styles/main.postcss ================================================ @import 'https://fonts.googleapis.com/icon?family=Material+Icons'; /* purgecss ignore */ @import '~github-syntax-dark/lib/github-dark.css'; /* purgecss start ignore */ html, body { @apply bg-gray-900 text-gray-300; scrollbar-color: theme('colors.gray.800') theme('colors.gray.900'); } body { overflow-x: hidden; } .material-icons { vertical-align: sub; } [type="button"], [type="submit"], .button-like { -webkit-appearance: none !important; outline: none !important; &:active { filter: brightness(90%); } &:focus-visible { @apply shadow-outline; } } /* Disable touch blue rectangle */ [type="button"], [type="submit"], .button-like, a { -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; } *::-webkit-scrollbar-track { background-color: theme('colors.gray.900'); } *::-webkit-scrollbar { width: 6px; } *::-webkit-scrollbar-thumb { background-color: theme('colors.gray.800'); border: theme('colors.gray.900') 1px solid; border-radius: 2px; } .bg-blur { backdrop-filter: blur(24px); } .no-scroll { @apply overflow-y-hidden; } /* purgecss end ignore */ ================================================ FILE: packages/frontend/src/assets/styles/tailwind.postcss ================================================ @tailwind base; /** * This injects any component classes registered by plugins. */ @tailwind components; /** * Here you would add any of your custom component classes; stuff that you'd * want loaded *before* the utilities so that the utilities could still * override them. * * Example: * @import 'components/buttons'; */ @import './components/grids.postcss'; @import './components/markdown.postcss'; @import './components/document.postcss'; @import './components/popper.postcss'; @import './components/multi-select.postcss'; /** * This injects all of Tailwind's utility classes, generated based on your * config file. */ @tailwind utilities; /** * Here you would add any custom utilities you need that don't come out of the * box with Tailwind. * * Example: * @import 'utilities/background-patterns'; */ @import './main.postcss'; @import './transitions.postcss'; ================================================ FILE: packages/frontend/src/assets/styles/transitions.postcss ================================================ .fade-enter-active, .fade-leave-active, .zoom-enter-active, .zoom-leave-active { transition: opacity .1s linear; } .fade-enter, .fade-leave-to, .zoom-enter, .zoom-leave-to { opacity: 0; } .zoom-enter-active, .zoom-leave-active { .zoomable { transition: transform .1s ease-out; } } .zoom-enter, .zoom-leave-to { .zoomable { transform: scale(.9); } } /* Page */ .page-zoom-child-enter-active, .page-zoom-child-leave-active, .page-zoom-parent-enter-active, .page-zoom-parent-leave-active { transition: opacity .2s cubic-bezier(0, 1, .5, 1), transform .4s cubic-bezier(0, 1, .5, 1); transform-origin: calc(50vw) calc(50vh); width: 100vw; height: 100vh; overflow: hidden; position: absolute; z-index: 0; } .page-zoom-child-enter-active { z-index: 1; background: theme('colors.gray.900'); } .page-zoom-child-enter, .page-zoom-child-leave-to, .page-zoom-parent-enter, .page-zoom-parent-leave-to { opacity: 0; } .page-zoom-child-enter, .page-zoom-parent-leave-to { transform: scale(.9); } .page-zoom-child-leave-to, .page-zoom-parent-enter { transform: scale(1.1); } ================================================ FILE: packages/frontend/src/cache.js ================================================ import { toIdValue } from 'apollo-utilities' import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory' import gql from 'graphql-tag' import introspectionQueryResultData from 'backend/schema-fragment-matcher' const fragmentMatcher = new IntrospectionFragmentMatcher({ introspectionQueryResultData, }) export const cache = new InMemoryCache({ fragmentMatcher, cacheRedirects: { Query: { projectTypeBySlug: (_, { slug }) => { try { // If we already have loaded the list of project types // we want to redirect to the corresponding project type // instead of doing an unnecessary request to the API const { projectTypes } = cache.readQuery({ query: gql`query ReachProjectTypesCache { projectTypes { id slug } }`, }) // Lookup for the project type with corresponding slug if (projectTypes) { const p = projectTypes.find(t => t.slug === slug) if (p) { // Cache redirect return toIdValue(cache.config.dataIdFromObject({ __typename: 'ProjectType', id: p.id })) } } } catch (e) { // https://github.com/apollographql/apollo-client/issues/1542 } }, projectType: (_, { id }) => toIdValue(cache.config.dataIdFromObject({ __typename: 'ProjectType', id })), package: (_, { id }) => toIdValue(cache.config.dataIdFromObject({ __typename: 'Package', id })), packageProposal: (_, { id }) => toIdValue(cache.config.dataIdFromObject({ __typename: 'PackageProposal', id })), }, }, }) ================================================ FILE: packages/frontend/src/components/App.vue ================================================ ================================================ FILE: packages/frontend/src/components/BaseButton.vue ================================================ ================================================ FILE: packages/frontend/src/components/BasePopper.vue ================================================ ================================================ FILE: packages/frontend/src/components/EmptyMessage.vue ================================================ ================================================ FILE: packages/frontend/src/components/ErrorMessage.vue ================================================ ================================================ FILE: packages/frontend/src/components/LoadingIndicator.vue ================================================ ================================================ FILE: packages/frontend/src/components/PageTitle.vue ================================================ ================================================ FILE: packages/frontend/src/components/PopupModal.vue ================================================ ================================================ FILE: packages/frontend/src/components/RouteTab.vue ================================================ ================================================ FILE: packages/frontend/src/components/SubmitAnimation.vue ================================================ ================================================ FILE: packages/frontend/src/components/ToastNotification.vue ================================================ ================================================ FILE: packages/frontend/src/components/about/Contributing.vue ================================================ ================================================ FILE: packages/frontend/src/components/about/Privacy.vue ================================================ ================================================ FILE: packages/frontend/src/components/admin/AdminDashboard.vue ================================================ ================================================ FILE: packages/frontend/src/components/admin/AdminHome.vue ================================================ ================================================ FILE: packages/frontend/src/components/admin/AdminTeamCreate.vue ================================================ ================================================ FILE: packages/frontend/src/components/admin/AdminTeamEditForm.vue ================================================ ================================================ FILE: packages/frontend/src/components/admin/AdminTeamView.vue ================================================ ================================================ FILE: packages/frontend/src/components/admin/AdminTeams.vue ================================================ ================================================ FILE: packages/frontend/src/components/admin/UserMultiSelect.vue ================================================ ================================================ FILE: packages/frontend/src/components/admin/fragments.js ================================================ import gql from 'graphql-tag' export const teamFragment = gql` fragment team on Team { id name } ` ================================================ FILE: packages/frontend/src/components/app/AppFooter.vue ================================================ ================================================ FILE: packages/frontend/src/components/app/AppGlobalLoading.vue ================================================ ================================================ FILE: packages/frontend/src/components/app/AppHeader.vue ================================================ ================================================ FILE: packages/frontend/src/components/app/AppHeaderLogo.vue ================================================ ================================================ FILE: packages/frontend/src/components/app/AppHome.vue ================================================ ================================================ FILE: packages/frontend/src/components/app/AppServiceWorkerManager.vue ================================================ ================================================ FILE: packages/frontend/src/components/app/AppSponsors.vue ================================================ ================================================ FILE: packages/frontend/src/components/chart/DotChart.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/NoPackageSelected.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageAddButton.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageAddWizard.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageAdded.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageBookmarkButton.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageCount.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageDataSource.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageDownloadsCount.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageEditForm.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageEditProjectTypesForm.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageGeneralInfo.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageInsightNpmDownloads.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageInstallButton.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageInstallationManager.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageLinks.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageList.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageListItem.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageLogo.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageProposalApproveButton.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageProposalList.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageProposalRemoveButton.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageProposalTabEdit.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageProposalTabGeneral.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageProposalUpvoteButton.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageProposalView.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageReadme.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageRelease.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageReleaseAsset.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageReleaseCount.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageShareButton.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageShareModal.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageTabDataSources.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageTabEdit.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageTabGeneral.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageTabInsight.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageTabReleases.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageTag.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageTags.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageView.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/PackageViewLayout.vue ================================================ ================================================ FILE: packages/frontend/src/components/pkg/fragments.js ================================================ import gql from 'graphql-tag' export const pkgFragment = gql` fragment pkg on Package { id name description stars defaultLogo homepage repo maintainers { name email avatar } info { tags } } ` export const pkgProposalFragment = gql` fragment pkgProposal on PackageProposal { id name description upvotes stars defaultLogo homepage repo maintainers { name email avatar } info { tags } } ` export const releaseFragment = gql` fragment release on PackageRelease { id date title tagName description prerelease assets { name downloadUrl size } } ` ================================================ FILE: packages/frontend/src/components/project-type/ProjectTypeAllTags.vue ================================================ ================================================ FILE: packages/frontend/src/components/project-type/ProjectTypeBookmarkButton.vue ================================================ ================================================ FILE: packages/frontend/src/components/project-type/ProjectTypeMultiSelect.vue ================================================ ================================================ FILE: packages/frontend/src/components/project-type/ProjectTypePackageProposalsButton.vue ================================================ ================================================ FILE: packages/frontend/src/components/project-type/ProjectTypePackageProposalsView.vue ================================================ ================================================ FILE: packages/frontend/src/components/project-type/ProjectTypeSelect.vue ================================================ ================================================ FILE: packages/frontend/src/components/project-type/ProjectTypeView.vue ================================================ ================================================ FILE: packages/frontend/src/components/project-type/ProjectTypes.vue ================================================ ================================================ FILE: packages/frontend/src/components/project-type/ProjectTypesGrid.vue ================================================ ================================================ FILE: packages/frontend/src/components/project-type/ProjectTypesItem.vue ================================================ ================================================ FILE: packages/frontend/src/components/project-type/fragments.js ================================================ import gql from 'graphql-tag' export const projectTypeFragment = gql` fragment projectType on ProjectType { id name slug logo bookmarked inTeam } ` ================================================ FILE: packages/frontend/src/components/search/SearchOverlay.vue ================================================ ================================================ FILE: packages/frontend/src/components/search/SearchOverlayAsyncState.vue ================================================ ================================================ FILE: packages/frontend/src/components/user/LoginView.vue ================================================ ================================================ FILE: packages/frontend/src/components/user/NoBookmarkPackageSelected.vue ================================================ ================================================ FILE: packages/frontend/src/components/user/UserCheckSignedIn.vue ================================================ ================================================ FILE: packages/frontend/src/components/user/UserDashboard.vue ================================================ ================================================ FILE: packages/frontend/src/components/user/UserMenu.vue ================================================ ================================================ FILE: packages/frontend/src/components/user/UserTabBookmarks.vue ================================================ ================================================ FILE: packages/frontend/src/components/user/UserTabHome.vue ================================================ ================================================ FILE: packages/frontend/src/components/user/UserTeams.vue ================================================ ================================================ FILE: packages/frontend/src/components/user/fragments.js ================================================ import gql from 'graphql-tag' export const userFragment = gql` fragment user on User { id email nickname avatar admin } ` ================================================ FILE: packages/frontend/src/components/user/useCurrentUser.js ================================================ import gql from 'graphql-tag' import { useQuery, useResult } from '@vue/apollo-composable' import { userFragment } from './fragments' import { computed } from '@vue/composition-api' export function useCurrentUser () { const { result, loading } = useQuery(gql` query CurrentUser { currentUser { ...user } } ${userFragment} `) const currentUser = useResult(result) const isAdmin = computed(() => !!(currentUser.value && currentUser.value.admin)) return { currentUser, isAdmin, loading, } } ================================================ FILE: packages/frontend/src/components.js ================================================ // --- Base components --- import Vue from 'vue' // Require all the components that start with 'BaseXXX.vue' const requireComponents = require.context('./components', true, /Base[a-z0-9]+\.(jsx?|vue)$/i) requireComponents.keys().forEach(fileName => { const component = requireComponents(fileName) const name = fileName.match(/([a-z0-9]+)\./i)[1] Vue.component(name, component.default || component) }) ================================================ FILE: packages/frontend/src/index.d.ts ================================================ import 'vue-apollo' ================================================ FILE: packages/frontend/src/main.js ================================================ import 'focus-visible' import './plugins' import Vue from 'vue' import App from './components/App.vue' import router from './router' import './registerServiceWorker' import { createClient } from './vue-apollo' import './components' import { provide } from '@vue/composition-api' import { DefaultApolloClient } from '@vue/apollo-composable' Vue.config.productionTip = false Vue.config.devtools = true Vue.config.errorHandler = (err, vm, info) => { console.error(err, vm, info) } const apolloClient = createClient() const app = new Vue({ router, setup () { provide(DefaultApolloClient, apolloClient) }, render: h => h(App), }) router.onReady(() => { app.$mount('#app') }) ================================================ FILE: packages/frontend/src/plugins.js ================================================ import Vue from 'vue' import Responsive from './util/responsive' import { focus } from 'vue-focus' import VueCompositionApi from '@vue/composition-api' import VueMeta from 'vue-meta' import VTooltip from 'v-tooltip' import 'v-tooltip/dist/v-tooltip.css' import GlobalEvents from 'vue-global-events' Vue.use(Responsive, { computed: { sm () { return this.width < 640 }, md () { return this.width < 768 }, mobile () { return this.width <= 768 }, lg () { return this.width < 1024 }, xl () { return this.width < 1280 }, }, }) Vue.directive('focus', focus) Vue.use(VueCompositionApi) Vue.use(VueMeta) Vue.use(VTooltip, { boundariesElement: 'viewport', themes: { tooltip: { // Delay (ms) delay: { show: 700, hide: 0, }, offset: 8, instantMove: true, }, dropdown: { offset: 8, }, 'yellow-arrow': { $extend: 'dropdown', }, }, }) Vue.component('GlobalEvents', GlobalEvents) ================================================ FILE: packages/frontend/src/registerServiceWorker.js ================================================ /* eslint-disable no-console */ import { register } from 'register-service-worker' import { updateAvailable, updateRegistration } from './util/service-worker' if (process.env.NODE_ENV === 'production') { register(`${process.env.BASE_URL}service-worker.js`, { ready () { console.log( 'App is being served from cache by a service worker.\n' + 'For more details, visit https://goo.gl/AFskqB' ) }, registered () { console.log('Service worker has been registered.') }, cached () { console.log('Content has been cached for offline use.') }, updatefound () { console.log('New content is downloading.') }, updated (registration) { console.log('New content is available; please refresh.') updateRegistration.value = registration updateAvailable.value = true }, offline () { console.log('No internet connection found. App is running in offline mode.') }, error (error) { console.error('Error during service worker registration:', error) }, }) } ================================================ FILE: packages/frontend/src/router.js ================================================ import Vue from 'vue' import Router from 'vue-router' import AppHome from './components/app/AppHome.vue' import { scrollBehavior } from './util/scroll-behavior' Vue.use(Router) const packageRoute = (namePrefix = '') => ({ path: 'pkg/:packageId', component: () => import(/* webpackChunkName: "package-view" */ './components/pkg/PackageView.vue'), props: route => ({ ...route.params, routePrefix: namePrefix, }), meta: { keepScroll: true, }, children: [ { path: '', name: `${namePrefix}package`, component: () => import(/* webpackChunkName: "package-tab-general" */ './components/pkg/PackageTabGeneral.vue'), }, { path: 'edit', name: `${namePrefix}package-edit`, component: () => import(/* webpackChunkName: "package-tab-edit" */ './components/pkg/PackageTabEdit.vue'), }, { path: 'data-sources', name: `${namePrefix}package-data-sources`, component: () => import(/* webpackChunkName: "package-tab-data-sources" */ './components/pkg/PackageTabDataSources.vue'), }, { path: 'releases', name: `${namePrefix}package-releases`, component: () => import(/* webpackChunkName: "package-tab-releases" */ './components/pkg/PackageTabReleases.vue'), }, { path: 'insight', component: () => import(/* webpackChunkName: "package-tab-insight" */ './components/pkg/PackageTabInsight.vue'), meta: { depthWeight: 0, }, children: [ { path: '', name: `${namePrefix}package-insight`, component: () => import(/* webpackChunkName: "package-tab-insight-npm-downloads" */ './components/pkg/PackageInsightNpmDownloads.vue'), }, ], }, ], }) export default new Router({ mode: 'history', base: process.env.BASE_URL, scrollBehavior, routes: [ { path: '/', name: 'home', component: AppHome, }, { path: '/login', name: 'login', component: () => import(/* webpackChunkName: "login" */ './components/user/LoginView.vue'), }, { path: '/me', component: () => import(/* webpackChunkName: "user-dashboard" */ './components/user/UserDashboard.vue'), children: [ { path: '', name: 'user-dashboard', component: () => import(/* webpackChunkName: "user-tab-home" */ './components/user/UserTabHome.vue'), meta: { depthWeight: 2, }, }, { path: 'bookmarks', component: () => import(/* webpackChunkName: "user-tab-bookmarks" */ './components/user/UserTabBookmarks.vue'), props: true, children: [ { path: '', name: 'user-bookmarks', component: () => import(/* webpackChunkName: "no-package-bookmar-selected" */ './components/user/NoBookmarkPackageSelected.vue'), }, packageRoute('user-bookmarks-'), ], }, ], }, { path: '/for/:projectTypeSlug', component: () => import(/* webpackChunkName: "project-type" */ './components/project-type/ProjectTypeView.vue'), props: true, children: [ { path: '', name: 'project-type', component: () => import(/* webpackChunkName: "no-package-selected" */ './components/pkg/NoPackageSelected.vue'), }, packageRoute(), ], }, { path: '/proposed/:projectTypeSlug', component: () => import(/* webpackChunkName: "project-type-package-proposals" */ './components/project-type/ProjectTypePackageProposalsView.vue'), props: true, meta: { depthWeight: 2, }, children: [ { path: '', name: 'project-type-proposals', component: () => import(/* webpackChunkName: "no-package-selected" */ './components/pkg/NoPackageSelected.vue'), }, { path: 'pkg/:packageId', component: () => import(/* webpackChunkName: "package-proposal-view" */ './components/pkg/PackageProposalView.vue'), props: true, meta: { keepScroll: true, }, children: [ { path: '', name: `package-proposal`, component: () => import(/* webpackChunkName: "package-proposal-tab-general" */ './components/pkg/PackageProposalTabGeneral.vue'), }, { path: 'edit', name: `package-proposal-edit`, component: () => import(/* webpackChunkName: "package-proposal-tab-edit" */ './components/pkg/PackageProposalTabEdit.vue'), }, { path: 'data-sources', name: `package-proposal-data-sources`, component: () => import(/* webpackChunkName: "package-tab-data-sources" */ './components/pkg/PackageTabDataSources.vue'), }, { path: 'releases', name: `package-proposal-releases`, component: () => import(/* webpackChunkName: "package-tab-releases" */ './components/pkg/PackageTabReleases.vue'), }, { path: 'insight', component: () => import(/* webpackChunkName: "package-tab-insight" */ './components/pkg/PackageTabInsight.vue'), meta: { depthWeight: 0, }, children: [ { path: '', name: `package-proposal-insight`, component: () => import(/* webpackChunkName: "package-tab-insight-npm-downloads" */ './components/pkg/PackageInsightNpmDownloads.vue'), }, ], }, ], }, ], }, { path: '/pkg/add', name: 'add-package', component: () => import(/* webpackChunkName: "package-add-wizard" */ './components/pkg/PackageAddWizard.vue'), }, { path: '/about/privacy', name: 'about-privacy', component: () => import(/* webpackChunkName: "about-privacy" */ './components/about/Privacy.vue'), }, { path: '/about/contributing', name: 'about-contributing', component: () => import(/* webpackChunkName: "about-contributing" */ './components/about/Contributing.vue'), }, { path: '/admin', component: () => import(/* webpackChunkName: "admin-dashboard" */ './components/admin/AdminDashboard.vue'), children: [ { path: '', name: 'admin', component: () => import(/* webpackChunkName: "admin-home" */ './components/admin/AdminHome.vue'), }, { path: 'teams', component: () => import(/* webpackChunkName: "admin-teams" */ './components/admin/AdminTeams.vue'), props: true, children: [ { path: '', name: 'admin-teams', component: { render () { return null } }, }, { path: 'create', name: 'admin-team-create', component: () => import(/* webpackChunkName: "admin-team-create" */ './components/admin/AdminTeamCreate.vue'), }, { path: ':teamId', name: 'admin-team-view', props: true, component: () => import(/* webpackChunkName: "admin-team-view" */ './components/admin/AdminTeamView.vue'), }, ], }, ], }, ], }) ================================================ FILE: packages/frontend/src/util/algolia-npm.js ================================================ import * as Algolia from 'algoliasearch' import { useSearch } from './algolia' const client = Algolia('OFCNCOG2CU', 'db283631f89b5b8a10707311f911fd00') export function useNpmSearch (queryParameters = {}) { return useSearch('npm-search', queryParameters, { attributesToRetrieve: [ 'name', 'description', 'repository', 'keywords', ], hitsPerPage: 10, }, client, { skipEmptyQuery: true, }) } ================================================ FILE: packages/frontend/src/util/algolia.js ================================================ import * as Algolia from 'algoliasearch' import { ref, isRef, watch } from '@vue/composition-api' const defaultClient = Algolia(process.env.VUE_APP_ALGOLIA_ID, process.env.VUE_APP_ALGOLIA_KEY) /** @typedef {import('@vue/composition-api').Ref} Ref */ /** @typedef {import('algoliasearch').QueryParameters} QueryParameters */ /** @typedef {import('algoliasearch').Response} Response */ /** * @param {string} indexName * @param {QueryParameters | Ref} queryParameters * @param {QueryParameters} defaultQueryParameters * @param {Algolia.Client} algoliaClient * @param {any} options Other options */ export function useSearch (indexName, queryParameters = {}, defaultQueryParameters = {}, algoliaClient = defaultClient, options = {}) { const index = algoliaClient.initIndex(indexName) const searchText = ref('') /** @type {Ref} */ const result = ref(null) watch(searchText, () => search()) watch(isRef(queryParameters) ? queryParameters : () => queryParameters, () => search(), { lazy: true, deep: true, }) async function search () { if (options.skipEmptyQuery && !searchText.value) { result.value = null } else { const response = await index.search({ query: searchText.value, ...defaultQueryParameters, ...isRef(queryParameters) ? queryParameters.value : queryParameters, }) response.hits.forEach((hit) => { hit.id = hit.objectID }) result.value = response } } return { searchText, result, search, } } ================================================ FILE: packages/frontend/src/util/emoji.js ================================================ import { ref, computed, watch } from '@vue/composition-api' const joypixels = () => import( /* webpackChunkName: "emoji-toolkit" */ /* webpackMode: "lazy" */ 'emoji-toolkit' ) export function useEmoji (sourceText) { const sourceRef = typeof sourceText === 'function' ? computed(sourceText) : sourceText const parsedText = ref(sourceRef.value) watch(sourceRef, async value => { const { default: emoji } = await joypixels() parsedText.value = emoji.shortnameToUnicode(sourceRef.value) }) return { parsedText, } } ================================================ FILE: packages/frontend/src/util/env.js ================================================ export const isMac = navigator.platform.startsWith('Mac') ================================================ FILE: packages/frontend/src/util/error.js ================================================ import router from '../router' export function checkNeedLogin (e) { if (e.graphQLErrors && e.graphQLErrors.some(e => e.extensions.code === 'guest')) { router.push({ name: 'login' }) } } ================================================ FILE: packages/frontend/src/util/favicon.js ================================================ export function setFavicon (url) { document.head.querySelectorAll('link[rel*="icon"]').forEach(el => { el.href = url }) } export function resetFavicon () { document.head.querySelectorAll('link[rel*="icon"]').forEach(el => { el.href = `${process.env.BASE_URL}favicon.png` }) } ================================================ FILE: packages/frontend/src/util/installation.js ================================================ import { ref } from '@vue/composition-api' export const installationAvailable = ref(false) ================================================ FILE: packages/frontend/src/util/lock-scroll.js ================================================ import { ref, watch, onMounted, onActivated, onDeactivated, onUnmounted } from '@vue/composition-api' const CLASS = 'no-scroll' export function useLockScroll (selector = 'body, .auto-lock-scroll', auto = true) { const locked = ref(false) function update () { const els = document.querySelectorAll(selector) els.forEach(el => { if (locked.value) { el.classList.add(CLASS) } else { el.classList.remove(CLASS) } }) } watch(locked, () => update()) function lock () { locked.value = true } function unlock () { locked.value = false } if (auto) { onMounted(lock) onActivated(lock) onDeactivated(unlock) onUnmounted(() => { unlock() // Need to manually update as the instance is being destroyed // (no more reactivity updates) update() }) } return { locked, lock, unlock, } } ================================================ FILE: packages/frontend/src/util/proposal.js ================================================ import { useRouter } from './router' import gql from 'graphql-tag' import { pkgProposalFragment } from '@/components/pkg/fragments' import { useApolloClient } from '@vue/apollo-composable' export function useSelectNextProposal () { const apolloClient = useApolloClient() const router = useRouter() async function selectNext (projectTypeId, proposalId) { const { data } = await apolloClient.client.query({ query: gql` query ProjectTypePackages ($id: ID!) { projectType (id: $id) { id packageProposals { id } } } `, variables: { id: projectTypeId, }, }) const proposals = data.projectType.packageProposals if (proposals.length === 1) { router.push({ name: 'project-type-proposals' }) } else { let index = proposals.findIndex(p => p.id === proposalId) if (index === proposals.length - 1) { index = 0 } else { index++ } const nextProposal = proposals[index] if (nextProposal) { router.push({ name: 'package-proposal', params: { packageId: nextProposal.id }, }) } } } return { selectNext, } } export function removeProposalFromCache (cache, projectTypeId, proposalId) { const query = { query: gql` query ProjectTypePackages ($id: ID!) { projectType (id: $id) { id packageProposals { ...pkgProposal } } } ${pkgProposalFragment} `, variables: { id: projectTypeId, }, } const data = cache.readQuery(query) const list = data.projectType.packageProposals const index = list.findIndex(p => p.id === proposalId) if (index !== -1) { list.splice(index, 1) cache.writeQuery({ ...query, data, }) } } ================================================ FILE: packages/frontend/src/util/qrcode.js ================================================ import QRCode from 'qrcode' import { ref, watch, isRef } from '@vue/composition-api' export function useQrcode (url) { const src = ref('') watch(isRef(url) ? url : () => url, async (value) => { if (value) { src.value = await QRCode.toDataURL(value, { errorCorrectionLevel: 'H', }) } }) return src } ================================================ FILE: packages/frontend/src/util/responsive.js ================================================ import { computed } from '@vue/composition-api' export let responsive let computedFields export default { install (Vue, options) { const finalOptions = Object.assign({}, { computed: {}, }, options) responsive = new Vue({ data () { return { width: window.innerWidth, height: window.innerHeight, } }, computed: finalOptions.computed, }) computedFields = Object.keys(finalOptions.computed) Object.defineProperty(Vue.prototype, '$responsive', { get: () => responsive, }) window.addEventListener('resize', () => { responsive.width = window.innerWidth responsive.height = window.innerHeight }) }, } export function useResponsive () { return { width: computed(() => responsive.width), height: computed(() => responsive.height), ...computedFields.reduce((obj, key) => { obj[key] = computed(() => responsive[key]) return obj }, {}), } } ================================================ FILE: packages/frontend/src/util/router.js ================================================ import { getCurrentInstance, computed } from '@vue/composition-api' export const STORE_ROUTE_BEFORE_REDIRECT = 'dev.awesomejs.route.before-redirect' export function useRouter () { return getCurrentInstance().$router } export function useRoute () { const vm = getCurrentInstance() return computed(() => vm.$route) } export function getNamedParents (routes, matched) { let parent = null const parents = [] const parentPath = [] for (const record of matched) { let path = record.path if (!path) { path = '/' } if (parent) { parentPath.push(parent.path) } if (parentPath.length) { path = path.substr(parentPath.join('/').length + 1) } let next = routes.find(r => r.path === path) if (next.path) { parent = next routes = next.children if (!next.name && next.children) { next = next.children.find(r => !r.path) } parents.push(next) } else { break } } return parents.slice(0, parents.length - 1) } export function computeDepthWeight (route) { return route.matched.reduce((total, m) => { if (typeof m.meta.depthWeight === 'number') { total += m.meta.depthWeight } else { total++ } return total }, 0) } ================================================ FILE: packages/frontend/src/util/scroll-behavior.js ================================================ // scrollBehavior: // - only available in html5 history mode // - defaults to no scroll behavior // - return false to prevent scroll export const scrollBehavior = function (to, from, savedPosition) { if (savedPosition) { // savedPosition is only available for popstate navigations. return savedPosition } else { const position = {} // scroll to anchor by returning the selector if (to.hash) { position.selector = to.hash // bypass #1number check if (/^#\d/.test(to.hash) || document.querySelector(to.hash)) { return position } // if the returned position is falsy or an empty object, // will retain current scroll position. return false } if (window.innerWidth < 1024 || !to.matched.some(m => m.meta.keepScroll)) { // coords will be used if no selector is provided, // or if the selector didn't match any element. position.x = 0 position.y = 0 } return position } } ================================================ FILE: packages/frontend/src/util/scroll.js ================================================ import { onUnmounted, watch } from '@vue/composition-api' function getScrollParent (node) { if (node == null) { return null } if (node.scrollHeight > node.clientHeight || (node.classList && node.classList.contains('scroll-parent'))) { if (node === document.documentElement) { return { emitter: document, scroller: document.documentElement } } return { emitter: node, scroller: node } } else { return getScrollParent(node.parentNode) } } export function onScrollBottom (handler, el, offsetFromBottom) { let scrollParent function onScroll () { const { scroller } = scrollParent if (scroller.scrollTop >= scroller.scrollHeight - scroller.clientHeight - offsetFromBottom) { handler() } } function addListeners () { removeListeners() if (!el.value) return scrollParent = getScrollParent(el.value) if (scrollParent) { scrollParent.emitter.addEventListener('scroll', onScroll) } } function removeListeners () { if (scrollParent) { scrollParent.emitter.removeEventListener('scroll', onScroll) } } watch(el, () => { addListeners() }) onUnmounted(() => { removeListeners() }) } ================================================ FILE: packages/frontend/src/util/service-worker.js ================================================ import { ref } from '@vue/composition-api' export const updateAvailable = ref(false) export const updateRegistration = ref(null) export function applyUpdate () { if (updateRegistration.value.installing) { updateRegistration.value.installing.addEventListener('statechange', () => { if (updateRegistration.value.installing.state === 'installed') { refreshApp(updateRegistration.value.installing) } }) } else if (updateRegistration.value.waiting) { refreshApp(updateRegistration.value.waiting) } } export function useAppUpdate () { return { updateAvailable, applyUpdate, } } function refreshApp (sw) { let refreshing = false navigator.serviceWorker.addEventListener('controllerchange', () => { if (refreshing) return refreshing = true window.location.reload() }) sw.postMessage({ type: 'SKIP_WAITING' }) } ================================================ FILE: packages/frontend/src/util/share.js ================================================ export function useShare (callback = (info) => info) { if (navigator.share) { return (info) => navigator.share(callback(info)) } return false } ================================================ FILE: packages/frontend/src/util/tags.js ================================================ import { computed } from '@vue/composition-api' import { useQuery } from '@vue/apollo-composable' import gql from 'graphql-tag' export function useTags (pkg) { if (typeof pkg === 'function') { pkg = computed(pkg) } const tags = computed(() => pkg.value._tags || pkg.value.info.tags) const isOfficial = computed(() => tags.value.includes('official')) return { tags, isOfficial, } } export function useAvailableTags (projectTypeIdRef, formTags) { if (typeof projectTypeIdRef === 'function') { projectTypeIdRef = computed(projectTypeIdRef) } if (typeof formTags === 'function') { formTags = computed(formTags) } const { result: projectTypeResult } = useQuery(gql` query ProjectTypeTags ($id: ID!) { projectType (id: $id) { id tags { id } } } `, () => ({ id: projectTypeIdRef.value, }), () => ({ enabled: !!projectTypeIdRef.value, })) const availableTags = computed(() => { return Array.from(new Set([ ...formTags.value, ...projectTypeResult.value ? projectTypeResult.value.projectType.tags.map(t => t.id) : [], ])) }) return { availableTags, } } ================================================ FILE: packages/frontend/src/vue-apollo.js ================================================ import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client' import { onError } from 'apollo-link-error' import { logErrorMessages } from '@vue/apollo-util' import { checkNeedLogin } from '@/util/error' import { cache } from './cache' // Name of the localStorage item const AUTH_TOKEN = 'apollo-token' // Http endpoint const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4040/graphql' // Config const defaultOptions = { // You can use `https` for secure connection (recommended in production) httpEndpoint, // You can use `wss` for secure connection (recommended in production) // Use `null` to disable subscriptions // wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4040/graphql', wsEndpoint: null, // LocalStorage token tokenName: AUTH_TOKEN, // Enable Automatic Query persisting with Apollo Engine persisting: false, // Use websockets for everything (no HTTP) // You need to pass a `wsEndpoint` for this to work websocketsOnly: false, // Is being rendered on the server? ssr: false, httpLinkOptions: { credentials: 'include', }, // Override default apollo link // note: don't override httpLink here, specify httpLink options in the // httpLinkOptions property of defaultOptions. // link: myLink // Override default cache cache, // Override the way the Authorization header is set // getAuth: (tokenName) => ... // Additional ApolloClient options // apollo: { ... } // Client local data (see apollo-link-state) // clientState: { resolvers: { ... }, defaults: { ... } } } // Call this in the Vue app file export function createClient (options = {}) { const link = onError(error => { logErrorMessages(error) checkNeedLogin(error) }) // Create apollo client const { apolloClient, wsClient } = createApolloClient({ ...defaultOptions, ...options, link, }) apolloClient.wsClient = wsClient return apolloClient } // Manually call this when user log in export async function onLogin (apolloClient, token) { if (typeof localStorage !== 'undefined' && token) { localStorage.setItem(AUTH_TOKEN, token) } if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient) try { await apolloClient.resetStore() } catch (e) { // eslint-disable-next-line no-console console.log('%cError on cache reset (login)', 'color: orange;', e.message) } } // Manually call this when user log out export async function onLogout (apolloClient) { if (typeof localStorage !== 'undefined') { localStorage.removeItem(AUTH_TOKEN) } if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient) try { await apolloClient.resetStore() } catch (e) { // eslint-disable-next-line no-console console.log('%cError on cache reset (logout)', 'color: orange;', e.message) } } ================================================ FILE: packages/frontend/tailwind.config.js ================================================ module.exports = { theme: { extend: { colors: { 'gray-850': '#212935', }, spacing: { 128: '32rem', 192: '48rem', 256: '64rem', }, inset: theme => ({ 'full': '100%', ...theme('spacing'), }), maxWidth: theme => ({ ...theme('width'), }), maxHeight: theme => ({ ...theme('width'), }), opacity: { '10': '0.1', '90': '0.9', }, }, }, variants: { appearance: ['responsive'], backgroundAttachment: ['responsive'], backgroundColor: ['responsive', 'hover', 'focus', 'group-hover'], backgroundPosition: ['responsive'], backgroundRepeat: ['responsive'], backgroundSize: ['responsive'], borderCollapse: [], borderColor: ['responsive', 'hover', 'focus'], borderRadius: ['responsive'], borderStyle: ['responsive'], borderWidth: ['responsive'], cursor: ['responsive'], display: ['responsive', 'group-hover'], flexDirection: ['responsive'], flexWrap: ['responsive'], alignItems: ['responsive'], alignSelf: ['responsive'], justifyContent: ['responsive'], alignContent: ['responsive'], flex: ['responsive'], flexGrow: ['responsive'], flexShrink: ['responsive'], float: ['responsive'], fontFamily: ['responsive'], fontWeight: ['responsive', 'hover', 'focus'], height: ['responsive'], lineHeight: ['responsive'], listStylePosition: ['responsive'], listStyleType: ['responsive'], margin: ['responsive', 'first'], maxHeight: ['responsive'], maxWidth: ['responsive'], minHeight: ['responsive'], minWidth: ['responsive'], negativeMargin: ['responsive'], objectFit: ['responsive'], objectPosition: ['responsive'], opacity: ['responsive', 'hover'], outline: ['focus'], overflow: ['responsive'], padding: ['responsive'], pointerEvents: ['responsive'], position: ['responsive'], inset: ['responsive'], resize: ['responsive'], boxShadow: ['responsive', 'hover', 'focus'], fill: [], stroke: [], tableLayout: ['responsive'], textAlign: ['responsive'], textColor: ['responsive', 'hover', 'focus', 'group-hover'], fontSize: ['responsive'], fontStyle: ['responsive'], textTransform: ['responsive'], textDecoration: ['responsive', 'hover', 'focus'], fontSmoothing: ['responsive'], letterSpacing: ['responsive'], userSelect: ['responsive'], verticalAlign: ['responsive'], visibility: ['responsive', 'hover', 'group-hover'], whitespace: ['responsive'], wordBreak: ['responsive'], width: ['responsive'], zIndex: ['responsive'], }, corePlugins: { container: false, }, plugins: [], } ================================================ FILE: packages/frontend/tests/e2e/.eslintrc.js ================================================ module.exports = { plugins: [ 'cypress', ], env: { mocha: true, 'cypress/globals': true, }, rules: { strict: 'off', }, } ================================================ FILE: packages/frontend/tests/e2e/plugins/index.js ================================================ // https://docs.cypress.io/guides/guides/plugins-guide.html // if you need a custom webpack configuration you can uncomment the following import // and then use the `file:preprocessor` event // as explained in the cypress docs // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples /* eslint-disable import/no-extraneous-dependencies, global-require, arrow-body-style */ // const webpack = require('@cypress/webpack-preprocessor') module.exports = (on, config) => { // on('file:preprocessor', webpack({ // webpackOptions: require('@vue/cli-service/webpack.config'), // watchOptions: {} // })) return Object.assign({}, config, { fixturesFolder: 'tests/e2e/fixtures', integrationFolder: 'tests/e2e/specs', screenshotsFolder: 'tests/e2e/screenshots', videosFolder: 'tests/e2e/videos', supportFile: 'tests/e2e/support/index.js', }) } ================================================ FILE: packages/frontend/tests/e2e/specs/test.js ================================================ // https://docs.cypress.io/api/introduction/api.html describe('My First Test', () => { it('Visits the app root url', () => { cy.visit('/') cy.contains('h1', 'Welcome to Your Vue.js App') }) }) ================================================ FILE: packages/frontend/tests/e2e/support/commands.js ================================================ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite // existing commands. // // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** // // // -- This is a parent command -- // Cypress.Commands.add("login", (email, password) => { ... }) // // // -- This is a child command -- // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) // // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) ================================================ FILE: packages/frontend/tests/e2e/support/index.js ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') ================================================ FILE: packages/frontend/vue.config.js ================================================ /** @type {import('@vue/cli-service').ProjectOptions} */ module.exports = { pwa: { name: 'AwesomeJS', themeColor: '#F7DF1D', msTileColor: '#25241F', appleMobileWebAppCapable: 'yes', appleMobileWebAppStatusBarStyle: 'black', iconPaths: { favicon32: 'img/icons/guijs-32.png', favicon16: 'img/icons/guijs-16.png', appleTouchIcon: 'img/icons/guijs-152.png', maskIcon: 'img/icons/guijs-safari-mask.svg', msTileImage: 'img/icons/guijs-144.png', }, manifestOptions: { 'background_color': '#25241F', 'icons': [ { 'src': 'img/icons/guijs-48.png', 'sizes': '48x48', 'type': 'image/png', }, { 'src': 'img/icons/guijs-72.png', 'sizes': '72x72', 'type': 'image/png', }, { 'src': 'img/icons/guijs-96.png', 'sizes': '96x96', 'type': 'image/png', }, { 'src': 'img/icons/guijs-144.png', 'sizes': '144x144', 'type': 'image/png', }, { 'src': 'img/icons/guijs-168.png', 'sizes': '168x168', 'type': 'image/png', }, { 'src': 'img/icons/guijs-192.png', 'sizes': '192x192', 'type': 'image/png', }, { 'src': 'img/icons/guijs-512.png', 'sizes': '512x512', 'type': 'image/png', }, ], }, }, chainWebpack (config) { config.resolve.symlinks(false) config.plugin('prefetch').tap(options => { if (!options[0].fileBlacklist) { options[0].fileBlacklist = [] } options[0].fileBlacklist.push(/emoji-toolkit(.)+?\.js$/) return options }) }, } ================================================ FILE: packages/shared-utils/package.json ================================================ { "name": "@awesomejs/shared-utils", "version": "1.0.0", "private": true, "engines": { "node": ">=10" }, "scripts": { "build": "tsc -d" }, "devDependencies": { "typescript": "^3.7.4" } } ================================================ FILE: packages/shared-utils/tags.d.ts ================================================ export declare function isSpecialTag(tag: string): boolean; ================================================ FILE: packages/shared-utils/tags.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); function isSpecialTag(tag) { return tag === 'official' || tag.startsWith('version:'); } exports.isSpecialTag = isSpecialTag; ================================================ FILE: packages/shared-utils/tags.ts ================================================ export function isSpecialTag (tag: string) { return tag === 'official' || tag.startsWith('version:') } ================================================ FILE: packages/shared-utils/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "module": "CommonJS", "skipLibCheck": true }, "include": [ "." ], "exclude": [ "node_modules" ] } ================================================ FILE: postcss.config.js ================================================ module.exports = {} ================================================ FILE: workspace.code-workspace ================================================ { "folders": [ { "path": "." }, { "path": "packages/frontend" }, { "path": "packages/backend" } ], "settings": {} }