├── .changeset ├── README.md └── config.json ├── .config └── devbox │ ├── init.sh │ └── scripts │ ├── dev-app-configure-and-start.sh │ └── test-plugins.sh ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── renovate.json5 └── workflows │ └── release.yml ├── .gitignore ├── .gitpod.yml ├── .gitpod └── tasks │ ├── 1-webstone-dev │ ├── before.sh │ └── command.sh │ ├── 2-app-dev │ ├── before.sh │ ├── command.sh │ └── init.sh │ └── 3-app-tests │ ├── before.sh │ └── command.sh ├── .husky ├── .gitignore ├── pre-commit └── prepare-commit-msg ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── devbox.json ├── devbox.lock ├── docs ├── README.md ├── assets │ ├── webstone-plugins-dark.excalidraw.png │ └── webstone-plugins-light.excalidraw.png └── deployment │ └── README.md ├── package.json ├── packages ├── cli │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ ├── docs │ │ ├── README.md │ │ └── plugins.md │ ├── package.json │ ├── src │ │ ├── bin.ts │ │ ├── commands │ │ │ ├── dev │ │ │ │ └── dev.ts │ │ │ ├── web │ │ │ │ ├── api │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── create.ts │ │ │ │ │ └── delete.ts │ │ │ │ ├── deployment │ │ │ │ │ ├── configure.ts │ │ │ │ │ ├── deploy.ts │ │ │ │ │ └── deployment.ts │ │ │ │ ├── dev │ │ │ │ │ └── dev.ts │ │ │ │ ├── route │ │ │ │ │ ├── create.ts │ │ │ │ │ ├── delete.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── svelte-add.ts │ │ │ │ └── web.ts │ │ │ └── webstone.ts │ │ ├── extensions │ │ │ ├── package-manager.ts │ │ │ ├── web.ts │ │ │ └── web │ │ │ │ ├── deployment │ │ │ │ └── configure.ts │ │ │ │ └── types.d.ts │ │ ├── helpers.ts │ │ ├── templates │ │ │ └── web │ │ │ │ ├── api │ │ │ │ └── create │ │ │ │ │ ├── [uid].ejs │ │ │ │ │ ├── index.ejs │ │ │ │ │ └── lib-types.d.ejs │ │ │ │ └── route │ │ │ │ └── create │ │ │ │ ├── +error.svelte.ejs │ │ │ │ ├── +layout.server.ts.ejs │ │ │ │ ├── +layout.svelte.ejs │ │ │ │ ├── +layout.ts.ejs │ │ │ │ ├── +page.server.ts.ejs │ │ │ │ ├── +page.svelte.ejs │ │ │ │ ├── +page.ts.ejs │ │ │ │ └── +server.ts.ejs │ │ └── toolbox │ │ │ ├── package-manager │ │ │ ├── add.ts │ │ │ ├── remove.ts │ │ │ └── types.d.ts │ │ │ └── web │ │ │ └── deployment │ │ │ └── configure │ │ │ ├── adapters.ts │ │ │ ├── get-installed-adapter.ts │ │ │ ├── install-adapter.ts │ │ │ ├── is-any-adapter-installed.ts │ │ │ ├── remove-adapter.ts │ │ │ └── types.d.ts │ └── tsconfig.json ├── core │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── create-webstone-app │ ├── CHANGELOG.md │ ├── README.md │ ├── bin.js │ ├── package.json │ ├── scripts │ │ └── build.js │ ├── src │ │ ├── bin.ts │ │ ├── functions.spec.ts │ │ ├── functions.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ └── package.ts │ ├── templates │ │ └── plugin-structure │ │ │ ├── build-cli.js │ │ │ ├── command.ts │ │ │ ├── extension.ts │ │ │ └── template.ejs │ ├── tsconfig.json │ └── types │ │ └── index.ts ├── plugin-request-logger │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── playwright.config.ts │ ├── scripts │ │ └── build-cli.js │ ├── src │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── cli │ │ │ ├── commands │ │ │ │ └── plugins │ │ │ │ │ └── request-logger │ │ │ │ │ └── hello-world.ts │ │ │ ├── extensions │ │ │ │ └── hello-world.ts │ │ │ └── templates │ │ │ │ └── template.ejs │ │ ├── hooks.server.ts │ │ ├── index.test.ts │ │ ├── lib │ │ │ ├── index.ts │ │ │ └── request-logger.ts │ │ └── routes │ │ │ └── +page.svelte │ ├── static │ │ └── favicon.png │ ├── svelte.config.js │ ├── tests │ │ └── test.ts │ ├── tsconfig.json │ └── vite.config.ts └── plugin-trpc │ ├── cli │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── commands │ │ │ └── trpc │ │ │ │ ├── generate.ts │ │ │ │ └── init.ts │ │ ├── extensions │ │ │ └── trpc │ │ │ │ └── hello.ts │ │ ├── lib │ │ │ ├── generate.ts │ │ │ ├── naming.ts │ │ │ └── parser.ts │ │ └── templates │ │ │ ├── base │ │ │ ├── router.ts.ejs │ │ │ └── trpc.ts.ejs │ │ │ └── subrouter.ejs │ ├── tests │ │ └── lib │ │ │ ├── generate.spec.ts │ │ │ ├── naming.spec.ts │ │ │ └── parser.spec.ts │ └── tsconfig.json │ └── web │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── playwright.config.ts │ ├── src │ ├── app.d.ts │ ├── app.html │ ├── lib │ │ └── plugin │ │ │ ├── client.ts │ │ │ ├── handler.ts │ │ │ └── index.ts │ └── routes │ │ └── +page.svelte │ ├── static │ └── favicon.png │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── playwright.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tests └── e2e │ ├── 1-web-pages │ └── create-and-delete.spec.ts │ ├── 2-api-endpoints │ └── create-and-delete.spec.ts │ └── globals.ts └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.5.0/schema.json", 3 | "changelog": [ 4 | "@svitejs/changesets-changelog-github-compact", 5 | { "repo": "WebstoneHQ/webstone-plugins" } 6 | ], 7 | "commit": false, 8 | "linked": [], 9 | "access": "public", 10 | "baseBranch": "main", 11 | "updateInternalDependencies": "patch", 12 | "ignore": [] 13 | } 14 | -------------------------------------------------------------------------------- /.config/devbox/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | pnpm install 5 | -------------------------------------------------------------------------------- /.config/devbox/scripts/dev-app-configure-and-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | ( 5 | if [ ! -d "_dev-app" ] 6 | then 7 | node ./packages/create-webstone-app/bin.js _dev-app --type=app 8 | fi 9 | cd _dev-app 10 | echo "⏳ Waiting for the CLI's binary to be built..." 11 | while [ ! -f ../packages/cli/dist/bin.js ]; do sleep 1; done 12 | npm install -D ../packages/cli 13 | 14 | # Install all plugin-*/ packages 15 | for FILE in $(find ../packages/plugin-*/package.json) 16 | do 17 | DIR=`dirname $FILE` 18 | npm install -D $DIR 19 | done 20 | 21 | # Install all plugin-*/cli packages (without nested monorepo) 22 | # This is for the plugin-trpc package. If / when we migrate that to a monorepo, we can remove this code 23 | for FILE in $(find ../packages/plugin-*/cli/package.json) 24 | do 25 | DIR=`dirname $FILE` 26 | npm install -D $DIR 27 | done 28 | 29 | # Install all plugin-*/web packages (without nested monorepo) 30 | # This is for the plugin-trpc package. If / when we migrate that to a monorepo, we can remove this code 31 | for FILE in $(find ../packages/plugin-*/web/package.json) 32 | do 33 | DIR=`dirname $FILE` 34 | npm install -D $DIR 35 | done 36 | 37 | npx ws dev 38 | ) 39 | -------------------------------------------------------------------------------- /.config/devbox/scripts/test-plugins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | pnpm playwright install 5 | 6 | echo "⏳ Waiting for port 5173 to be ready..." 7 | while ! lsof -i -P -n | grep LISTEN | grep :5173; do sleep 10; done 8 | pnpm test:e2e 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | extends: [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "prettier", 9 | ], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["@typescript-eslint"], 12 | root: true, 13 | rules: { 14 | "@typescript-eslint/ban-ts-comment": [ 15 | "error", 16 | { 17 | "ts-ignore": "allow-with-description", 18 | }, 19 | ], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [WebstoneHQ] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | // Reference docs: https://docs.renovatebot.com/configuration-options 2 | { 3 | extends: ["config:base", "schedule:weekends"], 4 | labels: ["dependencies"], 5 | rangeStrategy: "bump", 6 | timezone: "America/Vancouver", 7 | updateNotScheduled: false, 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@master 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | 19 | - name: Install devbox 20 | uses: jetpack-io/devbox-install-action@v0.5.0 21 | with: 22 | enable-cache: true 23 | 24 | - name: Install Dependencies 25 | run: devbox run -- pnpm install --frozen-lockfile 26 | 27 | - name: Create Release Pull Request or Publish to npm 28 | id: changesets 29 | uses: changesets/action@v1 30 | with: 31 | version: devbox run -- pnpm changeset:version 32 | publish: devbox run -- pnpm changeset:publish 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _dev-app 2 | .pnpm-debug.log 3 | coverage 4 | node_modules 5 | package-lock.json 6 | packages/**/dist 7 | packages/**/build 8 | packages/**/package 9 | tests/e2e/test-results 10 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Learn more about this file at https://www.gitpod.io/docs/references/gitpod-yml 3 | image: gitpod/workspace-full-vnc 4 | 5 | checkoutLocation: webstone 6 | workspaceLocation: . 7 | 8 | tasks: 9 | - name: Webstone Dev 10 | before: ./webstone/.gitpod/tasks/1-webstone-dev/before.sh 11 | command: ./webstone/.gitpod/tasks/1-webstone-dev/command.sh 12 | - name: App Dev 13 | before: ./webstone/.gitpod/tasks/2-app-dev/before.sh 14 | init: ./webstone/.gitpod/tasks/2-app-dev/init.sh 15 | command: ./webstone/.gitpod/tasks/2-app-dev/command.sh 16 | openMode: split-right 17 | - name: App Tests 18 | before: ./webstone/.gitpod/tasks/3-app-tests/before.sh 19 | command: | 20 | ./webstone/.gitpod/tasks/3-app-tests/command.sh 21 | cd webstone 22 | - name: App Dev (misc) 23 | command: cd webstone-dev-app 24 | 25 | ports: 26 | - port: 5173 27 | visibility: public 28 | onOpen: ignore 29 | - port: 5900 30 | onOpen: ignore 31 | - port: 6080 32 | onOpen: ignore 33 | 34 | vscode: 35 | extensions: 36 | - svelte.svelte-vscode 37 | - yzhang.markdown-all-in-one 38 | - dbaeumer.vscode-eslint 39 | - ms-playwright.playwright 40 | -------------------------------------------------------------------------------- /.gitpod/tasks/1-webstone-dev/before.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | npm install -g pnpm 5 | gp sync-done pnpm-installed-globally 6 | cd webstone 7 | pnpm install 8 | pnpm playwright install 9 | gp sync-done dependencies-installed 10 | -------------------------------------------------------------------------------- /.gitpod/tasks/1-webstone-dev/command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | cd webstone 5 | pnpm dev -------------------------------------------------------------------------------- /.gitpod/tasks/2-app-dev/before.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | gp sync-await pnpm-installed-globally -------------------------------------------------------------------------------- /.gitpod/tasks/2-app-dev/command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | cd webstone-dev-app 5 | while [ ! -f ../webstone/packages/cli/dist/bin.js ]; do sleep 1; done 6 | pnpm add -D ../webstone/packages/cli 7 | 8 | # Install all plugin-*/cli packages 9 | for FILE in $(find ../webstone/packages/plugin-*/cli/package.json) 10 | do 11 | DIR=`dirname $FILE` 12 | pnpm add -D $DIR 13 | done 14 | 15 | # Install all plugin-*/web packages 16 | for FILE in $(find ../webstone/packages/plugin-*/web/package.json) 17 | do 18 | DIR=`dirname $FILE` 19 | pnpm add -D $DIR 20 | done 21 | 22 | pnpm ws dev 23 | -------------------------------------------------------------------------------- /.gitpod/tasks/2-app-dev/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | gp sync-await dependencies-installed 5 | node ./webstone/packages/create-webstone-app/bin.js webstone-dev-app --type=application 6 | -------------------------------------------------------------------------------- /.gitpod/tasks/3-app-tests/before.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | gp sync-await pnpm-installed-globally -------------------------------------------------------------------------------- /.gitpod/tasks/3-app-tests/command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | cd webstone 5 | gp ports await 5173 6 | pnpm test:e2e 7 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | devbox run -- pnpx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | if [ -z "${CI}" ]; then 5 | exec < /dev/tty && ./node_modules/.bin/cz --hook || true 6 | fi 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.profiles.osx": { 3 | "devboxCompatibleShell": { 4 | "path": "/bin/zsh", 5 | "args": [] 6 | } 7 | }, 8 | "terminal.integrated.defaultProfile.osx": "devboxCompatibleShell" 9 | } 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | Twitter @webstonehq. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First and foremost, thank you for your interest in contributing to Webstone Plugins 🎉! 4 | 5 | To contribute to Webstone Plugins, we use [Devbox](https://github.com/jetpack-io/devbox). Click the button below to start your development environment - no setup required on your local computer. 6 | 7 | [![Open In Devbox.sh](https://jetpack.io/img/devbox/open-in-devbox.svg)](https://devbox.sh/github.com/WebstoneHQ/webstone-plugins) 8 | 9 | > Please note that the remaining content refers to contributions made via Devbox. 10 | 11 | ## Local development (with Devbox) 12 | 13 | Devbox also works locally. To get started, install it: 14 | 15 | ```shell 16 | curl -fsSL https://get.jetpack.io/devbox | bash 17 | ``` 18 | 19 | Next, open a new Devbox Shell: 20 | 21 | ```shell 22 | devbox shell 23 | ``` 24 | 25 | This installs all required dependencies and provides you with helpful scripts. To see a list of available scripts: 26 | 27 | ```shell 28 | devbox run -l 29 | ``` 30 | 31 | Open multiple terminals and execute the following commands within `devbox shell`: 32 | 33 | 1. `pnpm dev` 34 | - This starts the dev environment and watches all Webstone Plugins files for changes. 35 | 1. `devbox run dev-app:configure-and-start` 36 | - Creates a new `_dev-app` directory and starts its dev server. Use this app to test your Webstone Plugins code changes. 37 | 38 | ## Local development (without Devbox) 39 | 40 | If you prefer to install dependencies on your own, please refer to `devbox.json` for a list of required packages. The `package.json` file contains helpful `scripts` you can execute to build, run, and test Webstone Plugins and the dev app (see below). 41 | 42 | ## Directory structure 43 | 44 | Your Webstone Plugins workspace contains the following directory structure: 45 | 46 | ``` 47 | ./webstone-plugins 48 | ├── _dev-app 49 | ├── packages 50 | └── tests 51 | ``` 52 | 53 | ### `_dev-app` 54 | 55 | This is a development app where you can test changes made to the `webstone` directory above. The dev app's Webstone CLI is symlinked to the `../packages/cli` package. The app runs on port 5173. 56 | 57 | ### `packages` 58 | 59 | This is where you find the Webstone Plugins source code. 60 | 61 | The most important directories within are: 62 | 63 | 64 | 65 | ``` 66 | ./packages 67 | ├── cli 68 | ├── core 69 | ├── create-webstone-app 70 | │ └── templates 71 | └── plugin-* 72 | ``` 73 | 74 | **`create-webstone-app`** 75 | 76 | This is what `pnpm` downloads when a developer runs `pnpm init webstone-app my-web-app`. The `bin` script defined in the `package.json` is what gets executed. The `template` directory is the monorepo structure of the final project, e.g. the content of `my-web-app` in the previous `pnpm init` command or the content of the `_dev-app` directory discussed above. 77 | 78 | **`core` and `cli`** 79 | 80 | These are the only two Webstone dependencies used in the `create-webstone-app/template/package.json` file. As Webstone Plugins evolves, it is the job of the `core` package to depend on additional Webstone packages. With that, the developer experience for Webstone apps is as simple as it gets with only two dependencies needed. 81 | 82 | _Fun fact_: Thanks to this simplicity, any regular SvelteKit web application can be turned into a Webstone project by adding the `@webstone/cli` and `@webstone/core` dependencies. Once installed, the Webstone CLI can be leveraged to further develop the project. 83 | 84 | ## Test framework changes 85 | 86 | Each package in `webstone/packages/*` contains a `dev` script defined in the `package.json` file. This script watches source files and when it detects a change, generates the package's output. Often, this is either an `esbuild` or `tsc -w` kind of command. 87 | 88 | As you change packages, the `_dev-app` project has access to these changes instantly due to the symlink that exists from the Webstone CLI in `_dev-app/node_modules/@webstone/cli` to the `./packages/cli` directory. 89 | 90 | ## Release a package 91 | 92 | Packages configured in `pnpm-workspace.yaml` are released automatically when a pull request is merged into the default branch, as long as there is at least one changeset present for a given package. Please refer to https://github.com/atlassian/changesets for details on changesets. 93 | 94 | To add a changeset: 95 | 96 | - Run `pnpm changeset` 97 | - Commit all files as part of your pull request 98 | 99 | When the PR gets merged, the [`.github/workflows/release.yml`](.github/workflows/release.yml) workflow will open a release pull request. Review & merge this to publish the changed packages to the NPM registry. 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Webstone Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webstone Plugins 2 | 3 | > **Warning** 4 | > 5 | > This project is released as pre-beta. We do our best to not break anything, but can't guarantee it. We welcome feedback via [Discussions](https://github.com/WebstoneHQ/webstone/discussions) and are happy to prioritize new plugins based on community demand. 6 | 7 | Webstone Plugins is a CLI and an growing list of plugins to develop your full-stack web application. 8 | 9 | There is a core `webstone` CLI which can be extended with plugins either by the Webstone Plugins core team or anyone in the community. You can also create private Webstone Plugin components accessible to your team only. 10 | 11 | These are some of the available plugins today: 12 | 13 | - CRUD for REST APIs 14 | - CRUD for web pages 15 | - Create tRPC ([trpc.io](https://trpc.io)) APIs with a Prisma ([prisma.io](https://www.prisma.io)) database schema as the single source of truth 16 | 17 | When a plugin author fixes a bug or releases a new feature, you upgrade your plugin dependency and voilà, your project can take advantage of the latest plugin release. 18 | 19 | ## Getting started 20 | 21 | ```bash 22 | npm create webstone-app my-project 23 | ``` 24 | 25 | or 26 | 27 | ```bash 28 | yarn create webstone-app my-project 29 | ``` 30 | 31 | or 32 | 33 | ```bash 34 | pnpm create webstone-app my-project 35 | ``` 36 | 37 | This creates a skeleton [SvelteKit](https://kit.svelte.dev) app and installs the `@webstone/cli` Webstone CLI as a dev dependency. 38 | 39 | > **Note** > **You can also run the above command in an existing SvelteKit project**! 40 | > 41 | > This allows anyone to benefit from Webstone Plugins, even if you already have an app. 42 | 43 | ## Plugins 44 | 45 | Webstone Plugins at its core only consists of the `@webstone/cli` CLI. Any Webstone plugin is either created by the Webstone team and hosted in our monorepo or developed by anyone 46 | from the community. 47 | 48 | No matter who authors a plugin, each plugin is created with the following command: 49 | 50 | ``` 51 | [npm|yarn|pnpm] create webstone-app my-plugin --type=plugin 52 | ``` 53 | 54 | Note: If you omit the `--type` argument, the CLI will ask whether you want to create a new app or a plugin. 55 | 56 | The following diagram illustrates what Webstoe Plugins looks like at a high level. 57 | 58 | 59 | 60 | 61 | Shows the Webstone Plugins overview where each plugin's name starts with webstone-plugin-, regardless of whether the Webstone team or someone from the community authored a plugin. 62 | 63 | 64 | ## Documentation 65 | 66 | Please refer to the content in the [`docs`](./docs) directory. 67 | 68 | ## Contributing 69 | 70 | Please see [CONTRIBUTING.md](./CONTRIBUTING.md). 71 | 72 | ## FAQ 73 | 74 | ### Is Webstone a boilerplate project? 75 | 76 | **No.** When you create your new project with a boilerplate today, today's version of the boilerplate is what you get. You miss out on bug fixes and new features – unless you manually scan the boilerplate's PRs and copy & paste code to your project. With a boilerplate, you also opt-in to all technologies the author included whereas with Webstone Plugins, you are in control. 77 | 78 | With Webstone, you can add/remove the `@webstone/cli` CLI and any plugin to your project at any time. As new releases become available, you enjoy the benefits by simply upgrading your dependencies. 79 | Should a package upgrade require code changes in your project, Webstone provides automated migrations. 80 | 81 | ### Does Webstone lock me in? 82 | 83 | **No.** Webstone Plugins are just like any other NPM dependency. If you no longer want to use the CLI, simply remove `@webstone/cli` as well as any `webstone-plugin-*` dependencies from your project's `package.json` file. 84 | 85 | If you use plugins that provide Svelte components and you leverage these components in your project, you have to replace the components with your own code. Of course, feel free to copy & paste a Webstone component into your own project, we're cool with that :-). 86 | 87 | ## The backstory of Webstone 88 | 89 | ### 2017 90 | 91 | I ([mikenikles on Twitter](https://twitter.com/mikenikles)) started to write about developer experience & productivity [as far back as 2017](https://www.mikenikles.com/blog/a-mostly-automated-release-process) and continued to do so on a regular basis. 92 | 93 | ### 2019 94 | 95 | In January 2020, I open sourced a [`monorepo-template`](https://github.com/mikenikles/monorepo-template) repo which I had used as a template for a few projects. 96 | 97 | ### 2020 98 | 99 | In summer of 2020, I released the [Cloud Native Web Development](https://www.mikenikles.com/cloud-native-web-development) book and corresponding source code to help web developers go from zero to production. It contains everything from `git init` to monitoring a web application in a production environment. 100 | 101 | Ever since, I have received feedback from readers who thanked me for putting the source code together and how it saved them weeks of setting up their web app project. It's been encouraging to hear from individuals and from software agencies who are able to cut their time to market significantly. 102 | 103 | ### 2021 104 | 105 | Webstone Plugins is the next logical step! 106 | 107 | With Webstone Plugins, we package code we have repeatedly developed in various projects and provide that code as plugins to anyone who wants to speed up their web application development. At its core, Webstone Plugins is an open concept so anyone can create their own plugin and provide it to the community – or keep it private within their organization. 108 | 109 | ### 2022 110 | 111 | [Cahllagerfeld](https://github.com/Cahllagerfeld) joined the core team and started to contribute to the project. 112 | 113 | ## Community 114 | 115 | We share updates in the [Webstone Discord chat](https://discord.gg/WTyAkYe8t3). Join and help shape Webstone Plugins 🙏. 116 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Should you find a security vulnerabilty with this software, please contact us via Twitter direct message [@webstonehq](https://twitter.com/webstonehq). 6 | -------------------------------------------------------------------------------- /devbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["nodejs@latest", "nodePackages_latest.pnpm@latest"], 3 | "shell": { 4 | "init_hook": ["sh ./.config/devbox/init.sh"], 5 | "scripts": { 6 | "dev-app:configure-and-start": [ 7 | "sh ./.config/devbox/scripts/dev-app-configure-and-start.sh" 8 | ], 9 | "test:plugins": ["sh ./.config/devbox/scripts/test-plugins.sh"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbox.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfile_version": "1", 3 | "packages": { 4 | "nodePackages_latest.pnpm@latest": { 5 | "last_modified": "2023-08-08T03:07:33Z", 6 | "resolved": "github:NixOS/nixpkgs/844ffa82bbe2a2779c86ab3a72ff1b4176cec467#nodePackages_latest.pnpm", 7 | "source": "devbox-search", 8 | "version": "8.6.11" 9 | }, 10 | "nodejs@latest": { 11 | "last_modified": "2023-07-23T03:35:12Z", 12 | "resolved": "github:NixOS/nixpkgs/af8cd5ded7735ca1df1a1174864daab75feeb64a#nodejs_20", 13 | "source": "devbox-search", 14 | "version": "20.5.0" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Webstone Documentation 2 | 3 | For a brief introduction and how to get started, please refer to the [README.md](../README.md). 4 | 5 | ## Command Line Interface (CLI) 6 | 7 | Webstone provides a `pnpm webstone` (or `pnpm ws` for short) CLI, making interacting with your full-stack application simpler and more streamlined. 8 | 9 | A list of available commands is available in [`../packages/cli/docs`](../packages/cli/docs). 10 | 11 | ## Deployment 12 | 13 | To learn how to deploy your application, see the [Deployment](./deployment) documentation. 14 | -------------------------------------------------------------------------------- /docs/assets/webstone-plugins-dark.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebstoneHQ/webstone-plugins/5296c66e4191ea32b09b70bf728fc023f0f1ccc4/docs/assets/webstone-plugins-dark.excalidraw.png -------------------------------------------------------------------------------- /docs/assets/webstone-plugins-light.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebstoneHQ/webstone-plugins/5296c66e4191ea32b09b70bf728fc023f0f1ccc4/docs/assets/webstone-plugins-light.excalidraw.png -------------------------------------------------------------------------------- /docs/deployment/README.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | To deploy your application, it takes two steps: 4 | 5 | 1. Configure where to deploy it to 6 | 2. Deploy it 7 | 8 | ## Configure the deployment 9 | 10 | You have to configure the deployment once per service. This gives you the flexibility to deploy individual services independently. For example, you can deploy the `web` service to Vercel while you host the API & database on Google Cloud Platform. 11 | 12 | ### Configure the `web` service 13 | 14 | Please use the [`pnpm webstone web configure deployment`](../../packages/cli/docs#webstone-web-configure-deployment) CLI command and follow the prompts. 15 | 16 | ## Deploy 17 | 18 | You can deploy individual services independently or all at once. The CLI command to use is [`pnpm webstone deploy [service] [--preview]`](../../packages/cli/docs#https://github.com/WebstoneHQ/webstone/tree/main/packages/cli/docs#webstone-deploy). 19 | 20 | The `pnpm webstone deploy` CLI command prepares your application for deployment. Many hosting providers nowadays support a convenient integration with your git provider such as GitHub, GitLab or Bitbucket. Consult the following provider-specific sections to deploy your application. 21 | 22 | ### `web` service 23 | 24 | To prepare your `web` service for deployment, run `pnpm webstone deploy web`. 25 | 26 | #### Cloudflare Workers 27 | 28 | TBD: Pull requests welcome 🙏 29 | 30 | #### Netlify 31 | 32 | TBD: Pull requests welcome 🙏 33 | 34 | #### Node 35 | 36 | TBD: Pull requests welcome 🙏 37 | 38 | #### Static 39 | 40 | TBD: Pull requests welcome 🙏 41 | 42 | #### Vercel 43 | 44 | To get started, please use Vercel's "Create a New Project" instructions at https://vercel.com/docs/get-started. 45 | 46 | Vercel automatically detects that the `web` service is a SvelteKit application - no configuration necessary. 47 | 48 | What you do have to teach Vercel though is that the `web` service is located in the `services/web` directory rather than the root of the project which is assumed by Vercel by default. 49 | 50 | > As you set up (or configure) your project on Vercel, pay close attention to [the "Root Directory" configuration option](https://vercel.com/docs/concepts/deployments/build-step#root-directory). Make sure this is set to `services/web`. 51 | 52 | That's it, 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webstone-plugins", 3 | "version": "0.0.1", 4 | "description": "Start your next web application with Webstone Plugins and configure it as you go.", 5 | "private": true, 6 | "engines": { 7 | "node": ">=v20" 8 | }, 9 | "scripts": { 10 | "build": "pnpm --recursive --parallel build", 11 | "changeset": "changeset && pnpm install", 12 | "changeset:version": "changeset version && pnpm install --lockfile-only", 13 | "changeset:publish": "changeset publish", 14 | "clean": "pnpm --recursive --parallel clean", 15 | "clean:nodemodules": "find . -type d -name \"node_modules\" -exec rm -fr {} +", 16 | "dev": "pnpm --recursive --parallel --filter !./packages/plugin-*/web --filter !./packages/plugin-*/packages/web dev", 17 | "lint": "eslint . --fix --ignore-path .gitignore --max-warnings 0", 18 | "preinstall": "npx only-allow pnpm", 19 | "prepare": "husky install", 20 | "test": "pnpm test:unit", 21 | "test:e2e": "pnpm playwright test", 22 | "test:e2e:open": "pnpm playwright test --headed", 23 | "test:unit": "c8 --all --include=**/src --reporter=html pnpm test:unit:only", 24 | "test:unit:only": "NODE_OPTIONS='--loader tsx' uvu packages tests" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/WebstoneHQ/webstone.git" 29 | }, 30 | "author": "Mike Nikles, @mikenikles", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/WebstoneHQ/webstone/issues" 34 | }, 35 | "homepage": "https://github.com/WebstoneHQ/webstone#readme", 36 | "config": { 37 | "commitizen": { 38 | "path": "./node_modules/cz-conventional-changelog" 39 | } 40 | }, 41 | "lint-staged": { 42 | "*.{js,ts}": [ 43 | "pnpm lint" 44 | ], 45 | "*": "prettier --ignore-unknown --write" 46 | }, 47 | "devDependencies": { 48 | "@changesets/cli": "^2.26.2", 49 | "@playwright/test": "^1.37.0", 50 | "@svitejs/changesets-changelog-github-compact": "^1.1.0", 51 | "@types/fs-extra": "^11.0.1", 52 | "@types/node": "20.4.10", 53 | "@types/sinon": "^10.0.16", 54 | "@typescript-eslint/eslint-plugin": "^6.2.1", 55 | "@typescript-eslint/parser": "^6.2.1", 56 | "c8": "^8.0.1", 57 | "commitizen": "^4.3.0", 58 | "cz-conventional-changelog": "^3.3.0", 59 | "eslint": "^8.46.0", 60 | "eslint-config-prettier": "^9.0.0", 61 | "husky": "^8.0.3", 62 | "lint-staged": "^13.2.3", 63 | "prettier": "^3.0.1", 64 | "sinon": "^15.2.0", 65 | "tsx": "^3.12.7", 66 | "typescript": "^5.1.6", 67 | "uvu": "^0.5.6" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @webstone/cli 2 | 3 | ## 0.13.0 4 | 5 | ### Minor Changes 6 | 7 | - e4954e6: bump gluegun version 8 | - cbcfb98: follow-up fixes for new plugin structure 9 | 10 | ## 0.12.0 11 | 12 | ### Minor Changes 13 | 14 | - 770120d: Accept a `--types` option for `ws web route create` to bypass the interactive prompt. One or more values are accepted, comma-separated. Valid options are: `+page.svelte`, `+page.ts`, `+page.server.ts`, `+layout.svelte`, `+layout.ts`, `+layout.server.ts`, `+layout.server.ts`, `+error.svelte`. 15 | - 23575e3: Release the webstone-plugin-request-logger package. 16 | 17 | ## 0.11.1 18 | 19 | ### Patch Changes 20 | 21 | - 5df3a8c: change naming scheme for plugins 22 | 23 | ## 0.11.0 24 | 25 | ### Minor Changes 26 | 27 | - 369bc5b: create all types of routes 28 | - ab65f24: set up plugins for webstone 29 | 30 | ## 0.10.0 31 | 32 | ### Minor Changes 33 | 34 | - f7a4f1f: remove monorepo approach 35 | 36 | ## 0.9.0 37 | 38 | ### Minor Changes 39 | 40 | - 412041a: Implement latest Sveltekit breaking changes 41 | 42 | ## 0.8.3 43 | 44 | ### Patch Changes 45 | 46 | - 0665efd: Use create-svelte to instantiate the Webstone app. 47 | 48 | ## 0.8.2 49 | 50 | ### Patch Changes 51 | 52 | - 9354f10: Update Webstone to work with the latest version of SvelteKit. 53 | - 4fee40d: Migrate from Cypress to Playwright. 54 | 55 | ## 0.8.1 56 | 57 | ### Patch Changes 58 | 59 | - 710aa9f: Hotfix for the `webstone web api create` CLI command. 60 | 61 | ## 0.8.0 62 | 63 | ### Minor Changes 64 | 65 | - 19c0161: Add the `webstone web api create` and `webstone web api delete` CLI commands. 66 | 67 | ## 0.7.0 68 | 69 | ### Minor Changes 70 | 71 | - 49c17e6: Restructure the webstone CLI commands pattern. 72 | 73 | ## 0.6.0 74 | 75 | ### Minor Changes 76 | 77 | - 0b94911: Write unit tests for the `create-webstone-app` package. 78 | 79 | ## 0.5.1 80 | 81 | ### Patch Changes 82 | 83 | - a6fe827: Provide command aliases and print available sub-commands for no-op commands. 84 | 85 | ## 0.5.0 86 | 87 | ### Minor Changes 88 | 89 | - 2ecb412: Add a `webstone deploy web` CLI command. 90 | 91 | ## 0.4.0 92 | 93 | ### Minor Changes 94 | 95 | - 1edfbe5: Add a `webstone web configure deployment` CLI command. 96 | 97 | ## 0.3.0 98 | 99 | ### Minor Changes 100 | 101 | - db93524: Add a `webstone web delete page` CLI command. 102 | 103 | ## 0.2.0 104 | 105 | ### Minor Changes 106 | 107 | - 69ea7d6: Add a `webstone web create page` command. 108 | 109 | ## 0.1.0 110 | 111 | ### Minor Changes 112 | 113 | - 7e5f53a: Add a `webstone dev` command to start services. 114 | 115 | ## 0.0.7 116 | 117 | ### Patch Changes 118 | 119 | - 1fcc6e1: Add a "ws web svelte-add" command as a wrapper for svelte-add. 120 | 121 | ## 0.0.6 122 | 123 | ### Patch Changes 124 | 125 | - 217041f: Add a `ws dev gitpod web-patch-svelte-config-js` CLI command. 126 | 127 | ## 0.0.5 128 | 129 | ### Patch Changes 130 | 131 | - 42c0d8b: Use Gluegun as the Webstone CLI library. 132 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # Webstone Project CLI 2 | 3 | The Webstone CLI provides the necessary commands to extend, test, and deploy your full-stack application. 4 | 5 | ## How to use 6 | 7 | The CLI is available at the root of your project with `[npx|pnpm|yarn] webstone`, or as an alias `[npx|pnpm|yarn] ws`. 8 | 9 | ## Documentation 10 | 11 | Find a list of all available commands, how to extend the CLI with your own commands, and more, in the [`./docs`f directory](./docs). 12 | -------------------------------------------------------------------------------- /packages/cli/bin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | if (process.argv.includes("--tests")) { 3 | require('ts-node').register({ project: `${__dirname}/tsconfig.json` }) 4 | require(`${__dirname}/src/bin`).run() 5 | } else { 6 | require(`${__dirname}/dist/bin`).run(); 7 | } 8 | -------------------------------------------------------------------------------- /packages/cli/docs/README.md: -------------------------------------------------------------------------------- 1 | # Webstone CLI 2 | 3 | Webstone provides a `[npx|pnpm|yarn] webstone` (or `[npx|pnpm|yarn] ws` for short) CLI, making interacting with your full-stack application simpler and more streamlined. 4 | 5 | - [Webstone CLI](#webstone-cli) 6 | - [Extend the CLI](#extend-the-cli) 7 | - [Commands](#commands) 8 | - [webstone dev](#webstone-dev) 9 | - [webstone web api create](#webstone-web-api-create) 10 | - [webstone web api delete](#webstone-web-api-delete) 11 | - [webstone web deployment configure](#webstone-web-deployment-configure) 12 | - [webstone web deployment deploy](#webstone-web-deployment-deploy) 13 | - [webstone web dev](#webstone-web-dev) 14 | - [webstone web page create](#webstone-web-page-create) 15 | - [webstone web page delete](#webstone-web-page-delete) 16 | - [webstone web svelte-add](#webstone-web-svelte-add) 17 | 18 | ## Extend the CLI 19 | 20 | To extend the Webstone CLI with your own commands, please refer to [the Plugins documentation](./plugins.md). 21 | 22 | ## Commands 23 | 24 | 39 | 40 | ### webstone dev 41 | 42 | Starts the development servers. 43 | 44 | **Usage** 45 | 46 | ```bash 47 | webstone dev [service] 48 | ``` 49 | 50 | - `[service]` - An optional service to start, e.g. `web`. The list of available services can be found in your Webstone project's `services/` directory. Any of the directory name can be used for the `[service]` argument. If no service name is provided, all services will start in development mode. 51 | 52 | ### webstone web api create 53 | 54 | Creates new API **C**reate, **R**ead, **U**pdate, **D**elete (CRUD) endpoints in your `web` service. E.g.`/api/users`. 55 | 56 | **Usage** 57 | 58 | ```bash 59 | webstone web api create [api-path] 60 | ``` 61 | 62 | - `[api-path]` - The URL path of the API endpoints to create. For example, `/api/users` generates CRUD endpoints in `src/routes/api/users/`. If no API path is provided, you will be prompted to provide one interactively. 63 | 64 | ### webstone web api delete 65 | 66 | Deletes an API endpoint in your `web` service. E.g.`/api/users`. 67 | 68 | **Usage** 69 | 70 | ```bash 71 | webstone web api delete [api-path] 72 | ``` 73 | 74 | - `[api-path]` - The URL path of the API endpoints to delete. For example, `/api/users` deletes all endpoints in `src/routes/api/users/`. If no API path is provided, you will be prompted to provide one interactively. 75 | 76 | ### webstone web deployment configure 77 | 78 | Add a deployment adapter for the `web` service. Please refer to SvelteKit's ["Adapters"](https://kit.svelte.dev/docs#adapters) documentation if you are not familiar with that concept. 79 | 80 | If you deploy to a [supported environment](https://kit.svelte.dev/docs#adapters-supported-environments), no configuration is necessary and your web application deploys automatically once you set it up with the supported hosting provider. 81 | 82 | **Note**: Once the command completes, please make sure you read the console output and follow the link(s) provided to complete the adapter configuration. 83 | 84 | **Usage** 85 | 86 | ```bash 87 | webstone web deployment configure 88 | ``` 89 | 90 | ### webstone web deployment deploy 91 | 92 | Deploys the `web` service based on a configured deployment adapter. 93 | 94 | If you deploy to a [supported environment](https://kit.svelte.dev/docs#adapters-supported-environments), no configuration is necessary and your web application deploys automatically once you set it up with the supported hosting provider. 95 | 96 | **Note**: This command only works after you configured a deployment adapter. See [`webstone web configure deployment`](#webstone-web-configure-deployment). 97 | 98 | **Usage** 99 | 100 | ```bash 101 | webstone web deployment deploy [--preview] 102 | ``` 103 | 104 | - `[--preview]` - Preview your application in production mode before you deploy it. Equivalent to [`svelte-kit preview`](https://kit.svelte.dev/docs#command-line-interface-svelte-kit-build). 105 | 106 | ### webstone web dev 107 | 108 | Starts the `web` service. This is an alias for `webstone dev web`. 109 | 110 | **Usage** 111 | 112 | ```bash 113 | webstone web dev 114 | ``` 115 | 116 | ### webstone web page create 117 | 118 | Creates a new page in your `web` service. E.g.`/about-us`. 119 | 120 | **Usage** 121 | 122 | ```bash 123 | webstone web page create [name] 124 | ``` 125 | 126 | - `[name]` - The name of the page to create. This can be `about-us` or `"About Us"`. If no name is provided, the CLI will prompt you interactively. 127 | 128 | ### webstone web page delete 129 | 130 | Deletes a page in your `web` service. E.g.`/about-us`. 131 | 132 | **Usage** 133 | 134 | ```bash 135 | webstone web page delete [name] 136 | ``` 137 | 138 | - `[name]` - The name of the page to delete. This can be `about-us` or `"About Us"`. If no name is provided, the CLI will prompt you interactively. 139 | 140 | ### webstone web svelte-add 141 | 142 | This is a convenience wrapper around `svelte-add` (https://github.com/svelte-add/svelte-add) to add things like Tailwind CSS, mdsvex, etc. 143 | 144 | **Usage** 145 | 146 | ```bash 147 | webstone web svelte-add 148 | ``` 149 | 150 | - `` - Mandatory, the name of the integration to add, e.g. `tailwindcss` or `mdsvex`. Please refer to [the `svelte-add` docs](https://github.com/svelte-add/svelte-add) for a list of available integrations. 151 | -------------------------------------------------------------------------------- /packages/cli/docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugin guide for the Webstone CLI 2 | 3 | Plugins allow you to add features to the Webstone CLI, such as commands and 4 | extensions to the `toolbox` object that provides the majority of the functionality 5 | used by command line interface. 6 | 7 | Creating a CLI plugin is easy. Just create a repo with three optional folders: 8 | 9 | ``` 10 | commands/ 11 | extensions/ 12 | templates/ 13 | ``` 14 | 15 | A command is a file that looks something like this: 16 | 17 | ```js 18 | // commands/foo.js 19 | 20 | module.exports = { 21 | run: (toolbox) => { 22 | const { print, filesystem } = toolbox; 23 | 24 | const desktopDirectories = filesystem.subdirectories(`~/Desktop`); 25 | print.info(desktopDirectories); 26 | }, 27 | }; 28 | ``` 29 | 30 | An extension lets you add additional features to the `toolbox`. 31 | 32 | ```js 33 | // extensions/bar-extension.js 34 | 35 | module.exports = (toolbox) => { 36 | const { print } = toolbox; 37 | 38 | toolbox.bar = () => { 39 | print.info("Bar!"); 40 | }; 41 | }; 42 | ``` 43 | 44 | This is then accessible in your plugin's commands as `toolbox.bar`. 45 | 46 | ## Loading a plugin 47 | 48 | To load a particular plugin (which has to start with `webstone-*`), 49 | install it to your project using `[npx|pnpm|yarn] install --save-dev webstone-PLUGINNAME`, 50 | and the Webstone CLI will pick it up automatically. 51 | 52 | ## Further reading 53 | 54 | To learn more about plugins, please review [the official Gluegun Plugins documentation](https://infinitered.github.io/gluegun/#/plugins). 55 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webstone/cli", 3 | "version": "0.13.0", 4 | "description": "The Webstone command line interface", 5 | "types": "build/types/types.d.ts", 6 | "engines": { 7 | "node": ">=v20" 8 | }, 9 | "bin": { 10 | "webstone": "./bin", 11 | "ws": "./bin" 12 | }, 13 | "scripts": { 14 | "build": "pnpm clean && pnpm compile && pnpm copy-templates", 15 | "clean": "rm -rf ./dist", 16 | "compile": "tsc -p .", 17 | "copy-templates": "if [ -e ./src/templates ]; then cp -a ./src/templates ./dist/; fi", 18 | "dev": "pnpm clean && pnpm copy-templates && pnpm dev:watch-src & pnpm dev:watch-templates", 19 | "dev:watch-src": "tsc -w", 20 | "dev:watch-templates": "npm-watch copy-templates", 21 | "prepublishOnly": "pnpm build", 22 | "test": "pnpm test:unit", 23 | "test:unit": "c8 --all --include=src --reporter=html pnpm test:unit:only", 24 | "test:unit:only": "NODE_OPTIONS='--loader tsx' uvu tests" 25 | }, 26 | "watch": { 27 | "copy-templates": { 28 | "patterns": [ 29 | "src/templates" 30 | ], 31 | "extensions": "ejs" 32 | } 33 | }, 34 | "license": "MIT", 35 | "devDependencies": { 36 | "npm-watch": "^0.11.0" 37 | }, 38 | "dependencies": { 39 | "@webstone/gluegun": "^0.0.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/cli/src/bin.ts: -------------------------------------------------------------------------------- 1 | import { WebstoneToolbox } from "./extensions/web"; 2 | import { build } from "@webstone/gluegun"; 3 | 4 | export const run = async () => { 5 | const cli = build() 6 | .brand("webstone") 7 | .src(__dirname) 8 | .plugins("./node_modules", { matching: "webstone-plugin-*" }) 9 | .help() 10 | .version() 11 | .exclude([ 12 | // "filesystem", 13 | "http", 14 | // "meta", 15 | "package-manager", 16 | // "patching", 17 | // "print", 18 | // "prompt", 19 | "semver", 20 | // "strings", 21 | // "system", 22 | // "template", 23 | ]) 24 | .create(); 25 | const toolbox = (await cli.run(process.argv)) as WebstoneToolbox; 26 | 27 | // Return to use it in tests 28 | return toolbox; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/cli/src/commands/dev/dev.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from "@webstone/gluegun"; 2 | import { determinePackageManager } from "../../helpers"; 3 | 4 | const command: GluegunCommand = { 5 | alias: ["d"], 6 | description: "Start the dev server", 7 | run: async (toolbox) => { 8 | const { print, system } = toolbox; 9 | print.info("Starting dev server..."); 10 | 11 | await system.exec(`${determinePackageManager()} run dev`, { 12 | stdout: "inherit", 13 | }); 14 | }, 15 | }; 16 | 17 | module.exports = command; 18 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/api/api.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from "@webstone/gluegun"; 2 | 3 | const command: GluegunCommand = { 4 | alias: "a", 5 | description: "No-op entry for the `webstone web api` command.", 6 | hidden: true, 7 | run: async (toolbox) => { 8 | const { print } = toolbox; 9 | print.printCommands(toolbox, ["web", "api"]); 10 | }, 11 | }; 12 | 13 | module.exports = command; 14 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/api/create.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from "@webstone/gluegun"; 2 | 3 | const command: GluegunCommand = { 4 | alias: ["c"], 5 | description: "Create new web API CRUD endpoints", 6 | run: async (toolbox) => { 7 | const { filesystem, parameters, print, prompt, template } = toolbox; 8 | 9 | let apiPath = parameters.first; 10 | if (!apiPath) { 11 | const result = await prompt.ask({ 12 | type: "input", 13 | name: "apiPath", 14 | message: `What's the API endpoint path (e.g. /api/users)?`, 15 | }); 16 | if (result && result.apiPath) apiPath = result.apiPath; 17 | } 18 | 19 | if (!apiPath) { 20 | print.error("Please provide an API endpoint path (e.g. /api/users)."); 21 | return; 22 | } 23 | 24 | const filePath = apiPath.toLowerCase().replace(/^\//, ""); 25 | const targetDir = `src/routes/${filePath}`; 26 | 27 | if (filesystem.exists(targetDir) === "dir") { 28 | print.info(`The ${apiPath} API endpoint already exists.`); 29 | return; 30 | } 31 | 32 | const spinner = print.spin(`Creating API endpoint at "${targetDir}"...`); 33 | if (filesystem.exists("src/lib/types.d.ts") !== "file") { 34 | await template.generate({ 35 | template: "web/api/create/lib-types.d.ejs", 36 | target: "src/lib/types.d.ts", 37 | props: { 38 | apiPath, 39 | }, 40 | }); 41 | } 42 | 43 | await template.generate({ 44 | template: "web/api/create/index.ejs", 45 | target: `${targetDir}/+server.ts`, 46 | props: { 47 | apiPath, 48 | }, 49 | }); 50 | await template.generate({ 51 | template: "web/api/create/[uid].ejs", 52 | target: `${targetDir}/[uid]/+server.ts`, 53 | props: { 54 | apiPath, 55 | }, 56 | }); 57 | spinner.succeed(`API endpoint created at: ${targetDir}/`); 58 | }, 59 | }; 60 | 61 | module.exports = command; 62 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/api/delete.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from "@webstone/gluegun"; 2 | 3 | const command: GluegunCommand = { 4 | alias: ["d"], 5 | description: "Delete an API endpoint", 6 | run: async (toolbox) => { 7 | const { filesystem, parameters, print, prompt } = toolbox; 8 | 9 | let apiPath = parameters.first; 10 | if (!apiPath) { 11 | const result = await prompt.ask({ 12 | type: "input", 13 | name: "apiPath", 14 | message: `What's the API endpoint path (e.g. /api/users)?`, 15 | }); 16 | if (result && result.apiPath) apiPath = result.apiPath; 17 | } 18 | 19 | if (!apiPath) { 20 | print.error("Please provide an API endpoint path (e.g. /api/users)."); 21 | return; 22 | } 23 | 24 | const filePath = apiPath.toLowerCase().replace(/^\//, ""); 25 | const targetDir = `src/routes/${filePath}`; 26 | 27 | const spinner = print.spin(`Removing API endpoint at "${targetDir}"...`); 28 | filesystem.remove(targetDir); 29 | spinner.succeed(`API endpoint deleted at: ${targetDir}`); 30 | }, 31 | }; 32 | 33 | module.exports = command; 34 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/deployment/configure.ts: -------------------------------------------------------------------------------- 1 | import type { GluegunCommand } from "@webstone/gluegun"; 2 | import type { WebstoneToolbox } from "../../../extensions/web"; 3 | 4 | const command: GluegunCommand = { 5 | alias: ["c"], 6 | description: "Configure a web service deployment adapter", 7 | // @ts-ignore: WebstoneToolbox extends GluegunToolbox, ignore TS error. 8 | run: async (toolbox: WebstoneToolbox) => { 9 | const { print, prompt, web } = toolbox; 10 | 11 | const availableAdapters = web.configure.deployment.availableAdapters; 12 | const adapterPromptResult = await prompt.ask({ 13 | type: "select", 14 | name: "adapter", 15 | message: `Where would you like to deploy the "web" service to?`, 16 | choices: availableAdapters.map( 17 | (adapter) => `${adapter.name} (${adapter.npmPackage})`, 18 | ), 19 | }); 20 | let adapterIdentifier = ""; 21 | if (adapterPromptResult && adapterPromptResult.adapter) 22 | adapterIdentifier = adapterPromptResult.adapter; 23 | 24 | if (adapterIdentifier === "") { 25 | print.error("Please choose an adapter."); 26 | return; 27 | } 28 | 29 | const chosenAdapter = availableAdapters.find((availableAdapter) => 30 | adapterIdentifier.includes(availableAdapter.npmPackage), 31 | ); 32 | if (!chosenAdapter) { 33 | print.error(`The chosen adapter ${adapterIdentifier} is not available.`); 34 | return; 35 | } 36 | 37 | const nextStepsInstructions: string[] = []; 38 | if (web.configure.deployment.isAnyAdapterInstalled()) { 39 | const installedAdapter = web.configure.deployment.getInstalledAdapter(); 40 | 41 | if ( 42 | installedAdapter && 43 | installedAdapter?.npmPackage === chosenAdapter.npmPackage 44 | ) { 45 | print.info(`Adapter ${chosenAdapter.name} is already installed.`); 46 | return; 47 | } 48 | 49 | if (!installedAdapter) { 50 | print.error( 51 | `An adapter should be installed, but isn't... This is an unexpected error, please manually review the "package.json file."`, 52 | ); 53 | return; 54 | } 55 | const isReplaceInstalledAdapter = await prompt.confirm( 56 | `The ${installedAdapter.name} adapter is already installed. Would you like to replace it with ${chosenAdapter.name}?`, 57 | ); 58 | if (isReplaceInstalledAdapter) { 59 | await web.configure.deployment.removeAdapter(installedAdapter); 60 | nextStepsInstructions.push( 61 | `- to completely remove the ${installedAdapter.name} adapter: ${installedAdapter.nextStepsDocsLink}`, 62 | ); 63 | } else { 64 | return; 65 | } 66 | } 67 | 68 | await web.configure.deployment.installAdapter(chosenAdapter); 69 | nextStepsInstructions.push( 70 | `- to finalize the configuration of the newly installed ${chosenAdapter.name} adapter, undo the changes at: ${chosenAdapter.nextStepsDocsLink}`, 71 | ); 72 | print.highlight( 73 | `\nPlease perform the following next steps by reading the docs:\n${nextStepsInstructions.join( 74 | "\n", 75 | )}`, 76 | ); 77 | }, 78 | }; 79 | 80 | module.exports = command; 81 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/deployment/deploy.ts: -------------------------------------------------------------------------------- 1 | import type { WebstoneToolbox } from "../../../extensions/web"; 2 | 3 | import { GluegunCommand } from "@webstone/gluegun"; 4 | import { determinePackageManager } from "../../../helpers"; 5 | 6 | const command: GluegunCommand = { 7 | alias: ["d"], 8 | description: "Deploy the web service", 9 | // @ts-ignore: WebstoneToolbox extends GluegunToolbox, ignore TS error. 10 | run: async (toolbox: WebstoneToolbox) => { 11 | const { parameters, print, system, web } = toolbox; 12 | 13 | if (!web.configure.deployment.isAnyAdapterInstalled()) { 14 | print.warning( 15 | "No deployment adapter configured. Please run `[npx|pnpm|yarn] webstone web configure deployment` to fix this before you deploy the application.", 16 | ); 17 | return; 18 | } 19 | 20 | const buildSpinner = print.spin(`Building the web service...`); 21 | await system.run(`${determinePackageManager()} run build`); 22 | buildSpinner.succeed(); 23 | 24 | if (parameters.options.preview) { 25 | print.info(`Previewing the web service...`); 26 | await system.exec(`${determinePackageManager()} run preview`, { 27 | stdout: "inherit", 28 | }); 29 | } else { 30 | const installedAdapter = web.configure.deployment.getInstalledAdapter(); 31 | print.highlight( 32 | `Your web service is ready to be deployed. Please follow the instructions at https://github.com/WebstoneHQ/webstone/tree/main/docs/deployment#${installedAdapter.identifier.substring( 33 | "adapter-".length, 34 | )} to deploy to ${installedAdapter.name}`, 35 | ); 36 | } 37 | }, 38 | }; 39 | 40 | module.exports = command; 41 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/deployment/deployment.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from "@webstone/gluegun"; 2 | 3 | const command: GluegunCommand = { 4 | description: "No-op entry for the `webstone web deployment` command.", 5 | hidden: true, 6 | run: async (toolbox) => { 7 | const { print } = toolbox; 8 | print.printCommands(toolbox, ["web", "deployment"]); 9 | }, 10 | }; 11 | 12 | module.exports = command; 13 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/dev/dev.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from "@webstone/gluegun"; 2 | import { determinePackageManager } from "../../../helpers"; 3 | 4 | const command: GluegunCommand = { 5 | alias: ["d"], 6 | description: "Start the web dev server", 7 | run: async (toolbox) => { 8 | const { print, system } = toolbox; 9 | 10 | print.info(`Starting web service...`); 11 | await system.exec(`${determinePackageManager()} run dev`, { 12 | stdout: "inherit", 13 | }); 14 | }, 15 | }; 16 | 17 | module.exports = command; 18 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/route/create.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GluegunCommand, 3 | GluegunFilesystem, 4 | GluegunParameters, 5 | GluegunPrint, 6 | GluegunPrompt, 7 | GluegunStrings, 8 | } from "@webstone/gluegun"; 9 | 10 | interface PromptForParameters { 11 | filesystem?: GluegunFilesystem; 12 | parameters?: GluegunParameters; 13 | print?: GluegunPrint; 14 | prompt?: GluegunPrompt; 15 | strings?: GluegunStrings; 16 | } 17 | 18 | interface PromptForNameParameters extends PromptForParameters { 19 | parameters: GluegunParameters; 20 | prompt: GluegunPrompt; 21 | } 22 | 23 | interface PromptForTypesParameters extends PromptForParameters { 24 | filesystem: GluegunFilesystem; 25 | parameters: GluegunParameters; 26 | print: GluegunPrint; 27 | prompt: GluegunPrompt; 28 | strings: GluegunStrings; 29 | } 30 | 31 | const promptForName = async ({ 32 | parameters, 33 | prompt, 34 | }: PromptForNameParameters) => { 35 | if (parameters.first) { 36 | return parameters.first; 37 | } 38 | 39 | const result = await prompt.ask({ 40 | type: "input", 41 | name: "name", 42 | message: `What's the page name?`, 43 | }); 44 | if (result && result.name) return result.name; 45 | 46 | throw new Error("Please provide a page name."); 47 | }; 48 | 49 | const promptForFileTypes = async ( 50 | { filesystem, parameters, print, prompt, strings }: PromptForTypesParameters, 51 | name: string, 52 | ) => { 53 | // huge thank you to the Svelte language tools repo! https://github.com/sveltejs/language-tools/tree/master/packages/svelte-vscode/src/sveltekit/generateFiles/templates 54 | const allChoices = [ 55 | "+page.svelte", 56 | "+page.ts", 57 | "+page.server.ts", 58 | "+layout.svelte", 59 | "+layout.ts", 60 | "+layout.server.ts", 61 | "+server.ts", 62 | "+error.svelte", 63 | ]; 64 | const directoryName = strings.kebabCase(name || ""); 65 | const existingFiles = filesystem.list(`src/routes/${directoryName}`); 66 | 67 | if (parameters.options.types) { 68 | const types: string[] = parameters.options.types.split(","); 69 | const validTypes: string[] = []; 70 | const invalidTypes: string[] = []; 71 | types.forEach((type) => { 72 | const typeTrimmed = type.trim(); 73 | if (allChoices.includes(typeTrimmed)) { 74 | validTypes.push(typeTrimmed); 75 | } else { 76 | invalidTypes.push(typeTrimmed); 77 | } 78 | }); 79 | 80 | if (invalidTypes.length > 0) { 81 | print.warning(`Ignoring invalid types: ${invalidTypes.join(", ")}`); 82 | } 83 | 84 | if (validTypes.length > 0) { 85 | return { directoryName, existingFiles, types: validTypes }; 86 | } 87 | } 88 | 89 | const choices = allChoices.filter((choice) => { 90 | return !existingFiles?.includes(choice); 91 | }); 92 | 93 | const typesPrompt = (await prompt.ask({ 94 | type: "multiselect", 95 | name: "types", 96 | message: "What types of page do you want to create?", 97 | choices: choices, 98 | })) as { 99 | types: string[]; 100 | }; 101 | 102 | if (typesPrompt.types.length < 1) { 103 | throw new Error("You must select at least one type of page to create"); 104 | } 105 | 106 | return { directoryName, existingFiles, types: typesPrompt.types }; 107 | }; 108 | 109 | const command: GluegunCommand = { 110 | alias: ["c"], 111 | description: "Create a new web page", 112 | run: async (toolbox) => { 113 | const { parameters, print, prompt, strings, template, filesystem } = 114 | toolbox; 115 | 116 | try { 117 | // eslint-disable-next-line no-var 118 | var name = await promptForName({ prompt, parameters }); 119 | } catch (error: unknown) { 120 | print.error(error instanceof Error ? error.message : String(error)); 121 | return; 122 | } 123 | 124 | try { 125 | // eslint-disable-next-line no-var 126 | var { directoryName, existingFiles, types } = await promptForFileTypes( 127 | { filesystem, parameters, print, prompt, strings }, 128 | name, 129 | ); 130 | } catch (error: unknown) { 131 | print.error(error instanceof Error ? error.message : String(error)); 132 | return; 133 | } 134 | 135 | for (const type of types) { 136 | const target = `src/routes/${directoryName}/${type}`; 137 | const spinner = print.spin(`Creating file "${target}"...`); 138 | 139 | if (existingFiles?.includes(type)) { 140 | spinner.fail(`File "${target}" already exists, skipping...`); 141 | continue; 142 | } 143 | 144 | await template.generate({ 145 | template: `web/route/create/${type}.ejs`, 146 | target, 147 | props: { 148 | name, 149 | }, 150 | }); 151 | spinner.succeed(`File created at: ${target}`); 152 | } 153 | }, 154 | }; 155 | 156 | module.exports = command; 157 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/route/delete.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GluegunCommand, 3 | GluegunFilesystem, 4 | GluegunPrint, 5 | } from "@webstone/gluegun"; 6 | 7 | const command: GluegunCommand = { 8 | alias: ["d"], 9 | description: "Delete a route", 10 | run: async (toolbox) => { 11 | const { filesystem, parameters, print, prompt, strings } = toolbox; 12 | 13 | let name = parameters.first; 14 | if (!name) { 15 | const result = await prompt.ask({ 16 | type: "input", 17 | name: "name", 18 | message: `What's the route to delete?`, 19 | }); 20 | if (result && result.name) name = result.name; 21 | } 22 | 23 | if (!name) { 24 | print.error("Please provide a route, e.g. 'about-us'."); 25 | return; 26 | } 27 | 28 | const filename = strings.kebabCase(name); 29 | 30 | const existingFiles = filesystem.list(`src/routes/${name}`); 31 | 32 | if (!existingFiles || existingFiles.length < 1) { 33 | deleteAll(filename, print, filesystem); 34 | return; 35 | } 36 | 37 | const filesPrompt = (await prompt.ask({ 38 | type: "multiselect", 39 | name: "files", 40 | message: "What files do you want to delete?", 41 | choices: existingFiles, 42 | })) as { 43 | files: string[]; 44 | }; 45 | 46 | if (filesPrompt.files.length == existingFiles.length) { 47 | deleteAll(filename, print, filesystem); 48 | return; 49 | } 50 | 51 | for (const file of filesPrompt.files) { 52 | print.newline(); 53 | const target = `src/routes/${filename}/${file}`; 54 | const spinner = print.spin(`Removing file "${target}"...`); 55 | filesystem.remove(target); 56 | spinner.succeed(`File deleted at: ${target}`); 57 | } 58 | }, 59 | }; 60 | 61 | module.exports = command; 62 | 63 | function deleteAll( 64 | filename: string, 65 | print: GluegunPrint, 66 | filesystem: GluegunFilesystem, 67 | ) { 68 | print.newline(); 69 | const target = `src/routes/${filename}`; 70 | const spinner = print.spin(`Removing route "${target}"...`); 71 | filesystem.remove(target); 72 | spinner.succeed(`Route deleted at: ${target}`); 73 | return; 74 | } 75 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/route/route.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from "@webstone/gluegun"; 2 | 3 | const command: GluegunCommand = { 4 | alias: "p", 5 | description: "No-op entry for the `webstone web route` command.", 6 | hidden: true, 7 | run: async (toolbox) => { 8 | const { print } = toolbox; 9 | print.printCommands(toolbox, ["web", "route"]); 10 | }, 11 | }; 12 | 13 | module.exports = command; 14 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/svelte-add.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from "@webstone/gluegun"; 2 | 3 | /** 4 | * This is a wrapper for `npx svelte-add@latest `. 5 | * 6 | * @see https://github.com/svelte-add/svelte-add 7 | */ 8 | const command: GluegunCommand = { 9 | alias: ["sa"], 10 | description: 11 | "Delegates to `svelte-add` for integrations such as Tailwind CSS, mdsvex, etc", 12 | run: async (toolbox) => { 13 | const { print, parameters, system } = toolbox; 14 | 15 | if (parameters.first === undefined) { 16 | print.error( 17 | "Please provide an integration, i.e. `[npx|pnpm|yarn] webstone web svelte-add tailwindcss`. For available integrations, please visit https://github.com/svelte-add/svelte-add.", 18 | ); 19 | return; 20 | } 21 | 22 | const svelteAddCommand = `npx svelte-add@latest ${parameters.first}`; 23 | print.highlight(`Delegating to svelte-add: ${svelteAddCommand}`); 24 | const result = await system.run(svelteAddCommand, { 25 | cwd: "./", 26 | }); 27 | print.info(result); 28 | }, 29 | }; 30 | 31 | module.exports = command; 32 | -------------------------------------------------------------------------------- /packages/cli/src/commands/web/web.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from "@webstone/gluegun"; 2 | 3 | const command: GluegunCommand = { 4 | alias: "w", 5 | description: "No-op entry for the `webstone` command.", 6 | hidden: true, 7 | run: async (toolbox) => { 8 | const { print } = toolbox; 9 | print.printCommands(toolbox, ["web"]); 10 | }, 11 | }; 12 | 13 | module.exports = command; 14 | -------------------------------------------------------------------------------- /packages/cli/src/commands/webstone.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from "@webstone/gluegun"; 2 | 3 | const command: GluegunCommand = { 4 | description: "No-op entry for the webstone CLI", 5 | run: async (toolbox) => { 6 | const { print } = toolbox; 7 | 8 | print.printHelp(toolbox); 9 | }, 10 | }; 11 | 12 | export default command; 13 | -------------------------------------------------------------------------------- /packages/cli/src/extensions/package-manager.ts: -------------------------------------------------------------------------------- 1 | import type { GluegunToolbox } from "@webstone/gluegun"; 2 | 3 | import add from "../toolbox/package-manager/add"; 4 | import remove from "../toolbox/package-manager/remove"; 5 | 6 | export default (toolbox: GluegunToolbox) => { 7 | toolbox.pkgMngr = { 8 | add, 9 | remove, 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/cli/src/extensions/web.ts: -------------------------------------------------------------------------------- 1 | import type { GluegunToolbox } from "@webstone/gluegun"; 2 | import type { WebToolbox } from "./web/types"; 3 | 4 | import webConfigureDeployment from "./web/deployment/configure"; 5 | 6 | export interface WebstoneToolbox extends GluegunToolbox, WebToolbox {} 7 | 8 | export default (toolbox: GluegunToolbox) => { 9 | const webToolbox: WebToolbox = { 10 | web: { 11 | configure: { 12 | deployment: webConfigureDeployment, 13 | }, 14 | }, 15 | }; 16 | toolbox.web = { 17 | ...webToolbox.web, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/cli/src/extensions/web/deployment/configure.ts: -------------------------------------------------------------------------------- 1 | import { availableAdapters } from "../../../toolbox/web/deployment/configure/adapters"; 2 | import getInstalledAdapter from "../../../toolbox/web/deployment/configure/get-installed-adapter"; 3 | import installAdapter from "../../../toolbox/web/deployment/configure/install-adapter"; 4 | import isAnyAdapterInstalled from "../../../toolbox/web/deployment/configure/is-any-adapter-installed"; 5 | import removeAdapter from "../../../toolbox/web/deployment/configure/remove-adapter"; 6 | 7 | export default { 8 | availableAdapters, 9 | getInstalledAdapter, 10 | installAdapter, 11 | isAnyAdapterInstalled, 12 | removeAdapter, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/cli/src/extensions/web/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Adapter } from "../../toolbox/web/deployment/configure/types"; 2 | 3 | export interface WebToolbox { 4 | web: { 5 | configure: { 6 | deployment: { 7 | availableAdapters: Adapter[]; 8 | getInstalledAdapter: () => Adapter; 9 | installAdapter: (adapter: Adapter) => Promise; 10 | isAnyAdapterInstalled: () => boolean; 11 | removeAdapter: (adapter: Adapter) => Promise; 12 | }; 13 | }; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/cli/src/helpers.ts: -------------------------------------------------------------------------------- 1 | type PackageManagers = "npm" | "pnpm" | "yarn"; 2 | 3 | /** 4 | * If you modify this function, also change it in 5 | * webstone/packages/create-webstone-app/src/helpers.ts 6 | */ 7 | export const determinePackageManager = (): PackageManagers => { 8 | if (process.env.npm_execpath?.endsWith("npm-cli.js")) { 9 | return "npm"; 10 | } else if (process.env.npm_execpath?.endsWith("pnpm.cjs")) { 11 | return "pnpm"; 12 | } else if (process.env.npm_execpath?.endsWith("yarn.js")) { 13 | return "yarn"; 14 | } else { 15 | console.warn( 16 | `Could not determine package manager based on "process.env.npm_execpath". Value for env variable: ${process.env.npm_execpath}. Using npm as a fallback. Please report this as a bug, we'd love to make it more resilient.`, 17 | ); 18 | return "npm"; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/cli/src/templates/web/api/create/[uid].ejs: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@sveltejs/kit'; 2 | 3 | // DELETE {{apiPath}}/:uid 4 | export const DELETE: RequestHandler = async (event) => { 5 | return new Response(`${event.request.method.toUpperCase()} ${event.url.pathname} => Ok.`, { 6 | status: 200 7 | }); 8 | }; 9 | 10 | // PATCH {{apiPath}}/:uid 11 | export const PATCH: RequestHandler = async (event) => { 12 | return new Response(`${event.request.method.toUpperCase()} ${event.url.pathname} => Ok.`, { 13 | status: 200 14 | }); 15 | }; 16 | 17 | // PUT {{apiPath}}/:uid 18 | export const PUT: RequestHandler = async (event) => { 19 | return new Response(`${event.request.method.toUpperCase()} ${event.url.pathname} => Ok.`, { 20 | status: 200 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/cli/src/templates/web/api/create/index.ejs: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@sveltejs/kit'; 2 | 3 | // GET {{apiPath}} 4 | export const GET: RequestHandler = async (event) => { 5 | return new Response(`${event.request.method.toUpperCase()} ${event.url.pathname} => Ok.`, { 6 | status: 200 7 | }); 8 | }; 9 | 10 | // POST {{apiPath}} 11 | export const POST: RequestHandler = async (event) => { 12 | return new Response(`${event.request.method.toUpperCase()} ${event.url.pathname} => Ok.`, { 13 | status: 200 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/cli/src/templates/web/api/create/lib-types.d.ejs: -------------------------------------------------------------------------------- 1 | /** 2 | * Can be made globally available by placing this 3 | * inside `app.d.ts` and removing `export` keyword 4 | */ 5 | export interface Locals { 6 | // TODO: Provide your app's data. See https://kit.svelte.dev/docs#hooks-handle for examples. 7 | } 8 | -------------------------------------------------------------------------------- /packages/cli/src/templates/web/route/create/+error.svelte.ejs: -------------------------------------------------------------------------------- 1 | 4 |

