Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (create-gas-app) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
create-gas-app
The modern CLI to build Google Apps Script add-ons with React, Vue, Svelte, or SolidJS.
Write real TypeScript, get live reload inside GAS dialogs, call server functions with full type inference — then ship everything as the two files GAS actually understands.
npx create-gas-app@latest
# or
npx create-gas-app@latest my-sheets-addonCreate a project
Running the CLI starts an interactive prompt:
create-gas-app — Google Apps Script, your way
What is your project named?
› my-sheets-addon
What type of Google Apps Script project?
● Sheets Add-on
○ Docs Add-on
○ Forms Add-on
○ Standalone Script
Which frontend framework?
● React (TypeScript + SWC)
○ Vue 3
○ Svelte 5
○ SolidJS
Select addons:
◼ Tailwind CSS v4
◼ shadcn/ui
◻ Commitlint + Lefthook
Which package manager?
● bun ○ pnpm ○ npm ○ yarn
Install dependencies now? Yes
Initialize a git repository? YesSheets Add-on
A Sheets add-on appears in the Extensions menu and opens sidebars and dialogs inside Google Sheets. This is the most feature-complete project type.
Generated structure
my-sheets-addon/
├── apps/
│ └── my-sheets-addon/
│ ├── env.ts ← Runtime env (sheet ID, named ranges, etc.) — gitignored
│ └── dialogs/
│ ├── sidebar/
│ │ ├── index.html ← importmap + entry script (no bundled deps)
│ │ └── src/
│ │ ├── main.tsx
│ │ └── App.tsx
│ └── about/
│ ├── index.html
│ └── src/
│ ├── main.tsx
│ └── App.tsx
├── packages/
│ ├── server/
│ │ └── src/
│ │ ├── index.ts ← Export server functions here → auto-typed on client
│ │ ├── ui.ts ← onOpen(), openSidebar(), openAboutDialog()
│ │ └── env.ts ← Server-side secrets — gitignored
│ ├── shared/
│ │ └── src/
│ │ ├── utils/server.ts ← Typed serverFunctions proxy
│ │ └── styles/global.css ← Global styles shared by all dialogs
│ └── ui/
│ └── src/
│ └── index.ts ← Shared component library
├── vite.config.ts
├── appsscript.json ← GAS manifest with OAuth scopes
└── package.json ← Workspaces + all scriptsStep 1 — Connect to Google
Authenticate once with your Google account:
npx clasp loginThen create a new GAS project and link it to your repo:
npm run clasp:createThis writes .clasp.json with your script ID. Run it once — all future pushes go to the same project.
To link an existing GAS project instead, get the script ID from the Apps Script URL (https://script.google.com/d/<SCRIPT_ID>/edit) and create .clasp.json manually:
{ "scriptId": "<YOUR_SCRIPT_ID>", "rootDir": "./dist" }Step 2 — Set up local HTTPS
GAS only allows iframes from HTTPS origins. Generate a trusted local cert once:
# Requires mkcert: https://github.com/FiloSottile/mkcert
npm run setup:certsStep 3 — Start the dev server
npm run devThis pushes lightweight iframe wrappers to GAS, then starts Vite at https://localhost:3000. Open your Google Sheet → Extensions → My Sheets Addon → Open — the sidebar loads your local Vite app with full hot reload.
google.script.run calls are proxied through a postMessage bridge so real server functions execute in GAS while your UI hot-reloads locally.
Step 4 — Deploy
npm run deployBuilds all dialogs to single inlined HTML files, builds the server to a single IIFE, and pushes to GAS.
Type-safe server calls
Define functions in packages/server/src/index.ts:
// packages/server/src/index.ts
export const getSheetData = async (
sheetName: string,
): Promise<{ headers: string[]; rows: string[][] }> => {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
if (!sheet) throw new Error(`Sheet "${sheetName}" not found`);
const [headers, ...rows] = sheet.getDataRange().getValues();
return { headers, rows };
};Call them from any dialog with full type inference — no manual type declarations needed:
// apps/my-sheets-addon/dialogs/sidebar/src/App.tsx
import { serverFunctions } from "@my-sheets-addon/shared/utils/server";
// TypeScript knows the return type: { headers: string[], rows: string[][] }
const { headers, rows } = await serverFunctions.getSheetData("Responses");
// Type error caught at compile time — no silent runtime surprises
console.log(rows.typo); // ✗ Property 'typo' does not existThe serverFunctions proxy in packages/shared/src/utils/server.ts imports the server's TypeScript types directly via the @my-sheets-addon/server workspace alias. GAS globals (SpreadsheetApp, HtmlService, etc.) are scoped to packages/server only and won't leak into your client dialogs.
Adding a dialog
Generate a new dialog entrypoint:
npx create-gas-app add dialog settingsRegister it in vite.config.ts:
const entrypoints = [
{ name: "Sidebar", filename: "sidebar", appDir: "sidebar", template: "index.html" },
{ name: "Settings", filename: "settings", appDir: "settings", template: "index.html" }, // ← add
];Add an opener in packages/server/src/ui.ts:
export const openSettingsDialog = () => {
const html = HtmlService.createHtmlOutputFromFile("settings")
.setWidth(800)
.setHeight(600);
SpreadsheetApp.getUi().showModalDialog(html, "Settings");
};Export it from packages/server/src/index.ts:
export { onOpen, onInstall, openSidebar, openSettingsDialog } from "./ui";Now serverFunctions.openSettingsDialog() is available — typed — from any dialog.
Adding fonts
The easiest way is Google Fonts. Each dialog's index.html already includes preconnect links; add your font there:
<!-- apps/my-sheets-addon/dialogs/sidebar/index.html -->
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
/>
</head>Then use it in packages/shared/src/styles/global.css:
body {
font-family: 'Inter', sans-serif;
}If you're using Tailwind, set it as the default sans font in your CSS:
@theme inline {
--font-sans: 'Inter', sans-serif;
}For self-hosted fonts (no external requests at runtime), drop the font files in packages/shared/src/styles/fonts/ and use @font-face in global.css. Vite will inline them into the final HTML at build time since dialogs build with vite-plugin-singlefile.
Keeping bundles small
Each dialog builds as a single inlined HTML file. GAS has no hard file size limit for HTML output, but large bundles slow down dialog load time. The scaffolded project already externalizes your framework (React, Vue, etc.) and gas-client via an importmap — they load from esm.sh at runtime and are never bundled.
If you add a heavy library, externalize it the same way.
Step 1 — Add to the importmap in index.html:
<!-- apps/my-sheets-addon/dialogs/sidebar/index.html -->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19.2.4",
"react-dom/": "https://esm.sh/react-dom@19.2.4/",
"gas-client": "https://esm.sh/gas-client@1.2.1",
"recharts": "https://esm.sh/recharts@2.15.3"
}
}
</script>Step 2 — Mark it as external in vite.config.ts:
// vite.config.ts
rollupOptions: {
external: ["react", "react-dom", "react-dom/client", "gas-client", "recharts"],
output: { format: "es" },
}Now recharts is fetched from esm.sh by the browser — it's never inlined into your HTML. The importmap entry pins the exact version, so you get reproducible loads.
Tip: Check if the library is available on esm.sh before externalizing. Most npm packages work; native addons or Node-specific packages won't.
Docs Add-on
Coming soon.
Forms Add-on
Coming soon.
Standalone Script
Coming soon.
Addons
Tailwind CSS v4
CSS-first Tailwind with @tailwindcss/vite. No config file needed — just import in CSS and use classes. Global styles live in packages/shared/src/styles/global.css.
shadcn/ui
Generates components.json and a starter Button component using the unified radix-ui package. Add more components:
npx shadcn add card
npx shadcn add dialog
npx shadcn add data-tableOnly available with React.
Commitlint + Lefthook
Enforces Conventional Commits with @commitlint/config-conventional. Runs Prettier on staged files via lefthook before each commit.
Scripts reference
| Script | What it does |
|---|---|
dev |
deploy:dev + Vite dev server at https://localhost:$PORT |
build |
Production build → inlined HTML in dist/ |
build:dev |
Dev build (iframe wrappers) → dist/ |
deploy |
build + clasp:push |
deploy:dev |
build:dev + clasp:push |
setup:certs |
Generate local HTTPS certs with mkcert |
clasp:create |
Create a new GAS project and write .clasp.json |
clasp:push |
Push dist/ to GAS |
clasp:open:script |
Open the Apps Script editor in your browser |
clasp:open:container |
Open the linked Sheets/Docs/Forms file |
format |
Format all files with Prettier |
Override the dev port:
PORT=5173 npm run devContributing
git clone https://github.com/vazhioli/create-gas-app
cd create-gas-app
bun install
bun run dev # watch mode — rebuilds on save
bun test-scaffold.ts # integration testsLicense
MIT