├── .github └── images │ └── svelte+php.gif ├── .gitignore ├── .nvmrc ├── .prettierrc ├── README.md ├── example ├── .gitignore ├── .npmrc ├── README.md ├── composer.json ├── package.json ├── src │ ├── app.d.ts │ ├── app.html │ ├── lib │ │ └── images │ │ │ ├── github.svg │ │ │ ├── svelte-logo.svg │ │ │ ├── svelte-welcome.png │ │ │ └── svelte-welcome.webp │ ├── php │ │ ├── Game.php │ │ └── Words.php │ └── routes │ │ ├── +layout.svelte │ │ ├── +page.svelte │ │ ├── +page.ts │ │ ├── Counter.svelte │ │ ├── Header.svelte │ │ ├── cowsay │ │ ├── +page.server.php │ │ └── +page.svelte │ │ ├── pherdle │ │ ├── +page.server.php │ │ ├── +page.svelte │ │ ├── how-to-play │ │ │ ├── +page.js │ │ │ └── +page.svelte │ │ └── reduced-motion.js │ │ ├── php │ │ ├── +page.svelte │ │ ├── [fruite] │ │ │ ├── +page.js │ │ │ ├── +page.server.php │ │ │ └── +page.svelte │ │ └── emoji │ │ │ └── [fruite] │ │ │ └── +server.php │ │ └── styles.css ├── static │ ├── favicon.png │ └── robots.txt ├── svelte.config.js ├── tsconfig.json └── vite.config.ts ├── package.json ├── playground └── index.php ├── src ├── index.ts └── runtime.ts ├── tsconfig.json └── yarn.lock /.github/images/svelte+php.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basuke/vite-plugin-sveltekit-php-backend/3a23a45ccb4e26a8d6e98809d1221b7983c3a379/.github/images/svelte+php.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": true, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Backend for SvelteKit 2 | 3 |
4 |
5 |
{#each data.lines as line}{line}6 | 7 | -------------------------------------------------------------------------------- /example/src/routes/pherdle/+page.server.php: -------------------------------------------------------------------------------- 1 | $game->guesses, 14 | 15 | /** 16 | * An array of strings like '__x_c' corresponding to the guesses, where 'x' means 17 | * an exact match, and 'c' means a close match (right letter, wrong place) 18 | */ 19 | 'answers' => $game->answers, 20 | 21 | /** 22 | * The correct answer, revealed if the game is over 23 | */ 24 | 'answer' => count($game->answers) >= 6 ? $game->answer : null 25 | ]; 26 | } 27 | 28 | $actions = [ 29 | /** 30 | * Modify game state in reaction to a keypress. If client-side JavaScript 31 | * is available, this will happen in the browser instead of here 32 | */ 33 | 'update' => function ($event) { 34 | $game = new Game($_COOKIE['sverdle']); 35 | 36 | $key = $event->post['key']; 37 | 38 | $i = count($game->answers); 39 | 40 | if ($key === 'backspace') { 41 | if ($game->guesses[$i] !== '') { 42 | $game->guesses[$i] = substr($game->guesses[$i], 0, strlen($game->guesses[$i]) - 1); 43 | } 44 | } else { 45 | $game->guesses[$i] .= $key; 46 | } 47 | 48 | setcookie('sverdle', strval($game)); 49 | }, 50 | 51 | /** 52 | * Modify game state in reaction to a guessed word. This logic always runs on 53 | * the server, so that people can't cheat by peeking at the JavaScript 54 | */ 55 | 'enter' => function ($event) { 56 | $game = new Game($_COOKIE['sverdle']); 57 | 58 | $guess = $event->post->getAll('guess'); 59 | 60 | if (!$game->enter($guess)) { 61 | return fail(400, ['badGuess' => true ]); 62 | } 63 | 64 | setcookie('sverdle', strval($game)); 65 | }, 66 | 67 | 'restart' => function ($event) { 68 | setcookie('sverdle', ''); 69 | }, 70 | ]; 71 | -------------------------------------------------------------------------------- /example/src/routes/pherdle/+page.svelte: -------------------------------------------------------------------------------- 1 | 87 | 88 |
{/each}
10 | Pherdle is port of Swerdle which is a clone of Wordle, the 11 | word guessing game. To play, enter a five-letter English word. For example: 12 |
13 | 14 |23 | The y is in the right place. r and 24 | t 25 | are the right letters, but in the wrong place. The other letters are wrong, and can be discarded. 26 | Let's make another guess: 27 |
28 | 29 |This time we guessed right! You have six guesses to get the word.
38 | 39 |
40 | Unlike the original Wordle, Pherdle runs on the server instead of in the browser, making it
41 | impossible to cheat. It uses <form>
and cookies to submit data, meaning you can
42 | even play with JavaScript disabled!
43 |
Check the content of `routes/php/[fruite]/+page.server.php`
.
Last choise was {#if data.previous == data.name}also {/if}{data.previous}
25 | {/if} 26 | 27 |Result of `serialize($fruite)`
.
{data.encoded}32 | 33 |
These values are returned from PHP's $_SERVER.
37 |38 | {#each Object.keys(data.server) as key} 39 | {key} = {data.server[key]}42 | 43 | 63 | -------------------------------------------------------------------------------- /example/src/routes/php/emoji/[fruite]/+server.php: -------------------------------------------------------------------------------- 1 | params['fruite']; 8 | 9 | switch ($fruite) { 10 | case 'apple': 11 | $emoji = '🍎'; 12 | break; 13 | case 'orange': 14 | $emoji = '🍊'; 15 | break; 16 | default: 17 | $emoji = '🤷🏻♂️'; 18 | break; 19 | } 20 | 21 | json([ 22 | 'fruite' => $fruite, 23 | 'emoji' => $emoji, 24 | ]); 25 | } 26 | -------------------------------------------------------------------------------- /example/src/routes/styles.css: -------------------------------------------------------------------------------- 1 | @import '@fontsource/fira-mono'; 2 | 3 | :root { 4 | --font-body: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 5 | Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 6 | --font-mono: 'Fira Mono', monospace; 7 | --color-bg-0: rgb(202, 216, 228); 8 | --color-bg-1: hsl(209, 36%, 86%); 9 | --color-bg-2: hsl(224, 44%, 95%); 10 | --color-theme-1: #ff3e00; 11 | --color-theme-2: #4075a6; 12 | --color-text: rgba(0, 0, 0, 0.7); 13 | --column-width: 42rem; 14 | --column-margin-top: 4rem; 15 | font-family: var(--font-body); 16 | color: var(--color-text); 17 | } 18 | 19 | body { 20 | min-height: 100vh; 21 | margin: 0; 22 | background-attachment: fixed; 23 | background-color: var(--color-bg-1); 24 | background-size: 100vw 100vh; 25 | background-image: radial-gradient( 26 | 50% 50% at 50% 50%, 27 | rgba(255, 255, 255, 0.75) 0%, 28 | rgba(255, 255, 255, 0) 100% 29 | ), 30 | linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%); 31 | } 32 | 33 | h1, 34 | h2, 35 | p { 36 | font-weight: 400; 37 | } 38 | 39 | p { 40 | line-height: 1.5; 41 | } 42 | 43 | a { 44 | color: var(--color-theme-1); 45 | text-decoration: none; 46 | } 47 | 48 | a:hover { 49 | text-decoration: underline; 50 | } 51 | 52 | h1 { 53 | font-size: 2rem; 54 | text-align: center; 55 | } 56 | 57 | h2 { 58 | font-size: 1rem; 59 | } 60 | 61 | pre { 62 | font-size: 16px; 63 | font-family: var(--font-mono); 64 | background-color: rgba(255, 255, 255, 0.45); 65 | border-radius: 3px; 66 | box-shadow: 2px 2px 6px rgb(255 255 255 / 25%); 67 | padding: 0.5em; 68 | overflow-x: auto; 69 | color: var(--color-text); 70 | } 71 | 72 | .text-column { 73 | display: flex; 74 | max-width: 48rem; 75 | flex: 0.6; 76 | flex-direction: column; 77 | justify-content: center; 78 | margin: 0 auto; 79 | } 80 | 81 | input, 82 | button { 83 | font-size: inherit; 84 | font-family: inherit; 85 | } 86 | 87 | button:focus:not(:focus-visible) { 88 | outline: none; 89 | } 90 | 91 | @media (min-width: 720px) { 92 | h1 { 93 | font-size: 2.4rem; 94 | } 95 | } 96 | 97 | .visually-hidden { 98 | border: 0; 99 | clip: rect(0 0 0 0); 100 | height: auto; 101 | margin: 0; 102 | overflow: hidden; 103 | padding: 0; 104 | position: absolute; 105 | width: 1px; 106 | white-space: nowrap; 107 | } 108 | -------------------------------------------------------------------------------- /example/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basuke/vite-plugin-sveltekit-php-backend/3a23a45ccb4e26a8d6e98809d1221b7983c3a379/example/static/favicon.png -------------------------------------------------------------------------------- /example/static/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { vitePreprocess } from '@sveltejs/kit/vite'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter(), 12 | moduleExtensions: ['.ts', '.js', '.php'], 13 | } 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import phpBackend from 'vite-plugin-sveltekit-php-backend'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit(), phpBackend({ address: 'localhost:9000' })], 7 | }); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-sveltekit-php-backend", 3 | "version": "0.6.0", 4 | "description": "PHP Backend Vite Plugin for SvelteKit", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "type": "module", 8 | "files": [ 9 | "/dist" 10 | ], 11 | "scripts": { 12 | "build": "tsc", 13 | "watch": "tsc -w", 14 | "prepublishOnly": "npm run build" 15 | }, 16 | "author": "Basuke Suzuki", 17 | "license": "MIT", 18 | "dependencies": { 19 | "fastcgi-kit": "^0.16", 20 | "vite": "^4.2.1" 21 | }, 22 | "devDependencies": { 23 | "@sveltejs/kit": "^1.16.2", 24 | "@types/node": "^20.3.0", 25 | "typescript": "^5.0.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /playground/index.php: -------------------------------------------------------------------------------- 1 | function (string $url, array $params) { 37 | 38 | }, 39 | ]; 40 | 41 | \Basuke\SvelteKit\Backend::main(__NAMESPACE__); 42 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite'; 2 | 3 | import path from 'node:path'; 4 | import fs from 'node:fs'; 5 | import url from 'node:url'; 6 | import { createClient } from 'fastcgi-kit'; 7 | 8 | const name = 'vite-plugin-sveltekit-php-backend'; 9 | const sharedClientVirtualModule = `virtual:${name}/shared-client`; 10 | const sharedClientResolvedModuleId = '\0' + sharedClientVirtualModule; 11 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 12 | const runtimeCode = fs.readFileSync(path.join(__dirname, 'runtime.js'), 'utf8'); 13 | 14 | const verbose = { 15 | showCodeStructure: false, 16 | showGeneratedJSCode: false, 17 | }; 18 | 19 | interface PHPBackendOptions { 20 | address?: string; 21 | debug?: boolean; 22 | } 23 | 24 | type ParameterSpec = [string, string]; // [type, name] 25 | type FunctionSpec = ParameterSpec[]; 26 | 27 | interface CodeStructure { 28 | load: FunctionSpec | null; 29 | endpoints: Record
40 | {/each} 41 |