{$page.status}: {$page.error?.message}

-------------------------------------------------------------------------------- /packages/cli/src/templates/web/route/create/+layout.server.ts.ejs: -------------------------------------------------------------------------------- 1 | import type { LayoutServerLoad } from './$types'; 2 | export const load: LayoutServerLoad = async () => { 3 | return {}; 4 | }; -------------------------------------------------------------------------------- /packages/cli/src/templates/web/route/create/+layout.svelte.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/src/templates/web/route/create/+layout.ts.ejs: -------------------------------------------------------------------------------- 1 | import type { LayoutLoad } from './$types'; 2 | export const load: LayoutLoad = async () => { 3 | return {}; 4 | }; -------------------------------------------------------------------------------- /packages/cli/src/templates/web/route/create/+page.server.ts.ejs: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | export const load: PageServerLoad = async () => { 3 | return {}; 4 | }; -------------------------------------------------------------------------------- /packages/cli/src/templates/web/route/create/+page.svelte.ejs: -------------------------------------------------------------------------------- 1 | 6 | 7 |

<%= props.name %>

8 | -------------------------------------------------------------------------------- /packages/cli/src/templates/web/route/create/+page.ts.ejs: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types'; 2 | export const load: PageLoad = async () => { 3 | return {}; 4 | }; -------------------------------------------------------------------------------- /packages/cli/src/templates/web/route/create/+server.ts.ejs: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | export const GET: RequestHandler = async () => { 3 | return new Response(); 4 | }; -------------------------------------------------------------------------------- /packages/cli/src/toolbox/package-manager/add.ts: -------------------------------------------------------------------------------- 1 | import type { PkgMngrOptions } from "./types"; 2 | 3 | import { system } from "@webstone/gluegun"; 4 | import { determinePackageManager } from "../../helpers"; 5 | 6 | export default async (packageName: string, options?: PkgMngrOptions) => { 7 | const dev = options?.dev ? "-D " : ""; 8 | const stdout = await system.run( 9 | `${determinePackageManager()} add ${dev}${packageName}`, 10 | { 11 | cwd: options?.dir || ".", 12 | }, 13 | ); 14 | return { success: true, stdout }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/cli/src/toolbox/package-manager/remove.ts: -------------------------------------------------------------------------------- 1 | import type { PkgMngrOptions } from "./types"; 2 | 3 | import { system } from "@webstone/gluegun"; 4 | import { determinePackageManager } from "../../helpers"; 5 | 6 | export default async (packageName: string, options?: PkgMngrOptions) => { 7 | const dev = options?.dev ? "-D " : ""; 8 | const stdout = await system.run( 9 | `${determinePackageManager()} remove ${dev}${packageName}`, 10 | { 11 | cwd: options?.dir || "", 12 | }, 13 | ); 14 | return { success: true, stdout }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/cli/src/toolbox/package-manager/types.d.ts: -------------------------------------------------------------------------------- 1 | export type PkgMngrOptions = { 2 | dev: boolean; 3 | dir: string; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/cli/src/toolbox/web/deployment/configure/adapters.ts: -------------------------------------------------------------------------------- 1 | import { Adapter } from "./types"; 2 | 3 | export const availableAdapters: Adapter[] = [ 4 | { 5 | // Enable & test when https://github.com/architect/architect/issues/1236 is resolved. 6 | // identifier: "adapter-begin", 7 | // name: "Begin / Architect", 8 | // npmPackage: "@architect/sveltekit-adapter", 9 | // nextStepsDocsLink: "https://github.com/architect/sveltekit-adapter" 10 | // }, { 11 | identifier: "adapter-cloudflare-workers", 12 | name: "Cloudflare Workers", 13 | npmPackage: "@sveltejs/adapter-cloudflare-workers", 14 | npmPackageVersion: "@next", 15 | nextStepsDocsLink: 16 | "https://github.com/sveltejs/kit/tree/master/packages/adapter-cloudflare-workers", 17 | }, 18 | { 19 | identifier: "adapter-netlify", 20 | name: "Netlify", 21 | npmPackage: "@sveltejs/adapter-netlify", 22 | npmPackageVersion: "@next", 23 | nextStepsDocsLink: 24 | "https://github.com/sveltejs/kit/tree/master/packages/adapter-netlify", 25 | }, 26 | { 27 | identifier: "adapter-node", 28 | name: "Node.js", 29 | npmPackage: "@sveltejs/adapter-node", 30 | npmPackageVersion: "@next", 31 | nextStepsDocsLink: 32 | "https://github.com/sveltejs/kit/tree/master/packages/adapter-node", 33 | }, 34 | { 35 | identifier: "adapter-static", 36 | name: "Static", 37 | npmPackage: "@sveltejs/adapter-static", 38 | npmPackageVersion: "@next", 39 | nextStepsDocsLink: 40 | "https://github.com/sveltejs/kit/tree/master/packages/adapter-static", 41 | }, 42 | { 43 | identifier: "adapter-vercel", 44 | name: "Vercel", 45 | npmPackage: "@sveltejs/adapter-vercel", 46 | npmPackageVersion: "@next", 47 | nextStepsDocsLink: 48 | "https://github.com/sveltejs/kit/tree/master/packages/adapter-vercel", 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /packages/cli/src/toolbox/web/deployment/configure/get-installed-adapter.ts: -------------------------------------------------------------------------------- 1 | import { filesystem } from "@webstone/gluegun"; 2 | import { availableAdapters } from "./adapters"; 3 | import { Adapter } from "./types"; 4 | 5 | export default () => { 6 | const webPackageJson = filesystem.read("./package.json", "json"); 7 | const availableAdapterNpmPackages = availableAdapters.map( 8 | (adapter) => adapter.npmPackage, 9 | ); 10 | 11 | const installedAdapterNpmPackage = 12 | Object.keys(webPackageJson.devDependencies).find((devDependency) => 13 | availableAdapterNpmPackages.includes(devDependency), 14 | ) || ""; 15 | 16 | return availableAdapters.find( 17 | (adapter) => adapter.npmPackage === installedAdapterNpmPackage, 18 | ) as Adapter; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/cli/src/toolbox/web/deployment/configure/install-adapter.ts: -------------------------------------------------------------------------------- 1 | import type { Adapter } from "./types"; 2 | 3 | import { print } from "@webstone/gluegun"; 4 | import add from "../../../package-manager/add"; 5 | 6 | export default async (adapter: Adapter) => { 7 | const spinner = print.spin( 8 | `Adding adapter package "${adapter.npmPackage}${ 9 | adapter.npmPackageVersion || "" 10 | }"...`, 11 | ); 12 | await add(`${adapter.npmPackage}${adapter.npmPackageVersion || ""}`, { 13 | dev: true, 14 | dir: "./", 15 | }); 16 | spinner.succeed(`Adapter added: ${adapter.npmPackage}`); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/cli/src/toolbox/web/deployment/configure/is-any-adapter-installed.ts: -------------------------------------------------------------------------------- 1 | import getInstalledAdapter from "./get-installed-adapter"; 2 | 3 | export default () => !!getInstalledAdapter(); 4 | -------------------------------------------------------------------------------- /packages/cli/src/toolbox/web/deployment/configure/remove-adapter.ts: -------------------------------------------------------------------------------- 1 | import type { Adapter } from "./types"; 2 | 3 | import { print } from "@webstone/gluegun"; 4 | import remove from "../../../package-manager/remove"; 5 | 6 | export default async (adapter: Adapter) => { 7 | const spinner = print.spin( 8 | `Removing adapter package "${adapter.npmPackage}"...`, 9 | ); 10 | await remove(adapter.npmPackage, { 11 | dev: true, 12 | dir: "./", 13 | }); 14 | spinner.succeed(`Adapter removed: ${adapter.npmPackage}`); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/cli/src/toolbox/web/deployment/configure/types.d.ts: -------------------------------------------------------------------------------- 1 | export type Adapter = { 2 | identifier: 3 | | "adapter-begin" 4 | | "adapter-cloudflare-workers" 5 | | "adapter-netlify" 6 | | "adapter-node" 7 | | "adapter-static" 8 | | "adapter-vercel"; 9 | name: string; 10 | npmPackage: string; 11 | npmPackageVersion?: string; 12 | nextStepsDocsLink: string; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "declarationDir": "dist/types", 5 | "experimentalDecorators": true, 6 | "lib": [ 7 | "es2015", 8 | "scripthost", 9 | "es2015.promise", 10 | "es2015.generator", 11 | "es2015.iterable", 12 | "dom" 13 | ], 14 | "outDir": "dist", 15 | "target": "ES6" 16 | }, 17 | "include": ["src/**/*"], 18 | "extends": "../../tsconfig.json" 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @webstone/core 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [e4954e6] 8 | - Updated dependencies [cbcfb98] 9 | - @webstone/cli@0.13.0 10 | 11 | ## 0.1.0 12 | 13 | ### Minor Changes 14 | 15 | - 23575e3: Release the webstone-plugin-request-logger package. 16 | 17 | ### Patch Changes 18 | 19 | - Updated dependencies [770120d] 20 | - Updated dependencies [23575e3] 21 | - @webstone/cli@0.12.0 22 | 23 | ## 0.0.23 24 | 25 | ### Patch Changes 26 | 27 | - Updated dependencies [5df3a8c] 28 | - @webstone/cli@0.11.1 29 | 30 | ## 0.0.22 31 | 32 | ### Patch Changes 33 | 34 | - b5f7835: use tsup to bundle package for esm and cjs 35 | - Updated dependencies [369bc5b] 36 | - Updated dependencies [ab65f24] 37 | - @webstone/cli@0.11.0 38 | 39 | ## 0.0.21 40 | 41 | ### Patch Changes 42 | 43 | - Updated dependencies [f7a4f1f] 44 | - @webstone/cli@0.10.0 45 | 46 | ## 0.0.20 47 | 48 | ### Patch Changes 49 | 50 | - Updated dependencies [412041a] 51 | - @webstone/cli@0.9.0 52 | 53 | ## 0.0.19 54 | 55 | ### Patch Changes 56 | 57 | - 0665efd: Use create-svelte to instantiate the Webstone app. 58 | - Updated dependencies [0665efd] 59 | - @webstone/cli@0.8.3 60 | 61 | ## 0.0.18 62 | 63 | ### Patch Changes 64 | 65 | - Updated dependencies [9354f10] 66 | - Updated dependencies [4fee40d] 67 | - @webstone/cli@0.8.2 68 | 69 | ## 0.0.17 70 | 71 | ### Patch Changes 72 | 73 | - Updated dependencies [710aa9f] 74 | - @webstone/cli@0.8.1 75 | 76 | ## 0.0.16 77 | 78 | ### Patch Changes 79 | 80 | - Updated dependencies [19c0161] 81 | - @webstone/cli@0.8.0 82 | 83 | ## 0.0.15 84 | 85 | ### Patch Changes 86 | 87 | - Updated dependencies [49c17e6] 88 | - @webstone/cli@0.7.0 89 | 90 | ## 0.0.14 91 | 92 | ### Patch Changes 93 | 94 | - Updated dependencies [0b94911] 95 | - @webstone/cli@0.6.0 96 | 97 | ## 0.0.13 98 | 99 | ### Patch Changes 100 | 101 | - Updated dependencies [a6fe827] 102 | - @webstone/cli@0.5.1 103 | 104 | ## 0.0.12 105 | 106 | ### Patch Changes 107 | 108 | - Updated dependencies [2ecb412] 109 | - @webstone/cli@0.5.0 110 | 111 | ## 0.0.11 112 | 113 | ### Patch Changes 114 | 115 | - Updated dependencies [1edfbe5] 116 | - @webstone/cli@0.4.0 117 | 118 | ## 0.0.10 119 | 120 | ### Patch Changes 121 | 122 | - Updated dependencies [db93524] 123 | - @webstone/cli@0.3.0 124 | 125 | ## 0.0.9 126 | 127 | ### Patch Changes 128 | 129 | - Updated dependencies [69ea7d6] 130 | - @webstone/cli@0.2.0 131 | 132 | ## 0.0.8 133 | 134 | ### Patch Changes 135 | 136 | - Updated dependencies [7e5f53a] 137 | - @webstone/cli@0.1.0 138 | 139 | ## 0.0.7 140 | 141 | ### Patch Changes 142 | 143 | - Updated dependencies [1fcc6e1] 144 | - @webstone/cli@0.0.7 145 | 146 | ## 0.0.6 147 | 148 | ### Patch Changes 149 | 150 | - Updated dependencies [217041f] 151 | - @webstone/cli@0.0.6 152 | 153 | ## 0.0.5 154 | 155 | ### Patch Changes 156 | 157 | - Updated dependencies [42c0d8b] 158 | - @webstone/cli@0.0.5 159 | 160 | ## 0.0.4 161 | 162 | ### Patch Changes 163 | 164 | - Updated dependencies [4ff8dfa] 165 | - @webstone/cli@0.0.4 166 | 167 | ## 0.0.3 168 | 169 | ### Patch Changes 170 | 171 | - 46c5149: Use workspace:\* rather than workspace:^ for dependencies. 172 | - 1102a5a: Add an initial CLI skeleton file. 173 | - Updated dependencies [1102a5a] 174 | - @webstone/cli@0.0.3 175 | 176 | ## 0.0.2 177 | 178 | ### Patch Changes 179 | 180 | - 8662f47: Initialize core and cli packages; add core dependency to the app template. 181 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Webstone Core 2 | 3 | TODO: This is the only Webstone dev dependency required by the `create-webstone-app/template/package.json` file. 4 | 5 | TODO: How do we deal with extensibility so that anyone can write Webstone modules and provide them to Webstone projects? 6 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webstone/core", 3 | "version": "0.1.1", 4 | "main": "./dist/index.js", 5 | "module": "./dist/index.mjs", 6 | "types": "./dist/index.d.ts", 7 | "exports": { 8 | ".": { 9 | "require": "./dist/index.js", 10 | "import": "./dist/index.mjs", 11 | "types": "./dist/index.d.ts" 12 | } 13 | }, 14 | "files": [ 15 | "./dist" 16 | ], 17 | "scripts": { 18 | "build": "pnpm clean && pnpm compile", 19 | "clean": "rm -rf ./dist", 20 | "compile": "tsup src/index.ts --format cjs,esm --dts --clean", 21 | "dev": "npm run compile -- --watch src", 22 | "test": "pnpm test:unit", 23 | "test:unit": "c8 --all --include=src --reporter=html pnpm test:unit:only", 24 | "test:unit:only": "NODE_OPTIONS='--loader tsx' uvu tests" 25 | }, 26 | "author": "Mike Nikles, @mikenikles", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@webstone/cli": "workspace:*" 30 | }, 31 | "devDependencies": { 32 | "tsup": "^6.7.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export const sayHi = () => { 2 | console.log("Hi from core src/index.ts"); 3 | }; 4 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declarationDir": "dist/types", 4 | "outDir": "dist" 5 | }, 6 | "include": ["src/**/*"], 7 | "extends": "../../tsconfig.json" 8 | } 9 | -------------------------------------------------------------------------------- /packages/create-webstone-app/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # create-webstone-app 2 | 3 | ## 0.9.0 4 | 5 | ### Minor Changes 6 | 7 | - 28e2615: Group plugin commands under a "plugins" sub-command. 8 | 9 | ## 0.8.0 10 | 11 | ### Minor Changes 12 | 13 | - 01f44e5: refactor create-webstone-app to use new flow & new plugin template 14 | 15 | ## 0.7.0 16 | 17 | ### Minor Changes 18 | 19 | - a37c021: Make sure the plugin's web's prepublishOnly script runs pnpm package. 20 | 21 | ## 0.6.0 22 | 23 | ### Minor Changes 24 | 25 | - 23575e3: Release the webstone-plugin-request-logger package. 26 | 27 | ### Patch Changes 28 | 29 | - 0dfcb69: Update dependencies. 30 | 31 | ## 0.5.2 32 | 33 | ### Patch Changes 34 | 35 | - a119120: Add README.md templates for plugin cli and web directories. 36 | 37 | ## 0.5.1 38 | 39 | ### Patch Changes 40 | 41 | - 5df3a8c: change naming scheme for plugins 42 | 43 | ## 0.5.0 44 | 45 | ### Minor Changes 46 | 47 | - b3774cb: introduce new plugin-structure 48 | - ab65f24: set up plugins for webstone 49 | 50 | ### Patch Changes 51 | 52 | - 14975da: Apply missed code from the new plugin structure PR. 53 | - dfdf5d4: Warn instead of force-emptying the app directory if it's not empty. 54 | - e2f49b9: bump sveltekit-version to stable 55 | 56 | ## 0.4.1 57 | 58 | ### Patch Changes 59 | 60 | - 241c146: Use the user's preferred package manager (npm, pnpm, or yarn). 61 | 62 | ## 0.4.0 63 | 64 | ### Minor Changes 65 | 66 | - f7a4f1f: remove monorepo approach 67 | 68 | ## 0.3.0 69 | 70 | ### Minor Changes 71 | 72 | - 4556a68: Switch from tsc to esbuild for the create-webstone-app. 73 | - 412041a: Implement latest Sveltekit breaking changes 74 | 75 | ## 0.2.6 76 | 77 | ### Patch Changes 78 | 79 | - 3070a28: resolve prompt, created by enquirer, correctly 80 | - 77db1c4: fix issue with clearing directory while creating an app 81 | 82 | ## 0.2.5 83 | 84 | ### Patch Changes 85 | 86 | - 7abe8c3: Run a system requirements check before creating a Webstone app. 87 | 88 | ## 0.2.4 89 | 90 | ### Patch Changes 91 | 92 | - 0bd597b: Move prod dependencies out of devDependencies. 93 | 94 | ## 0.2.3 95 | 96 | ### Patch Changes 97 | 98 | - ac92329: Use listr2 to create the Webstone project. 99 | 100 | ## 0.2.2 101 | 102 | ### Patch Changes 103 | 104 | - 0665efd: Use create-svelte to instantiate the Webstone app. 105 | 106 | ## 0.2.1 107 | 108 | ### Patch Changes 109 | 110 | - 9354f10: Update Webstone to work with the latest version of SvelteKit. 111 | - 4fee40d: Migrate from Cypress to Playwright. 112 | 113 | ## 0.2.0 114 | 115 | ### Minor Changes 116 | 117 | - 0b94911: Write unit tests for the `create-webstone-app` package. 118 | 119 | ## 0.1.0 120 | 121 | ### Minor Changes 122 | 123 | - 7e5f53a: Add a `webstone dev` command to start services. 124 | 125 | ## 0.0.10 126 | 127 | ### Patch Changes 128 | 129 | - 4ff8dfa: Introduce the Webstone CLI skeleton. 130 | 131 | ## 0.0.9 132 | 133 | ### Patch Changes 134 | 135 | - cca9b7f: Remove the node_modules filter when copying the template. 136 | 137 | ## 0.0.8 138 | 139 | ### Patch Changes 140 | 141 | - 7b194c0: Use pnpm in the template to simplify commands. 142 | - 58cbcba: Ignore node_modules when copying the template. 143 | 144 | ## 0.0.7 145 | 146 | ### Patch Changes 147 | 148 | - 133b0c5: Always use the latest version of the CLI in the template. 149 | 150 | ## 0.0.6 151 | 152 | ### Patch Changes 153 | 154 | - 2d40fd8: Auto-update the template dependencies. 155 | 156 | ## 0.0.5 157 | 158 | ### Patch Changes 159 | 160 | - 1102a5a: Add an initial CLI skeleton file. 161 | 162 | ## 0.0.4 163 | 164 | ### Patch Changes 165 | 166 | - 8662f47: Initialize core and cli packages; add core dependency to the app template. 167 | 168 | ## 0.0.3 169 | 170 | ### Patch Changes 171 | 172 | - c3b9e1f: Fix the missing web template directory. 173 | 174 | ## 0.0.2 175 | 176 | ### Patch Changes 177 | 178 | - 075bc3d: Add an ASCII art banner to the README. 179 | -------------------------------------------------------------------------------- /packages/create-webstone-app/README.md: -------------------------------------------------------------------------------- 1 | # Webstone 2 | 3 | ``` 4 | ▄ ▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄ ▄ ▄▄▄▄▄▄▄ 5 | █ █ ▄ █ █ █ ▄ █ █ █ █ █ █ █ █ 6 | █ ██ ██ █ ▄▄▄█ █▄█ █ ▄▄▄▄▄█▄ ▄█ ▄ █ █▄█ █ ▄▄▄█ 7 | █ █ █▄▄▄█ █ █▄▄▄▄▄ █ █ █ █ █ █ █ █▄▄▄ 8 | █ █ ▄▄▄█ ▄ ██▄▄▄▄▄ █ █ █ █ █▄█ █ ▄ █ ▄▄▄█ 9 | █ ▄ █ █▄▄▄█ █▄█ █▄▄▄▄▄█ █ █ █ █ █ █ █ █ █▄▄▄ 10 | █▄▄█ █▄▄█▄▄▄▄▄▄▄█▄▄▄▄▄▄▄█▄▄▄▄▄▄▄█ █▄▄▄█ █▄▄▄▄▄▄▄█▄█ █▄▄█▄▄▄▄▄▄▄█ 11 | 12 | ``` 13 | 14 | Start your next web application with Webstone and configure it as you go. 15 | 16 | ## Getting Started 17 | 18 | ```sh 19 | # Create a new project 20 | [npm|pnpm|yarn] init webstone-app [my-app] 21 | 22 | # Start the dev servers 23 | cd $_ 24 | [npx|pnpm|yarn] ws dev 25 | ``` 26 | 27 | ## Access the web application 28 | 29 | The web application is available on port 3000, e.g. http://localhost:3000. 30 | 31 | ## Learn more 32 | 33 | Please refer to the documentation at https://github.com/WebstoneHQ/webstone for details of what is included, a list of all available CLI commands and how Webstone lets you focus on what matters, rather than dealing with boilerplate code. 34 | -------------------------------------------------------------------------------- /packages/create-webstone-app/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import("./dist/bin.js"); 3 | -------------------------------------------------------------------------------- /packages/create-webstone-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-webstone-app", 3 | "version": "0.9.0", 4 | "description": "Start your next web application with Webstone and configure it as you go.", 5 | "keywords": [ 6 | "svelte", 7 | "sveltekit", 8 | "boilerplate", 9 | "starterkit", 10 | "web app", 11 | "graphql" 12 | ], 13 | "license": "MIT", 14 | "author": "Mike Nikles, @mikenikles", 15 | "type": "module", 16 | "exports": { 17 | ".": "./dist/index.js" 18 | }, 19 | "types": "./dist/index.d.ts", 20 | "bin": "./bin.js", 21 | "scripts": { 22 | "build": "node ./scripts/build.js build", 23 | "clean": "rm -fr ./dist", 24 | "dev": "node ./scripts/build.js dev", 25 | "prepare": "pnpm build", 26 | "prepublishOnly": "pnpm build", 27 | "test": "node --loader tsx --test src/*.spec.ts", 28 | "test:unit": "c8 --all --include=src --reporter=html pnpm test:unit:only", 29 | "test:unit:only": "NODE_OPTIONS='--loader tsx' uvu tests" 30 | }, 31 | "dependencies": { 32 | "chalk": "5.3.0", 33 | "create-svelte": "^5.0.5", 34 | "enquirer": "2.4.1", 35 | "fs-extra": "11.1.1", 36 | "ts-deepmerge": "^6.2.0" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "20.4.10", 40 | "tsup": "^6.7.0", 41 | "type-fest": "^4.2.0", 42 | "typescript": "^5.1.6" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/create-webstone-app/scripts/build.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, build } from "tsup"; 2 | 3 | /** 4 | * @type {"build" | "dev"} 5 | */ 6 | 7 | const mode = process.argv[2]; 8 | 9 | // check if mode is valid 10 | if (!["build", "dev"].includes(mode)) { 11 | console.log("Usage: node ./scripts/build.js build|dev"); 12 | process.exit(1); 13 | } 14 | 15 | const config = defineConfig({ 16 | entry: ["src/index.ts", "src/bin.ts"], 17 | target: "esnext", 18 | format: "esm", 19 | treeshake: true, 20 | minify: true, 21 | dts: true, 22 | watch: mode === "dev" ? ["src"] : false, 23 | }); 24 | 25 | await build(config); 26 | -------------------------------------------------------------------------------- /packages/create-webstone-app/src/bin.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import chalk from "chalk"; 3 | import enquirer from "enquirer"; 4 | import { displayNextSteps, displayWelcome } from "./helpers"; 5 | import { createWebstone } from "./index"; 6 | import { parseArgs } from "node:util"; 7 | 8 | // argparsing 9 | const { values: argValues } = parseArgs({ 10 | allowPositionals: true, 11 | options: { 12 | type: { 13 | type: "string", 14 | alias: "t", 15 | }, 16 | "extend-cli": { 17 | type: "boolean", 18 | }, 19 | }, 20 | }); 21 | 22 | let extendCLI = argValues["extend-cli"] || false; 23 | let type: "app" | "plugin" | null = null; 24 | if (argValues.type && ["app", "plugin"].includes(argValues.type)) { 25 | type = argValues.type as "app" | "plugin"; 26 | } 27 | 28 | const { version } = JSON.parse( 29 | fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"), 30 | ); 31 | 32 | let cwd = 33 | process.argv[2] && !process.argv[2].startsWith("--") ? process.argv[2] : "."; 34 | 35 | displayWelcome(); 36 | console.log(chalk.bold(`create-webstone v${version}`)); 37 | 38 | if (cwd === ".") { 39 | const dir: { dir: string } = await enquirer.prompt({ 40 | type: "text", 41 | name: "dir", 42 | message: 43 | "Where should we create your project? (Hit enter to use current directory)", 44 | initial: ".", 45 | }); 46 | 47 | cwd = dir.dir; 48 | } 49 | 50 | if (fs.existsSync(cwd)) { 51 | if (fs.readdirSync(cwd).length > 0) { 52 | const forceCreate: { forceCreate: boolean } = await enquirer.prompt({ 53 | type: "confirm", 54 | name: "forceCreate", 55 | message: `The ./${cwd} directory is not empty. Do you want to continue?`, 56 | initial: false, 57 | }); 58 | 59 | if (!forceCreate.forceCreate) { 60 | console.log( 61 | chalk.red( 62 | `Exiting, please empty the ./${cwd} directory or choose a different one to create the Webstone app.`, 63 | ), 64 | ); 65 | process.exit(1); 66 | } 67 | } 68 | } 69 | 70 | if (!type) { 71 | const promptType: { type: "Webstone App" | "Webstone Plugin" } = 72 | await enquirer.prompt({ 73 | type: "select", 74 | name: "type", 75 | message: "What type of Webstone project do you want to create?", 76 | choices: ["Webstone App", "Webstone Plugin"], 77 | }); 78 | 79 | const typeMap = { 80 | "Webstone App": "app", 81 | "Webstone Plugin": "plugin", 82 | } as const; 83 | 84 | type = typeMap[promptType.type]; 85 | } 86 | 87 | if (type === "plugin" && !extendCLI) { 88 | const extendCLIAnswer: { extendCLI: boolean } = await enquirer.prompt({ 89 | type: "confirm", 90 | name: "extendCLI", 91 | message: "Does your plugin extend the Webstone CLI?", 92 | initial: false, 93 | }); 94 | extendCLI = extendCLIAnswer.extendCLI; 95 | } 96 | 97 | await createWebstone(cwd, { type, extendCLI }); 98 | 99 | displayNextSteps(cwd); 100 | -------------------------------------------------------------------------------- /packages/create-webstone-app/src/functions.spec.ts: -------------------------------------------------------------------------------- 1 | import { it, describe } from "node:test"; 2 | import { deepEqual, deepStrictEqual, strictEqual } from "node:assert"; 3 | import path from "node:path"; 4 | import { 5 | sortKeys, 6 | toValidPackageName, 7 | getAppName, 8 | getRawAppName, 9 | updatePackageJSON, 10 | copyBuildScript, 11 | copyCLIExtension, 12 | } from "./functions"; 13 | import { WebstoneAppType } from "../types"; 14 | import fs from "fs-extra"; 15 | 16 | describe("sortKeys", () => { 17 | it("should sort keys of an object in ascending order", () => { 18 | const input = { c: 3, a: 1, b: 2 }; 19 | const expectedOutput = { a: 1, b: 2, c: 3 }; 20 | const result = sortKeys(input); 21 | deepStrictEqual(result, expectedOutput); 22 | }); 23 | 24 | it("should handle an empty object", () => { 25 | const input = {}; 26 | const expectedOutput = {}; 27 | const result = sortKeys(input); 28 | deepStrictEqual(result, expectedOutput); 29 | }); 30 | 31 | it("should handle an object with a single key", () => { 32 | const input = { z: 26 }; 33 | const expectedOutput = { z: 26 }; 34 | const result = sortKeys(input); 35 | deepStrictEqual(result, expectedOutput); 36 | }); 37 | 38 | it("should handle an object with string and number keys", () => { 39 | const input = { b: 2, a: 1, 2: "value", 1: "value" }; 40 | const expectedOutput = { "1": "value", "2": "value", a: 1, b: 2 }; 41 | const result = sortKeys(input); 42 | deepStrictEqual(result, expectedOutput); 43 | }); 44 | 45 | it("should handle an object with nested objects", () => { 46 | const input = { b: { c: 3, a: 1 }, a: 2 }; 47 | const expectedOutput = { a: 2, b: { a: 1, c: 3 } }; 48 | const result = sortKeys(input); 49 | deepStrictEqual(result, expectedOutput); 50 | }); 51 | }); 52 | 53 | describe("toValidPackageName", () => { 54 | it("should convert a valid name to a valid package name", () => { 55 | const input = " My Package Name "; 56 | const expectedOutput = "my-package-name"; 57 | const result = toValidPackageName(input); 58 | deepStrictEqual(result, expectedOutput); 59 | }); 60 | 61 | it("should handle a name with leading periods and underscores", () => { 62 | const input = ".__my_package_name"; 63 | const expectedOutput = "-my-package-name"; 64 | const result = toValidPackageName(input); 65 | deepStrictEqual(result, expectedOutput); 66 | }); 67 | 68 | it("should handle a name with non-alphanumeric characters", () => { 69 | const input = "!@#$My_Package%^Name*"; 70 | const expectedOutput = "-my-package-name-"; 71 | const result = toValidPackageName(input); 72 | deepStrictEqual(result, expectedOutput); 73 | }); 74 | 75 | it("should handle an already valid package name", () => { 76 | const input = "already-valid-package-name"; 77 | const expectedOutput = "already-valid-package-name"; 78 | const result = toValidPackageName(input); 79 | deepStrictEqual(result, expectedOutput); 80 | }); 81 | 82 | it("should handle an empty name", () => { 83 | const input = ""; 84 | const expectedOutput = ""; 85 | const result = toValidPackageName(input); 86 | deepStrictEqual(result, expectedOutput); 87 | }); 88 | }); 89 | 90 | describe("getAppName", () => { 91 | it("should generate a valid app name for app type", () => { 92 | const cwd = "dummy"; 93 | const type: WebstoneAppType = "app"; 94 | const expectedOutput = "dummy"; 95 | const result = getAppName(cwd, type); 96 | deepStrictEqual(result, expectedOutput); 97 | }); 98 | 99 | it("should generate a valid app name for plugin type", () => { 100 | const cwd = "dummy"; 101 | const type: WebstoneAppType = "plugin"; 102 | const expectedOutput = "webstone-plugin-dummy"; 103 | const result = getAppName(cwd, type); 104 | deepStrictEqual(result, expectedOutput); 105 | }); 106 | 107 | it("should handle leading periods and underscores in app name", () => { 108 | const cwd = "__my_app"; 109 | const type: WebstoneAppType = "app"; 110 | const expectedOutput = "-my-app"; 111 | const result = getAppName(cwd, type); 112 | deepStrictEqual(result, expectedOutput); 113 | }); 114 | 115 | it("should handle plugin type and special characters", () => { 116 | const cwd = "/dummy"; 117 | const type = "plugin"; 118 | const expectedOutput = "webstone-plugin--dummy"; 119 | const result = getAppName(cwd, type); 120 | deepStrictEqual(result, expectedOutput); 121 | }); 122 | }); 123 | 124 | describe("getRawAppName", () => { 125 | it('should return the current dir when cwd is "."', (ctx) => { 126 | const cwdMock = ctx.mock.fn(process.cwd, () => "dummy-dir"); 127 | ctx.mock.method(process, "cwd", cwdMock); 128 | const cwd = "."; 129 | const expectedOutput = "dummy-dir"; 130 | const result = getRawAppName(cwd); 131 | deepStrictEqual(result, expectedOutput); 132 | }); 133 | 134 | it("should handle a single-character cwd", () => { 135 | const cwd = "/a"; 136 | const expectedOutput = "/a"; 137 | const result = getRawAppName(cwd); 138 | deepStrictEqual(result, expectedOutput); 139 | }); 140 | }); 141 | 142 | describe("updatePackageJson", () => { 143 | it("should update the package.json properly for type app", (ctx) => { 144 | const mockReadJsonSync = ctx.mock.fn(fs.readJSONSync, () => { 145 | return {}; 146 | }); 147 | const mockWriteJsonSync = ctx.mock.fn(fs.writeJsonSync, () => {}); 148 | const mockPathResolve = ctx.mock.fn(path.resolve, (cwd: string) => cwd); 149 | ctx.mock.method(fs, "readJSONSync", mockReadJsonSync); 150 | ctx.mock.method(fs, "writeJsonSync", mockWriteJsonSync); 151 | ctx.mock.method(path, "resolve", mockPathResolve); 152 | const cwd = "dummy"; 153 | updatePackageJSON(cwd, { extendCLI: false, type: "app" }); 154 | strictEqual(mockReadJsonSync.mock.callCount(), 1); 155 | strictEqual(mockWriteJsonSync.mock.callCount(), 1); 156 | deepStrictEqual(mockWriteJsonSync.mock.calls[0].arguments, [ 157 | "dummy/package.json", 158 | { 159 | devDependencies: { 160 | "@webstone/cli": "^0.12.0", 161 | }, 162 | }, 163 | { 164 | encoding: "utf-8", 165 | spaces: "\t", 166 | }, 167 | ]); 168 | }); 169 | 170 | it("should update the package.json properly for type plugin that does extend the cli", (ctx) => { 171 | const mockReadJsonSync = ctx.mock.fn(fs.readJSONSync, () => { 172 | return {}; 173 | }); 174 | const mockWriteJsonSync = ctx.mock.fn(fs.writeJsonSync, () => {}); 175 | const mockPathResolve = ctx.mock.fn(path.resolve, (cwd: string) => cwd); 176 | ctx.mock.method(fs, "readJSONSync", mockReadJsonSync); 177 | ctx.mock.method(fs, "writeJsonSync", mockWriteJsonSync); 178 | ctx.mock.method(path, "resolve", mockPathResolve); 179 | const cwd = "dummy"; 180 | updatePackageJSON(cwd, { extendCLI: false, type: "plugin" }); 181 | strictEqual(mockReadJsonSync.mock.callCount(), 1); 182 | strictEqual(mockWriteJsonSync.mock.callCount(), 1); 183 | deepStrictEqual(mockWriteJsonSync.mock.calls[0].arguments, [ 184 | "dummy/package.json", 185 | {}, 186 | { 187 | encoding: "utf-8", 188 | spaces: "\t", 189 | }, 190 | ]); 191 | }); 192 | 193 | it("should update the package.json properly for type plugin that doesn't extend the cli", (ctx) => { 194 | const mockReadJsonSync = ctx.mock.fn(fs.readJSONSync, () => { 195 | return {}; 196 | }); 197 | const mockWriteJsonSync = ctx.mock.fn(fs.writeJsonSync, () => {}); 198 | const mockPathResolve = ctx.mock.fn(path.resolve, (cwd: string) => cwd); 199 | ctx.mock.method(fs, "readJSONSync", mockReadJsonSync); 200 | ctx.mock.method(fs, "writeJsonSync", mockWriteJsonSync); 201 | ctx.mock.method(path, "resolve", mockPathResolve); 202 | const cwd = "dummy"; 203 | updatePackageJSON(cwd, { extendCLI: true, type: "plugin" }); 204 | strictEqual(mockReadJsonSync.mock.callCount(), 1); 205 | strictEqual(mockWriteJsonSync.mock.callCount(), 1); 206 | deepStrictEqual(mockWriteJsonSync.mock.calls[0].arguments, [ 207 | "dummy/package.json", 208 | { 209 | scripts: { 210 | build: 211 | "npm run clean:build && npm run cli:build && npm run web:build", 212 | "clean:build": "rimraf ./dist", 213 | "cli:build": 214 | "node scripts/build-cli.js build && npm run templates:copy", 215 | "cli:dev": "node scripts/build-cli.js dev", 216 | dev: "npm run clean:build && npm-run-all --parallel cli:dev web:dev templates:dev", 217 | package: "svelte-kit sync && svelte-package -o ./dist/web && publint", 218 | "templates:copy": "cp -a ./src/cli/templates ./dist/cli", 219 | "templates:dev": 220 | "nodemon --watch src/cli/templates --ext '.ejs' --exec 'npm run templates:copy'", 221 | "web:build": "vite build && npm run package", 222 | "web:dev": "vite dev", 223 | }, 224 | devDependencies: { 225 | "@webstone/gluegun": "0.0.5", 226 | "fs-jetpack": "^5.1.0", 227 | nodemon: "^3.0.1", 228 | "npm-run-all": "^4.1.5", 229 | rimraf: "^3.0.2", 230 | tsup: "^6.7.0", 231 | }, 232 | svelte: "./dist/web/index.js", 233 | types: "./dist/web/index.d.ts", 234 | exports: { 235 | ".": { 236 | types: "./dist/web/index.d.ts", 237 | svelte: "./dist/web/index.js", 238 | }, 239 | }, 240 | }, 241 | { 242 | encoding: "utf-8", 243 | spaces: "\t", 244 | }, 245 | ]); 246 | }); 247 | }); 248 | 249 | describe("copyBuildScript", () => { 250 | it("should copy the build script properly", (ctx) => { 251 | const mockCopySync = ctx.mock.fn(fs.copySync, () => {}); 252 | ctx.mock.method(fs, "copySync", mockCopySync); 253 | copyBuildScript("dummy"); 254 | strictEqual(mockCopySync.mock.callCount(), 1); 255 | deepEqual( 256 | // TODO find a solution for import.meta.url 257 | mockCopySync.mock.calls[0].arguments[1], 258 | "dummy/scripts/build-cli.js", 259 | ); 260 | }); 261 | }); 262 | 263 | describe("copyCLIExtension", () => { 264 | it("should copy the CLI extension properly", (ctx) => { 265 | const mockCopySync = ctx.mock.fn(fs.copySync, () => {}); 266 | ctx.mock.method(fs, "copySync", mockCopySync); 267 | copyCLIExtension("dummy"); 268 | strictEqual(mockCopySync.mock.callCount(), 3); 269 | 270 | //Commands 271 | deepStrictEqual( 272 | mockCopySync.mock.calls[0].arguments[1], 273 | "dummy/src/cli/commands/plugins/dummy/hello-world.ts", 274 | ); 275 | 276 | // Extensions 277 | deepStrictEqual( 278 | mockCopySync.mock.calls[1].arguments[1], 279 | "dummy/src/cli/extensions/hello-world.ts", 280 | ); 281 | 282 | // Templates 283 | deepStrictEqual( 284 | mockCopySync.mock.calls[2].arguments[1], 285 | "dummy/src/cli/templates/template.ejs", 286 | ); 287 | }); 288 | }); 289 | -------------------------------------------------------------------------------- /packages/create-webstone-app/src/functions.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import path from "node:path"; 3 | import { PackageJson } from "type-fest"; 4 | import { appPackageJson, pluginPackageJson } from "./package"; 5 | import merge from "ts-deepmerge"; 6 | import { create } from "create-svelte"; 7 | import { WebstoneAppType } from "../types"; 8 | 9 | export function getRawAppName(cwd: string) { 10 | let appName: string = cwd; 11 | if (cwd === ".") { 12 | appName = process.cwd().split("/").pop() || "webstone-project"; 13 | } 14 | return appName; 15 | } 16 | 17 | export function getAppName(cwd: string, type: WebstoneAppType) { 18 | let appName = getRawAppName(cwd); 19 | if (type === "plugin") { 20 | appName = `webstone-plugin-${appName}`; 21 | return toValidPackageName(appName); 22 | } 23 | return toValidPackageName(appName); 24 | } 25 | 26 | export function toValidPackageName(name: string) { 27 | return name 28 | .trim() 29 | .toLowerCase() 30 | .replace(/\s+/g, "-") 31 | .replace(/^[._]/, "") 32 | .replace(/[^a-z0-9~.-]+/g, "-"); 33 | } 34 | 35 | export function createBaseApp( 36 | cwd: string, 37 | options: { type: WebstoneAppType; appName: string }, 38 | ) { 39 | const { appName, type } = options; 40 | create(cwd, { 41 | name: appName, 42 | template: type === "app" ? "skeleton" : "skeletonlib", 43 | types: "typescript", 44 | eslint: true, 45 | prettier: true, 46 | playwright: true, 47 | vitest: true, 48 | }); 49 | } 50 | 51 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 52 | export function sortKeys(obj: any) { 53 | if (typeof obj !== "object" || obj === null) 54 | throw new Error("Invalid object"); 55 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 56 | const sortedObj: Record = {}; 57 | Object.keys(obj) 58 | .sort() 59 | .forEach((key) => { 60 | sortedObj[key] = obj[key]; 61 | }); 62 | return sortedObj; 63 | } 64 | 65 | export function updatePackageJSON( 66 | cwd: string, 67 | options: { type: WebstoneAppType; extendCLI: boolean }, 68 | ) { 69 | const { type, extendCLI } = options; 70 | const pkg: PackageJson = fs.readJSONSync(path.resolve(`${cwd}/package.json`)); 71 | let newPkg: PackageJson = pkg; 72 | if (type === "app") { 73 | newPkg = deepMergeWithSortedKeys(pkg, appPackageJson); 74 | } 75 | if (type === "plugin" && extendCLI) { 76 | newPkg = deepMergeWithSortedKeys(pkg, pluginPackageJson); 77 | } 78 | 79 | fs.writeJsonSync(path.resolve(`${cwd}/package.json`), newPkg, { 80 | encoding: "utf-8", 81 | spaces: "\t", 82 | }); 83 | } 84 | 85 | function deepMergeWithSortedKeys( 86 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 87 | target: Record, 88 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 89 | source: Record, 90 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 91 | ): Record { 92 | const merged = merge(target, source); 93 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 94 | function recursiveSort(obj: Record): Record { 95 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 96 | const sortedObj: Record = {}; 97 | 98 | Object.keys(obj).forEach((key) => { 99 | if (["dependencies", "devDependencies", "scripts"].includes(key)) { 100 | sortedObj[key] = sortKeys(obj[key]); 101 | } else { 102 | sortedObj[key] = obj[key]; 103 | } 104 | }); 105 | 106 | return sortedObj; 107 | } 108 | 109 | return recursiveSort(merged); 110 | } 111 | 112 | export function copyBuildScript(cwd: string) { 113 | fs.copySync( 114 | new URL("../templates/plugin-structure/build-cli.js", import.meta.url) 115 | .pathname, 116 | `${cwd}/scripts/build-cli.js`, 117 | ); 118 | } 119 | 120 | export function copyCLIExtension(cwd: string) { 121 | // copy command 122 | fs.copySync( 123 | new URL("../templates/plugin-structure/command.ts", import.meta.url) 124 | .pathname, 125 | `${cwd}/src/cli/commands/plugins/${getRawAppName(cwd)}/hello-world.ts`, 126 | ); 127 | 128 | //copy extension 129 | fs.copySync( 130 | new URL("../templates/plugin-structure/extension.ts", import.meta.url) 131 | .pathname, 132 | `${cwd}/src/cli/extensions/hello-world.ts`, 133 | ); 134 | 135 | // copy templates 136 | fs.copySync( 137 | new URL("../templates/plugin-structure/template.ejs", import.meta.url) 138 | .pathname, 139 | `${cwd}/src/cli/templates/template.ejs`, 140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /packages/create-webstone-app/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | export const displayWelcome = () => { 4 | // https://textfancy.com/ascii-art/ 5 | console.log(` 6 | ▄ ▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄ ▄ ▄▄▄▄▄▄▄ 7 | █ █ ▄ █ █ █ ▄ █ █ █ █ █ █ █ █ 8 | █ ██ ██ █ ▄▄▄█ █▄█ █ ▄▄▄▄▄█▄ ▄█ ▄ █ █▄█ █ ▄▄▄█ 9 | █ █ █▄▄▄█ █ █▄▄▄▄▄ █ █ █ █ █ █ █ █▄▄▄ 10 | █ █ ▄▄▄█ ▄ ██▄▄▄▄▄ █ █ █ █ █▄█ █ ▄ █ ▄▄▄█ 11 | █ ▄ █ █▄▄▄█ █▄█ █▄▄▄▄▄█ █ █ █ █ █ █ █ █ █▄▄▄ 12 | █▄▄█ █▄▄█▄▄▄▄▄▄▄█▄▄▄▄▄▄▄█▄▄▄▄▄▄▄█ █▄▄▄█ █▄▄▄▄▄▄▄█▄█ █▄▄█▄▄▄▄▄▄▄█ 13 | 14 | `); 15 | }; 16 | 17 | export const displayNextSteps = async (cwd: string) => { 18 | console.log(` 19 | =================================================== 20 | Congratulations 🎉! Your Webstone project is ready. 21 | 22 | To contribute: https://github.com/WebstoneHQ/webstone 23 | To chat & get in touch: https://discord.gg/NJRm6eRs 24 | 25 | 26 | Thank you for your interest in Webstone, We'd love to hear your feedback 🙏. 27 | 28 | Next steps: 29 | 30 | - ${chalk.bold(chalk.cyan(`cd ${cwd.split("/").pop()}`))} 31 | - ${chalk.bold.cyan("npm install")} (or pnpm install, etc...) 32 | `); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/create-webstone-app/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CreateWebstoneOptions } from "../types"; 2 | import { 3 | copyBuildScript, 4 | copyCLIExtension, 5 | createBaseApp, 6 | getAppName, 7 | updatePackageJSON, 8 | } from "./functions"; 9 | 10 | export async function createWebstone( 11 | cwd: string, 12 | options: CreateWebstoneOptions, 13 | ) { 14 | const { type, extendCLI } = options; 15 | const appName = getAppName(cwd, type); 16 | createBaseApp(cwd, { type, appName }); 17 | updatePackageJSON(cwd, { type, extendCLI }); 18 | if (type === "plugin" && extendCLI) { 19 | copyBuildScript(cwd); 20 | copyCLIExtension(cwd); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/create-webstone-app/src/package.ts: -------------------------------------------------------------------------------- 1 | import { PackageJson } from "type-fest"; 2 | 3 | export const appPackageJson: PackageJson = { 4 | devDependencies: { 5 | "@webstone/cli": "^0.12.0", 6 | }, 7 | }; 8 | 9 | export const pluginPackageJson: PackageJson = { 10 | scripts: { 11 | build: "npm run clean:build && npm run cli:build && npm run web:build", 12 | "clean:build": "rimraf ./dist", 13 | "cli:build": "node scripts/build-cli.js build && npm run templates:copy", 14 | "cli:dev": "node scripts/build-cli.js dev", 15 | dev: "npm run clean:build && npm-run-all --parallel cli:dev web:dev templates:dev", 16 | package: "svelte-kit sync && svelte-package -o ./dist/web && publint", 17 | "templates:copy": "cp -a ./src/cli/templates ./dist/cli", 18 | "templates:dev": 19 | "nodemon --watch src/cli/templates --ext '.ejs' --exec 'npm run templates:copy'", 20 | "web:build": "vite build && npm run package", 21 | "web:dev": "vite dev", 22 | }, 23 | devDependencies: { 24 | "@webstone/gluegun": "0.0.5", 25 | "fs-jetpack": "^5.1.0", 26 | nodemon: "^3.0.1", 27 | "npm-run-all": "^4.1.5", 28 | rimraf: "^3.0.2", 29 | tsup: "^6.7.0", 30 | }, 31 | svelte: "./dist/web/index.js", 32 | types: "./dist/web/index.d.ts", 33 | exports: { 34 | ".": { 35 | types: "./dist/web/index.d.ts", 36 | svelte: "./dist/web/index.js", 37 | }, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/create-webstone-app/templates/plugin-structure/build-cli.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, build } from "tsup"; 2 | import jetpack from "fs-jetpack"; 3 | 4 | /** 5 | * @type {"build" | "dev"} 6 | */ 7 | const mode = process.argv[2]; 8 | 9 | // check if mode is valid 10 | if (!["build", "dev"].includes(mode)) { 11 | console.log("Usage: node ./scripts/build-cli.js build|dev"); 12 | process.exit(1); 13 | } 14 | 15 | const config = defineConfig({ 16 | entry: ["src/cli/**/*.ts"], 17 | outDir: "dist/cli", 18 | splitting: true, 19 | target: "es6", 20 | format: "cjs", 21 | clean: true, 22 | treeshake: true, 23 | tsconfig: "./tsconfig.json", 24 | bundle: false, 25 | minify: true, 26 | watch: mode === "dev" ? ["src/cli"] : false, 27 | }); 28 | 29 | const tsFiles = jetpack 30 | .cwd("src/cli") 31 | .find({ matching: "*.ts", recursive: true }); 32 | const tsFilesCount = tsFiles.length; 33 | if (tsFilesCount === 0) { 34 | console.log("No files to build"); 35 | process.exit(1); 36 | } 37 | await build(config); 38 | -------------------------------------------------------------------------------- /packages/create-webstone-app/templates/plugin-structure/command.ts: -------------------------------------------------------------------------------- 1 | import type { GluegunCommand } from "@webstone/gluegun"; 2 | 3 | const command: GluegunCommand = { 4 | name: "hello", 5 | alias: ["h"], 6 | description: "Hello World Command", 7 | hidden: false, 8 | dashed: false, 9 | run: async (toolbox) => { 10 | const { print } = toolbox; 11 | 12 | print.info(`Hello World`); 13 | }, 14 | }; 15 | 16 | export default command; 17 | -------------------------------------------------------------------------------- /packages/create-webstone-app/templates/plugin-structure/extension.ts: -------------------------------------------------------------------------------- 1 | import type { GluegunToolbox } from "@webstone/gluegun"; 2 | 3 | const extension = (toolbox: GluegunToolbox) => { 4 | const { print } = toolbox; 5 | 6 | toolbox.sayhello = () => { 7 | print.info("Hello from an extension!"); 8 | }; 9 | }; 10 | 11 | export default extension; 12 | -------------------------------------------------------------------------------- /packages/create-webstone-app/templates/plugin-structure/template.ejs: -------------------------------------------------------------------------------- 1 |

