Tetsuya WakitaWrite standard TypeScript with export. Build with Vite. Push with clasp. Why I built...
Write standard TypeScript with export. Build with Vite. Push with clasp.
clasp 3 removed built-in TypeScript support. The recommendation is to use an external bundler — Google provides google/aside for Rollup.
I wanted Vite. The existing Vite plugins for GAS were either outdated (Rhino-era transforms like arrow function conversion) or didn't support web apps. So I built gas-vite-plugin — a minimal Vite plugin that only does what Vite can't do natively for GAS.
npm install -D gas-vite-plugin
// vite.config.ts
import gasPlugin from "gas-vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [gasPlugin()],
build: {
lib: {
entry: "src/main.ts",
formats: ["es"],
fileName: () => "Code.js",
},
},
});
// src/main.ts
export function onOpen() {
SpreadsheetApp.getUi()
.createMenu("My Menu")
.addItem("Run", "myFunction")
.addToUi();
}
export function myFunction() {
SpreadsheetApp.getActiveSpreadsheet().toast("Hello from Vite!");
}
npx vite build # → dist/Code.js (exports stripped) + dist/appsscript.json
npx clasp push
The plugin strips export keywords, copies appsscript.json, and sets GAS-safe defaults (no minification, no code splitting). That's the zero-config experience.
| Feature | Description |
|---|---|
| Export stripping | Removes export so functions are callable by GAS |
| Manifest copy | Copies appsscript.json to dist automatically |
| File include | Copies HTML/CSS flat to dist for web apps |
| Tree-shake protection | Keeps functions that GAS calls by string name |
| GAS-safe defaults | No minification, no code splitting, V8 assumed |
gasPlugin({
manifest: "src/appsscript.json", // default
include: ["src/**/*.html"], // copy files flat to dist
globals: ["processData"], // protect from tree-shaking
autoGlobals: true, // auto-protect exports (default)
});
globals — when tree-shaking removes too much
GAS calls functions by string name (menu handlers, triggers, google.script.run). If a function isn't exported and isn't referenced in code, Vite removes it.
export function onOpen() {
SpreadsheetApp.getUi()
.createMenu("Tools")
.addItem("Process", "processData") // string reference — Vite can't see this
.addToUi();
}
function processData() { /* removed by tree-shaking */ }
Fix: add it to globals.
gasPlugin({ globals: ["processData"] })
Exported functions are protected automatically (autoGlobals: true by default).
GAS web apps need HTML files and backend functions callable via google.script.run. Use include for files and globals for the backend functions:
// vite.config.ts
gasPlugin({
include: ["src/**/*.html"],
globals: ["getData", "saveData"],
})
// src/main.ts
export function doGet() {
return HtmlService.createHtmlOutputFromFile("index").setTitle("My App");
}
function getData() {
return SpreadsheetApp.getActiveSpreadsheet()
.getActiveSheet().getDataRange().getValues();
}
<!-- src/index.html — copied flat to dist -->
<script>
google.script.run.withSuccessHandler(render).getData();
</script>
Output:
dist/
├── Code.js # exports stripped
├── appsscript.json # auto-copied
└── index.html # flat-copied via include
Files are flattened — src/views/sidebar.html → dist/sidebar.html. GAS doesn't support subdirectories.
your-gas-project/
├── src/
│ ├── main.ts # entry point
│ ├── utils.ts # modules — import normally
│ ├── appsscript.json # GAS manifest
│ └── index.html # for web apps
├── dist/ # → clasp push from here
├── vite.config.ts
├── .clasp.json # rootDir: "dist"
└── package.json