├── .editorconfig ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml └── workflows │ ├── pull_request.yml │ ├── pull_request_lint.yml │ ├── push_main.yml │ └── push_release.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── workers.code-snippets ├── .yarn ├── releases │ └── yarn-4.0.0-rc.39.cjs └── sdks │ ├── eslint │ ├── bin │ │ └── eslint.js │ ├── lib │ │ └── api.js │ └── package.json │ ├── integrations.yml │ ├── prettier │ ├── index.js │ └── package.json │ └── typescript │ ├── bin │ ├── tsc │ └── tsserver │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ ├── tsserverlibrary.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── LICENSE ├── README.md ├── api ├── global.d.ts ├── index.test.ts ├── index.ts ├── package.json ├── tsconfig.json ├── vite.config.ts └── wrangler.toml ├── app ├── README.md ├── core │ └── firebase.ts ├── global.d.ts ├── index.html ├── index.tsx ├── layout │ ├── AppLayout.tsx │ └── components │ │ └── AppToolbar.tsx ├── package.json ├── public │ └── favicon.ico ├── routes │ ├── Home.tsx │ └── index.tsx ├── state │ ├── example.ts │ └── firebase.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── edge ├── global.d.ts ├── index.test.ts ├── index.ts ├── manifest.ts ├── package.json ├── transform.ts ├── tsconfig.json ├── vite.config.ts └── wrangler.toml ├── package.json ├── scripts ├── postinstall.js └── start.js ├── tsconfig.base.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in 2 | # this file, please see the EditorConfig documentation: 3 | # https://editorconfig.org/ 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # https://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | 4 | * text=auto 5 | 6 | # For the following file types, normalize line endings to LF on 7 | # checkin and prevent conversion to CRLF when they are checked out 8 | # (this is required in order to prevent newline related issues like, 9 | # for example, after the build script is run) 10 | 11 | .* text eol=lf 12 | *.css text eol=lf 13 | *.html text eol=lf 14 | *.js text eol=lf 15 | *.json text eol=lf 16 | *.md text eol=lf 17 | *.sh text eol=lf 18 | *.ts text eol=lf 19 | *.txt text eol=lf 20 | *.xml text eol=lf 21 | 22 | /.yarn/** linguist-vendored 23 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Cloudflare Starter Kit 2 | 3 | ♥ [Cloudflare Starter Kit](https://github.com/kriasoft/cloudflare-starter-kit) and 4 | want to get involved? Thanks! There are plenty of ways you can help! 5 | 6 | Please take a moment to review this document in order to make the contribution 7 | process easy and effective for everyone involved. 8 | 9 | Following these guidelines helps to communicate that you respect the time of 10 | the developers managing and developing this open source project. In return, 11 | they should reciprocate that respect in addressing your issue or assessing 12 | patches and features. 13 | 14 | ## Using the issue tracker 15 | 16 | The [issue tracker](https://github.com/kriasoft/cloudflare-starter-kit/issues) is 17 | the preferred channel for [bug reports](#bugs), [features requests](#features) 18 | and [submitting pull requests](#pull-requests), but please respect the following 19 | restrictions: 20 | 21 | - Please **do not** use the issue tracker for personal support requests (use 22 | [Stack Overflow](https://stackoverflow.com/questions/tagged/cloudflare-starter-kit), 23 | [Gitter](https://gitter.im/kriasoft/cloudflare-starter-kit), 24 | [HackHands](https://hackhands.com/koistya) or 25 | [Codementor](https://www.codementor.io/koistya)). 26 | 27 | - Please **do not** derail or troll issues. Keep the discussion on topic and 28 | respect the opinions of others. 29 | 30 | 31 | 32 | ## Bug reports 33 | 34 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 35 | Good bug reports are extremely helpful - thank you! 36 | 37 | Guidelines for bug reports: 38 | 39 | 1. **Use the GitHub issue search** — check if the issue has already been 40 | reported. 41 | 42 | 2. **Check if the issue has been fixed** — try to reproduce it using the 43 | latest `main` or development branch in the repository. 44 | 45 | 3. **Isolate the problem** — ideally create fork of this repo with an 46 | example of how to reproduce the problem. 47 | 48 | A good bug report shouldn't leave others needing to chase you up for more 49 | information. Please try to be as detailed as possible in your report. What is 50 | your environment? What steps will reproduce the issue? What would you expect 51 | to be the outcome? All these details will help people to fix any potential bugs. 52 | 53 | Example: 54 | 55 | > Short and descriptive example bug report title 56 | > 57 | > A summary of the issue and the Node.js/OS environment in which it occurs. If 58 | > suitable, include the steps required to reproduce the bug. 59 | > 60 | > 1. This is the first step 61 | > 2. This is the second step 62 | > 3. Further steps, etc. 63 | > 64 | > `` - a link to the reduced test case 65 | > 66 | > Any other information you want to share that is relevant to the issue being 67 | > reported. This might include the lines of code that you have identified as 68 | > causing the bug, and potential solutions (and your opinions on their 69 | > merits). 70 | 71 | 72 | 73 | ## Feature requests 74 | 75 | Feature requests are welcome. But take a moment to find out whether your idea 76 | fits with the scope and aims of the project. It's up to _you_ to make a strong 77 | case to convince the project's developers of the merits of this feature. Please 78 | provide as much detail and context as possible. 79 | 80 | 81 | 82 | ## Pull requests 83 | 84 | Good pull requests - patches, improvements, new features - are a fantastic 85 | help. They should remain focused in scope and avoid containing unrelated 86 | commits. 87 | 88 | **Please ask first** before embarking on any significant pull request (e.g. 89 | implementing features, refactoring code, porting to a different language), 90 | otherwise you risk spending a lot of time working on something that the 91 | project's developers might not want to merge into the project. 92 | 93 | Please adhere to the coding conventions used throughout a project (indentation, 94 | accurate comments, etc.) and any other requirements (such as test coverage). 95 | 96 | Adhering to the following process is the best way to get your work 97 | included in the project: 98 | 99 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your 100 | fork, and configure the remotes: 101 | 102 | ```bash 103 | # Clone your fork of the repo into the current directory 104 | git clone https://github.com//cloudflare-starter-kit.git 105 | # Navigate to the newly cloned directory 106 | cd cloudflare-starter-kit 107 | # Assign the original repo to a remote called "upstream" 108 | git remote add upstream https://github.com/kriasoft/cloudflare-starter-kit.git 109 | ``` 110 | 111 | 2. If you cloned a while ago, get the latest changes from upstream: 112 | 113 | ```bash 114 | git checkout main 115 | git pull upstream main 116 | ``` 117 | 118 | 3. Create a new topic branch (off the main project development branch) to 119 | contain your feature, change, or fix: 120 | 121 | ```bash 122 | git checkout -b 123 | ``` 124 | 125 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 126 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 127 | or your code is unlikely be merged into the main project. Use Git's 128 | [interactive rebase](https://help.github.com/articles/about-git-rebase/) 129 | feature to tidy up your commits before making them public. 130 | 131 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 132 | 133 | ```bash 134 | git pull [--rebase] upstream main 135 | ``` 136 | 137 | 6. Push your topic branch up to your fork: 138 | 139 | ```bash 140 | git push origin 141 | ``` 142 | 143 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 144 | with a clear title and description. 145 | 146 | **IMPORTANT**: By submitting a patch, you agree to allow the project 147 | owners to license your work under the terms of the [MIT License](LICENSE.txt). 148 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kriasoft 4 | patreon: koistya 5 | open_collective: react-starter-kit 6 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow 2 | # https://help.github.com/actions 3 | 4 | name: PR 5 | 6 | on: [pull_request] 7 | 8 | env: 9 | VERSION: ${{ github.event.pull_request.number }} 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 18 19 | cache: "yarn" 20 | 21 | # Install dependencies 22 | - run: yarn install 23 | 24 | # Analyze code for potential problems 25 | - run: yarn prettier --check . 26 | - run: yarn lint 27 | - run: yarn tsc --build 28 | 29 | # Compile and test 30 | - run: yarn workspace app build 31 | - run: yarn workspace api build 32 | - run: yarn workspace edge build 33 | - run: yarn workspace api test 34 | # - run: yarn workspace edge test 35 | 36 | # Build and deploy 37 | # - run: yarn workspace api deploy --env=test --version=$VERSION 38 | # - run: yarn workspace edge deploy --env=test --version=$VERSION 39 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_lint.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow 2 | # https://help.github.com/actions 3 | 4 | name: "conventionalcommits.org" 5 | 6 | on: 7 | pull_request: 8 | types: 9 | - opened 10 | - edited 11 | - synchronize 12 | 13 | jobs: 14 | main: 15 | name: lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@v4 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/push_main.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow 2 | # https://help.github.com/actions 3 | 4 | name: Push (main) 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | 10 | env: 11 | VERSION: ${{ github.event.pull_request.number }} 12 | 13 | jobs: 14 | deploy: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 10 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 18 22 | cache: "yarn" 23 | 24 | # Install dependencies 25 | - run: yarn install 26 | 27 | # Build 28 | - run: yarn workspace app build 29 | - run: yarn workspace api build 30 | - run: yarn workspace edge build 31 | 32 | # Deploy 33 | # - run: yarn workspace api deploy --env=test 34 | # env: 35 | # CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 36 | # - run: yarn workspace edge deploy --env=test 37 | # env: 38 | # CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/push_release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow 2 | # https://help.github.com/actions 3 | 4 | name: Push (release) 5 | 6 | on: 7 | push: 8 | branches: [release] 9 | 10 | env: 11 | VERSION: ${{ github.event.pull_request.number }} 12 | 13 | jobs: 14 | deploy: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 10 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 18 22 | cache: "yarn" 23 | 24 | # Install dependencies 25 | - run: yarn install 26 | 27 | # Build 28 | - run: yarn workspace app build 29 | - run: yarn workspace api build 30 | - run: yarn workspace edge build 31 | 32 | # Deploy 33 | # - run: yarn workspace api deploy --env=prod 34 | # env: 35 | # CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 36 | # - run: yarn workspace edge deploy --env=prod 37 | # env: 38 | # CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | # Compiled output 5 | /*/dist/ 6 | 7 | # Yarn with PnP 8 | # https://yarnpkg.com/getting-started/qa/#which-files-should-be-gitignored 9 | .pnp.* 10 | .yarn/* 11 | !.yarn/patches 12 | !.yarn/plugins 13 | !.yarn/releases 14 | !.yarn/sdks 15 | !.yarn/versions 16 | node_modules 17 | 18 | # Logs 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # Cache 23 | /.cache 24 | .eslintcache 25 | 26 | # Testing 27 | /coverage 28 | *.lcov 29 | 30 | # Environment variables 31 | .*.override.env 32 | .env 33 | 34 | # Visual Studio Code 35 | # https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore 36 | .vscode/* 37 | !.vscode/settings.json 38 | !.vscode/tasks.json 39 | !.vscode/launch.json 40 | !.vscode/extensions.json 41 | !.vscode/workers.code-snippets 42 | 43 | # WebStorm 44 | .idea 45 | 46 | # macOS 47 | # https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 48 | .DS_Store 49 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn prettier --check . 5 | yarn lint 6 | yarn tsc --build 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.cache 2 | /.husky 3 | /.yarn 4 | /*/dist/ 5 | /.pnp.* 6 | /tsconfig.base.json 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint", 5 | "eamodio.gitlens", 6 | "editorconfig.editorconfig", 7 | "esbenp.prettier-vscode", 8 | "mgmcdermott.vscode-language-babel", 9 | "mikestead.dotenv", 10 | "streetsidesoftware.code-spell-checker", 11 | "vscode-icons-team.vscode-icons" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Debug Current Test File", 9 | "autoAttachChildProcesses": true, 10 | "skipFiles": ["/**", "**/.yarn/**"], 11 | "runtimeExecutable": "yarn", 12 | "runtimeArgs": [ 13 | "workspace", 14 | "${relativeFileDirname}", 15 | "run", 16 | "vitest", 17 | "run", 18 | "${relativeFile}" 19 | ], 20 | "smartStep": true, 21 | "console": "integratedTerminal" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.organizeImports": true 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "editor.tabSize": 2, 8 | "eslint.nodePath": ".yarn/sdks", 9 | "eslint.runtime": "node", 10 | "prettier.prettierPath": ".yarn/sdks/prettier/index.js", 11 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 12 | "typescript.enablePromptUseWorkspaceTsdk": true, 13 | "terminal.integrated.env.linux": { 14 | "CACHE_DIR": "${workspaceFolder}/.cache", 15 | "DOTENV_CONFIG_PATH": "${workspaceFolder}/.env" 16 | }, 17 | "terminal.integrated.env.osx": { 18 | "CACHE_DIR": "${workspaceFolder}/.cache", 19 | "DOTENV_CONFIG_PATH": "${workspaceFolder}/.env" 20 | }, 21 | "terminal.integrated.env.windows": { 22 | "CACHE_DIR": "${workspaceFolder}\\.cache", 23 | "DOTENV_CONFIG_PATH": "${workspaceFolder}\\.env" 24 | }, 25 | "files.exclude": { 26 | "**/.cache": true, 27 | "**/.DS_Store": true, 28 | "**/.editorconfig": true, 29 | "**/.eslintcache": true, 30 | "**/.git": true, 31 | "**/.gitattributes": true, 32 | "**/.husky": true, 33 | "**/.pnp.*": true, 34 | "**/.prettierignore": true, 35 | "**/.yarn": true, 36 | "**/node_modules": true 37 | }, 38 | "search.exclude": { 39 | "**/dist/": true, 40 | "**/yarn-error.log": true, 41 | "**/yarn.lock": true, 42 | "**/.yarn": true, 43 | "**/.pnp.*": true 44 | }, 45 | "cSpell.ignoreWords": [ 46 | "browserslist", 47 | "cloudfunctions", 48 | "corejs", 49 | "endregion", 50 | "entrypoint", 51 | "envars", 52 | "eslintcache", 53 | "esmodules", 54 | "esnext", 55 | "execa", 56 | "firestore", 57 | "globby", 58 | "hono", 59 | "kriasoft", 60 | "miniflare", 61 | "pathinfo", 62 | "refetch", 63 | "refetchable", 64 | "sourcemap", 65 | "spdx", 66 | "tslib", 67 | "typechecking", 68 | "webflow", 69 | "yarnpkg", 70 | "yarnrc" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /.vscode/workers.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "React": { 3 | "scope": "typescript", 4 | "prefix": "worker", 5 | "body": [ 6 | "import { handleError } from \"core\";", 7 | "", 8 | "export default {", 9 | " fetch: handleError((req) => {", 10 | " const url = new URL(req.url);", 11 | "", 12 | " ${0}", 13 | "", 14 | " return fetch(url.toString(), req);", 15 | " }),", 16 | "} as ExportedHandler;", 17 | "" 18 | ], 19 | "description": "Cloudflare Worker" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/bin/eslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/bin/eslint.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/bin/eslint.js your application uses 20 | module.exports = absRequire(`eslint/bin/eslint.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint your application uses 20 | module.exports = absRequire(`eslint`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "8.34.0-sdk", 4 | "main": "./lib/api.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require prettier/index.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real prettier/index.js your application uses 20 | module.exports = absRequire(`prettier/index.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier", 3 | "version": "2.8.4-sdk", 4 | "main": "./index.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsc 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsc your application uses 20 | module.exports = absRequire(`typescript/bin/tsc`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsserver 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsserver your application uses 20 | module.exports = absRequire(`typescript/bin/tsserver`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/tsc.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/tsc.js your application uses 20 | module.exports = absRequire(`typescript/lib/tsc.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsserver.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | const moduleWrapper = tsserver => { 13 | if (!process.versions.pnp) { 14 | return tsserver; 15 | } 16 | 17 | const {isAbsolute} = require(`path`); 18 | const pnpApi = require(`pnpapi`); 19 | 20 | const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); 21 | const isPortal = str => str.startsWith("portal:/"); 22 | const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); 23 | 24 | const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { 25 | return `${locator.name}@${locator.reference}`; 26 | })); 27 | 28 | // VSCode sends the zip paths to TS using the "zip://" prefix, that TS 29 | // doesn't understand. This layer makes sure to remove the protocol 30 | // before forwarding it to TS, and to add it back on all returned paths. 31 | 32 | function toEditorPath(str) { 33 | // We add the `zip:` prefix to both `.zip/` paths and virtual paths 34 | if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { 35 | // We also take the opportunity to turn virtual paths into physical ones; 36 | // this makes it much easier to work with workspaces that list peer 37 | // dependencies, since otherwise Ctrl+Click would bring us to the virtual 38 | // file instances instead of the real ones. 39 | // 40 | // We only do this to modules owned by the the dependency tree roots. 41 | // This avoids breaking the resolution when jumping inside a vendor 42 | // with peer dep (otherwise jumping into react-dom would show resolution 43 | // errors on react). 44 | // 45 | const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; 46 | if (resolved) { 47 | const locator = pnpApi.findPackageLocator(resolved); 48 | if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { 49 | str = resolved; 50 | } 51 | } 52 | 53 | str = normalize(str); 54 | 55 | if (str.match(/\.zip\//)) { 56 | switch (hostInfo) { 57 | // Absolute VSCode `Uri.fsPath`s need to start with a slash. 58 | // VSCode only adds it automatically for supported schemes, 59 | // so we have to do it manually for the `zip` scheme. 60 | // The path needs to start with a caret otherwise VSCode doesn't handle the protocol 61 | // 62 | // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 63 | // 64 | // 2021-10-08: VSCode changed the format in 1.61. 65 | // Before | ^zip:/c:/foo/bar.zip/package.json 66 | // After | ^/zip//c:/foo/bar.zip/package.json 67 | // 68 | // 2022-04-06: VSCode changed the format in 1.66. 69 | // Before | ^/zip//c:/foo/bar.zip/package.json 70 | // After | ^/zip/c:/foo/bar.zip/package.json 71 | // 72 | // 2022-05-06: VSCode changed the format in 1.68 73 | // Before | ^/zip/c:/foo/bar.zip/package.json 74 | // After | ^/zip//c:/foo/bar.zip/package.json 75 | // 76 | case `vscode <1.61`: { 77 | str = `^zip:${str}`; 78 | } break; 79 | 80 | case `vscode <1.66`: { 81 | str = `^/zip/${str}`; 82 | } break; 83 | 84 | case `vscode <1.68`: { 85 | str = `^/zip${str}`; 86 | } break; 87 | 88 | case `vscode`: { 89 | str = `^/zip/${str}`; 90 | } break; 91 | 92 | // To make "go to definition" work, 93 | // We have to resolve the actual file system path from virtual path 94 | // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) 95 | case `coc-nvim`: { 96 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 97 | str = resolve(`zipfile:${str}`); 98 | } break; 99 | 100 | // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) 101 | // We have to resolve the actual file system path from virtual path, 102 | // everything else is up to neovim 103 | case `neovim`: { 104 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 105 | str = `zipfile://${str}`; 106 | } break; 107 | 108 | default: { 109 | str = `zip:${str}`; 110 | } break; 111 | } 112 | } 113 | } 114 | 115 | return str; 116 | } 117 | 118 | function fromEditorPath(str) { 119 | switch (hostInfo) { 120 | case `coc-nvim`: { 121 | str = str.replace(/\.zip::/, `.zip/`); 122 | // The path for coc-nvim is in format of //zipfile://.yarn/... 123 | // So in order to convert it back, we use .* to match all the thing 124 | // before `zipfile:` 125 | return process.platform === `win32` 126 | ? str.replace(/^.*zipfile:\//, ``) 127 | : str.replace(/^.*zipfile:/, ``); 128 | } break; 129 | 130 | case `neovim`: { 131 | str = str.replace(/\.zip::/, `.zip/`); 132 | // The path for neovim is in format of zipfile:////.yarn/... 133 | return str.replace(/^zipfile:\/\//, ``); 134 | } break; 135 | 136 | case `vscode`: 137 | default: { 138 | return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) 139 | } break; 140 | } 141 | } 142 | 143 | // Force enable 'allowLocalPluginLoads' 144 | // TypeScript tries to resolve plugins using a path relative to itself 145 | // which doesn't work when using the global cache 146 | // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 147 | // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but 148 | // TypeScript already does local loads and if this code is running the user trusts the workspace 149 | // https://github.com/microsoft/vscode/issues/45856 150 | const ConfiguredProject = tsserver.server.ConfiguredProject; 151 | const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; 152 | ConfiguredProject.prototype.enablePluginsWithOptions = function() { 153 | this.projectService.allowLocalPluginLoads = true; 154 | return originalEnablePluginsWithOptions.apply(this, arguments); 155 | }; 156 | 157 | // And here is the point where we hijack the VSCode <-> TS communications 158 | // by adding ourselves in the middle. We locate everything that looks 159 | // like an absolute path of ours and normalize it. 160 | 161 | const Session = tsserver.server.Session; 162 | const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; 163 | let hostInfo = `unknown`; 164 | 165 | Object.assign(Session.prototype, { 166 | onMessage(/** @type {string | object} */ message) { 167 | const isStringMessage = typeof message === 'string'; 168 | const parsedMessage = isStringMessage ? JSON.parse(message) : message; 169 | 170 | if ( 171 | parsedMessage != null && 172 | typeof parsedMessage === `object` && 173 | parsedMessage.arguments && 174 | typeof parsedMessage.arguments.hostInfo === `string` 175 | ) { 176 | hostInfo = parsedMessage.arguments.hostInfo; 177 | if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { 178 | const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( 179 | // The RegExp from https://semver.org/ but without the caret at the start 180 | /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ 181 | ) ?? []).map(Number) 182 | 183 | if (major === 1) { 184 | if (minor < 61) { 185 | hostInfo += ` <1.61`; 186 | } else if (minor < 66) { 187 | hostInfo += ` <1.66`; 188 | } else if (minor < 68) { 189 | hostInfo += ` <1.68`; 190 | } 191 | } 192 | } 193 | } 194 | 195 | const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { 196 | return typeof value === 'string' ? fromEditorPath(value) : value; 197 | }); 198 | 199 | return originalOnMessage.call( 200 | this, 201 | isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) 202 | ); 203 | }, 204 | 205 | send(/** @type {any} */ msg) { 206 | return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { 207 | return typeof value === `string` ? toEditorPath(value) : value; 208 | }))); 209 | } 210 | }); 211 | 212 | return tsserver; 213 | }; 214 | 215 | if (existsSync(absPnpApiPath)) { 216 | if (!process.versions.pnp) { 217 | // Setup the environment to be able to require typescript/lib/tsserver.js 218 | require(absPnpApiPath).setup(); 219 | } 220 | } 221 | 222 | // Defer to the real typescript/lib/tsserver.js your application uses 223 | module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`)); 224 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsserverlibrary.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | const moduleWrapper = tsserver => { 13 | if (!process.versions.pnp) { 14 | return tsserver; 15 | } 16 | 17 | const {isAbsolute} = require(`path`); 18 | const pnpApi = require(`pnpapi`); 19 | 20 | const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); 21 | const isPortal = str => str.startsWith("portal:/"); 22 | const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); 23 | 24 | const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { 25 | return `${locator.name}@${locator.reference}`; 26 | })); 27 | 28 | // VSCode sends the zip paths to TS using the "zip://" prefix, that TS 29 | // doesn't understand. This layer makes sure to remove the protocol 30 | // before forwarding it to TS, and to add it back on all returned paths. 31 | 32 | function toEditorPath(str) { 33 | // We add the `zip:` prefix to both `.zip/` paths and virtual paths 34 | if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { 35 | // We also take the opportunity to turn virtual paths into physical ones; 36 | // this makes it much easier to work with workspaces that list peer 37 | // dependencies, since otherwise Ctrl+Click would bring us to the virtual 38 | // file instances instead of the real ones. 39 | // 40 | // We only do this to modules owned by the the dependency tree roots. 41 | // This avoids breaking the resolution when jumping inside a vendor 42 | // with peer dep (otherwise jumping into react-dom would show resolution 43 | // errors on react). 44 | // 45 | const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; 46 | if (resolved) { 47 | const locator = pnpApi.findPackageLocator(resolved); 48 | if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { 49 | str = resolved; 50 | } 51 | } 52 | 53 | str = normalize(str); 54 | 55 | if (str.match(/\.zip\//)) { 56 | switch (hostInfo) { 57 | // Absolute VSCode `Uri.fsPath`s need to start with a slash. 58 | // VSCode only adds it automatically for supported schemes, 59 | // so we have to do it manually for the `zip` scheme. 60 | // The path needs to start with a caret otherwise VSCode doesn't handle the protocol 61 | // 62 | // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 63 | // 64 | // 2021-10-08: VSCode changed the format in 1.61. 65 | // Before | ^zip:/c:/foo/bar.zip/package.json 66 | // After | ^/zip//c:/foo/bar.zip/package.json 67 | // 68 | // 2022-04-06: VSCode changed the format in 1.66. 69 | // Before | ^/zip//c:/foo/bar.zip/package.json 70 | // After | ^/zip/c:/foo/bar.zip/package.json 71 | // 72 | // 2022-05-06: VSCode changed the format in 1.68 73 | // Before | ^/zip/c:/foo/bar.zip/package.json 74 | // After | ^/zip//c:/foo/bar.zip/package.json 75 | // 76 | case `vscode <1.61`: { 77 | str = `^zip:${str}`; 78 | } break; 79 | 80 | case `vscode <1.66`: { 81 | str = `^/zip/${str}`; 82 | } break; 83 | 84 | case `vscode <1.68`: { 85 | str = `^/zip${str}`; 86 | } break; 87 | 88 | case `vscode`: { 89 | str = `^/zip/${str}`; 90 | } break; 91 | 92 | // To make "go to definition" work, 93 | // We have to resolve the actual file system path from virtual path 94 | // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) 95 | case `coc-nvim`: { 96 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 97 | str = resolve(`zipfile:${str}`); 98 | } break; 99 | 100 | // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) 101 | // We have to resolve the actual file system path from virtual path, 102 | // everything else is up to neovim 103 | case `neovim`: { 104 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 105 | str = `zipfile://${str}`; 106 | } break; 107 | 108 | default: { 109 | str = `zip:${str}`; 110 | } break; 111 | } 112 | } 113 | } 114 | 115 | return str; 116 | } 117 | 118 | function fromEditorPath(str) { 119 | switch (hostInfo) { 120 | case `coc-nvim`: { 121 | str = str.replace(/\.zip::/, `.zip/`); 122 | // The path for coc-nvim is in format of //zipfile://.yarn/... 123 | // So in order to convert it back, we use .* to match all the thing 124 | // before `zipfile:` 125 | return process.platform === `win32` 126 | ? str.replace(/^.*zipfile:\//, ``) 127 | : str.replace(/^.*zipfile:/, ``); 128 | } break; 129 | 130 | case `neovim`: { 131 | str = str.replace(/\.zip::/, `.zip/`); 132 | // The path for neovim is in format of zipfile:////.yarn/... 133 | return str.replace(/^zipfile:\/\//, ``); 134 | } break; 135 | 136 | case `vscode`: 137 | default: { 138 | return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) 139 | } break; 140 | } 141 | } 142 | 143 | // Force enable 'allowLocalPluginLoads' 144 | // TypeScript tries to resolve plugins using a path relative to itself 145 | // which doesn't work when using the global cache 146 | // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 147 | // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but 148 | // TypeScript already does local loads and if this code is running the user trusts the workspace 149 | // https://github.com/microsoft/vscode/issues/45856 150 | const ConfiguredProject = tsserver.server.ConfiguredProject; 151 | const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; 152 | ConfiguredProject.prototype.enablePluginsWithOptions = function() { 153 | this.projectService.allowLocalPluginLoads = true; 154 | return originalEnablePluginsWithOptions.apply(this, arguments); 155 | }; 156 | 157 | // And here is the point where we hijack the VSCode <-> TS communications 158 | // by adding ourselves in the middle. We locate everything that looks 159 | // like an absolute path of ours and normalize it. 160 | 161 | const Session = tsserver.server.Session; 162 | const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; 163 | let hostInfo = `unknown`; 164 | 165 | Object.assign(Session.prototype, { 166 | onMessage(/** @type {string | object} */ message) { 167 | const isStringMessage = typeof message === 'string'; 168 | const parsedMessage = isStringMessage ? JSON.parse(message) : message; 169 | 170 | if ( 171 | parsedMessage != null && 172 | typeof parsedMessage === `object` && 173 | parsedMessage.arguments && 174 | typeof parsedMessage.arguments.hostInfo === `string` 175 | ) { 176 | hostInfo = parsedMessage.arguments.hostInfo; 177 | if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { 178 | const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( 179 | // The RegExp from https://semver.org/ but without the caret at the start 180 | /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ 181 | ) ?? []).map(Number) 182 | 183 | if (major === 1) { 184 | if (minor < 61) { 185 | hostInfo += ` <1.61`; 186 | } else if (minor < 66) { 187 | hostInfo += ` <1.66`; 188 | } else if (minor < 68) { 189 | hostInfo += ` <1.68`; 190 | } 191 | } 192 | } 193 | } 194 | 195 | const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { 196 | return typeof value === 'string' ? fromEditorPath(value) : value; 197 | }); 198 | 199 | return originalOnMessage.call( 200 | this, 201 | isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) 202 | ); 203 | }, 204 | 205 | send(/** @type {any} */ msg) { 206 | return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { 207 | return typeof value === `string` ? toEditorPath(value) : value; 208 | }))); 209 | } 210 | }); 211 | 212 | return tsserver; 213 | }; 214 | 215 | if (existsSync(absPnpApiPath)) { 216 | if (!process.versions.pnp) { 217 | // Setup the environment to be able to require typescript/lib/tsserverlibrary.js 218 | require(absPnpApiPath).setup(); 219 | } 220 | } 221 | 222 | // Defer to the real typescript/lib/tsserverlibrary.js your application uses 223 | module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`)); 224 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/typescript.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/typescript.js your application uses 20 | module.exports = absRequire(`typescript/lib/typescript.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "4.9.5-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: true 2 | 3 | nodeLinker: pnp 4 | 5 | packageExtensions: 6 | "@miniflare/r2@*": 7 | dependencies: 8 | "@miniflare/core": ^2.12.0 9 | 10 | yarnPath: .yarn/releases/yarn-4.0.0-rc.39.cjs 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-present Konstantin Tarkus, Kriasoft (hello@kriasoft.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Workers Starter Kit 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Project template for [scaffolding](https://github.com/kriasoft/cloudflare-starter-kit/generate) 10 | [Cloudflare Workers](https://workers.cloudflare.com/) projects. 11 | 12 | ## Features 13 | 14 | - Supports [multiple CF Workers](https://miniflare.dev/core/mount) within the same (mono)repo; using ES modules syntax 15 | - Pre-configured with [TypeScript](https://typescriptlang.org/), [Babel](https://babeljs.io/), 16 | [Rollup](https://rollupjs.org/), [ESLint](https://eslint.org/), [Vitest](https://vitest.dev/), 17 | [Prettier](https://prettier.io/), [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/), 18 | [Miniflare](https://miniflare.dev/) 19 | - Pre-configured with `local`, `test` (staging/QA), and `prod` (production) environments 20 | - Pre-configured with local testing and debugging; loading environment variables from `*.env` files 21 | - [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) usage example for integrating with 3rd party services (Google Cloud, etc.) 22 | - Code snippets and other VSCode settings; CI/CD workflows with GitHub Actions 23 | 24 | --- 25 | 26 | This project was bootstrapped with [Cloudflare Starter Kit](https://github.com/kriasoft/cloudflare-starter-kit). 27 | Be sure to join our [Discord channel](https://discord.gg/QEd934tZvR) for assistance. 28 | 29 | ## Directory Structure 30 | 31 | `├──`[`.github/workflows`](./.github/workflows/) — CI/CD workflows powered by [GitHub Actions](https://github.com/features/actions)
32 | `├──`[`.vscode`](.vscode) — [VSCode](https://code.visualstudio.com/) settings including code snippets, recommended extensions etc.
33 | `├──`[`api`](./api) — Cloudflare Worker script for handling API requests
34 | `├──`[`app`](./app) — Web application front-end powered by [Vite](https://vitejs.dev/) and [React.js](https://reactjs.org/)
35 | `├──`[`edge`](./edge) — [Cloudflare Workers](https://workers.cloudflare.com/) script for serving static websites (reverse proxy)
36 | `├──`[`scripts`](./scripts) — Automation scripts, such as `yarn deploy`
37 | `├──`[`package.json`](./project.json) — The list of [NPM](https://www.npmjs.com/) dependencies and [Yarn](https://yarnpkg.com/) workspaces
38 | `└──`[`tsconfig.base.json`](./tsconfig.base.json) — [TypeScript](https://www.typescriptlang.org/) configuration shared across packages/workspaces
39 | 40 | ## Tech Stack 41 | 42 | - [TypeScript](https://www.typescriptlang.org/), [Cloudflare Workers](https://workers.cloudflare.com/), 43 | [Hono](https://honojs.dev/) 44 | - [Vite](https://vitejs.dev/), [Rollup](https://rollupjs.org/), 45 | [Miniflare](https://miniflare.dev/), [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/), 46 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), 47 | [Vitest](https://vitest.dev/), [Yarn](https://yarnpkg.com/) with PnP 48 | 49 | ## Requirements 50 | 51 | - [Node.js](https://nodejs.org/) v18+ with [Corepack](https://nodejs.org/api/corepack.html) (`$ corepack enable`) 52 | - [VS Code](https://code.visualstudio.com/) editor with [recommended extensions](.vscode/extensions.json) 53 | 54 | ## Getting Started 55 | 56 | [Generate a new repository](https://github.com/kriasoft/cloudflare-starter-kit/generate) 57 | from this template, clone, install dependencies, open it in VSCode and start hacking: 58 | 59 | ```bash 60 | $ git clone https://github.com/kriasoft/cloudflare-starter-kit.git 61 | $ cd ./cloudflare-starter-kit 62 | $ yarn install 63 | $ yarn start 64 | $ yarn test 65 | ``` 66 | 67 | Find the worker scripts inside of the [`./edge`](./edge/) and [`./api`](./api/) folders. 68 | 69 | **IMPORTANT**: Ensure that VSCode is using the [workspace version of TypeScript](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-newer-typescript-versions). 70 | 71 | ## Scripts 72 | 73 | - **`yarn start`** - Launches web application on [`http://localhost:5173/`](http://localhost:5173/) 74 | - **`yarn lint`** — Validates the code using [ESLint](https://eslint.org/) 75 | - **`yarn tsc`** — Validates the code using [TypeScript](https://www.typescriptlang.org/) compiler 76 | - **`yarn test`** — Runs unit tests with [Vitest](https://vitest.dev/), [Miniflare](https://miniflare.dev/), and [Supertest](https://github.com/visionmedia/supertest) 77 | - **`yarn build`** — Compiles and bundles worker scripts into the `./dist` folder(s) 78 | - **`yarn deploy`** — Deploys the app to [Cloudflare Workers](https://developers.cloudflare.com/workers/) / [GCF](https://cloud.google.com/functions) 79 | 80 | ## How to Create a CF Worker 81 | 82 | Find below the minimal boilerplate for creating a new CF Worker script using TypeScript with ESM syntax: 83 | 84 | #### `example/index.ts` — CF Worker script 85 | 86 | ```ts 87 | import { Hono } from "hono"; 88 | 89 | const app = new Hono(); 90 | 91 | app.get("/", ({ text }) => { 92 | return text("Hello world!", 200); 93 | }); 94 | 95 | export default app; 96 | ``` 97 | 98 | #### `example/index.test.ts` — unit test powered by Miniflare 99 | 100 | ```ts 101 | import { expect, test } from "vitest"; 102 | import app from "./index.js"; 103 | 104 | test("GET /", async () => { 105 | const req = new Request(`https://${env.APP_HOSTNAME}/`); 106 | const res = await app.fetch(req, bindings); 107 | const body = await res.text(); 108 | 109 | expect({ status: res.status, body }).toEqual({ 110 | status: 200, 111 | body: "Hello world!", 112 | }); 113 | }); 114 | ``` 115 | 116 | #### `example/wrangler.toml` — deployment configuration 117 | 118 | ```toml 119 | name = "example" 120 | main = "index.js" 121 | compatibility_date = "2022-04-18" 122 | account_id = "$CLOUDFLARE_ACCOUNT_ID" 123 | route = "$APP_HOSTNAME/*" 124 | 125 | [vars] 126 | APP_ENV = "$APP_ENV" 127 | APP_HOSTNAME = "$APP_HOSTNAME" 128 | 129 | [[rules]] 130 | type = "ESModule" 131 | globs = ["**/*.js"] 132 | ``` 133 | 134 | Plus [`package.json`](./edge/package.json), [`tsconfig.json`](./edge/tsconfig.json), 135 | and [`global.d.ts`](./edge/global.d.ts) files configuring TypeScript for the workspace. 136 | 137 | Note that `$APP_HOSTNAME` and `$CLOUDFLARE_ACCOUNT_ID` placeholders in the 138 | example above will be automatically replaced with values from [`*.env`](./env/) 139 | files for the target environment during local testing or deployment. 140 | 141 | For more sophisticated examples visit [Cloudflare Workers Examples](https://developers.cloudflare.com/workers/examples/) directory. 142 | 143 | ## How to Deploy 144 | 145 | The deployments are handled automatically by [GitHub Actions](https://github.com/features/actions) 146 | (see [`.github/workflows`](.github/workflows/)) whenever a new commit lands onto 147 | one of these branches: 148 | 149 | - **`main`** — Deploys the app to [`https://test.example.com`](https://test.example.com/) (test/QA) 150 | - **`release`** — Deploys the app to [`https://example.com`](https://example.com/) (production) 151 | 152 | Alternatively, you can deploy the app manually by ensuring the all the 153 | required environment variables found in the [`*.env`](./env/) files are 154 | up-to-date (e.g. `CLOUDFLARE_API_TOKEN`), then running `yarn deploy [--env #0]`, 155 | specifying the target deployment area via `--env` flag, e.g. `--env=test` 156 | (default) or `--env=prod`. 157 | 158 | You can also deploy packages (workspaces) individually, for example: 159 | 160 | ```bash 161 | $ yarn api:deploy --env=prod 162 | $ yarn edge:deploy --env=prod 163 | ``` 164 | 165 |

166 | 167 | ## How to View Logs 168 | 169 | ``` 170 | $ yarn workspace api wrangler tail [--env #0] 171 | $ yarn workspace api wrangler tail [--env #0] 172 | ``` 173 | 174 | ## How to Update 175 | 176 | - `yarn set version stable` — Bump Yarn to the latest version 177 | - `yarn upgrade-interactive` — Update Node.js modules (dependencies) 178 | - `yarn dlx @yarnpkg/sdks vscode` — Update TypeScript, ESLint, and Prettier settings in VSCode 179 | 180 | ## Backers 💰 181 | 182 |                183 | 184 | ## Related Projects 185 | 186 | - [React Starter Kit](https://github.com/kriasoft/react-starter-kit) — front-end template for React and Relay using Jamstack architecture 187 | - [Node.js API Starter Kit](https://github.com/kriasoft/node-starter-kit) — project template, pre-configured with Node.js, GraphQL, and PostgreSQL 188 | - [GraphQL API and Relay Starter Kit](https://github.com/kriasoft/graphql-starter) — monorepo template, pre-configured with GraphQL API, React, and Relay 189 | 190 | ## How to Contribute 191 | 192 | Anyone and everyone is welcome to [contribute](.github/CONTRIBUTING.md). Start 193 | by checking out the list of [open issues](https://github.com/kriasoft/cloudflare-starter-kit/issues) 194 | marked [help wanted](https://github.com/kriasoft/cloudflare-starter-kit/issues?q=label:"help+wanted"). 195 | However, if you decide to get involved, please take a moment to review the 196 | [guidelines](.github/CONTRIBUTING.md). 197 | 198 | ## License 199 | 200 | Copyright © 2020-present Kriasoft. This source code is licensed under the MIT license found in the 201 | [LICENSE](https://github.com/kriasoft/cloudflare-starter-kit/blob/main/LICENSE) file. 202 | 203 | --- 204 | 205 | Made with ♥ by Konstantin Tarkus ([@koistya](https://twitter.com/koistya), [blog](https://medium.com/@koistya)) 206 | and [contributors](https://github.com/kriasoft/cloudflare-starter-kit/graphs/contributors). 207 | -------------------------------------------------------------------------------- /api/global.d.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | declare type Bindings = { 5 | APP_ENV: "local" | "test" | "prod"; 6 | APP_NAME: string; 7 | APP_HOSTNAME: string; 8 | GOOGLE_CLOUD_CREDENTIALS: string; 9 | }; 10 | 11 | declare type Env = { 12 | Bindings: Bindings; 13 | }; 14 | 15 | declare const bindings: Bindings; 16 | 17 | declare function getMiniflareBindings(): T; 18 | -------------------------------------------------------------------------------- /api/index.test.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { expect, test } from "vitest"; 5 | import app from "./index.js"; 6 | 7 | test("GET /api/people/1", async () => { 8 | const req = new Request("http://0.0.0.0/api/people/1"); 9 | const res = await app.fetch(req, bindings); 10 | const body = await res.json(); 11 | 12 | expect({ status: res.status, body }).toEqual({ 13 | status: 200, 14 | body: expect.objectContaining({ 15 | name: "Luke Skywalker", 16 | url: "https://swapi.dev/api/people/1/", 17 | }), 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { Hono } from "hono"; 5 | 6 | const app = new Hono(); 7 | 8 | // An example of forwarding HTTP requests to a 3rd party API 9 | app.use("*", async ({ req }) => { 10 | const { pathname, search } = new URL(req.url); 11 | const targetUrl = `https://swapi.dev${pathname}${search}`; 12 | const targetReq = new Request(targetUrl, req); 13 | targetReq.headers.set("Accept", "application/json"); 14 | return await fetch(targetReq); 15 | }); 16 | 17 | app.onError((err, ctx) => { 18 | return ctx.json( 19 | { 20 | message: (err as Error).message ?? "Application Error", 21 | stack: ctx.env.APP_ENV === "prod" ? undefined : (err as Error).stack, 22 | }, 23 | 500, 24 | { 25 | "Content-Type": "application/json", 26 | } 27 | ); 28 | }); 29 | 30 | export default app; 31 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "vite build", 8 | "test": "vitest", 9 | "coverage": "vitest run --coverage", 10 | "deploy": "node -r dotenv/config $(yarn bin wrangler) publish --no-bundle dist/index.js" 11 | }, 12 | "dependencies": { 13 | "hono": "^3.0.0" 14 | }, 15 | "devDependencies": { 16 | "@cloudflare/workers-types": "^4.20230215.0", 17 | "dotenv": "^16.0.3", 18 | "toml": "^3.0.0", 19 | "typescript": "^4.9.5", 20 | "vite": "^4.1.2", 21 | "vitest": "^0.28.5", 22 | "wrangler": "^2.10.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["ESNext"], 5 | "types": ["@cloudflare/workers-types"], 6 | "outDir": "./dist" 7 | }, 8 | "include": ["**/*.ts", "**/*.json"], 9 | "exclude": ["dist/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /api/vite.config.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { readFileSync } from "node:fs"; 5 | import { parse } from "toml"; 6 | import { defineConfig } from "vitest/config"; 7 | 8 | const config = parse(readFileSync("./wrangler.toml", "utf8")); 9 | 10 | export default defineConfig({ 11 | cacheDir: "../.cache/vite-api", 12 | build: { 13 | lib: { 14 | name: "api", 15 | entry: "index.ts", 16 | fileName: "index", 17 | formats: ["es"], 18 | }, 19 | }, 20 | define: { 21 | bindings: JSON.stringify(config.vars), 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /api/wrangler.toml: -------------------------------------------------------------------------------- 1 | # Cloudflare Workers configuration 2 | # https://developers.cloudflare.com/workers/wrangler/configuration/ 3 | 4 | name = "example-api" 5 | main = "index.js" 6 | 7 | # https://developers.cloudflare.com/workers/platform/compatibility-dates/ 8 | compatibility_date = "2022-11-30" 9 | 10 | account_id = "xxxxx" 11 | 12 | routes = [ 13 | { pattern = "example.com/api/*", zone_id = "xxxxx" }, 14 | { pattern = "example.com/auth/*", zone_id = "xxxxx" }, 15 | ] 16 | 17 | rules = [ 18 | { type = "ESModule", globs = ["dist/*.js"] }, 19 | { type = "Text", globs = ["dist/*.md"], fallthrough = true } 20 | ] 21 | 22 | [vars] 23 | APP_ENV = "production" 24 | APP_NAME = "example" 25 | APP_HOSTNAME = "example.com" 26 | 27 | # [secrets] 28 | # GOOGLE_CLOUD_CREDENTIALS 29 | 30 | [env.test] 31 | name = "example-api-test" 32 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # Web Application 2 | 3 | Web application package powered by [Vite](https://vitejs.dev/), 4 | [React](https://reactjs.org/), [React Router](https://reactrouter.com/), 5 | [Recoil](https://recoiljs.org/), and [Material UI](https://mui.com/core/). 6 | 7 | ### How to Access the Logged-In User? 8 | 9 | ```tsx 10 | import { useCurrentUser } from "../state/firebase.js"; 11 | 12 | function Example(): JSX.Element { 13 | const me = useCurrentUser(); 14 | return Hello, {me?.name}!; 15 | } 16 | ``` 17 | 18 | ### How to Sign In / Sign Out? 19 | 20 | ```tsx 21 | import { useAuth } from "../state/firebase.js"; 22 | 23 | function Example(): JSX.Element { 24 | const auth = useAuth(); 25 | return ( 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /app/core/firebase.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | // ----------------------------------------------------------------------------- 5 | // NOTE: This file is intended to be loaded asynchronously, on-demand 6 | // in order not to affect the initial page rendering. 7 | // See `useFirebase()` React hook found in `/state/firebase.ts`. 8 | // ----------------------------------------------------------------------------- 9 | 10 | import { initializeApp } from "firebase/app"; 11 | import { getAuth, GoogleAuthProvider, signInWithPopup } from "firebase/auth"; 12 | 13 | export const app = initializeApp({ 14 | projectId: import.meta.env.VITE_GOOGLE_CLOUD_PROJECT, 15 | appId: import.meta.env.VITE_FIREBASE_APP_ID, 16 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY, 17 | authDomain: `${import.meta.env.VITE_GOOGLE_CLOUD_PROJECT}.web.app`, 18 | }); 19 | 20 | export const auth = getAuth(app); 21 | 22 | export function signIn(): ReturnType { 23 | const provider = new GoogleAuthProvider(); 24 | return signInWithPopup(auth, provider); 25 | } 26 | -------------------------------------------------------------------------------- /app/global.d.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | interface ImportMetaEnv { 5 | readonly VITE_APP_HOSTNAME: string; 6 | readonly VITE_GOOGLE_CLOUD_PROJECT: string; 7 | readonly VITE_FIREBASE_APP_ID: string; 8 | readonly VITE_FIREBASE_API_KEY: string; 9 | readonly VITE_FIREBASE_AUTH_DOMAIN: string; 10 | readonly VITE_GA_MEASUREMENT_ID: string; 11 | } 12 | 13 | interface Window { 14 | dataLayer: unknown[]; 15 | } 16 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cloudflare Starter Kit 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { CssBaseline } from "@mui/material"; 5 | import * as React from "react"; 6 | import { createRoot } from "react-dom/client"; 7 | import { BrowserRouter } from "react-router-dom"; 8 | import { RecoilRoot } from "recoil"; 9 | import { AppRoutes } from "./routes/index.js"; 10 | 11 | const root = createRoot(document.getElementById("root") as HTMLElement); 12 | 13 | root.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /app/layout/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { Toolbar } from "@mui/material"; 5 | import * as React from "react"; 6 | import { useOutlet } from "react-router-dom"; 7 | import { AppToolbar } from "./components/AppToolbar.js"; 8 | 9 | export function AppLayout(): JSX.Element { 10 | const outlet = useOutlet(); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | {outlet} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/layout/components/AppToolbar.tsx: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { AppBar, Box, Button, Toolbar, Typography } from "@mui/material"; 5 | import { useAuth, useCurrentUser } from "../../state/firebase.js"; 6 | 7 | export function AppToolbar(): JSX.Element { 8 | const me = useCurrentUser(); 9 | const auth = useAuth(); 10 | 11 | return ( 12 | 13 | 14 | 15 | App Name 16 | 17 | 18 | 19 | 20 | {me === null && ( 21 | 75 | * 76 | * 77 | * ); 78 | * } 79 | */ 80 | export function useAuth() { 81 | const value = useRecoilValueLoadable(Firebase); 82 | return React.useMemo( 83 | () => ({ 84 | signIn() { 85 | return value.toPromise().then((fb) => fb.signIn()); 86 | }, 87 | signOut() { 88 | return value.toPromise().then((fb) => fb.auth.signOut()); 89 | }, 90 | }), 91 | [value.toPromise] 92 | ); 93 | } 94 | 95 | type Firebase = typeof firebase; 96 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ESNext"], 5 | "jsx": "react-jsx", 6 | "jsxImportSource": "@emotion/react", 7 | "types": ["vite/client"], 8 | "outDir": "./dist" 9 | }, 10 | "include": ["**/*.ts", "**/*.tsx", "**/*.json"], 11 | "exclude": ["dist/**/*", "vite.config.ts"], 12 | "references": [{ "path": "./tsconfig.node.json" }] 13 | } 14 | -------------------------------------------------------------------------------- /app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "moduleResolution": "Node", 5 | "outDir": "../.cache/typescript-app" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /app/vite.config.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import react from "@vitejs/plugin-react"; 5 | import * as dotenv from "dotenv"; 6 | import { defineConfig } from "vite"; 7 | 8 | // Load environment variables from .env file 9 | dotenv.config({ path: "../.env" }); 10 | 11 | // Tells Vite which environment variables need to be injected into the app 12 | // https://vitejs.dev/guide/env-and-mode.html#env-variables-and-modes 13 | [ 14 | "APP_HOSTNAME", 15 | "GOOGLE_CLOUD_PROJECT", 16 | "FIREBASE_APP_ID", 17 | "FIREBASE_API_KEY", 18 | "GA_MEASUREMENT_ID", 19 | ].forEach((key) => (process.env[`VITE_${key}`] = process.env[key])); 20 | 21 | /** 22 | * Vite configuration 23 | * https://vitejs.dev/config/ 24 | */ 25 | export default defineConfig({ 26 | cacheDir: `../.cache/vite-app`, 27 | 28 | build: { 29 | rollupOptions: { 30 | output: { 31 | manualChunks: { 32 | firebase: ["firebase/app", "firebase/auth"], 33 | react: ["react", "react-dom", "react-router-dom", "recoil"], 34 | }, 35 | }, 36 | }, 37 | }, 38 | 39 | plugins: [ 40 | // https://github.com/vitejs/vite/tree/main/packages/plugin-react 41 | react({ 42 | jsxRuntime: "classic", 43 | jsxImportSource: "@emotion/react", 44 | babel: { 45 | plugins: ["@emotion/babel-plugin"], 46 | }, 47 | }), 48 | ], 49 | 50 | server: { 51 | proxy: { 52 | "/api": { 53 | target: "https://swapi.dev/", 54 | changeOrigin: true, 55 | }, 56 | }, 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /edge/global.d.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | declare module "__STATIC_CONTENT_MANIFEST" { 5 | const JSON: string; 6 | export default JSON; 7 | } 8 | 9 | declare type Bindings = { 10 | APP_ENV: "local" | "test" | "prod"; 11 | APP_NAME: string; 12 | APP_HOSTNAME: string; 13 | __STATIC_CONTENT: KVNamespace; 14 | }; 15 | 16 | declare type Env = { 17 | Bindings: Bindings; 18 | }; 19 | 20 | declare const bindings: Bindings; 21 | 22 | declare function getMiniflareBindings(): T; 23 | -------------------------------------------------------------------------------- /edge/index.test.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { expect, test } from "vitest"; 5 | import app from "./index.js"; 6 | 7 | test.skip("GET /", async () => { 8 | const req = new Request(`https://${env.APP_HOSTNAME}/`); 9 | const res = await app.fetch(req, bindings); 10 | const body = await res.text(); 11 | 12 | expect(res.status).toEqual(200); 13 | expect(/(.*)<\/title>/.test(body)).toEqual(true); 14 | }); 15 | -------------------------------------------------------------------------------- /edge/index.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { Hono } from "hono"; 5 | import { serveStatic } from "hono/cloudflare-workers"; 6 | import assetManifest from "__STATIC_CONTENT_MANIFEST"; 7 | 8 | const app = new Hono<Env>(); 9 | 10 | app.get("/echo", ({ json, req }) => { 11 | return json({ 12 | headers: Object.fromEntries(req.headers.entries()), 13 | cf: req.raw.cf, 14 | }); 15 | }); 16 | 17 | // Rewrite HTTP requests starting with "/api/" 18 | // to the Star Wars API as an example 19 | app.use("/api/*", ({ req }) => { 20 | const { pathname, search } = new URL(req.url); 21 | return fetch(`https://swapi.dev${pathname}${search}`, req); 22 | }); 23 | 24 | app.use("*", serveStatic({ manifest: assetManifest })); 25 | 26 | export default app; 27 | -------------------------------------------------------------------------------- /edge/manifest.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | /** 5 | * __STATIC_CONTENT_MANIFEST stub for unit testing 6 | */ 7 | export default "{}"; 8 | -------------------------------------------------------------------------------- /edge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "vite build", 8 | "test": "vitest", 9 | "coverage": "vitest run --coverage", 10 | "deploy": "node -r dotenv/config $(yarn bin wrangler) publish --no-bundle dist/index.js" 11 | }, 12 | "dependencies": { 13 | "hono": "^3.0.0" 14 | }, 15 | "devDependencies": { 16 | "@cloudflare/workers-types": "^4.20230215.0", 17 | "dotenv": "^16.0.3", 18 | "toml": "^3.0.0", 19 | "typescript": "^4.9.5", 20 | "vite": "^4.1.2", 21 | "vitest": "^0.28.5", 22 | "wrangler": "^2.10.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /edge/transform.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | type Page = { 5 | title?: string; 6 | description?: string; 7 | data?: Record<string, unknown>; 8 | }; 9 | 10 | /** 11 | * Injects HTML page metadata (title, description, etc.) as well as 12 | * the serialized application store. 13 | */ 14 | export function transform(res: Response, page: Page): Response { 15 | return ( 16 | new HTMLRewriter() 17 | // <title>... 18 | .on("title:first-of-type", { 19 | element(el) { 20 | if (page.title) { 21 | el.setInnerContent(page.title); 22 | } 23 | }, 24 | }) 25 | 26 | // 27 | .on('meta[name="description"]:first-of-type', { 28 | element(el) { 29 | if (page.description) { 30 | el.setAttribute("content", page.description); 31 | } 32 | }, 33 | }) 34 | 35 | // 36 | // https://developer.mozilla.org/docs/Web/HTML/Element/script#embedding_data_in_html 37 | .on("script#data", { 38 | element(el) { 39 | if (page.data) { 40 | const json = JSON.stringify(page.data).replace( 41 | /<\/script/g, 42 | " { 23 | cd("./api"); 24 | let initialized = false; 25 | rollup.watch(apiConfig).on("event", (event) => { 26 | if (event.code === "END") { 27 | if (initialized) { 28 | mf.reload(); 29 | } else { 30 | initialized = true; 31 | mf.startServer() 32 | .then(async (server) => { 33 | await mf.startScheduler(); 34 | cd(".."); 35 | resolve(server); 36 | }) 37 | .catch(reject); 38 | } 39 | } else if (event.code === "ERROR") { 40 | if (initialized) { 41 | initialized = true; 42 | reject(event.error); 43 | } else { 44 | console.log(event.error); 45 | } 46 | } 47 | }); 48 | }); 49 | 50 | console.clear(); 51 | console.log(""); 52 | 53 | console.log( 54 | [ 55 | `${chalk.gray("Application")}: ${chalk.greenBright($.env.APP_NAME)}`, 56 | `${chalk.gray("environment")}: ${chalk.greenBright($.env.APP_ENV)}`, 57 | ].join(", ") 58 | ); 59 | 60 | console.log(""); 61 | 62 | // Launch the web application (front-end) server 63 | const app = await createServer({ 64 | root: "app", 65 | base: argv.base, 66 | logLevel: argv.logLevel ?? argv.l, 67 | clearScreen: argv.clearScreen, 68 | optimizeDeps: { 69 | force: argv.force, 70 | }, 71 | server: { 72 | host: argv.host, 73 | port: argv.port, 74 | https: argv.https, 75 | open: argv.open, 76 | cors: argv.cors, 77 | strictPort: argv.strictPort, 78 | proxy: { 79 | "/api": { 80 | target: `http://localhost:${api.address().port}`, 81 | changeOrigin: true, 82 | }, 83 | }, 84 | }, 85 | }); 86 | 87 | await app.listen(); 88 | app.printUrls(); 89 | 90 | console.log(""); 91 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ESNext", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./api" }, { "path": "./app" }, { "path": "./edge" }] 4 | } 5 | --------------------------------------------------------------------------------