Template

2 | -------------------------------------------------------------------------------- /packages/create-webstone-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "outDir": "./dist", 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "skipLibCheck": true 8 | }, 9 | "include": ["src/**/*"], 10 | "extends": "../../tsconfig.json" 11 | } 12 | -------------------------------------------------------------------------------- /packages/create-webstone-app/types/index.ts: -------------------------------------------------------------------------------- 1 | export type CreateWebstoneOptions = { 2 | type: WebstoneAppType; 3 | extendCLI: boolean; 4 | }; 5 | 6 | export type WebstoneAppType = "app" | "plugin"; 7 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:svelte/recommended', 7 | 'prettier' 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint'], 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020, 14 | extraFileExtensions: ['.svelte'] 15 | }, 16 | env: { 17 | browser: true, 18 | es2017: true, 19 | node: true 20 | }, 21 | overrides: [ 22 | { 23 | files: ['*.svelte'], 24 | parser: 'svelte-eslint-parser', 25 | parserOptions: { 26 | parser: '@typescript-eslint/parser' 27 | } 28 | } 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # webstone-plugin-request-logger 2 | 3 | ## 0.3.4 4 | 5 | ### Patch Changes 6 | 7 | - Revert v0.3.3, there is no need to export types manually. ([`77dfdf9`](https://github.com/WebstoneHQ/webstone-plugins/commit/77dfdf9949cdf0c03f59f0eaaa571ade25a3e877)) 8 | 9 | ## 0.3.3 10 | 11 | ### Patch Changes 12 | 13 | - Export the `RequestLogger` type from the generated index.d.ts file. ([#412](https://github.com/WebstoneHQ/webstone-plugins/pull/412)) 14 | 15 | ## 0.3.2 16 | 17 | ### Patch Changes 18 | 19 | - Export the `RequestLogger` Typescript interface. ([#410](https://github.com/WebstoneHQ/webstone-plugins/pull/410)) 20 | 21 | ## 0.3.1 22 | 23 | ### Patch Changes 24 | 25 | - 5c020b9: Populate the `README.md` with installation and usage instructions. 26 | 27 | ## 0.3.0 28 | 29 | ### Minor Changes 30 | 31 | - c2f2e29: Migrate to the new plugin structure which combines the CLI & web parts. 32 | - 28e2615: Migrate to the new plugins CLI command structure. 33 | 34 | ## 0.2.0 35 | 36 | ### Minor Changes 37 | 38 | - cbcfb98: follow-up fixes for new plugin structure 39 | 40 | ## 0.1.0 41 | 42 | ### Minor Changes 43 | 44 | - 39a1c96: Initial release with the new plugin structure. 45 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/README.md: -------------------------------------------------------------------------------- 1 | # `request-logger` Webstone Plugin 2 | 3 | ## About 4 | 5 | This library provides an `event.locals.logger` object for [SvelteKit](https://kit.svelte.dev/) applications. It can be accessed wherever `event.locals` is available. This includes hooks (`handle`, and `handleError`), server-only `load` functions, and `+server.js` files. See [the docs](https://kit.svelte.dev/docs/types#app-locals) for potential future locations. 6 | 7 | ## Installation 8 | 9 | Install this plugin with the following command: 10 | 11 | ```shell 12 | npm install -D webstone-plugin-request-logger 13 | ``` 14 | 15 | ## Usage 16 | 17 | Create a `src/hooks.server.js` file, if it doesn't already exist, with the following code: 18 | 19 | ```js 20 | import { sequence } from '@sveltejs/kit/hooks'; 21 | import { addRequestLogger, logRequestDetails } from 'webstone-plugin-request-logger'; 22 | 23 | /** @type {import('@sveltejs/kit').Handle} */ 24 | export const handle = sequence(addRequestLogger, logRequestDetails /*, yourHandlers*/); 25 | ``` 26 | 27 | ### `locals.logger` 28 | 29 | The logger is available wherever `event.locals` is available ([docs](https://kit.svelte.dev/docs/types#app-locals)). For example, to use it in a server-only `load` function or an action within a `+page.server.js` file: 30 | 31 | ```ts 32 | import type { Actions, PageServerLoad } from './$types'; 33 | 34 | import { fail } from '@sveltejs/kit'; 35 | 36 | export const load = (async ({ locals }) => { 37 | locals.logger.debug('Fetching posts from database...'); 38 | 39 | try { 40 | // TODO: Fetch posts from database 41 | locals.logger.debug(`Successfully fetched ${posts.length} posts`); 42 | } catch (error) { 43 | locals.logger.error(`Failed to fetched ${posts.length} posts`, error); 44 | } 45 | 46 | return {}; 47 | }) satisfies PageServerLoad; 48 | 49 | export const actions = { 50 | create_post: async (event) => { 51 | const data = await event.request.formData(); 52 | const author = data.get('author')?.toString() || ''; 53 | const content = data.get('content')?.toString() || ''; 54 | event.locals.logger.log('Creating a new post...', { 55 | author, 56 | content 57 | }); 58 | 59 | try { 60 | // TODO: Persist the new post in the database 61 | event.locals.logger.debug('Successfully created the post'); 62 | } catch (error) { 63 | event.locals.logger.error('Could not persist the post', { error }); 64 | return fail(500, { 65 | id: 'form_create_post', 66 | reason: 'unexpected' 67 | }); 68 | } 69 | } 70 | } satisfies Actions; 71 | ``` 72 | 73 | ## Feedback / bugs / ideas 74 | 75 | If you have any feedback, run into bugs, or have ideas on how to improve thie plugin, please [open a GitHub issue](https://github.com/WebstoneHQ/webstone-plugins/issues/new?labels=plugin:request-logger). 76 | 77 | ## Learn more about Webstone Plugins 78 | 79 | This plugin is part of a wider ecosystem called [Webstone Plugins](https://github.com/WebstoneHQ/webstone-plugins). 80 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webstone-plugin-request-logger", 3 | "version": "0.3.4", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/WebstoneHQ/webstone-plugins", 7 | "directory": "packages/plugin-request-logger" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/WebstoneHQ/webstone-plugins/issues?q=is%3Aopen+is%3Aissue+label%3Aplugin%3Arequest-logger" 11 | }, 12 | "scripts": { 13 | "build": "npm run clean:build && npm run cli:build && npm run web:build", 14 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 15 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 16 | "clean:build": "rimraf ./dist", 17 | "cli:build": "node scripts/build-cli.js build && npm run templates:copy", 18 | "cli:dev": "node scripts/build-cli.js dev", 19 | "dev": "npm run clean:build && npm-run-all --parallel cli:dev web:dev templates:dev", 20 | "format": "prettier --plugin-search-dir . --write .", 21 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 22 | "package": "svelte-kit sync && svelte-package -o ./dist/web && publint", 23 | "prepublishOnly": "npm run package", 24 | "preview": "vite preview", 25 | "templates:copy": "cp -a ./src/cli/templates ./dist/cli", 26 | "templates:dev": "nodemon --watch src/cli/templates --ext '.ejs' --exec 'npm run templates:copy'", 27 | "test": "npm run test:integration && npm run test:unit", 28 | "test:integration": "playwright test", 29 | "test:unit": "vitest", 30 | "web:build": "vite build && npm run package", 31 | "web:dev": "vite dev" 32 | }, 33 | "exports": { 34 | ".": { 35 | "types": "./dist/web/index.d.ts", 36 | "svelte": "./dist/web/index.js" 37 | } 38 | }, 39 | "files": [ 40 | "dist", 41 | "!dist/**/*.test.*", 42 | "!dist/**/*.spec.*" 43 | ], 44 | "peerDependencies": { 45 | "svelte": "^4.0.0" 46 | }, 47 | "devDependencies": { 48 | "@playwright/test": "^1.37.1", 49 | "@sveltejs/adapter-auto": "^2.1.0", 50 | "@sveltejs/kit": "^1.22.6", 51 | "@sveltejs/package": "^2.2.1", 52 | "@typescript-eslint/eslint-plugin": "^6.4.0", 53 | "@typescript-eslint/parser": "^6.4.0", 54 | "@webstone/gluegun": "0.0.5", 55 | "eslint": "^8.47.0", 56 | "eslint-config-prettier": "^9.0.0", 57 | "eslint-plugin-svelte": "^2.32.4", 58 | "fs-jetpack": "^5.1.0", 59 | "nodemon": "^3.0.1", 60 | "npm-run-all": "^4.1.5", 61 | "prettier": "^3.0.2", 62 | "prettier-plugin-svelte": "^3.0.3", 63 | "publint": "^0.2.1", 64 | "rimraf": "^5.0.1", 65 | "svelte": "^4.2.0", 66 | "svelte-check": "^3.5.0", 67 | "tslib": "^2.6.2", 68 | "tsup": "^7.2.0", 69 | "typescript": "^5.1.6", 70 | "vite": "^4.4.9", 71 | "vitest": "^0.34.2" 72 | }, 73 | "svelte": "./dist/web/index.js", 74 | "types": "./dist/web/index.d.ts", 75 | "type": "module" 76 | } 77 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: 'npm run build && npm run preview', 6 | port: 4173 7 | }, 8 | testDir: 'tests', 9 | testMatch: /(.+\.)?(test|spec)\.[jt]s/ 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/scripts/build-cli.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, build } from 'tsup'; 2 | import jetpack from 'fs-jetpack'; 3 | 4 | /** 5 | * @type {"build" | "dev"} 6 | */ 7 | const mode = process.argv[2]; 8 | 9 | // check if mode is valid 10 | if (!['build', 'dev'].includes(mode)) { 11 | console.log('Usage: node ./scripts/build-cli.js build|dev'); 12 | process.exit(1); 13 | } 14 | 15 | const config = defineConfig({ 16 | entry: ['src/cli/**/*.ts'], 17 | outDir: 'dist/cli', 18 | splitting: true, 19 | target: 'es6', 20 | format: 'cjs', 21 | clean: true, 22 | treeshake: true, 23 | tsconfig: './tsconfig.json', 24 | bundle: false, 25 | minify: true, 26 | watch: mode === 'dev' ? ['src/cli'] : false 27 | }); 28 | 29 | const tsFiles = jetpack.cwd('src/cli').find({ matching: '*.ts', recursive: true }); 30 | const tsFilesCount = tsFiles.length; 31 | if (tsFilesCount === 0) { 32 | console.log('No files to build'); 33 | process.exit(1); 34 | } 35 | await build(config); 36 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | import type { RequestLogger } from '$lib/request-logger.ts'; 4 | 5 | declare global { 6 | namespace App { 7 | // interface Error {} 8 | interface Locals { 9 | logger: RequestLogger; 10 | } 11 | // interface PageData {} 12 | // interface Platform {} 13 | } 14 | } 15 | 16 | export {}; 17 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/src/cli/commands/plugins/request-logger/hello-world.ts: -------------------------------------------------------------------------------- 1 | import type { GluegunCommand } from '@webstone/gluegun'; 2 | 3 | const command: GluegunCommand = { 4 | name: 'hello', 5 | alias: ['h'], 6 | description: 'Hello World Command', 7 | hidden: false, 8 | dashed: false, 9 | run: async (toolbox) => { 10 | const { print } = toolbox; 11 | 12 | print.info(`Hello World`); 13 | } 14 | }; 15 | 16 | export default command; 17 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/src/cli/extensions/hello-world.ts: -------------------------------------------------------------------------------- 1 | import type { GluegunToolbox } from '@webstone/gluegun'; 2 | 3 | const extension = (toolbox: GluegunToolbox) => { 4 | const { print } = toolbox; 5 | 6 | toolbox.sayhello = () => { 7 | print.info('Hello from an extension!'); 8 | }; 9 | }; 10 | 11 | export default extension; 12 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/src/cli/templates/template.ejs: -------------------------------------------------------------------------------- 1 |

Template

2 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { sequence } from '@sveltejs/kit/hooks'; 2 | import { addRequestLogger, logRequestDetails } from 'webstone-plugin-request-logger'; 3 | 4 | export const handle = sequence(addRequestLogger, logRequestDetails); 5 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // Reexport your entry components here 2 | export * from './request-logger.js'; 3 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/src/lib/request-logger.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | import { randomUUID } from 'node:crypto'; 3 | 4 | export interface RequestLogger extends Console { 5 | /** 6 | * Overrides the Console's debug function. 7 | * Logs a debug message with optional additional data. 8 | * 9 | * @param message - The debug message to log. 10 | * @param log_data - Optional additional data to include in the log. 11 | */ 12 | debug(message: string, log_data?: Record): void; 13 | 14 | /** 15 | * Overrides the Console's error function. 16 | * Logs an error message with optional additional data. 17 | * 18 | * @param message - The error message to log. 19 | * @param log_data - Optional additional data to include in the log. 20 | */ 21 | error(message: string, log_data?: Record): void; 22 | 23 | /** 24 | * Overrides the Console's info function. 25 | * Logs an info message with optional additional data. 26 | * 27 | * @param message - The info message to log. 28 | * @param log_data - Optional additional data to include in the log. 29 | */ 30 | info(message: string, log_data?: Record): void; 31 | 32 | /** 33 | * Overrides the Console's log function. 34 | * Logs a log message with optional additional data. 35 | * 36 | * @param message - The log message to log. 37 | * @param log_data - Optional additional data to include in the log. 38 | */ 39 | log(message: string, log_data?: Record): void; 40 | 41 | /** 42 | * Overrides the Console's warn function. 43 | * Logs a warning message with optional additional data. 44 | * 45 | * @param message - The warning message to log. 46 | * @param log_data - Optional additional data to include in the log. 47 | */ 48 | warn(message: string, log_data?: Record): void; 49 | } 50 | 51 | export const addRequestLogger = (async ({ event, resolve }) => { 52 | const request_id = randomUUID(); 53 | 54 | const overwrite_methods = ['debug', 'error', 'info', 'log', 'warn'] as const; 55 | const overwrites = overwrite_methods.reduce((result, method) => { 56 | result[method] = (message, log_data = {}) => { 57 | console[method]({ 58 | request_id, 59 | ts: Date.now(), 60 | message, 61 | ...log_data 62 | }); 63 | }; 64 | return result; 65 | }, {} as Console); 66 | 67 | const logger: RequestLogger = { 68 | ...console, 69 | ...overwrites 70 | }; 71 | event.locals.logger = logger; 72 | 73 | const response = await resolve(event); 74 | return response; 75 | }) satisfies Handle; 76 | 77 | export const logRequestDetails = (async ({ event, resolve }) => { 78 | const timeStart = Date.now(); 79 | event.locals.logger.log('Incoming request', { 80 | method: event.request.method, 81 | route: event.route.id, 82 | params: event.params, 83 | cookies: event.cookies.getAll() 84 | }); 85 | 86 | const response = await resolve(event); 87 | event.locals.logger.log('Request processed', { 88 | duration_ms: Date.now() - timeStart 89 | }); 90 | 91 | return response; 92 | }) satisfies Handle; 93 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 |

Welcome to your library project

2 |

Create your package using @sveltejs/package and preview/showcase your work with SvelteKit

3 |

Visit kit.svelte.dev to read the documentation

4 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebstoneHQ/webstone-plugins/5296c66e4191ea32b09b70bf728fc023f0f1ccc4/packages/plugin-request-logger/static/favicon.png -------------------------------------------------------------------------------- /packages/plugin-request-logger/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/kit/vite'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter(), 15 | alias: { 16 | 'webstone-plugin-request-logger': 'src/lib/index.js' 17 | } 18 | } 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/tests/test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('index page has expected h1', async ({ page }) => { 4 | await page.goto('/'); 5 | await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible(); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "NodeNext", 13 | "paths": { 14 | "$lib": ["./src/lib"], 15 | "$lib/*": ["./src/lib/*"], 16 | "webstone-plugin-request-logger": ["./src/lib/index.js"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/plugin-request-logger/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | test: { 7 | include: ['src/**/*.{test,spec}.{js,ts}'] 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # webstone-plugin-trpc-cli 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - e4954e6: bump gluegun version 8 | 9 | ## 0.1.0 10 | 11 | ### Minor Changes 12 | 13 | - 2826fcb: Release a beta version of the tRPC Webstone Plugin. 14 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/README.md: -------------------------------------------------------------------------------- 1 | # tRPC CLI - Webstone Plugin 2 | 3 | ## Installation 4 | 5 | Install this plugin with the following command: 6 | 7 | ``` 8 | npm install -D webstone-plugin-trpc-cli 9 | ``` 10 | 11 | That's it. Your project's `webstone` CLI is now extended with this plugin's functionality. 12 | 13 | ## Usage 14 | 15 | Run `webstone --help` in your project and look for the help output of the `trpc` command. 16 | 17 | ### Initialize tRPC for your project 18 | 19 | To set up the tRPC boilerplate code, such as the router, run the following command at the root of your project: 20 | 21 | ``` 22 | webstone trpc init 23 | ``` 24 | 25 | ## Learn more about Webstone Plugins 26 | 27 | This plugin is part of a wider ecosystem called [Webstone Plugins](https://github.com/WebstoneHQ/webstone). 28 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webstone-plugin-trpc-cli", 3 | "version": "0.2.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "clean": "rimraf build", 8 | "copy-templates": "copyfiles -u 2 ./src/templates/* ./src/templates/**/* build/templates", 9 | "format": "prettier --plugin-search-dir . --write .", 10 | "build": "npm run clean && run-s build:cli copy-templates", 11 | "build:cli": "tsc -p tsconfig.json", 12 | "dev": "pnpm clean && pnpm copy-templates && run-p dev:watch-src dev:watch-templates", 13 | "dev:watch-src": "tsc -p tsconfig.json --watch", 14 | "dev:watch-templates": "npm-watch copy-templates" 15 | }, 16 | "keywords": [ 17 | "webstone", 18 | "plugin", 19 | "template" 20 | ], 21 | "watch": { 22 | "copy-templates": { 23 | "patterns": [ 24 | "src/templates" 25 | ], 26 | "extensions": "ejs" 27 | } 28 | }, 29 | "author": "Cahllagerfeld", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "@webstone/gluegun": "^0.0.5", 33 | "copyfiles": "^2.4.1", 34 | "npm-run-all": "^4.1.5", 35 | "npm-watch": "^0.11.0", 36 | "prettier": "^2.8.8", 37 | "rimraf": "^3.0.2", 38 | "typescript": "^4.7.4" 39 | }, 40 | "dependencies": { 41 | "@mrleebo/prisma-ast": "^0.4.3", 42 | "ts-morph": "^17.0.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/src/commands/trpc/generate.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from '@webstone/gluegun'; 2 | import { populateSubrouterFile, getIDType, prepareApprouter } from '../../lib/generate'; 3 | import { Project } from 'ts-morph'; 4 | import { generateCompleteModelName, generateRouterFilename } from '../../lib/naming'; 5 | import { getAllModels, getModelByName } from '../../lib/parser'; 6 | 7 | const command: GluegunCommand = { 8 | name: 'generate', 9 | alias: ['g'], 10 | description: 'Generate one or more tRPC model(s)', 11 | hidden: false, 12 | dashed: false, 13 | run: async (toolbox) => { 14 | const { print, parameters, prompt, template, strings, filesystem } = toolbox; 15 | try { 16 | let modelNames = 17 | parameters.string && parameters.string?.split(',').map((modelName) => modelName.trim()); 18 | 19 | // check if the trpc plugin is initialized 20 | if (!filesystem.exists('src/lib/server/trpc/router.ts')) { 21 | print.error( 22 | "Please initialize the trpc plugin first, by running ' webstone trpc init'" 23 | ); 24 | return; 25 | } 26 | 27 | if (!modelNames) { 28 | if (!filesystem.exists('prisma/schema.prisma')) { 29 | print.error( 30 | 'Please create a prisma/schema.prisma file. To learn more, see https://www.prisma.io/docs/concepts/components/prisma-schema.' 31 | ); 32 | return; 33 | } 34 | 35 | const prismaModelNames = getAllModels() 36 | .filter((model) => model.type === 'model') 37 | .map((model) => model.type === 'model' && model.name); 38 | 39 | if (!prismaModelNames) { 40 | print.error('No models found in prisma/schema.prisma'); 41 | return; 42 | } 43 | 44 | const result = await prompt.ask({ 45 | type: 'multiselect', 46 | name: 'models', 47 | message: 'Please select your model(s)', 48 | choices: prismaModelNames as string[] 49 | }); 50 | modelNames = result.models as unknown as string[]; 51 | } 52 | 53 | const models = modelNames.map((modelName) => getModelByName(modelName)); 54 | 55 | for (let index = 0; index < models.length; index++) { 56 | const model = models[index]; 57 | const modelName = modelNames[index]; 58 | if (!model) { 59 | print.error(`Model ${modelName} not found, skipping...`); 60 | continue; 61 | } 62 | 63 | if (model.type !== 'model') return; 64 | 65 | const spinner = print.spin(`Generating tRPC for model "${modelName}..."`); 66 | 67 | const idFieldType = getIDType(model); 68 | 69 | const subrouterFilename = generateRouterFilename(model.name); 70 | const subrouterTarget = `src/lib/server/trpc/subrouters/${subrouterFilename}.ts`; 71 | const zodModelName = generateCompleteModelName(model.name); 72 | 73 | await template.generate({ 74 | template: 'subrouter.ejs', 75 | target: subrouterTarget, 76 | props: { 77 | capitalizedPlural: strings.upperFirst(strings.plural(modelName)), 78 | capitalizedSingular: strings.upperFirst(strings.singular(modelName)), 79 | lowercaseSingular: strings.lowerCase(modelName), 80 | zodModelName, 81 | idFieldType 82 | } 83 | }); 84 | 85 | const project = new Project({ 86 | tsConfigFilePath: 'tsconfig.json' 87 | }); 88 | 89 | populateSubrouterFile(project, model); 90 | 91 | const indexRouter = project.getSourceFileOrThrow('src/lib/server/trpc/router.ts'); 92 | 93 | prepareApprouter(indexRouter, `${strings.lowerCase(modelName)}Router`, subrouterFilename); 94 | 95 | indexRouter.formatText({ 96 | tabSize: 1 97 | }); 98 | 99 | project.saveSync(); 100 | 101 | spinner.succeed(`Generated tRPC for model "${modelName}"`); 102 | } 103 | } catch (error) { 104 | print.error(error); 105 | } 106 | } 107 | }; 108 | 109 | export default command; 110 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/src/commands/trpc/init.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from '@webstone/gluegun'; 2 | 3 | const command: GluegunCommand = { 4 | name: 'init', 5 | alias: ['i'], 6 | description: 'Initialize the tRPC Webstone plugin', 7 | hidden: false, 8 | dashed: false, 9 | run: async (toolbox) => { 10 | const { template, filesystem, print } = toolbox; 11 | 12 | const containsTrpc = filesystem.isDirectory(`${process.cwd()}/src/lib/server/trpc`); 13 | if (containsTrpc) { 14 | print.warning('tRPC seems to be already initialized, skipping...'); 15 | return; 16 | } 17 | 18 | const isWebPackageInstalled = filesystem.exists( 19 | `${process.cwd()}/node_modules/webstone-plugin-trpc-web` 20 | ); 21 | if (!isWebPackageInstalled) { 22 | print.error('Webstone tRPC Web package is not installed'); 23 | return; 24 | } 25 | 26 | const spinner = print.spin('Initializing tRPC...'); 27 | await template.generate({ 28 | template: 'base/router.ts.ejs', 29 | target: 'src/lib/server/trpc/router.ts' 30 | }); 31 | await template.generate({ 32 | template: 'base/trpc.ts.ejs', 33 | target: 'src/lib/server/trpc/trpc.ts' 34 | }); 35 | 36 | spinner.succeed('tRPC initialized'); 37 | } 38 | }; 39 | 40 | export default command; 41 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/src/extensions/trpc/hello.ts: -------------------------------------------------------------------------------- 1 | import { GluegunToolbox } from '@webstone/gluegun'; 2 | 3 | const extension = (toolbox: GluegunToolbox) => { 4 | const { print } = toolbox; 5 | 6 | toolbox.sayhello = () => { 7 | print.info('Hello from an extension!'); 8 | }; 9 | }; 10 | 11 | export default extension; 12 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/src/lib/generate.ts: -------------------------------------------------------------------------------- 1 | import { Field, Model, Enum } from '@mrleebo/prisma-ast'; 2 | import { Project, SourceFile, SyntaxKind, VariableDeclarationKind } from 'ts-morph'; 3 | import { 4 | generateCompleteModelName, 5 | generateEnumFilename, 6 | generateEnumName, 7 | generateModelFilename, 8 | generateRouterFilename, 9 | generateZodEnumName, 10 | generateZodModelName 11 | } from './naming'; 12 | import { getAllEnums, getModelByName } from './parser'; 13 | 14 | const scalarTypes = ['Int', 'String', 'BigInt', 'DateTime', 'Float', 'Decimal', 'Boolean']; 15 | const nullishAttributes = ['default']; 16 | 17 | export const generateModelSchema = (sourceFile: SourceFile, model: Model) => { 18 | sourceFile.addImportDeclaration({ 19 | moduleSpecifier: 'webstone-plugin-trpc-web', 20 | namedImports: ['z'] 21 | }); 22 | 23 | const nonScalarFileds = getNonScalarFields(model); 24 | 25 | sourceFile.addVariableStatement({ 26 | declarationKind: VariableDeclarationKind.Const, 27 | isExported: true, 28 | leadingTrivia: (writer) => writer.blankLineIfLastNot(), 29 | trailingTrivia: (writer) => writer.blankLineIfLastNot(), 30 | declarations: [ 31 | { 32 | name: 33 | nonScalarFileds.length > 0 34 | ? generateZodModelName(model.name) 35 | : generateCompleteModelName(model.name), 36 | initializer: (writer) => { 37 | writer 38 | .write('z.object(') 39 | .inlineBlock(() => { 40 | model.properties 41 | .filter((prop) => prop.type === 'field') 42 | .filter( 43 | (prop) => prop.type === 'field' && scalarTypes.includes(prop.fieldType as string) 44 | ) 45 | .forEach((prop) => { 46 | if (prop.type !== 'field') return; 47 | writer 48 | .write(`${prop.name}: ${mapZodType(prop)}`) 49 | .write(',') 50 | .newLine(); 51 | }); 52 | }) 53 | .write(')'); 54 | } 55 | } 56 | ] 57 | }); 58 | 59 | if (nonScalarFileds.length > 0) { 60 | nonScalarFileds.forEach((field) => { 61 | if (field.type !== 'field') return; 62 | const isEnum = determineEnum(field); 63 | if (isEnum && isEnum.type === 'enum') { 64 | sourceFile.addImportDeclaration({ 65 | moduleSpecifier: `../enums/${generateEnumFilename(isEnum.name)}`, 66 | namedImports: [generateZodEnumName(isEnum.name), generateEnumName(isEnum.name)] 67 | }); 68 | } 69 | if (!isEnum) { 70 | sourceFile.addImportDeclarations([ 71 | { 72 | moduleSpecifier: `../models/${generateModelFilename(field.fieldType as string)}`, 73 | namedImports: [`Complete${field.fieldType}Model`] 74 | }, 75 | { 76 | moduleSpecifier: `../models/${generateModelFilename(field.fieldType as string)}`, 77 | namedImports: [`${field.fieldType as string}WithRelations`], 78 | isTypeOnly: true 79 | } 80 | ]); 81 | } 82 | }); 83 | sourceFile.addInterface({ 84 | isExported: true, 85 | name: `${model.name}WithRelations`, 86 | leadingTrivia: (writer) => writer.blankLineIfLastNot(), 87 | trailingTrivia: (writer) => writer.blankLineIfLastNot(), 88 | extends: [`z.infer`], 89 | properties: nonScalarFileds.map((field) => { 90 | let type = ''; 91 | if (field.type !== 'field') return { name: '', type: 'any' }; 92 | const isEnum = determineEnum(field); 93 | if (isEnum && isEnum.type === 'enum') { 94 | type = `${generateEnumName(isEnum.name)}${field.array ? '[]' : ''}${ 95 | field.optional ? ' | null' : '' 96 | }`; 97 | } else { 98 | type = `${field.fieldType as string}WithRelations${field.array ? '[]' : ''}${ 99 | field.optional ? ' | null' : '' 100 | }`; 101 | } 102 | return { 103 | hasQuestionToken: field.optional, 104 | name: field.name, 105 | type 106 | }; 107 | }) 108 | }); 109 | 110 | sourceFile.addVariableStatement({ 111 | declarationKind: VariableDeclarationKind.Const, 112 | isExported: true, 113 | leadingTrivia: (writer) => writer.blankLineIfLastNot(), 114 | trailingTrivia: (writer) => writer.blankLineIfLastNot(), 115 | declarations: [ 116 | { 117 | name: `Complete${model.name}Model`, 118 | type: `z.ZodSchema<${model.name}WithRelations>`, 119 | initializer: (writer) => { 120 | writer 121 | .write(`z.lazy(() => ${generateZodModelName(model.name)}.extend(`) 122 | .inlineBlock(() => { 123 | nonScalarFileds.forEach((prop) => { 124 | if (prop.type !== 'field') return; 125 | writer 126 | .write(`${prop.name}: ${mapZodType(prop)}`) 127 | .write(',') 128 | .newLine(); 129 | }); 130 | }) 131 | .write('))'); 132 | } 133 | } 134 | ] 135 | }); 136 | } 137 | }; 138 | 139 | export const generateEnumSchema = (sourceFile: SourceFile, enumModel: Enum) => { 140 | sourceFile.addImportDeclaration({ 141 | moduleSpecifier: 'webstone-plugin-trpc-web', 142 | namedImports: ['z'] 143 | }); 144 | 145 | const enumValues = enumModel.enumerators.map( 146 | (enumerator) => enumerator.type === 'enumerator' && enumerator.name 147 | ); 148 | 149 | sourceFile.addEnum({ 150 | isExported: true, 151 | name: generateEnumName(enumModel.name), 152 | leadingTrivia: (writer) => writer.blankLineIfLastNot(), 153 | trailingTrivia: (writer) => writer.blankLineIfLastNot(), 154 | members: enumValues.map((value) => ({ 155 | name: value ? value : '' 156 | })) 157 | }); 158 | 159 | sourceFile.addVariableStatement({ 160 | declarationKind: VariableDeclarationKind.Const, 161 | isExported: true, 162 | leadingTrivia: (writer) => writer.blankLineIfLastNot(), 163 | trailingTrivia: (writer) => writer.blankLineIfLastNot(), 164 | declarations: [ 165 | { 166 | name: `${generateZodEnumName(enumModel.name)}`, 167 | initializer: (writer) => { 168 | writer.write(`z.nativeEnum(${generateEnumName(enumModel.name)})`); 169 | } 170 | } 171 | ] 172 | }); 173 | }; 174 | 175 | export const mapZodType = (prop: Field) => { 176 | let zodType = ''; 177 | let modifiers: string[] = ['']; 178 | switch (prop.fieldType) { 179 | case 'Int': 180 | zodType = 'z.number()'; 181 | modifiers = [...modifiers, 'int()']; 182 | break; 183 | case 'String': 184 | zodType = 'z.string()'; 185 | break; 186 | case 'BigInt': 187 | zodType = 'z.bigint()'; 188 | break; 189 | case 'DateTime': 190 | zodType = 'z.date()'; 191 | break; 192 | case 'Float': 193 | zodType = 'z.number()'; 194 | break; 195 | case 'Decimal': 196 | zodType = 'z.number()'; 197 | break; 198 | case 'Boolean': 199 | zodType = 'z.boolean()'; 200 | break; 201 | } 202 | 203 | if (!zodType) { 204 | const isEnum = determineEnum(prop); 205 | if (isEnum && isEnum.type === 'enum') { 206 | zodType = `${generateZodEnumName(isEnum.name)}`; 207 | } 208 | if (!isEnum) { 209 | zodType = `Complete${prop.fieldType}Model`; 210 | } 211 | } 212 | 213 | if (prop.array) modifiers = [...modifiers, 'array()']; 214 | if (prop.optional) modifiers = [...modifiers, 'nullish()']; 215 | if (prop.attributes && prop.attributes.find((attr) => nullishAttributes.includes(attr.name))) 216 | modifiers = [...modifiers, 'nullish()']; 217 | return `${zodType}${modifiers.join('.')}`; 218 | }; 219 | 220 | export const getIDType = (model: Model) => { 221 | const idField = model.properties.find( 222 | (prop) => prop.type === 'field' && prop.attributes?.find((attr) => attr.name === 'id') 223 | ); 224 | if (idField?.type !== 'field') throw new Error('ID field not found'); 225 | return idField ? mapZodType(idField) : 'z.unknown()'; 226 | }; 227 | 228 | export const populateSubrouterFile = (project: Project, model: Model) => { 229 | const subrouterFilename = generateRouterFilename(model.name); 230 | const subrouterTarget = `src/lib/server/trpc/subrouters/${subrouterFilename}.ts`; 231 | 232 | generateSchemaForModel(project, model, new Set()); 233 | 234 | const subRouter = project.getSourceFileOrThrow(subrouterTarget); 235 | 236 | subRouter.addImportDeclaration({ 237 | moduleSpecifier: 'webstone-plugin-trpc-web', 238 | namedImports: ['z'] 239 | }); 240 | 241 | subRouter.formatText({ 242 | tabSize: 1 243 | }); 244 | 245 | subRouter.addImportDeclaration({ 246 | moduleSpecifier: `../models/${generateModelFilename(model.name)}`, 247 | namedImports: [generateCompleteModelName(model.name)] 248 | }); 249 | }; 250 | 251 | export const getNonScalarFields = (model: Model) => { 252 | return model.properties.filter( 253 | (prop) => prop.type === 'field' && !scalarTypes.includes(prop.fieldType as string) 254 | ); 255 | }; 256 | 257 | export function determineEnum(field: Field) { 258 | const fieldName = field.fieldType; 259 | const allEnums = getAllEnums(); 260 | const enumType = allEnums.find( 261 | (enumItem) => enumItem.type === 'enum' && enumItem.name === fieldName 262 | ); 263 | return enumType; 264 | } 265 | 266 | const generateSchemaForModel = (project: Project, model: Model, generatedEntities: Set) => { 267 | generatedEntities.add(model.name); 268 | const nonScalarFields = getNonScalarFields(model); 269 | nonScalarFields.forEach((field) => { 270 | if (field.type !== 'field') return; 271 | const enumType = determineEnum(field); 272 | if (enumType && enumType.type === 'enum') { 273 | if (generatedEntities.has(enumType.name)) return; 274 | const sourceFile = project.createSourceFile( 275 | `src/lib/server/trpc/${ 276 | enumType && enumType.type === 'enum' 277 | ? `enums/${generateEnumFilename(enumType.name)}` 278 | : `models/${generateModelFilename(model.name)}` 279 | }.ts`, 280 | '', 281 | { overwrite: true } 282 | ); 283 | generateEnumSchema(sourceFile, enumType); 284 | generatedEntities.add(enumType.name); 285 | } else { 286 | const dependantModel = getModelByName(field.fieldType as string); 287 | if (generatedEntities.has(field.fieldType as string)) return; 288 | if (dependantModel && dependantModel.type === 'model') { 289 | generatedEntities.add(dependantModel.name); 290 | generateSchemaForModel(project, dependantModel, generatedEntities); 291 | } 292 | } 293 | }); 294 | 295 | const mainModelFile = project.createSourceFile( 296 | `src/lib/server/trpc/models/${generateModelFilename(model.name)}.ts`, 297 | '', 298 | { overwrite: true } 299 | ); 300 | generateModelSchema(mainModelFile, model); 301 | }; 302 | 303 | export const prepareApprouter = ( 304 | sourceFile: SourceFile, 305 | routerName: string, 306 | routerFile: string 307 | ) => { 308 | const existingImport = sourceFile.getImportDeclaration((declaration) => { 309 | return declaration.getModuleSpecifierValue() === `./subrouters/${routerFile}`; 310 | }); 311 | 312 | if (!existingImport) { 313 | sourceFile.addImportDeclaration({ 314 | moduleSpecifier: `./subrouters/${routerFile}`, 315 | namedImports: [routerName] 316 | }); 317 | } 318 | 319 | const appRouterDeclaration = sourceFile.getVariableDeclaration('appRouter'); 320 | 321 | const existingArgument = appRouterDeclaration 322 | ?.getInitializerIfKindOrThrow(SyntaxKind.CallExpression) 323 | .getArguments() 324 | .find((arg) => arg.getText() === routerName); 325 | 326 | if (!existingArgument) { 327 | appRouterDeclaration 328 | ?.getInitializerIfKindOrThrow(SyntaxKind.CallExpression) 329 | .addArgument(routerName); 330 | } 331 | }; 332 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/src/lib/naming.ts: -------------------------------------------------------------------------------- 1 | export const generateZodModelName = (name: string) => { 2 | return `${name.toLowerCase()}Model`; 3 | }; 4 | 5 | export const generateEnumName = (name: string) => { 6 | return `${name.toLowerCase()}Enum`; 7 | }; 8 | 9 | export const generateCompleteModelName = (name: string) => { 10 | return `Complete${name}Model`; 11 | }; 12 | 13 | export const generateZodEnumName = (name: string) => { 14 | return `${name.toLowerCase()}EnumModel`; 15 | }; 16 | 17 | export const generateRouterFilename = (name: string) => { 18 | return `${name.toLowerCase()}-router`; 19 | }; 20 | 21 | export const generateModelFilename = (name: string) => { 22 | return `${name.toLowerCase()}`; 23 | }; 24 | 25 | export const generateEnumFilename = (name: string) => { 26 | return `${name.toLowerCase()}`; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/src/lib/parser.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { getSchema } from '@mrleebo/prisma-ast'; 3 | 4 | export const getModelByName = (modelName: string) => { 5 | const schema = getSchema(readFileSync('prisma/schema.prisma', { encoding: 'utf8' })); 6 | return schema.list.find( 7 | (item) => item.type === 'model' && item.name.toLowerCase() === modelName.toLowerCase() 8 | ); 9 | }; 10 | 11 | export const getAllModels = () => { 12 | const schema = getSchema(readFileSync('prisma/schema.prisma', { encoding: 'utf8' })); 13 | return schema.list.filter((item) => item.type === 'model'); 14 | }; 15 | 16 | export const getAllEnums = () => { 17 | const schema = getSchema(readFileSync('prisma/schema.prisma', { encoding: 'utf8' })); 18 | return schema.list.filter((item) => item.type === 'enum'); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/src/templates/base/router.ts.ejs: -------------------------------------------------------------------------------- 1 | import { router, publicProcedure, mergeRouters } from './trpc'; 2 | 3 | const defaultRouter = router({ 4 | greeting: publicProcedure.query(() => 'hello webstone tRPC') 5 | }); 6 | 7 | export const appRouter = mergeRouters(defaultRouter); 8 | 9 | export type AppRouter = typeof appRouter; 10 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/src/templates/base/trpc.ts.ejs: -------------------------------------------------------------------------------- 1 | import { initTRPC } from 'webstone-plugin-trpc-web'; 2 | const t = initTRPC.create(); 3 | 4 | export const middleware = t.middleware; 5 | export const router = t.router; 6 | export const publicProcedure = t.procedure; 7 | export const mergeRouters = t.mergeRouters; 8 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/src/templates/subrouter.ejs: -------------------------------------------------------------------------------- 1 | import { router, publicProcedure } from '../trpc'; 2 | 3 | export const <%=props.lowercaseSingular%>Router = router({ 4 | create<%=props.capitalizedSingular%>: publicProcedure 5 | .input(<%=props.zodModelName%>) 6 | .mutation(({ input }) => { 7 | return input; 8 | }), 9 | getAll<%=props.capitalizedPlural%>: publicProcedure.query(() => { 10 | return []; 11 | }), 12 | get<%=props.capitalizedSingular%> : publicProcedure.input(<%=props.idFieldType%>).query(({ input }) => { 13 | return input; 14 | }), 15 | update<%=props.capitalizedSingular%>: publicProcedure.input(<%=props.zodModelName%>).mutation(({ input }) => { 16 | return input; 17 | }), 18 | delete<%=props.capitalizedSingular%>: publicProcedure.input(<%=props.idFieldType%>).mutation(({ input }) => { 19 | return input; 20 | }) 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/tests/lib/generate.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import sinon from 'sinon'; 4 | import fs from 'node:fs'; 5 | import { 6 | getNonScalarFields, 7 | determineEnum, 8 | mapZodType, 9 | prepareApprouter, 10 | getIDType 11 | } from '../../src/lib/generate'; 12 | import { Field, Model } from '@mrleebo/prisma-ast'; 13 | import { Project, SourceFile } from 'ts-morph'; 14 | 15 | test.after.each(() => { 16 | sinon.restore(); 17 | }); 18 | 19 | test('should return non-scalar fields', async () => { 20 | const model: Model = { 21 | name: 'TestModel', 22 | type: 'model', 23 | properties: [ 24 | { 25 | type: 'field', 26 | fieldType: 'String', 27 | name: 'stringField' 28 | }, 29 | { 30 | type: 'field', 31 | fieldType: 'Non-Scalar', 32 | name: 'nonScalarField' 33 | }, 34 | { 35 | type: 'field', 36 | fieldType: 'Int', 37 | name: 'numberField' 38 | } 39 | ] 40 | }; 41 | const nonScalarFields = getNonScalarFields(model); 42 | 43 | assert.is(nonScalarFields.length, 1); 44 | assert.is(nonScalarFields[0].type === 'field' && nonScalarFields[0].name, 'nonScalarField'); 45 | assert.equal(nonScalarFields, [ 46 | { 47 | type: 'field', 48 | fieldType: 'Non-Scalar', 49 | name: 'nonScalarField' 50 | } 51 | ]); 52 | }); 53 | 54 | test('should determine enum', async () => { 55 | const fakeReadFileSync = sinon.fake.returns(` 56 | generator client { 57 | provider = "prisma-client-js" 58 | } 59 | 60 | datasource db { 61 | provider = "postgresql" 62 | url = env("DATABASE_URL") 63 | } 64 | 65 | model User { 66 | id Int @id @default(autoincrement()) 67 | createdAt DateTime @default(now()) 68 | email String @unique 69 | name String? 70 | role Role? @default(USER) 71 | postId Int? 72 | posts Post[] 73 | } 74 | 75 | model Post { 76 | id Int @id @default(autoincrement()) 77 | user User @relation(fields: [userId], references: [id]) 78 | role Role 79 | userId Int 80 | } 81 | 82 | enum Role { 83 | USER 84 | ADMIN 85 | } 86 | `); 87 | 88 | sinon.replace(fs, 'readFileSync', fakeReadFileSync); 89 | 90 | const field: Field = { 91 | type: 'field', 92 | fieldType: 'Role', 93 | name: 'Role' 94 | }; 95 | 96 | const determinedEnum = determineEnum(field); 97 | 98 | assert.ok(determinedEnum); 99 | assert.is(determinedEnum?.type, 'enum'); 100 | assert.is(determinedEnum.type === 'enum' && determinedEnum?.name, 'Role'); 101 | assert.is(determinedEnum.type === 'enum' && determinedEnum?.enumerators.length, 2); 102 | }); 103 | 104 | test("should determine enum doesn't exist", async () => { 105 | const fakeReadFileSync = sinon.fake.returns(` 106 | generator client { 107 | provider = "prisma-client-js" 108 | } 109 | 110 | datasource db { 111 | provider = "postgresql" 112 | url = env("DATABASE_URL") 113 | } 114 | 115 | model User { 116 | id Int @id @default(autoincrement()) 117 | createdAt DateTime @default(now()) 118 | email String @unique 119 | name String? 120 | role Role? @default(USER) 121 | postId Int? 122 | posts Post[] 123 | } 124 | 125 | model Post { 126 | id Int @id @default(autoincrement()) 127 | user User @relation(fields: [userId], references: [id]) 128 | role Role 129 | userId Int 130 | } 131 | 132 | enum Role { 133 | USER 134 | ADMIN 135 | } 136 | `); 137 | 138 | sinon.replace(fs, 'readFileSync', fakeReadFileSync); 139 | 140 | const field: Field = { 141 | type: 'field', 142 | fieldType: 'Title', 143 | name: 'Title' 144 | }; 145 | 146 | const determinedEnum = determineEnum(field); 147 | assert.not.ok(determinedEnum); 148 | }); 149 | 150 | test('map zod type to string', async () => { 151 | const field: Field = { 152 | type: 'field', 153 | fieldType: 'String', 154 | name: 'stringField' 155 | }; 156 | 157 | const mappedType = mapZodType(field); 158 | assert.is(mappedType, 'z.string()'); 159 | }); 160 | 161 | test('map zod type to string array', async () => { 162 | const field: Field = { 163 | type: 'field', 164 | fieldType: 'String', 165 | name: 'stringField', 166 | array: true 167 | }; 168 | 169 | const mappedType = mapZodType(field); 170 | assert.is(mappedType, 'z.string().array()'); 171 | }); 172 | 173 | test('map zod type to optional string array', async () => { 174 | const field: Field = { 175 | type: 'field', 176 | fieldType: 'String', 177 | name: 'stringField', 178 | array: true, 179 | optional: true 180 | }; 181 | 182 | const mappedType = mapZodType(field); 183 | assert.is(mappedType, 'z.string().array().nullish()'); 184 | }); 185 | 186 | test('map zod type to number', async () => { 187 | const field: Field = { 188 | type: 'field', 189 | fieldType: 'Int', 190 | name: 'numberField' 191 | }; 192 | 193 | const mappedType = mapZodType(field); 194 | assert.is(mappedType, 'z.number().int()'); 195 | }); 196 | 197 | test('map zod type to boolean', async () => { 198 | const field: Field = { 199 | type: 'field', 200 | fieldType: 'Boolean', 201 | name: 'booleanField' 202 | }; 203 | 204 | const mappedType = mapZodType(field); 205 | assert.is(mappedType, 'z.boolean()'); 206 | }); 207 | 208 | test('map zod type to BigInt', async () => { 209 | const field: Field = { 210 | type: 'field', 211 | fieldType: 'BigInt', 212 | name: 'bigIntField' 213 | }; 214 | 215 | const mappedType = mapZodType(field); 216 | assert.is(mappedType, 'z.bigint()'); 217 | }); 218 | 219 | test('map zod type to DateTime', async () => { 220 | const field: Field = { 221 | type: 'field', 222 | fieldType: 'DateTime', 223 | name: 'dateField' 224 | }; 225 | 226 | const mappedType = mapZodType(field); 227 | assert.is(mappedType, 'z.date()'); 228 | }); 229 | 230 | test('map zod type to Float', async () => { 231 | const field: Field = { 232 | type: 'field', 233 | fieldType: 'Float', 234 | name: 'floatField' 235 | }; 236 | 237 | const mappedType = mapZodType(field); 238 | assert.is(mappedType, 'z.number()'); 239 | }); 240 | 241 | test('map zod type to Float', async () => { 242 | const field: Field = { 243 | type: 'field', 244 | fieldType: 'Float', 245 | name: 'floatField' 246 | }; 247 | 248 | const mappedType = mapZodType(field); 249 | assert.is(mappedType, 'z.number()'); 250 | }); 251 | 252 | test('map zod type to Decimal', async () => { 253 | const field: Field = { 254 | type: 'field', 255 | fieldType: 'Decimal', 256 | name: 'decimalField' 257 | }; 258 | 259 | const mappedType = mapZodType(field); 260 | assert.is(mappedType, 'z.number()'); 261 | }); 262 | 263 | test('should import and extend the approuter', async () => { 264 | const project = new Project({ useInMemoryFileSystem: true }); 265 | 266 | const sourceFile: SourceFile = project.createSourceFile( 267 | '/testRouter.ts', 268 | ` 269 | import { router, publicProcedure, mergeRouters } from './trpc'; 270 | 271 | const defaultRouter = router({ 272 | greeting: publicProcedure.query(() => 'hello webstone tRPC') 273 | }); 274 | 275 | export const appRouter = mergeRouters(defaultRouter); 276 | 277 | export type AppRouter = typeof appRouter; 278 | 279 | ` 280 | ); 281 | 282 | prepareApprouter(sourceFile, 'testRouter', 'testRouter'); 283 | 284 | const imports = sourceFile.getImportDeclarations(); 285 | 286 | const namedImports = imports.flatMap((imp) => 287 | imp.getNamedImports().map((namedImp) => namedImp.getText()) 288 | ); 289 | 290 | const moduleSpecifiers = imports.map((imp) => imp.getModuleSpecifierValue()); 291 | 292 | assert.is(imports.length, 2); 293 | assert.is(namedImports.length, 4); 294 | 295 | // check named imports 296 | assert.ok(namedImports.includes('testRouter')); 297 | 298 | // check module specifiers 299 | assert.ok(moduleSpecifiers.includes('./subrouters/testRouter')); 300 | }); 301 | 302 | test('should return string as id type', async () => { 303 | const model: Model = { 304 | name: 'TestModel', 305 | type: 'model', 306 | properties: [ 307 | { 308 | type: 'field', 309 | fieldType: 'String', 310 | name: 'stringField', 311 | attributes: [ 312 | { 313 | name: 'id', 314 | type: 'attribute', 315 | kind: 'field' 316 | } 317 | ] 318 | }, 319 | { 320 | type: 'field', 321 | fieldType: 'Non-Scalar', 322 | name: 'nonScalarField' 323 | }, 324 | { 325 | type: 'field', 326 | fieldType: 'Int', 327 | name: 'numberField' 328 | } 329 | ] 330 | }; 331 | 332 | const idType = getIDType(model); 333 | assert.is(idType, 'z.string()'); 334 | }); 335 | 336 | test('should return number as id type', async () => { 337 | const model: Model = { 338 | name: 'TestModel', 339 | type: 'model', 340 | properties: [ 341 | { 342 | type: 'field', 343 | fieldType: 'Int', 344 | name: 'stringField', 345 | attributes: [ 346 | { 347 | name: 'id', 348 | type: 'attribute', 349 | kind: 'field' 350 | } 351 | ] 352 | }, 353 | { 354 | type: 'field', 355 | fieldType: 'Non-Scalar', 356 | name: 'nonScalarField' 357 | }, 358 | { 359 | type: 'field', 360 | fieldType: 'Int', 361 | name: 'numberField' 362 | } 363 | ] 364 | }; 365 | 366 | const idType = getIDType(model); 367 | assert.is(idType, 'z.number().int()'); 368 | }); 369 | 370 | test('should return error, because no ID provided', async () => { 371 | try { 372 | const model: Model = { 373 | name: 'TestModel', 374 | type: 'model', 375 | properties: [ 376 | { 377 | type: 'field', 378 | fieldType: 'Int', 379 | name: 'stringField' 380 | }, 381 | { 382 | type: 'field', 383 | fieldType: 'Non-Scalar', 384 | name: 'nonScalarField' 385 | }, 386 | { 387 | type: 'field', 388 | fieldType: 'Int', 389 | name: 'numberField' 390 | } 391 | ] 392 | }; 393 | getIDType(model); 394 | } catch (error) { 395 | assert.is(error.message, 'ID field not found'); 396 | } 397 | }); 398 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/tests/lib/naming.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { 4 | generateZodEnumName, 5 | generateZodModelName, 6 | generateRouterFilename, 7 | generateEnumFilename, 8 | generateModelFilename, 9 | generateCompleteModelName, 10 | generateEnumName 11 | } from '../../src/lib/naming'; 12 | 13 | test('should return zod model name', async () => { 14 | const modelName = generateZodModelName('User'); 15 | assert.is(modelName, 'userModel'); 16 | }); 17 | 18 | test('should return zod enum name', async () => { 19 | const enumName = generateZodEnumName('Role'); 20 | assert.is(enumName, 'roleEnumModel'); 21 | }); 22 | 23 | test('should return subrouter filename', async () => { 24 | const filename = generateRouterFilename('User'); 25 | assert.is(filename, 'user-router'); 26 | }); 27 | 28 | test('should return model filename', async () => { 29 | const filename = generateModelFilename('User'); 30 | assert.is(filename, 'user'); 31 | }); 32 | 33 | test('should return enum filename', async () => { 34 | const filename = generateEnumFilename('Role'); 35 | assert.is(filename, 'role'); 36 | }); 37 | 38 | test('should return enum name', async () => { 39 | const enumName = generateEnumName('Role'); 40 | assert.is(enumName, 'roleEnum'); 41 | }); 42 | 43 | test('should return complete name', async () => { 44 | const completeName = generateCompleteModelName('User'); 45 | assert.is(completeName, 'CompleteUserModel'); 46 | }); 47 | 48 | test.run(); 49 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/tests/lib/parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import sinon from 'sinon'; 4 | import fs from 'fs'; 5 | import { getAllEnums, getAllModels, getModelByName } from '../../src/lib/parser'; 6 | 7 | test.after.each(() => { 8 | sinon.restore(); 9 | }); 10 | 11 | test('should get all enums', async () => { 12 | const fakeReadFileSync = sinon.fake.returns(` 13 | generator client { 14 | provider = "prisma-client-js" 15 | } 16 | 17 | datasource db { 18 | provider = "postgresql" 19 | url = env("DATABASE_URL") 20 | } 21 | 22 | model User { 23 | id Int @id @default(autoincrement()) 24 | createdAt DateTime @default(now()) 25 | email String @unique 26 | name String? 27 | role Role? @default(USER) 28 | postId Int? 29 | posts Post[] 30 | } 31 | 32 | model Post { 33 | id Int @id @default(autoincrement()) 34 | user User @relation(fields: [userId], references: [id]) 35 | role Role 36 | userId Int 37 | } 38 | 39 | enum Role { 40 | USER 41 | ADMIN 42 | } 43 | `); 44 | 45 | sinon.replace(fs, 'readFileSync', fakeReadFileSync); 46 | 47 | const enums = getAllEnums(); 48 | 49 | assert.is(enums.length, 1); 50 | assert.is(enums[0].type === 'enum' && enums[0].name, 'Role'); 51 | assert.is(enums[0].type, 'enum'); 52 | }); 53 | 54 | test('should return all models', async () => { 55 | const fakeReadFileSync = sinon.fake.returns(` 56 | generator client { 57 | provider = "prisma-client-js" 58 | } 59 | 60 | datasource db { 61 | provider = "postgresql" 62 | url = env("DATABASE_URL") 63 | } 64 | 65 | model User { 66 | id Int @id @default(autoincrement()) 67 | createdAt DateTime @default(now()) 68 | email String @unique 69 | name String? 70 | role Role? @default(USER) 71 | postId Int? 72 | posts Post[] 73 | } 74 | 75 | model Post { 76 | id Int @id @default(autoincrement()) 77 | user User @relation(fields: [userId], references: [id]) 78 | role Role 79 | userId Int 80 | } 81 | 82 | enum Role { 83 | USER 84 | ADMIN 85 | } 86 | `); 87 | 88 | sinon.replace(fs, 'readFileSync', fakeReadFileSync); 89 | const models = getAllModels(); 90 | 91 | assert.is(models.length, 2); 92 | assert.is(models[0].type, 'model'); 93 | assert.is(models[0].type === 'model' && models[0].name, 'User'); 94 | }); 95 | 96 | test('should get single model by name', async () => { 97 | const fakeReadFileSync = sinon.fake.returns(` 98 | generator client { 99 | provider = "prisma-client-js" 100 | } 101 | 102 | datasource db { 103 | provider = "postgresql" 104 | url = env("DATABASE_URL") 105 | } 106 | 107 | model User { 108 | id Int @id @default(autoincrement()) 109 | createdAt DateTime @default(now()) 110 | email String @unique 111 | name String? 112 | role Role? @default(USER) 113 | postId Int? 114 | posts Post[] 115 | } 116 | 117 | model Post { 118 | id Int @id @default(autoincrement()) 119 | user User @relation(fields: [userId], references: [id]) 120 | role Role 121 | userId Int 122 | } 123 | 124 | enum Role { 125 | USER 126 | ADMIN 127 | } 128 | `); 129 | 130 | sinon.replace(fs, 'readFileSync', fakeReadFileSync); 131 | 132 | const model = getModelByName('User'); 133 | 134 | assert.ok(model); 135 | assert.is(model?.type, 'model'); 136 | assert.is(model?.type === 'model' && model?.name, 'User'); 137 | assert.is(model?.type === 'model' && model?.properties.length, 7); 138 | }); 139 | 140 | test("should return undefined if models doesn't exist", async () => { 141 | const fakeReadFileSync = sinon.fake.returns(` 142 | generator client { 143 | provider = "prisma-client-js" 144 | } 145 | 146 | datasource db { 147 | provider = "postgresql" 148 | url = env("DATABASE_URL") 149 | } 150 | 151 | model User { 152 | id Int @id @default(autoincrement()) 153 | createdAt DateTime @default(now()) 154 | email String @unique 155 | name String? 156 | role Role? @default(USER) 157 | postId Int? 158 | posts Post[] 159 | } 160 | 161 | model Post { 162 | id Int @id @default(autoincrement()) 163 | user User @relation(fields: [userId], references: [id]) 164 | role Role 165 | userId Int 166 | } 167 | 168 | enum Role { 169 | USER 170 | ADMIN 171 | } 172 | `); 173 | 174 | sinon.replace(fs, 'readFileSync', fakeReadFileSync); 175 | 176 | const model = getModelByName('User2'); 177 | 178 | assert.not.ok(model); 179 | }); 180 | 181 | test.run(); 182 | -------------------------------------------------------------------------------- /packages/plugin-trpc/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "outDir": "./build", 5 | "module": "commonjs", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "allowSyntheticDefaultImports": true, 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "declaration": true, 12 | "moduleResolution": "Node", 13 | "rootDir": "src", 14 | "declarationDir": "./build/types" 15 | }, 16 | "include": ["src/**/*.ts"], 17 | "exclude": ["src/fixtures", "src/**/*.test.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript') 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # webstone-plugin-trpc-web 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 2826fcb: Release a beta version of the tRPC Webstone Plugin. 8 | - 23575e3: Release the webstone-plugin-request-logger package. 9 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/README.md: -------------------------------------------------------------------------------- 1 | # tRPC Web - Webstone Plugin 2 | 3 | ## Installation 4 | 5 | Install this plugin with the following command: 6 | 7 | ``` 8 | npm install -D webstone-plugin-trpc-web 9 | ``` 10 | 11 | ## Usage 12 | 13 | > **_Document how developers can use this plugin. Do they import a Svelte component? Is there a Svelte action they can use?_** 14 | 15 | ## Learn more about Webstone Plugins 16 | 17 | This plugin is part of a wider ecosystem called [Webstone Plugins](https://github.com/WebstoneHQ/webstone). 18 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webstone-plugin-trpc-web", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build": "svelte-kit sync && svelte-package --input src/lib/plugin && cp package.json ./dist/", 7 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 8 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 9 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 10 | "format": "prettier --plugin-search-dir . --write .", 11 | "prepublishOnly": "pnpm remove webstone-plugin-trpc-cli @webstone/cli" 12 | }, 13 | "devDependencies": { 14 | "@playwright/test": "^1.37.0", 15 | "@sveltejs/adapter-auto": "^2.1.0", 16 | "@sveltejs/kit": "^1.22.5", 17 | "@sveltejs/package": "^2.2.1", 18 | "@typescript-eslint/eslint-plugin": "^6.3.0", 19 | "@typescript-eslint/parser": "^6.3.0", 20 | "@webstone/cli": "workspace:^0.13.0", 21 | "eslint": "^8.46.0", 22 | "eslint-config-prettier": "^9.0.0", 23 | "eslint-plugin-svelte3": "^4.0.0", 24 | "prettier": "^3.0.1", 25 | "prettier-plugin-svelte": "^3.0.3", 26 | "svelte": "^4.1.2", 27 | "svelte-check": "^3.4.6", 28 | "tslib": "^2.6.1", 29 | "typescript": "^5.1.6", 30 | "vite": "^4.4.9", 31 | "webstone-plugin-trpc-cli": "workspace:^0.2.0" 32 | }, 33 | "type": "module", 34 | "private": true, 35 | "dependencies": { 36 | "@trpc/client": "^10.37.1", 37 | "@trpc/server": "^10.37.1", 38 | "zod": "^3.21.4" 39 | }, 40 | "exports": { 41 | "./package.json": "./package.json", 42 | ".": { 43 | "types": "./index.d.ts", 44 | "svelte": "./index.js", 45 | "default": "./index.js" 46 | } 47 | }, 48 | "files": [ 49 | "dist" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: 'npm run build && npm run preview', 6 | port: 4173 7 | } 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // See https://kit.svelte.dev/docs/types#app 4 | // for information about these interfaces 5 | // and what to do when importing types 6 | declare namespace App { 7 | // interface Locals {} 8 | // interface PageData {} 9 | // interface Error {} 10 | // interface Platform {} 11 | } 12 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/src/lib/plugin/client.ts: -------------------------------------------------------------------------------- 1 | import type { AnyRouter, ClientDataTransformerOptions } from '@trpc/server'; 2 | import { 3 | createTRPCProxyClient, 4 | httpBatchLink, 5 | type HTTPHeaders, 6 | type TRPCLink 7 | } from '@trpc/client'; 8 | 9 | export function createTrpcClient({ 10 | endpointUrl = '/trpc', 11 | loadFetch, 12 | transformer, 13 | headers 14 | }: { 15 | endpointUrl: string; 16 | loadFetch?: typeof window.fetch; 17 | transformer?: ClientDataTransformerOptions; 18 | headers?: HTTPHeaders | (() => HTTPHeaders | Promise); 19 | }) { 20 | const link: TRPCLink = httpBatchLink({ 21 | url: endpointUrl, 22 | ...(loadFetch && { fetch: loadFetch, headers }) 23 | }); 24 | 25 | return createTRPCProxyClient({ transformer, links: [link] }); 26 | } 27 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/src/lib/plugin/handler.ts: -------------------------------------------------------------------------------- 1 | // thank you icflorescu for the inspiration in https://github.com/icflorescu/trpc-sveltekit/blob/main/package/src/server.ts 2 | 3 | import type { Handle, RequestEvent } from '@sveltejs/kit'; 4 | import type { 5 | AnyRouter, 6 | Dict, 7 | inferRouterContext, 8 | inferRouterError, 9 | TRPCError 10 | } from '@trpc/server'; 11 | import type { HTTPRequest } from '@trpc/server/dist/http/internals/types'; 12 | import { resolveHTTPResponse, type ResponseMeta } from '@trpc/server/http'; 13 | import type { TRPCResponse } from '@trpc/server/rpc'; 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | export const createTrpcHandler = ({ 17 | endpointURL = '/trpc', 18 | router, 19 | createContext, 20 | responseMeta, 21 | onError 22 | }: { 23 | /** 24 | * URL the Handler is mounted on 25 | */ 26 | endpointURL: string; 27 | 28 | /** 29 | * TRPC router 30 | */ 31 | router: Router; 32 | createContext?: (event: RequestEvent) => Promise>; 33 | responseMeta?: (options: { 34 | data: TRPCResponse>[]; 35 | ctx?: inferRouterContext; 36 | paths?: string[]; 37 | type: 'query' | 'mutation' | 'subscription' | 'unknown'; 38 | errors: TRPCError[]; 39 | }) => ResponseMeta; 40 | 41 | onError?: (options: { 42 | error: TRPCError; 43 | type: 'query' | 'mutation' | 'subscription' | 'unknown'; 44 | path?: string; 45 | input: unknown; 46 | ctx?: inferRouterContext; 47 | req: HTTPRequest; 48 | }) => void; 49 | }): Handle => { 50 | const trpcHandler: Handle = async ({ event, resolve }) => { 51 | if (event.url.pathname.startsWith(endpointURL)) { 52 | const request = event.request as Request & { 53 | headers: Dict; 54 | }; 55 | 56 | const req = { 57 | method: request.method, 58 | headers: request.headers, 59 | query: event.url.searchParams, 60 | body: await request.text() 61 | }; 62 | 63 | const httpResponse = await resolveHTTPResponse({ 64 | router, 65 | req, 66 | path: event.url.pathname.substring(endpointURL.length + 1), 67 | createContext: async () => createContext?.(event), 68 | responseMeta, 69 | onError 70 | }); 71 | 72 | const { status, headers, body } = httpResponse as { 73 | status: number; 74 | headers: Record; 75 | body: string; 76 | }; 77 | 78 | return new Response(body, { status, headers }); 79 | } 80 | 81 | return await resolve(event); 82 | }; 83 | 84 | return trpcHandler; 85 | }; 86 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/src/lib/plugin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './handler'; 2 | export { z } from 'zod'; 3 | export * from '@trpc/server'; 4 | export * from './client'; 5 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 |

Welcome to your library project

2 |

Create your package using @sveltejs/package and preview/showcase your work with SvelteKit

3 |

Visit kit.svelte.dev to read the documentation

4 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebstoneHQ/webstone-plugins/5296c66e4191ea32b09b70bf728fc023f0f1ccc4/packages/plugin-trpc/web/static/favicon.png -------------------------------------------------------------------------------- /packages/plugin-trpc/web/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/kit/vite'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://github.com/sveltejs/svelte-preprocess 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter() 12 | } 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /packages/plugin-trpc/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import type { UserConfig } from 'vite'; 3 | 4 | const config: UserConfig = { 5 | plugins: [sveltekit()] 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | // playwright.config.ts 2 | import { type PlaywrightTestConfig } from "@playwright/test"; 3 | 4 | const config: PlaywrightTestConfig = { 5 | testIgnore: "**/_dev-app/**", 6 | use: { 7 | baseURL: "http://localhost:5173", 8 | screenshot: "only-on-failure", 9 | video: "retain-on-failure", 10 | }, 11 | outputDir: "tests/e2e/test-results", 12 | }; 13 | export default config; 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/**" 3 | - "!_dev-app/**" 4 | - "!packages/create-webstone-app/templates/**" 5 | -------------------------------------------------------------------------------- /tests/e2e/1-web-pages/create-and-delete.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | import { execSync } from "child_process"; 3 | import { resolve } from "path"; 4 | import { sleep } from "../globals"; 5 | 6 | const devAppPath = resolve("./_dev-app"); 7 | 8 | test.describe("web/page/create & web/page/delete", () => { 9 | test("creates and deletes an About Us page", async ({ page }) => { 10 | execSync("pnpm ws web route create 'About Us' --types '+page.svelte'", { 11 | cwd: devAppPath, 12 | }); 13 | await page.goto("/about-us"); 14 | await expect(page.locator("h1")).toContainText("About Us"); 15 | await page.goto("/"); 16 | execSync("pnpm ws web route delete 'About Us'", { 17 | cwd: devAppPath, 18 | }); 19 | 20 | // The previous `execSync` call to delete /about-us results in a dev server restart. 21 | // Let's wait a tiny bit for the restart to complete. 22 | await sleep(300); 23 | 24 | await page.goto("/about-us"); 25 | await expect(page.locator("h1")).toContainText("404"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/e2e/2-api-endpoints/create-and-delete.spec.ts: -------------------------------------------------------------------------------- 1 | import { APIResponse, expect, test } from "@playwright/test"; 2 | import { execSync } from "child_process"; 3 | import { resolve } from "path"; 4 | import { sleep } from "../globals"; 5 | 6 | const devAppPath = resolve("./_dev-app"); 7 | 8 | test.describe("web/api/create & web/api/delete", () => { 9 | test("creates and deletes CRUD API endpoints for /api/users", async ({ 10 | request, 11 | }) => { 12 | execSync("pnpm ws web api create /api/users", { 13 | cwd: devAppPath, 14 | }); 15 | 16 | let response: APIResponse; 17 | response = await request.get("/api/users"); 18 | expect(response.ok()).toBeTruthy(); 19 | expect(response.status()).toEqual(200); 20 | expect(await response.text()).toEqual("GET /api/users => Ok."); 21 | 22 | response = await request.post("/api/users"); 23 | expect(response.ok()).toBeTruthy(); 24 | expect(response.status()).toEqual(200); 25 | expect(await response.text()).toEqual("POST /api/users => Ok."); 26 | 27 | response = await request.delete("/api/users/123"); 28 | expect(response.ok()).toBeTruthy(); 29 | expect(response.status()).toEqual(200); 30 | expect(await response.text()).toEqual("DELETE /api/users/123 => Ok."); 31 | 32 | response = await request.patch("/api/users/123"); 33 | expect(response.ok()).toBeTruthy(); 34 | expect(response.status()).toEqual(200); 35 | expect(await response.text()).toEqual("PATCH /api/users/123 => Ok."); 36 | 37 | response = await request.put("/api/users/123"); 38 | expect(response.ok()).toBeTruthy(); 39 | expect(response.status()).toEqual(200); 40 | expect(await response.text()).toEqual("PUT /api/users/123 => Ok."); 41 | 42 | execSync("pnpm ws web api delete /api/users", { 43 | cwd: devAppPath, 44 | }); 45 | 46 | // The previous `execSync` call to delete /api/users results in a dev server restart. 47 | // Let's wait a tiny bit for the restart to complete. 48 | await sleep(300); 49 | 50 | response = await request.get("/api/users"); 51 | expect(response.status()).toEqual(404); 52 | 53 | response = await request.post("/api/users"); 54 | expect(response.status()).toEqual(404); 55 | 56 | response = await request.delete("/api/users/123"); 57 | expect(response.status()).toEqual(404); 58 | 59 | response = await request.patch("/api/users/123"); 60 | expect(response.status()).toEqual(404); 61 | 62 | response = await request.put("/api/users/123"); 63 | expect(response.status()).toEqual(404); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/e2e/globals.ts: -------------------------------------------------------------------------------- 1 | export const sleep = async (ms: number) => { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | }; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "noUnusedLocals": true, 9 | "sourceMap": false, 10 | "inlineSourceMap": true, 11 | "strict": true, 12 | "declaration": true 13 | } 14 | } 15 | --------------------------------------------------------------------------------