A template for building single-page applications that run entirely inside Google Apps Script. The frontend is compiled into a single self-contained HTML file and served by GAS's HtmlService.
| Layer | Technology |
|---|---|
| Frontend framework | Svelte 5 |
| Build tool | Vite + vite-plugin-singlefile |
| Styling | Tailwind CSS v4 + DaisyUI |
| Routing | svelte-spa-router (hash-based) |
| GAS deployment | clasp |
| Runtime / package manager | Bun |
├── src/
│ ├── client/ # Svelte SPA (compiled → dist/index.html)
│ ├── server/ # Google Apps Script server-side code
│ └── api/ # Local Express mock API for development
├── dist/ # Build output — pushed to GAS via clasp
├── appsscript.json # GAS manifest
└── .clasp.json # clasp project config
Each source section has its own README:
src/client/ — Frontend SPAsrc/server/ — GAS serversrc/api/ — Local dev mock API>= 1.0clasp login).clasp.json.example to .clasp.json and fill in your Script ID# Install dependencies
bun install
# Start local dev (Vite frontend + Express mock API)
bun run dev
Open http://localhost:5173 in your browser.
| Script | Description |
|---|---|
bun run dev |
Starts Vite dev server and local mock API concurrently |
bun run build |
Builds frontend to dist/ and copies server-side files |
bun run deploy |
Full clean → build → copy manifest → clasp push |
bun run clean |
Removes the dist/ directory |
vite build compiles the Svelte app into a single dist/index.html (all JS and CSS inlined).build:backend copies src/server/ into dist/server/.copy:manifest copies appsscript.json to dist/.clasp push uploads everything in dist/ to the linked GAS project.GAS serves the app via doGet() in src/server/index.ts, which returns the compiled index.html using HtmlService.
The requester utility in src/client/lib/requester.ts automatically detects the environment:
google.script.runfetch() request to the local Express mock API on port 3000This means the same frontend code works in both environments with no changes.
src/client/index.html uses GAS HTML template syntax to inject server-side values at render time:
<script>
window.currentUser = "<?= currentUser ?>";
</script>
Add more server-side values to doGet() in src/server/index.ts and access them via window on the client.