├── .eslintignore
├── .eslintrc.cjs
├── .github
├── .gitignore
├── build-examples.js
├── dependabot.yml
├── pages
│ └── .nojekyll
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── examples
├── cloudflare
│ ├── .gitignore
│ ├── .nvmrc
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ ├── About.tsx
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── Counter.island.tsx
│ │ ├── Expandable.island.tsx
│ │ ├── Home.tsx
│ │ ├── MediaQuery.island.tsx
│ │ ├── Preview.tsx
│ │ ├── ServerContent.tsx
│ │ ├── StaticContent.lagoon.tsx
│ │ ├── capri.svg
│ │ ├── main.server.tsx
│ │ └── main.tsx
│ ├── tsconfig.json
│ └── vite.config.ts
├── preact
│ ├── .gitignore
│ ├── capri.svg
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ ├── About.tsx
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── AsyncData.tsx
│ │ ├── Counter.island.tsx
│ │ ├── Expandable.island.tsx
│ │ ├── Home.tsx
│ │ ├── MediaQuery.island.tsx
│ │ ├── Preview.tsx
│ │ ├── ServerContent.tsx
│ │ ├── StaticContent.lagoon.tsx
│ │ ├── capri.svg
│ │ ├── main.server.tsx
│ │ ├── main.tsx
│ │ └── useFetch.tsx
│ ├── tsconfig.json
│ └── vite.config.ts
├── react
│ ├── .gitignore
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ ├── About.tsx
│ │ ├── AsyncData.tsx
│ │ ├── Counter.island.tsx
│ │ ├── Expandable.island.tsx
│ │ ├── Home.tsx
│ │ ├── MediaQuery.island.tsx
│ │ ├── NotFound.tsx
│ │ ├── Preview.tsx
│ │ ├── ServerContent.tsx
│ │ ├── StaticContent.lagoon.tsx
│ │ ├── capri.svg
│ │ ├── main.css
│ │ ├── main.server.tsx
│ │ ├── main.tsx
│ │ └── routes.tsx
│ ├── tsconfig.json
│ └── vite.config.ts
├── solid
│ ├── .gitignore
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ ├── About.tsx
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── AsyncData.tsx
│ │ ├── Counter.island.tsx
│ │ ├── Expandable.island.tsx
│ │ ├── Home.tsx
│ │ ├── MediaQuery.island.tsx
│ │ ├── ServerContent.tsx
│ │ ├── StaticContent.lagoon.tsx
│ │ ├── capri.svg
│ │ ├── main.server.tsx
│ │ ├── main.tsx
│ │ └── vite.d.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── svelte
│ ├── .gitignore
│ ├── capri.svg
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ ├── About.svelte
│ │ ├── Counter.island.svelte
│ │ ├── Expandable.island.svelte
│ │ ├── Home.svelte
│ │ ├── MediaQuery.island.svelte
│ │ ├── ServerContent.svelte
│ │ ├── StaticContent.lagoon.svelte
│ │ ├── capri.svg
│ │ ├── global.css
│ │ ├── main.server.ts
│ │ ├── main.ts
│ │ ├── router.ts
│ │ └── svelte.d.ts
│ ├── tsconfig.json
│ └── vite.config.js
├── vercel
│ ├── .gitignore
│ ├── index.html
│ ├── package.json
│ ├── src
│ │ ├── About.tsx
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── Counter.island.tsx
│ │ ├── Expandable.island.tsx
│ │ ├── Home.tsx
│ │ ├── MediaQuery.island.tsx
│ │ ├── Preview.tsx
│ │ ├── ServerContent.tsx
│ │ ├── StaticContent.lagoon.tsx
│ │ ├── capri.svg
│ │ ├── main.server.tsx
│ │ └── main.tsx
│ ├── tsconfig.json
│ ├── vercel.json
│ └── vite.config.ts
└── vue
│ ├── .gitignore
│ ├── index.html
│ ├── package.json
│ ├── src
│ ├── About.vue
│ ├── App.vue
│ ├── AsyncData.vue
│ ├── Counter.island.vue
│ ├── Expandable.island.vue
│ ├── Home.vue
│ ├── MediaQuery.island.vue
│ ├── PreviewApp.vue
│ ├── ServerContent.vue
│ ├── StaticContent.lagoon.vue
│ ├── capri.svg
│ ├── global.css
│ ├── main.server.ts
│ ├── main.ts
│ ├── router.ts
│ └── vue.d.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── logo.svg
├── package-lock.json
├── package.json
├── packages
├── capri
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── Template.test.ts
│ │ ├── Template.ts
│ │ ├── assets.ts
│ │ ├── bundle.ts
│ │ ├── context.ts
│ │ ├── dev.ts
│ │ ├── entry.ts
│ │ ├── fsutils.ts
│ │ ├── html.ts
│ │ ├── index.ts
│ │ ├── options.ts
│ │ ├── polyfills.ts
│ │ ├── prerender.ts
│ │ ├── render.ts
│ │ ├── types.ts
│ │ ├── utils.ts
│ │ ├── virtual
│ │ │ ├── client.ts
│ │ │ ├── hydration.ts
│ │ │ ├── ssr.ts
│ │ │ └── virtual.d.ts
│ │ ├── vite-plugin.ts
│ │ ├── vite.d.ts
│ │ └── wrapper.ts
│ ├── ssr.d.ts
│ └── tsconfig.json
├── cloudflare
│ ├── README.md
│ ├── files
│ │ └── 404.html
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── middleware.ts
│ │ ├── polyfill.ts
│ │ └── worker.ts
│ └── tsconfig.json
├── create
│ ├── .npmignore
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── create-capri.ts
│ │ ├── helpers
│ │ │ ├── examples.ts
│ │ │ ├── get-pkg-manager.ts
│ │ │ ├── git.ts
│ │ │ ├── install.ts
│ │ │ ├── is-folder-empty.ts
│ │ │ ├── is-online.ts
│ │ │ ├── is-writeable.ts
│ │ │ ├── make-dir.ts
│ │ │ └── validate-pkg.ts
│ │ └── index.ts
│ └── tsconfig.json
├── preact
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── capri.d.ts
│ │ ├── hydrate.ts
│ │ ├── index.ts
│ │ ├── island.server.jsx
│ │ ├── lagoon.client.jsx
│ │ ├── lagoon.server.jsx
│ │ └── server.ts
│ └── tsconfig.json
├── react-render-to-string
│ ├── .npmignore
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ └── node.ts
│ └── tsconfig.json
├── react
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── capri.d.ts
│ │ ├── hydrate.ts
│ │ ├── index.ts
│ │ ├── island.server.jsx
│ │ ├── lagoon.client.jsx
│ │ ├── lagoon.server.jsx
│ │ └── server.ts
│ └── tsconfig.json
├── solid
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── capri.d.ts
│ │ ├── hydrate.tsx
│ │ ├── index.ts
│ │ ├── island.server.jsx
│ │ ├── jsx.d.ts
│ │ ├── lagoon.client.jsx
│ │ └── server.ts
│ └── tsconfig.json
├── svelte
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── capri.d.ts
│ │ ├── hydrate.ts
│ │ ├── index.ts
│ │ ├── island.server.js
│ │ ├── lagoon.client.js
│ │ ├── lagoon.server.js
│ │ └── server.ts
│ └── tsconfig.json
├── tsconfig.base.json
├── tsconfig.json
├── vercel
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── edge.ts
│ │ ├── index.ts
│ │ ├── isg.ts
│ │ └── serverless.ts
│ └── tsconfig.json
└── vue
│ ├── README.md
│ ├── package.json
│ ├── src
│ ├── capri.d.ts
│ ├── hydrate.ts
│ ├── index.ts
│ ├── island.server.js
│ ├── lagoon.client.jsx
│ ├── lagoon.server.jsx
│ └── server.ts
│ └── tsconfig.json
├── test
├── dom.ts
├── e2e.test.ts
└── setup.ts
├── tsconfig.json
└── vitest.config.ts
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | lib
3 | .vercel
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | es6: true,
5 | node: true,
6 | worker: true,
7 | },
8 | parser: "@typescript-eslint/parser",
9 | parserOptions: {
10 | ecmaVersion: 12,
11 | sourceType: "module",
12 | },
13 | settings: {
14 | "import/parsers": {
15 | "@typescript-eslint/parser": [".ts", ".tsx"],
16 | },
17 | },
18 | extends: [
19 | "eslint:recommended",
20 | "plugin:@typescript-eslint/recommended",
21 | "plugin:import/recommended",
22 | "plugin:import/typescript",
23 | ],
24 | plugins: ["@typescript-eslint", "simple-import-sort"],
25 | rules: {
26 | "@typescript-eslint/no-explicit-any": "off", // Sometimes it's okay to take shortcuts. Use responsibly!
27 | "@typescript-eslint/no-non-null-assertion": "off", // Sometimes we know more than the compiler. Use responsibly!
28 | "@typescript-eslint/no-unused-vars": [
29 | "warn",
30 | {
31 | args: "none", // Unused args are fine. Helps future devs to know that they are there.
32 | ignoreRestSiblings: true, // This is a useful pattern to exclude properties from an object so we allow it.
33 | },
34 | ],
35 | "@typescript-eslint/triple-slash-reference": "off", // We need them to surface our ambient module declarations.
36 |
37 | // Enforce extensions for all imports except for jsx/tsx as they will be handled by Vite anyways.
38 | // Unfortunately this doesn't work as we'd like: https://github.com/import-js/eslint-plugin-import/issues/2111
39 | "import/extensions": [
40 | "error",
41 | "ignorePackages",
42 | {
43 | tsx: "never",
44 | jsx: "never",
45 | },
46 | ],
47 | "import/no-unresolved": "off", // Also see https://github.com/import-js/eslint-plugin-import/issues/2111
48 |
49 | "simple-import-sort/imports": "error", // Set to "error" so that --fix will do its magic
50 | "simple-import-sort/exports": "error", // Set to "error" so that --fix will do its magic
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/.github/.gitignore:
--------------------------------------------------------------------------------
1 | pages/*
2 | !pages/.*
--------------------------------------------------------------------------------
/.github/build-examples.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs/promises");
2 | const path = require("path");
3 | const { spawn } = require("child_process");
4 |
5 | async function build() {
6 | //const dirs = await fs.readdir(path.resolve(__dirname, "../examples"));
7 | const dirs = ["react", "preact", "solid", "svelte", "vue"];
8 |
9 | await Promise.all(
10 | dirs.map(
11 | (dir) =>
12 | new Promise(async (resolve, reject) => {
13 | const cwd = path.resolve(__dirname, "../examples", dir);
14 | const src = path.join(cwd, "dist");
15 | const dest = path.resolve(__dirname, "pages", dir);
16 |
17 | await fs.rm(dest, { recursive: true, force: true });
18 | const child = spawn("npm", ["run", "build"], {
19 | cwd,
20 | stdio: "inherit",
21 | env: {
22 | ...process.env,
23 | BASE_URL: `/capri/${dir}/`,
24 | },
25 | });
26 | child.once("exit", async (code) => {
27 | if (code) reject(code);
28 | else {
29 | await fs.rename(src, dest);
30 | resolve();
31 | }
32 | });
33 | }),
34 | ),
35 | );
36 | }
37 |
38 | build()
39 | .then(() => console.log("done."))
40 | .catch(console.error);
41 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | target-branch: "next"
6 | # Disable version updates for npm dependencies
7 | open-pull-requests-limit: 0
8 | schedule:
9 | interval: "daily"
10 | - package-ecosystem: "npm"
11 | directory: "/packages/capri"
12 | target-branch: "next"
13 | open-pull-requests-limit: 0
14 | schedule:
15 | interval: "daily"
16 | - package-ecosystem: "npm"
17 | directory: "/packages/create"
18 | target-branch: "next"
19 | open-pull-requests-limit: 0
20 | schedule:
21 | interval: "daily"
22 | - package-ecosystem: "npm"
23 | directory: "/packages/preact"
24 | target-branch: "next"
25 | open-pull-requests-limit: 0
26 | schedule:
27 | interval: "daily"
28 | - package-ecosystem: "npm"
29 | directory: "/packages/react"
30 | target-branch: "next"
31 | open-pull-requests-limit: 0
32 | schedule:
33 | interval: "daily"
34 | - package-ecosystem: "npm"
35 | directory: "/packages/react-render-to-string"
36 | target-branch: "next"
37 | open-pull-requests-limit: 0
38 | schedule:
39 | interval: "daily"
40 | - package-ecosystem: "npm"
41 | directory: "/packages/solid"
42 | target-branch: "next"
43 | open-pull-requests-limit: 0
44 | schedule:
45 | interval: "daily"
46 | - package-ecosystem: "npm"
47 | directory: "/packages/svelte"
48 | target-branch: "next"
49 | open-pull-requests-limit: 0
50 | schedule:
51 | interval: "daily"
52 | - package-ecosystem: "npm"
53 | directory: "/examples/preact"
54 | target-branch: "next"
55 | open-pull-requests-limit: 0
56 | schedule:
57 | interval: "daily"
58 | - package-ecosystem: "npm"
59 | directory: "/examples/react"
60 | target-branch: "next"
61 | open-pull-requests-limit: 0
62 | schedule:
63 | interval: "daily"
64 | - package-ecosystem: "npm"
65 | directory: "/examples/solid"
66 | target-branch: "next"
67 | open-pull-requests-limit: 0
68 | schedule:
69 | interval: "daily"
70 | - package-ecosystem: "npm"
71 | directory: "/examples/svelte"
72 | target-branch: "next"
73 | open-pull-requests-limit: 0
74 | schedule:
75 | interval: "daily"
76 |
--------------------------------------------------------------------------------
/.github/pages/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/capri-js/capri/aebc1391fc65eea0c3450e0976dff1a970c2e1d9/.github/pages/.nojekyll
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [main, next]
7 |
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-node@v3
14 | with:
15 | node-version: "20.x"
16 | cache: "npm"
17 | - uses: actions/cache@v3
18 | with:
19 | path: ~/.npm
20 | key: npm-${{ hashFiles('**/package-lock.json') }}
21 | - run: npm install
22 | - run: npm run lint
23 | - run: npm run build
24 | - run: npm test
25 | - run: npm run examples
26 | - run: npm run release
27 | env:
28 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
29 | - name: deploy examples
30 | if: github.ref == 'refs/heads/main'
31 | uses: JamesIves/github-pages-deploy-action@v4.3.3
32 | with:
33 | branch: gh-pages
34 | folder: .github/pages
35 | clean: true
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | lib/
4 | dist/
5 | tsconfig.tsbuildinfo
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | access=public
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | lib
4 | CHANGELOG.md
5 | .github/pages
6 | .vercel
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "typescript.preferences.importModuleSpecifierEnding": "js"
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Felix Gnass
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 | # 
2 |
3 | Capri allows you to build static websites using a frontend framework of your choice ([React/Preact/Vue/Solid/Svelte](https://capri.build/docs/frameworks/)).
4 |
5 | ## Carbon-friendly 🌱
6 |
7 | By default, **zero KB** of JavaScript is shipped to the browser.
8 |
9 | You can sprinkle in client-side interactivity by turning some of your components into [islands](https://jasonformat.com/islands-architecture/). Capri will make sure, that only that part of your JavaScript is sent down the wire that is required to let these islands become interactive.
10 |
11 | ## Use what you already know 🎓
12 |
13 | With Capri, you don't have to learn any new APIs. In fact, Capri doesn't even have an API! Use your framework's regular ecosystem as if you were building a single page app and follow these two rules:
14 |
15 | 1. 📍 Pick a router that supports server-side rendering (pretty much all popular routing libraries do this).
16 | 2. 🏝️ If a component needs to become interactive, name it `*.island.*`. Capri will take care of the rest.
17 |
18 | 👉 Visit https://capri.build to get started.
19 |
20 | ## No lock-in 🔓
21 |
22 | Should you ever decide to remove Capri from your project, you will be left with a 100% working [Vite](https://vitejs.dev/) app. Of course, instead of pre-rendered static pages, the output will then be a regular SPA.
23 |
24 | ## Bonus: Live CMS previews 🔮
25 |
26 | When you [connect](https://capri.build/docs/integrations/) your Capri website to a headless CMS, you can take further advantage of Capri's architecture, as it allows you to generate a separate SPA version of your site that can be used to live-preview any content changes without requiring a build-step or server-side rendering. You can use cheap and energy efficient static file hosting and still get real-time previews right inside your CMS.
27 |
28 | # License
29 |
30 | MIT
31 |
--------------------------------------------------------------------------------
/examples/cloudflare/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | functions/_middleware.js
5 | functions/_ssr.js
--------------------------------------------------------------------------------
/examples/cloudflare/.nvmrc:
--------------------------------------------------------------------------------
1 | 16
--------------------------------------------------------------------------------
/examples/cloudflare/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/cloudflare/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-capri-react-cloudflare-site",
3 | "private": true,
4 | "description": "Capri on Cloudflare Pages example",
5 | "version": "1.0.0",
6 | "license": "MIT",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "vite dev",
10 | "build": "vite build && vite build --ssr",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "@vitejs/plugin-react": "^4.2.1",
15 | "react": "^18.1.0",
16 | "react-dom": "^18.1.0",
17 | "react-render-to-string": "*",
18 | "react-router-dom": "^6.7.0"
19 | },
20 | "devDependencies": {
21 | "@capri-js/cloudflare": "^1.2.0",
22 | "@capri-js/react": "^5.2.0",
23 | "@types/react": "^18.0.9",
24 | "@types/react-dom": "^18.0.4",
25 | "vite": "^4.0.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/About.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | export function About() {
4 | return (
5 |
6 | This page was server-rendered on {new Date().toLocaleString()}
7 |
8 | An since it does not contain any interactive islands, no JavaScript is
9 | shipped to the browser.
10 |
11 | Home
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/App.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | min-height: 100%;
4 | padding: 0;
5 | margin: 0;
6 | background: #fafafa;
7 | font-family: "Helvetica Neue", arial, sans-serif;
8 | font-weight: 400;
9 | color: #444;
10 | }
11 |
12 | main {
13 | max-width: 60ch;
14 | margin: auto;
15 | font-size: 1.5em;
16 | line-height: 1.5;
17 | padding: 2em;
18 | }
19 |
20 | h1 {
21 | line-height: 1.1;
22 | }
23 | h1 i {
24 | font-style: normal;
25 | }
26 | h1 i::after {
27 | content: "";
28 | display: inline-block;
29 | width: 0.8em;
30 | height: 0.8em;
31 | margin-left: 0.25ch;
32 | background: url("./capri.svg") no-repeat;
33 | background-size: contain;
34 | }
35 |
36 | section {
37 | margin-bottom: 1em;
38 | }
39 |
40 | button {
41 | font-family: inherit;
42 | font-size: 0.7em;
43 | padding: 0.5em 1em;
44 | background: #15992b;
45 | color: #fff;
46 | border: none;
47 | border-radius: 4px;
48 | }
49 | button:hover {
50 | background: #56c13f;
51 | }
52 |
53 | .counter {
54 | display: inline-flex;
55 | align-items: center;
56 | gap: 0.2em;
57 | }
58 |
59 | .expandable button::after {
60 | display: inline-block;
61 | margin-left: 0.5em;
62 | content: ">";
63 | transition: all 0.2s ease-in-out;
64 | }
65 | .expandable[data-expanded="true"] > button::after {
66 | transform: rotateZ(90deg);
67 | }
68 |
69 | .expandable-content {
70 | overflow: hidden;
71 | margin: 0.5em 0;
72 | }
73 | [data-expanded="false"] > .expandable-content {
74 | height: 0;
75 | }
76 |
77 | a {
78 | color: inherit;
79 | }
80 |
81 | .box {
82 | padding: 1em;
83 | border: 1px solid #aaa;
84 | border-radius: 6px;
85 | margin: 1em 0;
86 | }
87 |
88 | .banner {
89 | background: #ff0f7f;
90 | color: #fff;
91 | padding: 0.5rem;
92 | text-transform: uppercase;
93 | font-size: 0.8rem;
94 | }
95 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 |
3 | import { Suspense } from "react";
4 | import { Route, Routes } from "react-router-dom";
5 |
6 | import { About } from "./About";
7 | import { Home } from "./Home";
8 | import { Preview } from "./Preview.jsx";
9 |
10 | export function App() {
11 | return (
12 |
13 |
14 | } />
15 | } />
16 | } />
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/Counter.island.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | type Props = {
4 | start?: number;
5 | };
6 |
7 | export default function Counter({ start = 0 }: Props) {
8 | const [counter, setCounter] = useState(start);
9 | return (
10 |
11 | setCounter((c) => c - 1)}>-
12 | {counter}
13 | setCounter((c) => c + 1)}>+
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/Expandable.island.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useState } from "react";
2 |
3 | import StaticContent from "./StaticContent.lagoon.jsx";
4 |
5 | type Props = {
6 | title: string;
7 | children?: ReactNode;
8 | };
9 | export default function Expandable({ title, children }: Props) {
10 | const [expanded, setExpanded] = useState(false);
11 | return (
12 |
13 |
14 | This is static content inside an island. We call this a lagoon.
15 |
16 |
setExpanded(!expanded)}>{title}
17 |
18 |
19 | This a second lagoon. Below you see the children that were passed to
20 | the Expandable island:
21 |
22 | {children}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/Home.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | import CounterIsland from "./Counter.island.jsx";
4 | import ExpandableIsland from "./Expandable.island.jsx";
5 | import MediaQueryIsland from "./MediaQuery.island.jsx";
6 | import { ServerContent } from "./ServerContent";
7 |
8 | export function Home() {
9 | return (
10 |
11 |
12 | Partial hydration with React and Capri
13 |
14 |
15 | This page was server-rendered on {new Date().toLocaleString()}
16 |
17 |
18 | Here is a simple counter:
19 |
20 |
21 | And here is another one, independent from the one above:{" "}
22 |
23 |
24 |
25 | This island receives children as prop. They are only rendered upon build
26 | time.
27 |
28 | The code for ServerContent
won't show up in the client
29 | bundle.
30 |
31 |
32 |
33 | Link to another page
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/MediaQuery.island.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export const options = {
4 | media: "(max-width:500px)",
5 | };
6 |
7 | export default function MediaQuery() {
8 | const [content, setContent] = useState(
9 | "Resize your browser below 500px to hydrate this island.",
10 | );
11 | useEffect(() => {
12 | setContent("The island has been hydrated.");
13 | }, []);
14 | return {content}
;
15 | }
16 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/Preview.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from "react-router-dom";
2 |
3 | /**
4 | * Handle preview requests like `/preview?slug=/about` by redirecting
5 | * to the given slug parameter.
6 | */
7 | export function Preview() {
8 | const url = new URL(window.location.href);
9 | const slug = url.searchParams.get("slug") ?? "/";
10 | return ;
11 | }
12 |
13 | /**
14 | * Component to display a banner when the site is viewed as SPA.
15 | */
16 | export function PreviewBanner() {
17 | return Preview Mode
;
18 | }
19 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/ServerContent.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | type Props = {
4 | children: ReactNode;
5 | };
6 |
7 | export function ServerContent({ children }: Props) {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/StaticContent.lagoon.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | type Props = {
4 | children?: ReactNode;
5 | };
6 |
7 | export default function StaticContent({ children }: Props) {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/capri.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/main.server.tsx:
--------------------------------------------------------------------------------
1 | import { RenderFunction, renderToString } from "@capri-js/react/server";
2 | import { StrictMode } from "react";
3 | import { StaticRouter } from "react-router-dom/server.js";
4 |
5 | import { App } from "./App";
6 |
7 | export const render: RenderFunction = async (url, context) => {
8 | context.setHeader("Cache-Control", "s-maxage=60, stale-while-revalidate=60");
9 | return {
10 | "#app": await renderToString(
11 |
12 |
13 |
14 |
15 | ,
16 | ),
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/examples/cloudflare/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { BrowserRouter } from "react-router-dom";
4 |
5 | import { App } from "./App";
6 | import { PreviewBanner } from "./Preview.jsx";
7 |
8 | ReactDOM.createRoot(document.getElementById("app")!).render(
9 |
10 |
11 |
12 |
13 |
14 | ,
15 | );
16 |
--------------------------------------------------------------------------------
/examples/cloudflare/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noEmit": true,
5 | "module": "ES2020",
6 | "moduleResolution": "Node",
7 | "target": "ES2017",
8 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
9 | "types": ["vite/client"],
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "jsx": "preserve"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/cloudflare/vite.config.ts:
--------------------------------------------------------------------------------
1 | import cloudflare from "@capri-js/cloudflare";
2 | import capri from "@capri-js/react";
3 | import react from "@vitejs/plugin-react";
4 | import { defineConfig } from "vite";
5 |
6 | export default defineConfig({
7 | plugins: [
8 | react(),
9 | capri({
10 | spa: "/preview",
11 | prerender: false,
12 | followLinks: false,
13 | target: cloudflare({ webStreamsPolyfill: true }),
14 | }),
15 | ],
16 | });
17 |
--------------------------------------------------------------------------------
/examples/preact/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | dist/
3 | node_modules/
--------------------------------------------------------------------------------
/examples/preact/capri.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/preact/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/preact/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-capri-preact-site",
3 | "private": true,
4 | "description": "Capri Preact example",
5 | "version": "1.0.0",
6 | "license": "MIT",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "vite dev",
10 | "build": "vite build && vite build --ssr",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "preact": "^10.8.2",
15 | "wouter-preact": "^2.8.1"
16 | },
17 | "devDependencies": {
18 | "@capri-js/preact": "^5.1.1",
19 | "@preact/preset-vite": "^2.8.1",
20 | "vite": "^4.0.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/preact/src/About.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "preact/compat";
2 | import { Link } from "wouter-preact";
3 |
4 | import { AsyncData } from "./AsyncData.jsx";
5 |
6 | export function About() {
7 | return (
8 |
9 |
10 | This page is completely static. Async data:
11 | loading...}>
12 |
13 |
14 |
15 |
16 | An since it does not contain any interactive islands, no JavaScript is
17 | shipped to the browser.
18 |
19 |
20 | Home
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/examples/preact/src/App.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | min-height: 100%;
4 | padding: 0;
5 | margin: 0;
6 | background: #fafafa;
7 | font-family: "Helvetica Neue", arial, sans-serif;
8 | font-weight: 400;
9 | color: #444;
10 | }
11 |
12 | main {
13 | max-width: 60ch;
14 | margin: auto;
15 | font-size: 1.5em;
16 | line-height: 1.5;
17 | padding: 2em;
18 | }
19 |
20 | h1 {
21 | line-height: 1.1;
22 | }
23 | h1 i {
24 | font-style: normal;
25 | }
26 | h1 i::after {
27 | content: "";
28 | display: inline-block;
29 | width: 0.8em;
30 | height: 0.8em;
31 | margin-left: 0.25ch;
32 | background: url("./capri.svg") no-repeat;
33 | background-size: contain;
34 | }
35 |
36 | section {
37 | margin-bottom: 1em;
38 | }
39 |
40 | button {
41 | font-family: inherit;
42 | font-size: 0.7em;
43 | padding: 0.5em 1em;
44 | background: #15992b;
45 | color: #fff;
46 | border: none;
47 | border-radius: 4px;
48 | }
49 | button:hover {
50 | background: #56c13f;
51 | }
52 |
53 | .counter {
54 | display: inline-flex;
55 | align-items: center;
56 | gap: 0.2em;
57 | }
58 |
59 | .expandable button::after {
60 | display: inline-block;
61 | margin-left: 0.5em;
62 | content: ">";
63 | transition: all 0.2s ease-in-out;
64 | }
65 | .expandable[data-expanded="true"] > button::after {
66 | transform: rotateZ(90deg);
67 | }
68 |
69 | .expandable-content {
70 | overflow: hidden;
71 | margin: 0.5em 0;
72 | }
73 | [data-expanded="false"] > .expandable-content {
74 | height: 0;
75 | }
76 |
77 | a {
78 | color: inherit;
79 | }
80 |
81 | .box {
82 | padding: 1em;
83 | border: 1px solid #aaa;
84 | border-radius: 6px;
85 | margin: 1em 0;
86 | }
87 |
88 | .banner {
89 | background: #ff0f7f;
90 | color: #fff;
91 | padding: 0.5rem;
92 | text-transform: uppercase;
93 | font-size: 0.8rem;
94 | }
95 |
--------------------------------------------------------------------------------
/examples/preact/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 |
3 | import { Route } from "wouter-preact";
4 |
5 | import { About } from "./About";
6 | import { Home } from "./Home";
7 | import { Preview } from "./Preview";
8 |
9 | export function App() {
10 | return (
11 | <>
12 |
13 |
14 |
15 | >
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/examples/preact/src/AsyncData.tsx:
--------------------------------------------------------------------------------
1 | import { useFetch } from "./useFetch.jsx";
2 |
3 | export function AsyncData() {
4 | const data = useFetch("/data");
5 | return {data} ;
6 | }
7 |
--------------------------------------------------------------------------------
/examples/preact/src/Counter.island.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "preact/hooks";
2 |
3 | export default function Counter({ start = 0 }: { start?: number }) {
4 | const [counter, setCounter] = useState(start);
5 | return (
6 |
7 | setCounter((c) => c - 1)}>-
8 | {counter}
9 | setCounter((c) => c + 1)}>+
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/examples/preact/src/Expandable.island.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentChildren } from "preact";
2 | import { useState } from "preact/hooks";
3 |
4 | import StaticContent from "./StaticContent.lagoon.jsx";
5 |
6 | type Props = {
7 | title: string;
8 | children?: ComponentChildren;
9 | };
10 | export default function Expandable({ title, children }: Props) {
11 | const [expanded, setExpanded] = useState(false);
12 | return (
13 |
14 |
15 | This is static content inside an island. We call this a lagoon.
16 |
17 |
setExpanded(!expanded)}>{title}
18 |
19 |
20 | This a second lagoon. Below you see the children that were passed to
21 | the Expandable island:
22 |
23 | {children}
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/examples/preact/src/Home.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "wouter-preact";
2 |
3 | import CounterIsland from "./Counter.island.jsx";
4 | import ExpandableIsland from "./Expandable.island.jsx";
5 | import MediaQueryIsland from "./MediaQuery.island.jsx";
6 | import { ServerContent } from "./ServerContent.jsx";
7 |
8 | export function Home() {
9 | return (
10 |
11 |
12 | Partial hydration with Preact and Capri
13 |
14 | This page is static, but contains some dynamic parts.
15 |
16 | Here is a simple counter:
17 |
18 |
19 | And here is another one, independent from the one above:{" "}
20 |
21 |
22 |
23 | This island receives children as prop. They are only rendered upon build
24 | time.
25 |
26 | The code for ServerContent
won't show up in the client
27 | bundle.
28 |
29 |
30 |
31 |
32 | Link to another page
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/examples/preact/src/MediaQuery.island.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "preact/hooks";
2 |
3 | export const options = {
4 | media: "(max-width:500px)",
5 | };
6 |
7 | export default function MediaQuery() {
8 | const [content, setContent] = useState(
9 | "Resize your browser below 500px to hydrate this island.",
10 | );
11 | useEffect(() => {
12 | setContent("The island has been hydrated.");
13 | }, []);
14 | return {content}
;
15 | }
16 |
--------------------------------------------------------------------------------
/examples/preact/src/Preview.tsx:
--------------------------------------------------------------------------------
1 | import { Redirect } from "wouter-preact";
2 |
3 | /**
4 | * Handle preview requests like `/preview?slug=/about` by redirecting
5 | * to the given slug parameter.
6 | */
7 | export function Preview() {
8 | const url = new URL(window.location.href);
9 | const slug = url.searchParams.get("slug") ?? "/";
10 | return ;
11 | }
12 |
13 | /**
14 | * Component to display a banner when the site is viewed as SPA.
15 | */
16 | export function PreviewBanner() {
17 | return Preview Mode
;
18 | }
19 |
--------------------------------------------------------------------------------
/examples/preact/src/ServerContent.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentChildren } from "preact";
2 |
3 | type Props = {
4 | children: ComponentChildren;
5 | };
6 |
7 | export function ServerContent({ children }: Props) {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/preact/src/StaticContent.lagoon.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentChildren } from "preact";
2 |
3 | type Props = {
4 | children?: ComponentChildren;
5 | };
6 |
7 | export default function StaticContent({ children }: Props) {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/preact/src/capri.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/preact/src/main.server.tsx:
--------------------------------------------------------------------------------
1 | import { RenderFunction, renderToString } from "@capri-js/preact/server";
2 | import { Router } from "wouter-preact";
3 | import staticLocationHook from "wouter-preact/static-location";
4 |
5 | import { App } from "./App";
6 |
7 | // Provide a base path. You only need this if you want to deploy your
8 | // site to a non-root directory.
9 | const base = import.meta.env.BASE_URL.slice(0, -1);
10 |
11 | export const render: RenderFunction = async (url: string) => {
12 | const hook = staticLocationHook(url.slice(base.length));
13 | const html = await renderToString(
14 |
15 |
16 | ,
17 | );
18 | return {
19 | "#app": html,
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/examples/preact/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { render } from "preact";
2 | import { Router } from "wouter-preact";
3 |
4 | import { App } from "./App";
5 | import { PreviewBanner } from "./Preview.jsx";
6 |
7 | const base = import.meta.env.BASE_URL.slice(0, -1);
8 |
9 | render(
10 |
11 |
12 |
13 | ,
14 | document.body,
15 | );
16 |
--------------------------------------------------------------------------------
/examples/preact/src/useFetch.tsx:
--------------------------------------------------------------------------------
1 | const promises = new Map();
2 | const response = new Map();
3 |
4 | const mockData: Record = {
5 | "/data": "loaded!",
6 | };
7 |
8 | function mockFetch(url: string) {
9 | return new Promise((resolve) =>
10 | setTimeout(() => resolve(mockData[url]), 100),
11 | );
12 | }
13 |
14 | export function useFetch(url: string) {
15 | const data = response.get(url);
16 | if (data) return data;
17 | let promise = promises.get(url);
18 | if (!promise) {
19 | promise = mockFetch(url).then((res) => response.set(url, res));
20 | promises.set(url, promise);
21 | }
22 | throw promise;
23 | }
24 |
--------------------------------------------------------------------------------
/examples/preact/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noEmit": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "target": "ES2017",
8 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
9 | "types": ["vite/client"],
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "jsx": "preserve",
13 | "jsxImportSource": "preact"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/preact/vite.config.ts:
--------------------------------------------------------------------------------
1 | import capri from "@capri-js/preact";
2 | import preact from "@preact/preset-vite";
3 | import { defineConfig } from "vite";
4 |
5 | export default defineConfig({
6 | plugins: [
7 | preact(),
8 | capri({
9 | spa: "/preview",
10 | }),
11 | ],
12 | });
13 |
--------------------------------------------------------------------------------
/examples/react/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | dist/
3 | node_modules/
--------------------------------------------------------------------------------
/examples/react/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-capri-react-site",
3 | "private": true,
4 | "description": "Capri react example",
5 | "version": "1.0.0",
6 | "license": "MIT",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "vite dev",
10 | "build": "vite build && vite build --ssr",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "@vitejs/plugin-react": "^4.2.1",
15 | "react": "^18.1.0",
16 | "react-dom": "^18.1.0",
17 | "react-router-dom": "^6.7.0"
18 | },
19 | "devDependencies": {
20 | "@capri-js/react": "^5.2.0",
21 | "@types/react": "^18.2.46",
22 | "@types/react-dom": "^18.0.4",
23 | "vite": "^4.0.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/react/src/About.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | import { AsyncData } from "./AsyncData.jsx";
5 |
6 | export function About() {
7 | return (
8 |
9 |
10 | This page is completely static. Async data:{" "}
11 | loading...}>
12 |
13 |
14 |
15 |
16 | An since it does not contain any interactive islands, no JavaScript is
17 | shipped to the browser.
18 |
19 | Home
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/react/src/AsyncData.tsx:
--------------------------------------------------------------------------------
1 | import { useLoaderData } from "react-router-dom";
2 |
3 | export function AsyncData() {
4 | const data = useLoaderData() as string;
5 | console.log("Data from loader", data);
6 | return {data} ;
7 | }
8 |
--------------------------------------------------------------------------------
/examples/react/src/Counter.island.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | type Props = {
4 | start?: number;
5 | };
6 |
7 | export default function Counter({ start = 0 }: Props) {
8 | const [counter, setCounter] = useState(start);
9 | return (
10 |
11 | setCounter((c) => c - 1)}>-
12 | {counter}
13 | setCounter((c) => c + 1)}>+
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/examples/react/src/Expandable.island.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useState } from "react";
2 |
3 | import StaticContent from "./StaticContent.lagoon.jsx";
4 |
5 | type Props = {
6 | title: string;
7 | children?: ReactNode;
8 | };
9 | export default function Expandable({ title, children }: Props) {
10 | const [expanded, setExpanded] = useState(false);
11 | return (
12 |
13 |
14 | This is static content inside an island. We call this a lagoon.
15 |
16 |
setExpanded(!expanded)}>{title}
17 |
18 |
19 | This a second lagoon. Below you see the children that were passed to
20 | the Expandable island:
21 |
22 | {children}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/examples/react/src/Home.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | import CounterIsland from "./Counter.island.jsx";
4 | import ExpandableIsland from "./Expandable.island.jsx";
5 | import MediaQueryIsland from "./MediaQuery.island.jsx";
6 | import { ServerContent } from "./ServerContent";
7 |
8 | export function Home() {
9 | return (
10 |
11 |
12 | Partial hydration with React and Capri
13 |
14 | This page is static, but contains some dynamic parts.
15 |
16 | Here is a simple counter:
17 |
18 |
19 | And here is another one, independent from the one above:{" "}
20 |
21 |
22 |
23 | This island receives children as prop. They are only rendered upon build
24 | time.
25 |
26 | The code for ServerContent
won't show up in the client
27 | bundle.
28 |
29 |
30 |
31 | Link to another page
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/examples/react/src/MediaQuery.island.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export const options = {
4 | media: "(max-width:500px)",
5 | };
6 |
7 | export default function MediaQuery() {
8 | const [content, setContent] = useState(
9 | "Resize your browser below 500px to hydrate this island.",
10 | );
11 | useEffect(() => {
12 | setContent("The island has been hydrated.");
13 | }, []);
14 | return {content}
;
15 | }
16 |
--------------------------------------------------------------------------------
/examples/react/src/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import { useStatus } from "@capri-js/react/server";
2 |
3 | export function NotFound() {
4 | useStatus(404);
5 | return (
6 |
7 | 404 - Not found.
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/examples/react/src/Preview.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from "react-router-dom";
2 |
3 | /**
4 | * Handle preview requests like `/preview?slug=/about` by redirecting
5 | * to the given slug parameter.
6 | */
7 | export function Preview() {
8 | const url = new URL(window.location.href);
9 | const slug = url.searchParams.get("slug") ?? "/";
10 | return ;
11 | }
12 |
13 | /**
14 | * Component to display a banner when the site is viewed as SPA.
15 | */
16 | export function PreviewBanner() {
17 | return Preview Mode
;
18 | }
19 |
--------------------------------------------------------------------------------
/examples/react/src/ServerContent.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | type Props = {
4 | children: ReactNode;
5 | };
6 |
7 | export function ServerContent({ children }: Props) {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/react/src/StaticContent.lagoon.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | type Props = {
4 | children?: ReactNode;
5 | };
6 |
7 | export default function StaticContent({ children }: Props) {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/react/src/capri.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/react/src/main.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | min-height: 100%;
4 | padding: 0;
5 | margin: 0;
6 | background: #fafafa;
7 | font-family: "Helvetica Neue", arial, sans-serif;
8 | font-weight: 400;
9 | color: #444;
10 | }
11 |
12 | main {
13 | max-width: 60ch;
14 | margin: auto;
15 | font-size: 1.5em;
16 | line-height: 1.5;
17 | padding: 2em;
18 | }
19 |
20 | h1 {
21 | line-height: 1.1;
22 | }
23 | h1 i {
24 | font-style: normal;
25 | }
26 | h1 i::after {
27 | content: "";
28 | display: inline-block;
29 | width: 0.8em;
30 | height: 0.8em;
31 | margin-left: 0.25ch;
32 | background: url("./capri.svg") no-repeat;
33 | background-size: contain;
34 | }
35 |
36 | section {
37 | margin-bottom: 1em;
38 | }
39 |
40 | button {
41 | font-family: inherit;
42 | font-size: 0.7em;
43 | padding: 0.5em 1em;
44 | background: #15992b;
45 | color: #fff;
46 | border: none;
47 | border-radius: 4px;
48 | }
49 | button:hover {
50 | background: #56c13f;
51 | }
52 |
53 | .counter {
54 | display: inline-flex;
55 | align-items: center;
56 | gap: 0.2em;
57 | }
58 |
59 | .expandable button::after {
60 | display: inline-block;
61 | margin-left: 0.5em;
62 | content: ">";
63 | transition: all 0.2s ease-in-out;
64 | }
65 | .expandable[data-expanded="true"] > button::after {
66 | transform: rotateZ(90deg);
67 | }
68 |
69 | .expandable-content {
70 | overflow: hidden;
71 | margin: 0.5em 0;
72 | }
73 | [data-expanded="false"] > .expandable-content {
74 | height: 0;
75 | }
76 |
77 | a {
78 | color: inherit;
79 | }
80 |
81 | .box {
82 | padding: 1em;
83 | border: 1px solid #aaa;
84 | border-radius: 6px;
85 | margin: 1em 0;
86 | }
87 |
88 | .banner {
89 | background: #ff0f7f;
90 | color: #fff;
91 | padding: 0.5rem;
92 | text-transform: uppercase;
93 | font-size: 0.8rem;
94 | }
95 |
--------------------------------------------------------------------------------
/examples/react/src/main.server.tsx:
--------------------------------------------------------------------------------
1 | import "./main.css";
2 |
3 | import { RenderFunction, renderToString } from "@capri-js/react/server";
4 | import { StrictMode } from "react";
5 | import { createMemoryRouter, RouterProvider } from "react-router-dom";
6 |
7 | import { routes } from "./routes.jsx";
8 |
9 | export const render: RenderFunction = async (url, context) => {
10 | const router = createMemoryRouter(routes, {
11 | basename: import.meta.env.BASE_URL,
12 | initialEntries: [url],
13 | });
14 |
15 | // Wait until the data is loaded ...
16 | await isInitialized(router);
17 |
18 | const root = (
19 |
20 |
21 |
22 | );
23 | return {
24 | "#app": renderToString(root, context),
25 | };
26 | };
27 |
28 | function isInitialized(router: ReturnType) {
29 | return new Promise((resolve) => {
30 | if (router.state.initialized) return resolve();
31 | router.subscribe((state) => {
32 | if (state.initialized) resolve();
33 | });
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/examples/react/src/main.tsx:
--------------------------------------------------------------------------------
1 | import "./main.css";
2 |
3 | import { StrictMode } from "react";
4 | import ReactDOM from "react-dom/client";
5 | import { createBrowserRouter, RouterProvider } from "react-router-dom";
6 |
7 | import { PreviewBanner } from "./Preview.jsx";
8 | import { routes } from "./routes.jsx";
9 |
10 | const router = createBrowserRouter(routes, {
11 | basename: import.meta.env.BASE_URL,
12 | });
13 |
14 | function App() {
15 | return (
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | ReactDOM.createRoot(document.getElementById("app")!).render( );
24 |
--------------------------------------------------------------------------------
/examples/react/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import { RouteObject } from "react-router-dom";
2 |
3 | import { About } from "./About.jsx";
4 | import { Home } from "./Home.jsx";
5 | import { NotFound } from "./NotFound.jsx";
6 | import { Preview } from "./Preview.jsx";
7 |
8 | export const routes: RouteObject[] = [
9 | { path: "/", index: true, element: },
10 | {
11 | path: "/about",
12 | element: ,
13 | loader: () =>
14 | new Promise((resolve) => {
15 | setTimeout(() => resolve("Loaded."), 250);
16 | }),
17 | },
18 | { path: "/preview", element: },
19 | { path: "*", element: },
20 | ];
21 |
--------------------------------------------------------------------------------
/examples/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noEmit": true,
5 | "module": "ES2020",
6 | "moduleResolution": "Node",
7 | "target": "ES2017",
8 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
9 | "types": ["vite/client"],
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "jsx": "preserve"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/react/vite.config.ts:
--------------------------------------------------------------------------------
1 | import capri from "@capri-js/react";
2 | import react from "@vitejs/plugin-react";
3 | import { defineConfig } from "vite";
4 |
5 | export default defineConfig({
6 | plugins: [
7 | react(),
8 | capri({
9 | spa: "/preview",
10 | }),
11 | ],
12 | });
13 |
--------------------------------------------------------------------------------
/examples/solid/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | dist/
3 | node_modules/
--------------------------------------------------------------------------------
/examples/solid/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/solid/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-capri-solid-site",
3 | "private": true,
4 | "description": "Capri SolidJS example",
5 | "version": "1.0.0",
6 | "license": "MIT",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "vite dev",
10 | "build": "vite build && vite build --ssr",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "@solidjs/router": "^0.5.1",
15 | "solid-js": "^1.6.5"
16 | },
17 | "devDependencies": {
18 | "@capri-js/solid": "^5.1.1",
19 | "vite": "^4.0.0",
20 | "vite-plugin-solid": "^2.5.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/solid/src/About.tsx:
--------------------------------------------------------------------------------
1 | import { A } from "@solidjs/router";
2 | import { Suspense } from "solid-js";
3 |
4 | import { AsyncData } from "./AsyncData.jsx";
5 |
6 | export function About() {
7 | return (
8 |
9 |
10 | This page is completely static. Async data:
11 | loading...}>
12 |
13 |
14 |
15 |
16 | An since it does not contain any interactive islands, no JavaScript is
17 | shipped to the browser.
18 |
19 | Home
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/solid/src/App.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | min-height: 100%;
4 | padding: 0;
5 | margin: 0;
6 | background: #fafafa;
7 | font-family: "Helvetica Neue", arial, sans-serif;
8 | font-weight: 400;
9 | color: #444;
10 | }
11 |
12 | main {
13 | max-width: 60ch;
14 | margin: auto;
15 | font-size: 1.5em;
16 | line-height: 1.5;
17 | padding: 2em;
18 | }
19 |
20 | h1 {
21 | line-height: 1.1;
22 | }
23 | h1 i {
24 | font-style: normal;
25 | }
26 | h1 i::after {
27 | content: "";
28 | display: inline-block;
29 | width: 0.8em;
30 | height: 0.8em;
31 | margin-left: 0.25ch;
32 | background: url("./capri.svg") no-repeat;
33 | background-size: contain;
34 | }
35 |
36 | section {
37 | margin-bottom: 1em;
38 | }
39 |
40 | button {
41 | font-family: inherit;
42 | font-size: 0.7em;
43 | padding: 0.5em 1em;
44 | background: #15992b;
45 | color: #fff;
46 | border: none;
47 | border-radius: 4px;
48 | }
49 | button:hover {
50 | background: #56c13f;
51 | }
52 |
53 | .counter {
54 | display: inline-flex;
55 | align-items: center;
56 | gap: 0.2em;
57 | }
58 |
59 | .expandable button::after {
60 | display: inline-block;
61 | margin-left: 0.5em;
62 | content: ">";
63 | transition: all 0.2s ease-in-out;
64 | }
65 | .expandable[data-expanded="true"] > button::after {
66 | transform: rotateZ(90deg);
67 | }
68 |
69 | .expandable-content {
70 | overflow: hidden;
71 | margin: 0.5em 0;
72 | }
73 | [data-expanded="false"] > .expandable-content {
74 | height: 0;
75 | }
76 |
77 | a {
78 | color: inherit;
79 | }
80 |
81 | .box {
82 | padding: 1em;
83 | border: 1px solid #aaa;
84 | border-radius: 6px;
85 | margin: 1em 0;
86 | }
87 |
88 | .banner {
89 | background: #ff0f7f;
90 | color: #fff;
91 | padding: 0.5rem;
92 | text-transform: uppercase;
93 | font-size: 0.8rem;
94 | }
95 |
--------------------------------------------------------------------------------
/examples/solid/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 |
3 | import { Route, Routes } from "@solidjs/router";
4 | import { NoHydration } from "solid-js/web";
5 |
6 | import { About } from "./About";
7 | import { Home } from "./Home";
8 |
9 | export function App() {
10 | return (
11 |
12 |
13 | } />
14 | } />
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/examples/solid/src/AsyncData.tsx:
--------------------------------------------------------------------------------
1 | import { createResource } from "solid-js";
2 |
3 | function fetchData() {
4 | return new Promise((resolve) =>
5 | setTimeout(() => resolve("loaded!"), 500),
6 | );
7 | }
8 |
9 | export function AsyncData() {
10 | const [data] = createResource(fetchData);
11 | return {data} ;
12 | }
13 |
--------------------------------------------------------------------------------
/examples/solid/src/Counter.island.tsx:
--------------------------------------------------------------------------------
1 | import { createSignal } from "solid-js";
2 |
3 | type Props = {
4 | start?: number;
5 | };
6 |
7 | export default function Counter({ start = 0 }: Props) {
8 | const [counter, setCounter] = createSignal(start);
9 | return (
10 |
11 | setCounter((c) => c - 1)}>-
12 | {counter()}
13 | setCounter((c) => c + 1)}>+
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/examples/solid/src/Expandable.island.tsx:
--------------------------------------------------------------------------------
1 | import { createSignal } from "solid-js";
2 |
3 | import StaticContent from "./StaticContent.lagoon.jsx";
4 |
5 | type Props = {
6 | title: string;
7 | children?: any;
8 | };
9 |
10 | export default function Expandable({ title, children }: Props) {
11 | const [expanded, setExpanded] = createSignal(false);
12 | return (
13 |
14 |
15 | This is static content inside an island. We call this a lagoon.
16 |
17 |
setExpanded((expanded) => !expanded)}>
18 | {title}
19 |
20 |
21 |
22 | This a second lagoon. Below you see the children that were passed to
23 | the Expandable island:
24 |
25 | {children}
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/examples/solid/src/Home.tsx:
--------------------------------------------------------------------------------
1 | import { A } from "@solidjs/router";
2 |
3 | import CounterIsland from "./Counter.island";
4 | import ExpandableIsland from "./Expandable.island.jsx";
5 | import MediaQueryIsland from "./MediaQuery.island.jsx";
6 | import { ServerContent } from "./ServerContent.jsx";
7 |
8 | export function Home() {
9 | return (
10 |
11 |
12 | Partial hydration with SolidJS and Capri
13 |
14 | This page is static, but contains some dynamic parts.
15 |
16 | This counter is an interactive island:
17 |
18 |
19 | And here is another one, independent from the one above:{" "}
20 |
21 |
22 |
23 | This island receives children as prop. They are only rendered upon build
24 | time.
25 |
26 | The code for ServerContent
won't show up in the client
27 | bundle.
28 |
29 |
30 |
31 | Link to another page
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/examples/solid/src/MediaQuery.island.tsx:
--------------------------------------------------------------------------------
1 | import { createSignal, onMount } from "solid-js";
2 |
3 | export const options = {
4 | media: "(max-width:500px)",
5 | };
6 |
7 | export default function MediaQuery() {
8 | const [content, setContent] = createSignal(
9 | "Resize your browser below 500px to hydrate this island.",
10 | );
11 | onMount(() => {
12 | setContent("The island has been hydrated.");
13 | });
14 | return {content}
;
15 | }
16 |
--------------------------------------------------------------------------------
/examples/solid/src/ServerContent.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | children: any;
3 | };
4 |
5 | export function ServerContent({ children }: Props) {
6 | if (!children) {
7 | throw new Error("TEST: THIS CODE MUST NOT SHOW UP IN THE CLIENT BUNDLE");
8 | }
9 | return {children}
;
10 | }
11 |
--------------------------------------------------------------------------------
/examples/solid/src/StaticContent.lagoon.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | children?: any;
3 | };
4 |
5 | export default function StaticContent({ children }: Props) {
6 | return {children}
;
7 | }
8 |
--------------------------------------------------------------------------------
/examples/solid/src/capri.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/solid/src/main.server.tsx:
--------------------------------------------------------------------------------
1 | import { RenderFunction, renderToString } from "@capri-js/solid/server";
2 | import { Router } from "@solidjs/router";
3 | import { generateHydrationScript } from "solid-js/web";
4 |
5 | import { App } from "./App";
6 |
7 | export const render: RenderFunction = async (url: string) => {
8 | const html = await renderToString(() => (
9 |
10 |
11 |
12 | ));
13 | return {
14 | "#app": html,
15 | body: generateHydrationScript(),
16 | };
17 | };
18 |
--------------------------------------------------------------------------------
/examples/solid/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { Router } from "@solidjs/router";
2 | import { render } from "solid-js/web";
3 |
4 | import { App } from "./App";
5 |
6 | render(
7 | () => (
8 |
9 |
10 |
11 | ),
12 | document.getElementById("app")!,
13 | );
14 |
--------------------------------------------------------------------------------
/examples/solid/src/vite.d.ts:
--------------------------------------------------------------------------------
1 | import "vite";
2 |
3 | declare module "vite" {
4 | export interface UserConfig {
5 | ssr?: SSROptions;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/solid/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noEmit": true,
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleResolution": "Node",
8 | "esModuleInterop": true,
9 | "jsx": "preserve",
10 | "jsxImportSource": "solid-js",
11 | "types": ["vite/client"]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/examples/solid/vite.config.ts:
--------------------------------------------------------------------------------
1 | import capri from "@capri-js/solid";
2 | import { defineConfig } from "vite";
3 | import solid from "vite-plugin-solid";
4 |
5 | export default defineConfig({
6 | ssr: {
7 | // In order to make solid-app-router work in SSR mode we have to
8 | // prevent it from being externalized ...
9 | noExternal: ["solid-app-router"],
10 | },
11 | plugins: [
12 | solid({
13 | ssr: true,
14 | }),
15 | capri({
16 | spa: "/preview",
17 | }),
18 | ],
19 | });
20 |
--------------------------------------------------------------------------------
/examples/svelte/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | dist/
3 | node_modules/
--------------------------------------------------------------------------------
/examples/svelte/capri.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/svelte/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/svelte/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-capri-svelte-site",
3 | "private": true,
4 | "description": "Capri Svelte example",
5 | "version": "1.0.0",
6 | "license": "MIT",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "vite dev",
10 | "build": "vite build && vite build --ssr",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "@capri-js/svelte": "^2.1.1",
15 | "svelte-pilot": "^0.3.1",
16 | "svelte-preprocess": "^4.10.7"
17 | },
18 | "devDependencies": {
19 | "@sveltejs/vite-plugin-svelte": "^2.0.2",
20 | "svelte": "^3.48.0",
21 | "vite": "^4.0.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/svelte/src/About.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
29 |
30 |
31 | This page is completely static.
32 |
33 | An since it does not contain any interactive islands, no JavaScript is
34 | shipped to the browser. Async data: {ssrState}
35 |
36 | Home
37 |
38 |
--------------------------------------------------------------------------------
/examples/svelte/src/Counter.island.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | counter--}>-
7 | {counter}
8 | counter++}>+
9 |
10 |
11 |
17 |
--------------------------------------------------------------------------------
/examples/svelte/src/Expandable.island.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
(expanded = !expanded)}>
10 |
11 |
12 |
13 |
14 | This a second lagoon. Below you see the body slot that was passed to the
15 | Expandable island:
16 |
17 |
18 |
19 |
20 |
21 |
42 |
--------------------------------------------------------------------------------
/examples/svelte/src/Home.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 | Partial hydration with Svelte and Capri
12 |
13 | This page is static, but contains some dynamic parts.
14 |
15 | Here is a simple counter:
16 |
17 |
18 | And here is another one, independent from the one above:
21 |
22 |
23 | Click to expand
24 |
25 |
26 | The code for ServerContent
won't show up in the client bundle.
27 |
28 |
29 |
30 |
31 | Link to another page
32 |
33 |
--------------------------------------------------------------------------------
/examples/svelte/src/MediaQuery.island.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
14 |
15 |
16 | {content}
17 |
18 |
--------------------------------------------------------------------------------
/examples/svelte/src/ServerContent.svelte:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/svelte/src/StaticContent.lagoon.svelte:
--------------------------------------------------------------------------------
1 |
2 | This is static content inside an island. We call this a lagoon.
3 |
4 |
--------------------------------------------------------------------------------
/examples/svelte/src/capri.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/svelte/src/global.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | min-height: 100%;
4 | padding: 0;
5 | margin: 0;
6 | background: #fafafa;
7 | font-family: "Helvetica Neue", arial, sans-serif;
8 | font-weight: 400;
9 | color: #444;
10 | }
11 |
12 | main {
13 | max-width: 60ch;
14 | margin: auto;
15 | font-size: 1.5em;
16 | line-height: 1.5;
17 | padding: 2em;
18 | }
19 |
20 | h1 {
21 | line-height: 1.1;
22 | }
23 | h1 i {
24 | font-style: normal;
25 | }
26 | h1 i::after {
27 | content: "";
28 | display: inline-block;
29 | width: 0.8em;
30 | height: 0.8em;
31 | margin-left: 0.25ch;
32 | background: url("./capri.svg") no-repeat;
33 | background-size: contain;
34 | }
35 |
36 | section {
37 | margin-bottom: 1em;
38 | }
39 |
40 | button {
41 | font-family: inherit;
42 | font-size: 0.7em;
43 | padding: 0.5em 1em;
44 | background: #15992b;
45 | color: #fff;
46 | border: none;
47 | border-radius: 4px;
48 | }
49 | button:hover {
50 | background: #56c13f;
51 | }
52 |
53 | a {
54 | color: inherit;
55 | }
56 |
57 | .box {
58 | padding: 1em;
59 | border: 1px solid #aaa;
60 | border-radius: 6px;
61 | margin: 1em 0;
62 | }
63 |
64 | .banner {
65 | background: #ff0f7f;
66 | color: #fff;
67 | padding: 0.5rem;
68 | text-transform: uppercase;
69 | font-size: 0.8rem;
70 | }
71 |
--------------------------------------------------------------------------------
/examples/svelte/src/main.server.ts:
--------------------------------------------------------------------------------
1 | import { RenderFunction } from "@capri-js/svelte/server";
2 | import { ServerApp } from "svelte-pilot";
3 |
4 | import router from "./router.js";
5 |
6 | export const render: RenderFunction = async (url: string) => {
7 | const matched = await router.handle(`http://127.0.0.1${url}`);
8 | if (!matched) throw new Error(`No matching route: ${url}`);
9 | const { route, ssrState } = matched;
10 | const { head, html } = ServerApp.render({ router, route, ssrState });
11 | return {
12 | head,
13 | body: html,
14 | };
15 | };
16 |
--------------------------------------------------------------------------------
/examples/svelte/src/main.ts:
--------------------------------------------------------------------------------
1 | import "./global.css";
2 |
3 | import { ClientApp } from "svelte-pilot";
4 |
5 | import router from "./router.js";
6 |
7 | new ClientApp({
8 | target: document.body,
9 | props: {
10 | router,
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/examples/svelte/src/router.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "svelte-pilot";
2 |
3 | import * as About from "./About.svelte";
4 | import * as Home from "./Home.svelte";
5 |
6 | const routes = [
7 | {
8 | path: "/",
9 | component: Home,
10 | },
11 |
12 | {
13 | path: "/about",
14 | component: About,
15 | },
16 | ];
17 |
18 | export default new Router({
19 | routes,
20 | base: import.meta.env.BASE_URL,
21 | mode: import.meta.env.SSR ? "server" : "client",
22 | });
23 |
--------------------------------------------------------------------------------
/examples/svelte/src/svelte.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.svelte" {
2 | const value: any;
3 | export default value;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/svelte/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noEmit": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "target": "ES2017",
8 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
9 | "types": ["vite/client"],
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "jsx": "preserve",
13 | "jsxImportSource": "preact"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/svelte/vite.config.js:
--------------------------------------------------------------------------------
1 | import capri from "@capri-js/svelte";
2 | import { svelte } from "@sveltejs/vite-plugin-svelte";
3 | import sveltePreprocess from "svelte-preprocess";
4 | import { defineConfig } from "vite";
5 |
6 | export default defineConfig((env) => ({
7 | ssr: {
8 | noExternal: ["svelte-pilot"],
9 | },
10 | plugins: [
11 | svelte({
12 | preprocess: [
13 | sveltePreprocess({
14 | preserve: ["json"],
15 | }),
16 | ],
17 | compilerOptions: {
18 | hydratable: true,
19 | },
20 | }),
21 | capri(),
22 | ],
23 | }));
24 |
--------------------------------------------------------------------------------
/examples/vercel/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | .vercel
4 |
--------------------------------------------------------------------------------
/examples/vercel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/vercel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-capri-react-vercel-site",
3 | "private": true,
4 | "description": "Capri React on Vercel example",
5 | "version": "1.0.0",
6 | "license": "MIT",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "vite dev",
10 | "build": "vite build && vite build --ssr",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "@vitejs/plugin-react": "^3.0.0",
15 | "react": "^18.1.0",
16 | "react-dom": "^18.1.0",
17 | "react-router-dom": "^6.3.0"
18 | },
19 | "devDependencies": {
20 | "@capri-js/react": "^5.2.0",
21 | "@capri-js/vercel": "^1.1.0",
22 | "@types/react": "^18.0.9",
23 | "@types/react-dom": "^18.0.4",
24 | "vite": "^4.0.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/examples/vercel/src/About.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | export function About() {
4 | return (
5 |
6 | This page was server-rendered on {new Date().toLocaleString()}
7 |
8 | An since it does not contain any interactive islands, no JavaScript is
9 | shipped to the browser.
10 |
11 | Home
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/examples/vercel/src/App.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | min-height: 100%;
4 | padding: 0;
5 | margin: 0;
6 | background: #fafafa;
7 | font-family: "Helvetica Neue", arial, sans-serif;
8 | font-weight: 400;
9 | color: #444;
10 | }
11 |
12 | main {
13 | max-width: 60ch;
14 | margin: auto;
15 | font-size: 1.5em;
16 | line-height: 1.5;
17 | padding: 2em;
18 | }
19 |
20 | h1 {
21 | line-height: 1.1;
22 | }
23 | h1 i {
24 | font-style: normal;
25 | }
26 | h1 i::after {
27 | content: "";
28 | display: inline-block;
29 | width: 0.8em;
30 | height: 0.8em;
31 | margin-left: 0.25ch;
32 | background: url("./capri.svg") no-repeat;
33 | background-size: contain;
34 | }
35 |
36 | section {
37 | margin-bottom: 1em;
38 | }
39 |
40 | button {
41 | font-family: inherit;
42 | font-size: 0.7em;
43 | padding: 0.5em 1em;
44 | background: #15992b;
45 | color: #fff;
46 | border: none;
47 | border-radius: 4px;
48 | }
49 | button:hover {
50 | background: #56c13f;
51 | }
52 |
53 | .counter {
54 | display: inline-flex;
55 | align-items: center;
56 | gap: 0.2em;
57 | }
58 |
59 | .expandable button::after {
60 | display: inline-block;
61 | margin-left: 0.5em;
62 | content: ">";
63 | transition: all 0.2s ease-in-out;
64 | }
65 | .expandable[data-expanded="true"] > button::after {
66 | transform: rotateZ(90deg);
67 | }
68 |
69 | .expandable-content {
70 | overflow: hidden;
71 | margin: 0.5em 0;
72 | }
73 | [data-expanded="false"] > .expandable-content {
74 | height: 0;
75 | }
76 |
77 | a {
78 | color: inherit;
79 | }
80 |
81 | .box {
82 | padding: 1em;
83 | border: 1px solid #aaa;
84 | border-radius: 6px;
85 | margin: 1em 0;
86 | }
87 |
88 | .banner {
89 | background: #ff0f7f;
90 | color: #fff;
91 | padding: 0.5rem;
92 | text-transform: uppercase;
93 | font-size: 0.8rem;
94 | }
95 |
--------------------------------------------------------------------------------
/examples/vercel/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 |
3 | import { Suspense } from "react";
4 | import { Route, Routes } from "react-router-dom";
5 |
6 | import { About } from "./About";
7 | import { Home } from "./Home";
8 | import { Preview } from "./Preview.jsx";
9 |
10 | export function App() {
11 | return (
12 |
13 |
14 | } />
15 | } />
16 | } />
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/examples/vercel/src/Counter.island.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | type Props = {
4 | start?: number;
5 | };
6 |
7 | export default function Counter({ start = 0 }: Props) {
8 | const [counter, setCounter] = useState(start);
9 | return (
10 |
11 | setCounter((c) => c - 1)}>-
12 | {counter}
13 | setCounter((c) => c + 1)}>+
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/examples/vercel/src/Expandable.island.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useState } from "react";
2 |
3 | import StaticContent from "./StaticContent.lagoon.jsx";
4 |
5 | type Props = {
6 | title: string;
7 | children?: ReactNode;
8 | };
9 | export default function Expandable({ title, children }: Props) {
10 | const [expanded, setExpanded] = useState(false);
11 | return (
12 |
13 |
14 | This is static content inside an island. We call this a lagoon.
15 |
16 |
setExpanded(!expanded)}>{title}
17 |
18 |
19 | This a second lagoon. Below you see the children that were passed to
20 | the Expandable island:
21 |
22 | {children}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/examples/vercel/src/Home.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | import CounterIsland from "./Counter.island.jsx";
4 | import ExpandableIsland from "./Expandable.island.jsx";
5 | import MediaQueryIsland from "./MediaQuery.island.jsx";
6 | import { ServerContent } from "./ServerContent";
7 |
8 | export function Home() {
9 | return (
10 |
11 |
12 | Partial hydration with React and Capri
13 |
14 |
15 | This page was server-rendered on {new Date().toLocaleString()}
16 |
17 |
18 | Here is a simple counter:
19 |
20 |
21 | And here is another one, independent from the one above:{" "}
22 |
23 |
24 |
25 | This island receives children as prop. They are only rendered upon build
26 | time.
27 |
28 | The code for ServerContent
won't show up in the client
29 | bundle.
30 |
31 |
32 |
33 | Link to another page
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/examples/vercel/src/MediaQuery.island.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export const options = {
4 | media: "(max-width:500px)",
5 | };
6 |
7 | export default function MediaQuery() {
8 | const [content, setContent] = useState(
9 | "Resize your browser below 500px to hydrate this island.",
10 | );
11 | useEffect(() => {
12 | setContent("The island has been hydrated.");
13 | }, []);
14 | return {content}
;
15 | }
16 |
--------------------------------------------------------------------------------
/examples/vercel/src/Preview.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from "react-router-dom";
2 |
3 | /**
4 | * Handle preview requests like `/preview?slug=/about` by redirecting
5 | * to the given slug parameter.
6 | */
7 | export function Preview() {
8 | const url = new URL(window.location.href);
9 | const slug = url.searchParams.get("slug") ?? "/";
10 | return ;
11 | }
12 |
13 | /**
14 | * Component to display a banner when the site is viewed as SPA.
15 | */
16 | export function PreviewBanner() {
17 | return Preview Mode
;
18 | }
19 |
--------------------------------------------------------------------------------
/examples/vercel/src/ServerContent.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | type Props = {
4 | children: ReactNode;
5 | };
6 |
7 | export function ServerContent({ children }: Props) {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/vercel/src/StaticContent.lagoon.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | type Props = {
4 | children?: ReactNode;
5 | };
6 |
7 | export default function StaticContent({ children }: Props) {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/vercel/src/capri.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vercel/src/main.server.tsx:
--------------------------------------------------------------------------------
1 | import { RenderFunction, renderToString } from "@capri-js/react/server";
2 | import { StrictMode } from "react";
3 | import { StaticRouter } from "react-router-dom/server.js";
4 |
5 | import { App } from "./App";
6 |
7 | export const render: RenderFunction = async (url, context) => {
8 | context.setHeader("Cache-Control", "s-maxage=60, stale-while-revalidate=60");
9 | return {
10 | "#app": await renderToString(
11 |
12 |
13 |
14 |
15 | ,
16 | ),
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/examples/vercel/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { BrowserRouter } from "react-router-dom";
4 |
5 | import { App } from "./App";
6 | import { PreviewBanner } from "./Preview.jsx";
7 |
8 | ReactDOM.createRoot(document.getElementById("app")!).render(
9 |
10 |
11 |
12 |
13 |
14 | ,
15 | );
16 |
--------------------------------------------------------------------------------
/examples/vercel/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noEmit": true,
5 | "module": "ES2020",
6 | "moduleResolution": "Node",
7 | "target": "ES2017",
8 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
9 | "types": ["vite/client"],
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "jsx": "preserve"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/vercel/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "build": {
3 | "env": {
4 | "ENABLE_VC_BUILD": "1"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/vercel/vite.config.ts:
--------------------------------------------------------------------------------
1 | import capri from "@capri-js/react";
2 | import vercel from "@capri-js/vercel";
3 | import react from "@vitejs/plugin-react";
4 | import { defineConfig } from "vite";
5 |
6 | export default defineConfig({
7 | plugins: [
8 | react(),
9 | capri({
10 | spa: "/preview",
11 | prerender: false,
12 | target: vercel({
13 | isg: {
14 | expiration: 60,
15 | },
16 | }),
17 | }),
18 | ],
19 | });
20 |
--------------------------------------------------------------------------------
/examples/vue/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | dist/
3 | node_modules/
--------------------------------------------------------------------------------
/examples/vue/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-capri-vue-site",
3 | "private": true,
4 | "description": "Capri Vue example",
5 | "version": "1.0.0",
6 | "license": "MIT",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "vite dev",
10 | "build": "vite build && vite build --ssr",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "vue": "^3.2.37",
15 | "vue-router": "^4.1.6"
16 | },
17 | "devDependencies": {
18 | "@capri-js/vue": "^2.1.1",
19 | "@vitejs/plugin-vue": "^4.0.0",
20 | "typescript": "^4.7.4",
21 | "vite": "^4.0.0",
22 | "vue-tsc": "^0.38.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/vue/src/About.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | This page is completely static.
7 |
8 | An since it does not contain any interactive islands, no JavaScript is
9 | shipped to the browser. Async data:
10 |
11 | Loading...
12 |
13 |
14 |
15 | Home
16 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/vue/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/examples/vue/src/AsyncData.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 | {{ data }}
12 |
13 |
--------------------------------------------------------------------------------
/examples/vue/src/Counter.island.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 | -
14 | {{ count }}
15 | +
16 |
17 |
18 |
19 |
37 |
--------------------------------------------------------------------------------
/examples/vue/src/Expandable.island.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 | Title
12 |
13 |
14 |
15 | This a second lagoon. Below you see the body slot that was passed to the
16 | Expandable island:
17 |
18 | Body
19 |
20 |
21 |
22 |
23 |
56 |
--------------------------------------------------------------------------------
/examples/vue/src/Home.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 | Partial hydration with Vue and Capri
10 | This page is static, but contains some dynamic parts.
11 | Here is a simple counter:
12 |
13 | And here is another one, independent from the one above:
14 |
15 |
16 |
17 | Click to expand
18 |
19 |
20 | The code for ServerContent
won't show up in the client
21 | bundle.
22 |
23 |
24 |
25 |
26 | Another page
27 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/vue/src/MediaQuery.island.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
16 |
17 |
18 | {{ content }}
19 |
20 |
--------------------------------------------------------------------------------
/examples/vue/src/PreviewApp.vue:
--------------------------------------------------------------------------------
1 |
2 | Preview
3 |
4 |
5 |
6 |
15 |
--------------------------------------------------------------------------------
/examples/vue/src/ServerContent.vue:
--------------------------------------------------------------------------------
1 |
2 | Empty
3 |
4 |
--------------------------------------------------------------------------------
/examples/vue/src/StaticContent.lagoon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | This is static content inside an island. We call this a lagoon.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/examples/vue/src/capri.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vue/src/global.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | min-height: 100%;
4 | padding: 0;
5 | margin: 0;
6 | background: #fafafa;
7 | font-family: "Helvetica Neue", arial, sans-serif;
8 | font-weight: 400;
9 | color: #444;
10 | }
11 |
12 | main {
13 | max-width: 60ch;
14 | margin: auto;
15 | font-size: 1.5em;
16 | line-height: 1.5;
17 | padding: 2em;
18 | }
19 |
20 | h1 {
21 | line-height: 1.1;
22 | }
23 | h1 i {
24 | font-style: normal;
25 | }
26 | h1 i::after {
27 | content: "";
28 | display: inline-block;
29 | width: 0.8em;
30 | height: 0.8em;
31 | margin-left: 0.25ch;
32 | background: url("./capri.svg") no-repeat;
33 | background-size: contain;
34 | }
35 |
36 | section {
37 | margin-bottom: 1em;
38 | }
39 |
40 | a {
41 | color: inherit;
42 | }
43 |
44 | .box {
45 | padding: 1em;
46 | border: 1px solid #aaa;
47 | border-radius: 6px;
48 | margin: 1em 0;
49 | }
50 |
--------------------------------------------------------------------------------
/examples/vue/src/main.server.ts:
--------------------------------------------------------------------------------
1 | import "./global.css";
2 |
3 | import { RenderFunction } from "@capri-js/vue/server";
4 | import { createSSRApp } from "vue";
5 | import { renderToString } from "vue/server-renderer";
6 |
7 | import App from "./App.vue";
8 | import createRouter from "./router.js";
9 |
10 | export const render: RenderFunction = async (url, context) => {
11 | const app = createSSRApp(App);
12 | const router = createRouter();
13 | app.use(router);
14 |
15 | // Note: vue-router's MemoryHistory does not strip the base, so we have to do
16 | // it manually. If you don't use a BASE_URL you can skip this step:
17 | const relativeUrl = url.slice(import.meta.env.BASE_URL.length);
18 |
19 | router.push(relativeUrl);
20 |
21 | await router.isReady();
22 | const { matched } = router.currentRoute.value;
23 | if (matched.length) {
24 | const html = await renderToString(app);
25 | return {
26 | "#app": html,
27 | };
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/examples/vue/src/main.ts:
--------------------------------------------------------------------------------
1 | import "./global.css";
2 |
3 | import { createApp } from "vue";
4 |
5 | import PreviewApp from "./PreviewApp.vue";
6 | import createRouter from "./router.js";
7 |
8 | const app = createApp(PreviewApp);
9 | app.use(createRouter());
10 |
11 | app.mount("#app");
12 |
--------------------------------------------------------------------------------
/examples/vue/src/router.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createMemoryHistory,
3 | createRouter,
4 | createWebHistory,
5 | RouteRecordRaw,
6 | } from "vue-router";
7 |
8 | import About from "./About.vue";
9 | import Home from "./Home.vue";
10 |
11 | const history = import.meta.env.SSR
12 | ? createMemoryHistory(import.meta.env.BASE_URL)
13 | : createWebHistory(import.meta.env.BASE_URL);
14 |
15 | const routes: RouteRecordRaw[] = [
16 | { path: "/", name: "home", component: Home },
17 | { path: "/about", name: "about", component: About },
18 | {
19 | path: "/preview",
20 | redirect: (to) => {
21 | const slug = to.query["slug"];
22 | return { path: typeof slug === "string" ? slug : "/" };
23 | },
24 | },
25 | ];
26 |
27 | export default () => createRouter({ routes, history });
28 |
--------------------------------------------------------------------------------
/examples/vue/src/vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.vue" {
2 | import type { DefineComponent } from "vue";
3 | const component: DefineComponent<
4 | Record,
5 | Record,
6 | unknown
7 | >;
8 | export default component;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/vue/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noEmit": true,
5 | "allowJs": true,
6 | "module": "ES2020",
7 | "moduleResolution": "Node",
8 | "target": "ES2017",
9 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
10 | "types": ["vite/client"],
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "jsx": "preserve"
14 | },
15 | "include": ["vite.config.ts", "src"]
16 | }
17 |
--------------------------------------------------------------------------------
/examples/vue/vite.config.ts:
--------------------------------------------------------------------------------
1 | import capri from "@capri-js/vue";
2 | import vue from "@vitejs/plugin-vue";
3 | import { defineConfig } from "vite";
4 |
5 | export default defineConfig({
6 | plugins: [
7 | vue(),
8 | capri({
9 | spa: "/preview",
10 | }),
11 | ],
12 | });
13 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@capri-js/monorepo",
3 | "private": true,
4 | "scripts": {
5 | "dev": "tsc --build packages --watch",
6 | "clean": "tsc --build packages --clean",
7 | "build": "tsc --build packages",
8 | "lint": "eslint . && prettier --check .",
9 | "lint:fix": "eslint --fix . && prettier --write .",
10 | "test": "vitest --run",
11 | "examples": "node .github/build-examples",
12 | "release": "multi-semantic-release --sequential-init --deps.prefix ^"
13 | },
14 | "devDependencies": {
15 | "@qiwi/multi-semantic-release": "^7.1.1",
16 | "@semantic-release/commit-analyzer": "^11.1.0",
17 | "@semantic-release/git": "^10.0.1",
18 | "@semantic-release/npm": "^11.0.2",
19 | "@testing-library/dom": "^9.3.3",
20 | "@testing-library/jest-dom": "^6.2.0",
21 | "@testing-library/user-event": "^14.5.2",
22 | "@types/jsdom": "^21.1.6",
23 | "@types/node": "^20.10.6",
24 | "@typescript-eslint/eslint-plugin": "^6.17.0",
25 | "@typescript-eslint/parser": "^6.17.0",
26 | "@vitejs/plugin-legacy": "^3.0.1",
27 | "eslint": "^8.56.0",
28 | "eslint-plugin-import": "^2.29.1",
29 | "eslint-plugin-simple-import-sort": "^10.0.0",
30 | "jsdom": "^23.0.1",
31 | "prettier": "^3.1.1",
32 | "typescript": "^4.9.4",
33 | "vitest": "^0.32.0"
34 | },
35 | "workspaces": [
36 | "packages/*",
37 | "examples/*"
38 | ],
39 | "release": {
40 | "branches": [
41 | "main",
42 | {
43 | "name": "next",
44 | "prerelease": true
45 | }
46 | ],
47 | "plugins": [
48 | "@semantic-release/commit-analyzer",
49 | "@semantic-release/npm",
50 | "@semantic-release/git"
51 | ]
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/capri/README.md:
--------------------------------------------------------------------------------
1 | # Capri 🍋
2 |
3 | Capri is a static site generator that supports partial hydration.
4 |
5 | Instead of using this package directly, please pick one of the framework adapters:
6 |
7 | - @capri-js/react
8 | - @capri-js/preact
9 | - @capri-js/solid
10 | - @capri-js/vue
11 | - @capri-js/svelte
12 |
--------------------------------------------------------------------------------
/packages/capri/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "capri",
3 | "version": "5.2.3",
4 | "description": "Static site generator for Vite",
5 | "author": "Felix Gnass ",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/capri-js/capri"
9 | },
10 | "license": "MIT",
11 | "type": "module",
12 | "main": "./lib/index.js",
13 | "files": [
14 | "lib",
15 | "ssr.d.ts"
16 | ],
17 | "exports": {
18 | ".": {
19 | "default": "./lib/index.js"
20 | },
21 | "./context": {
22 | "default": "./lib/context.js"
23 | },
24 | "./vite-plugin": {
25 | "default": "./lib/vite-plugin.js"
26 | }
27 | },
28 | "typesVersions": {
29 | "*": {
30 | "index.d.ts": [
31 | "lib/index.d.ts"
32 | ],
33 | "context": [
34 | "lib/context.d.ts"
35 | ],
36 | "vite-plugin": [
37 | "lib/vite-plugin.d.ts"
38 | ],
39 | "ssr": [
40 | "ssr.d.ts"
41 | ]
42 | }
43 | },
44 | "dependencies": {
45 | "cheerio": "^1.0.0-rc.11",
46 | "esbuild": "^0.19.11",
47 | "micromatch": "^4.0.5",
48 | "node-fetch": "^3.3.2",
49 | "url-join": "^5.0.0",
50 | "web-streams-polyfill": "^3.3.2"
51 | },
52 | "devDependencies": {
53 | "@types/micromatch": "^4.0.6",
54 | "vite": "^4.0.0"
55 | },
56 | "peerDependencies": {
57 | "vite": "^4.0.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/packages/capri/src/Template.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Template to analyze and modify the HTML.
3 | */
4 | export class Template {
5 | private html: string;
6 |
7 | constructor(html: string) {
8 | this.html = html;
9 | }
10 |
11 | /**
12 | * Find all hydration scripts and return their data-island attribute and
13 | * their text content.
14 | */
15 | getIslands() {
16 | return [
17 | ...this.html.matchAll(
18 | /
22 | `;
23 | });
24 |
--------------------------------------------------------------------------------
/packages/svelte/src/lagoon.client.js:
--------------------------------------------------------------------------------
1 | import {
2 | claim_element,
3 | init,
4 | insert_hydration,
5 | noop,
6 | safe_not_equal,
7 | SvelteComponent,
8 | } from "svelte/internal";
9 |
10 | function create_fragment(ctx) {
11 | let el;
12 | return {
13 | c: noop,
14 | m(target, anchor) {
15 | insert_hydration(target, el, anchor);
16 | },
17 | l(nodes) {
18 | el = claim_element(nodes, "CAPRI-LAGOON", {});
19 | },
20 | p: noop,
21 | d: noop,
22 | };
23 | }
24 |
25 | export default class Lagoon extends SvelteComponent {
26 | constructor(options) {
27 | super();
28 | init(this, options, null, create_fragment, safe_not_equal, {});
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/svelte/src/lagoon.server.js:
--------------------------------------------------------------------------------
1 | import { create_ssr_component } from "svelte/internal";
2 | import component from "virtual:capri-component";
3 |
4 | export default create_ssr_component((result, props, bindings, slots) => {
5 | const html = component.$$render(result, props, bindings, slots);
6 | return `${html} `;
7 | });
8 |
--------------------------------------------------------------------------------
/packages/svelte/src/server.ts:
--------------------------------------------------------------------------------
1 | export type { RenderFunction } from "capri";
2 |
--------------------------------------------------------------------------------
/packages/svelte/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src"
6 | },
7 | "references": [{ "path": "../capri" }]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "strict": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "allowJs": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "ES2020",
10 | "lib": ["ES2020", "DOM"],
11 | "declaration": true,
12 | "isolatedModules": true,
13 | "skipLibCheck": true,
14 | "jsx": "preserve"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "references": [
3 | { "path": "create" },
4 | { "path": "preact" },
5 | { "path": "react" },
6 | { "path": "solid" },
7 | { "path": "svelte" },
8 | { "path": "vue" },
9 | { "path": "vercel" },
10 | { "path": "cloudflare" },
11 | { "path": "react-render-to-string" }
12 | ],
13 | "exclude": ["*"]
14 | }
15 |
--------------------------------------------------------------------------------
/packages/vercel/README.md:
--------------------------------------------------------------------------------
1 | # Capri 🍋 SSR on Vercel
2 |
3 | Use `@capri-js/vercel` to render your static pages using either
4 |
5 | - a [serverless function](https://vercel.com/docs/concepts/functions/serverless-functions)
6 | - a [prerender function](https://vercel.com/docs/concepts/next.js/incremental-static-regeneration)
7 | - or an [edge function](https://vercel.com/docs/build-output-api/v3#vercel-primitives/edge-functions)
8 |
9 | _NOTE:_ This is only needed for SSR. For static sites that are prerendered upon build,
10 | leave the target set to `undefined`.
11 |
12 | https://vercel.com/docs/build-output-api/v3
13 |
14 | ## Usage
15 |
16 | ```ts
17 | // vite.config.ts
18 | import { defineConfig } from "vite";
19 | import react from "@vitejs/plugin-react";
20 | import capri from "@capri-js/react";
21 | import vercel from "@capri-js/vercel";
22 |
23 | export default defineConfig({
24 | plugins: [
25 | react(),
26 | capri({
27 | target: vercel({
28 | // options (see below)
29 | }),
30 | }),
31 | ],
32 | });
33 | ```
34 |
35 | ## Options
36 |
37 | ### `edge`
38 |
39 | Whether to create an edge function (default `false`).
40 |
41 | ### `isg`
42 |
43 | Settings for [Incremental Static Rendering](https://vercel.com/docs/build-output-api/v3#vercel-primitives/prerender-functions/configuration).
44 |
45 | ```ts
46 | {
47 | expiration: number | false;
48 | bypassToken?: string;
49 | }
50 | ```
51 |
--------------------------------------------------------------------------------
/packages/vercel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@capri-js/vercel",
3 | "version": "1.1.3",
4 | "description": "",
5 | "author": "Felix Gnass ",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/capri-js/capri",
9 | "directory": "packages/vercel"
10 | },
11 | "license": "MIT",
12 | "type": "module",
13 | "main": "./lib/index.js",
14 | "files": [
15 | "lib"
16 | ],
17 | "dependencies": {
18 | "capri": "^5.2.3"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/vercel/src/edge.ts:
--------------------------------------------------------------------------------
1 | import ssr from "virtual:capri-ssr";
2 |
3 | export default async (request: Request) => {
4 | const url = new URL(request.url).pathname;
5 |
6 | let status = 200;
7 | const headers = new Headers({
8 | "Content-Type": "text/html; charset=utf-8",
9 | });
10 |
11 | const html = await ssr(url, {
12 | status(code) {
13 | status = code;
14 | },
15 | getHeader: request.headers.get.bind(request.headers),
16 | setHeader: headers.set.bind(headers),
17 | });
18 | return new Response(html, {
19 | status,
20 | headers,
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/packages/vercel/src/index.ts:
--------------------------------------------------------------------------------
1 | import { BuildTarget } from "capri/vite-plugin";
2 | import { builtinModules } from "module";
3 | import * as path from "path";
4 |
5 | interface VercelConfig {
6 | edge?: boolean;
7 | isg?: {
8 | expiration: number | false;
9 | bypassToken?: string;
10 | };
11 | }
12 | export default function vercel({
13 | edge = false,
14 | isg,
15 | }: VercelConfig = {}): BuildTarget {
16 | return {
17 | config() {
18 | return {
19 | build: {
20 | outDir: path.join(".vercel", "output", "static"),
21 | },
22 | ssr: {
23 | target: edge ? "webworker" : "node",
24 | // Inline everything except for node builtins...
25 | noExternal: /.*/,
26 | external: builtinModules,
27 | },
28 | };
29 | },
30 | async build({ outDir, bundle, fsutils }) {
31 | const dirName = path.dirname(new URL(import.meta.url).pathname);
32 | const rootDir = path.resolve(outDir, "..");
33 | const funcDir = path.resolve(rootDir, "functions", "render.func");
34 |
35 | if (!edge) {
36 | // Create package.json to enable ESM
37 | fsutils.write(
38 | path.resolve(funcDir, "package.json"),
39 | JSON.stringify({ type: "module" }),
40 | );
41 | }
42 |
43 | // Create the handler
44 | await bundle(
45 | path.resolve(
46 | dirName,
47 | edge ? "edge.js" : isg ? "isg.js" : "serverless.js",
48 | ),
49 | path.resolve(funcDir, "index.js"),
50 | {
51 | platform: edge ? "browser" : "node",
52 | },
53 | );
54 |
55 | // Create .vc-config.json
56 | const runtime = edge
57 | ? { runtime: "edge" }
58 | : { runtime: "nodejs16.x", launcherType: "Nodejs" };
59 |
60 | fsutils.write(
61 | path.resolve(funcDir, ".vc-config.json"),
62 | JSON.stringify({
63 | ...runtime,
64 | [edge ? "entrypoint" : "handler"]: "index.js",
65 | }),
66 | );
67 |
68 | if (isg) {
69 | if (edge) {
70 | throw new Error(
71 | "Incremental Static Generation is not supported on the edge.",
72 | );
73 | }
74 | fsutils.write(
75 | path.resolve(funcDir, "..", "render.prerender-config.json"),
76 | JSON.stringify({ allowQuery: [], ...isg }),
77 | );
78 | }
79 |
80 | // Create config.json
81 | fsutils.write(
82 | path.resolve(rootDir, "config.json"),
83 | JSON.stringify({
84 | version: 3,
85 | routes: [
86 | {
87 | handle: "filesystem",
88 | },
89 | {
90 | src: isg ? "(?.*)" : "/.*",
91 | [edge ? "middlewarePath" : "dest"]: "render",
92 | },
93 | ],
94 | }),
95 | );
96 | },
97 | };
98 | }
99 |
--------------------------------------------------------------------------------
/packages/vercel/src/isg.ts:
--------------------------------------------------------------------------------
1 | import type { IncomingMessage, ServerResponse } from "node:http";
2 |
3 | import ssr from "virtual:capri-ssr";
4 |
5 | export default async (req: IncomingMessage, res: ServerResponse) => {
6 | if (!req.headers.accept?.includes("text/html")) {
7 | return res.writeHead(404).end();
8 | }
9 | const route = req.headers["x-now-route-matches"];
10 | if (typeof route !== "string") {
11 | return res.writeHead(400, "Header x-now-route-matches expected").end();
12 | }
13 | const match = decodeURIComponent(new URLSearchParams(route).get("1")!);
14 | res.setHeader("Content-Type", "text/html; charset=utf-8");
15 | const html = await ssr(match, {
16 | status(code) {
17 | res.statusCode = code;
18 | },
19 | getHeader(name: string) {
20 | const header = req.headers[name] ?? null;
21 | return Array.isArray(header) ? header[0] : header;
22 | },
23 | setHeader: res.setHeader.bind(res),
24 | });
25 | res.end(html);
26 | };
27 |
--------------------------------------------------------------------------------
/packages/vercel/src/serverless.ts:
--------------------------------------------------------------------------------
1 | import type { IncomingMessage, ServerResponse } from "node:http";
2 |
3 | import ssr from "virtual:capri-ssr";
4 |
5 | export default async (req: IncomingMessage, res: ServerResponse) => {
6 | if (!req.headers.accept?.includes("text/html")) {
7 | res.writeHead(404).end();
8 | } else {
9 | res.setHeader("Content-Type", "text/html; charset=utf-8");
10 | const html = await ssr(req.url!, {
11 | status(code) {
12 | res.statusCode = code;
13 | },
14 | getHeader(name: string) {
15 | const header = req.headers[name] ?? null;
16 | return Array.isArray(header) ? header[0] : header;
17 | },
18 | setHeader: res.setHeader.bind(res),
19 | });
20 | res.end(html);
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/packages/vercel/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src",
6 | "types": ["capri/ssr"]
7 | },
8 | "references": [{ "path": "../capri" }]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/vue/README.md:
--------------------------------------------------------------------------------
1 | # Vue bindings for Capri 🍋
2 |
3 | ## Usage
4 |
5 | ```ts
6 | // vite.config.ts
7 | import { defineConfig } from "vite";
8 | import react from "@vitejs/plugin-vue";
9 | import capri from "@capri-js/vue";
10 |
11 | export default defineConfig({
12 | plugins: [react(), capri()],
13 | });
14 | ```
15 |
16 | Visit [capri.build](https://capri.build) for docs and more information.
17 |
--------------------------------------------------------------------------------
/packages/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@capri-js/vue",
3 | "version": "2.1.4",
4 | "description": "",
5 | "author": "Felix Gnass ",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/capri-js/capri",
9 | "directory": "packages/vue"
10 | },
11 | "license": "MIT",
12 | "type": "module",
13 | "main": "./lib/index.js",
14 | "files": [
15 | "lib"
16 | ],
17 | "exports": {
18 | ".": {
19 | "default": "./lib/index.js"
20 | },
21 | "./server": {
22 | "default": "./lib/server.js"
23 | }
24 | },
25 | "typesVersions": {
26 | "*": {
27 | "index.d.ts": [
28 | "lib/index.d.ts"
29 | ],
30 | "server": [
31 | "lib/server.d.ts"
32 | ]
33 | }
34 | },
35 | "dependencies": {
36 | "capri": "^5.2.3"
37 | },
38 | "devDependencies": {
39 | "vue": "^3.2.37"
40 | },
41 | "peerDependencies": {
42 | "vite": "3.x || 4.x",
43 | "vue": "^3.2.37"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/vue/src/capri.d.ts:
--------------------------------------------------------------------------------
1 | declare module "virtual:capri-component" {
2 | import { IslandOptions } from "capri";
3 | import { ComponentType } from "react";
4 |
5 | const value: ComponentType;
6 | export default value;
7 |
8 | export const options: IslandOptions;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/vue/src/hydrate.ts:
--------------------------------------------------------------------------------
1 | import { createSSRApp, h } from "vue";
2 |
3 | export default function hydrate(
4 | component: any,
5 | props: Record,
6 | element: Element,
7 | ) {
8 | const slots: any = {};
9 | element.querySelectorAll("capri-slot").forEach((el) => {
10 | const name = el.getAttribute("name")!;
11 | slots[name] = h("capri-slot", {
12 | name,
13 | style: "display:contents",
14 | innerHTML: el.innerHTML,
15 | });
16 | });
17 | const app = createSSRApp({
18 | /* name, */ render: () => h(component, props, slots),
19 | });
20 | app.mount(element.parentElement!);
21 | }
22 |
--------------------------------------------------------------------------------
/packages/vue/src/index.ts:
--------------------------------------------------------------------------------
1 | import { capri, CapriAdapterPluginOptions } from "capri/vite-plugin";
2 |
3 | export default function (opts: CapriAdapterPluginOptions = {}) {
4 | const resolve = (f: string) => new URL(f, import.meta.url).pathname;
5 | return capri({
6 | ...opts,
7 | adapter: {
8 | injectWrapper: "onTransform",
9 | hydrate: resolve("./hydrate.js"),
10 | island: {
11 | server: resolve("./island.server.js"),
12 | },
13 | lagoon: {
14 | server: resolve("./lagoon.server.jsx"),
15 | client: resolve("./lagoon.client.jsx"),
16 | },
17 | },
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/packages/vue/src/island.server.js:
--------------------------------------------------------------------------------
1 | import * as componentModule from "virtual:capri-component";
2 | import { withCtx } from "vue";
3 | import { ssrRenderComponent, ssrRenderSlot } from "vue/server-renderer";
4 |
5 | const { default: component, options } = componentModule;
6 |
7 | const __sfc__ = {
8 | __name: "Island",
9 | __ssrInlineRender: true,
10 | setup(__props) {
11 | return (_ctx, _push, _parent, _attrs) => {
12 | const slots = Object.fromEntries(
13 | Object.entries(_ctx.$slots).map(([name, fn]) => [
14 | name,
15 | withCtx((_, _push, _parent, _scopeId) => {
16 | _push(``);
17 | ssrRenderSlot(
18 | _ctx.$slots,
19 | name,
20 | {},
21 | null,
22 | _push,
23 | _parent,
24 | _scopeId,
25 | );
26 | _push(` `);
27 | }),
28 | ]),
29 | );
30 |
31 | _push(``);
32 | _push(ssrRenderComponent(component, _attrs, slots, _parent));
33 | _push(
34 | ``,
37 | );
38 | _push(` `);
39 | };
40 | },
41 | };
42 | __sfc__.__file = "%COMPONENT_ID%";
43 | export default __sfc__;
44 |
--------------------------------------------------------------------------------
/packages/vue/src/lagoon.client.jsx:
--------------------------------------------------------------------------------
1 | import { createElementVNode, createStaticVNode } from "vue";
2 |
3 | export default {
4 | render() {
5 | return createElementVNode("capri-lagoon", {}, [createStaticVNode("", 1)]);
6 | },
7 | __file: "%COMPONENT_ID%",
8 | };
9 |
--------------------------------------------------------------------------------
/packages/vue/src/lagoon.server.jsx:
--------------------------------------------------------------------------------
1 | import component from "virtual:capri-component";
2 | import { ssrRenderComponent } from "vue/server-renderer";
3 |
4 | const __sfc__ = {
5 | __name: "Lagoon",
6 | __ssrInlineRender: true,
7 | setup(__props) {
8 | return (_ctx, _push, _parent, _attrs) => {
9 | _push(``);
10 | _push(ssrRenderComponent(component, _attrs, _ctx.$slots, _parent));
11 | _push(` `);
12 | };
13 | },
14 | };
15 | __sfc__.__file = "%COMPONENT_ID%";
16 | export default __sfc__;
17 |
--------------------------------------------------------------------------------
/packages/vue/src/server.ts:
--------------------------------------------------------------------------------
1 | export type { RenderFunction } from "capri";
2 |
--------------------------------------------------------------------------------
/packages/vue/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "rootDir": "src"
6 | },
7 | "references": [
8 | { "path": "../capri" },
9 | { "path": "../react-render-to-string" }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/test/dom.ts:
--------------------------------------------------------------------------------
1 | import legacy from "@vitejs/plugin-legacy";
2 | import { JSDOM } from "jsdom";
3 | import * as path from "path";
4 | import { build } from "vite";
5 |
6 | export async function getExampleDOM(root: string) {
7 | const base = path.join(root, "dist") + "/";
8 | await build({ root, base, plugins: [legacy()] });
9 | await build({ root, base, build: { ssr: true } });
10 | const dom = await JSDOM.fromFile(path.join(root, "dist", "index.html"), {
11 | url: `file://${root}/dist/index.html`,
12 | runScripts: "dangerously",
13 | resources: "usable",
14 | });
15 | return dom;
16 | }
17 |
--------------------------------------------------------------------------------
/test/e2e.test.ts:
--------------------------------------------------------------------------------
1 | import { waitFor, within } from "@testing-library/dom";
2 | import * as path from "path";
3 | import { describe, expect, test } from "vitest";
4 |
5 | import { getExampleDOM } from "./dom.js";
6 |
7 | describe("examples", () => {
8 | test.each([["vue"], ["react"], ["preact"], ["solid"], ["svelte"]])(
9 | "%s",
10 | async (example) => {
11 | const root = path.resolve(__dirname, "../examples", example);
12 | const dom = await getExampleDOM(root);
13 |
14 | global.window = dom.window as any;
15 | global.document = dom.window.document;
16 |
17 | const { default: userEvent } = await import(
18 | "@testing-library/user-event"
19 | );
20 | const user = userEvent.setup({ document });
21 |
22 | const screen = within(dom.window.document.body);
23 | const styles = document.querySelector('link[rel="stylesheet"]');
24 | expect(styles).toBeTruthy();
25 |
26 | const [counter1, counter2] = await screen.findAllByTestId("counter");
27 | const value1 = counter1.querySelector("span");
28 | const value2 = counter2.querySelector("span");
29 | const inc1 = await within(counter1).findByText("+");
30 | const inc2 = await within(counter2).findByText("+");
31 |
32 | expect(value1).toHaveTextContent("0");
33 | expect(value2).toHaveTextContent("100");
34 |
35 | await waitFor(async () => {
36 | await user.click(inc1);
37 | expect(value1?.textContent).not.toBe("0");
38 | });
39 |
40 | await user.click(inc2);
41 | await waitFor(async () => {
42 | expect(value2?.textContent).toBe("101");
43 | });
44 | },
45 | {
46 | timeout: 60000,
47 | },
48 | );
49 | });
50 |
--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------
1 | // Extend Vitest's matchers with the ones provided by Testing Library:
2 | import "@testing-library/jest-dom";
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowJs": true,
7 | "allowSyntheticDefaultImports": true,
8 | "target": "ES2020",
9 | "lib": ["ES2020", "DOM"],
10 | "skipLibCheck": true,
11 | "noEmit": true
12 | },
13 | "include": ["./*.ts", "./.eslintrc.cjs", "./test"]
14 | }
15 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | globals: true,
6 | setupFiles: "test/setup.ts",
7 | testTimeout: 15000,
8 | },
9 | });
10 |
--------------------------------------------------------------------------------