@edsallrd/vite-plugin-gasforge
v0.3.0
Published
A Vite plugin for building Google Apps Script projects with type-safe server functions.
Readme
vite-plugin-gasforge
A Vite plugin for building Google Apps Script (GAS) projects with type-safe, validated RPC server functions. Write your server and client code in TypeScript, define validated functions using Standard Schema, and let the plugin manage compilation, serialization, context injection, and tree-shaking.
Key Features
- Type-Safe RPCs: Call server-side GAS functions from client-side browser code with compile-time type safety.
- Standard Schema Validation: Validate input parameters and output values using any Standard Schema v1 compatible library (Zod, Valibot, ArkType).
- Rich Serialization: SuperJSON integration allows you to send JavaScript types like
Date,Map,Set,BigInt, andUint8Arraydirectly over the RPC bridge. - Middleware and Context API: Inject execution context (such as active users, spreadsheet instances, or auth tokens) through composable middleware chains.
- TanStack Query Ready: Every server function includes
.queryKey()and.queryOptions()helper adapters for React Query or Vue Query compatibility. - Structured Errors: Errors thrown on the server are reconstructed into typed
GASForgeErrorinstances on the client, maintaining error codes and stack traces.
Installation
pnpm add vite-plugin-gasforge vite vite-plugin-singlefile @standard-schema/spec
# or
npm install vite-plugin-gasforge vite vite-plugin-singlefile @standard-schema/specSetup
1. vite.config.ts
Add the plugin to your Vite configuration:
import { defineConfig } from "vite";
import gas from "@edsallrd/vite-plugin-gasforge";
export default defineConfig({
plugins: [gas()],
});2. Project Directory Structure
Organize your source code with folders for client and server entries:
src/
server/
index.ts # Server entry point
client/
index.html # Client HTML entry pointCustomize these paths using plugin options:
gas({
server: "src/server/index.ts",
client: {
entry: "src/client/index.html",
plugins: [], // Additional client-side plugins (e.g. react(), vue())
rollupOptions: {}, // Custom Rollup configuration for the client bundle
},
});3. tsconfig.json
Include the type definitions to enable support for google.script globals, and add the auto-generated virtual types declarations file (gasforge-virtual.d.ts) to your compilation:
{
"compilerOptions": {
"types": ["@edsallrd/vite-plugin-gasforge/google.script"]
},
"include": [
"src",
"gasforge-virtual.d.ts"
]
}[!NOTE] The
gasforge-virtual.d.tsfile is automatically generated by the plugin in your project root during compilation/development. It dynamically registers TypeScript types for any server function you define.
Guide and API Reference
Defining Server Functions
Use createServerFn to declare typed endpoints. In client builds, the handler implementation is automatically stripped out, while in server builds, the validation and handler logic are preserved.
import { createServerFn } from "@edsallrd/vite-plugin-gasforge";
import { z } from "zod";
export const getGreeting = createServerFn({
input: z.string(),
output: z.string(),
handler: async (name) => {
return `Hello, ${name}!`;
},
});Call the function directly from client-side code:
const message = await getGreeting("World");
// => "Hello, World!"Local Execution (Server-Side)
If you need to call your server functions locally from within other server-side logic (e.g., in unit tests or internal utility functions) without going through the RPC serialization bridge, you can use the .local method. This will still execute your input validation, middleware chain, handler, and output validation directly:
const message = await getGreeting.local("World");
// => Executes validation + middleware + handler locallyRich Data Serialization
By incorporating SuperJSON into the RPC bridge, you can transmit rich data structures (such as Date, Map, and Set) without degrading them to strings or empty objects:
import { createServerFn } from "@edsallrd/vite-plugin-gasforge";
import { z } from "zod";
export const createTodo = createServerFn({
input: z.object({
title: z.string(),
tags: z.instanceof(Set),
dueDate: z.date(),
}),
output: z.object({
id: z.string(),
createdAt: z.date(),
}),
handler: async (todo) => {
return {
id: "todo-99",
createdAt: new Date(),
};
},
});Middleware and Context Injection
Define composable middleware to populate execution context (such as verifying spreadsheet permissions or fetching user credentials) before invoking the main handler:
import {
createMiddleware,
createServerFn,
} from "@edsallrd/vite-plugin-gasforge";
import { z } from "zod";
// 1. Define middleware and context outputs
const authMiddleware = createMiddleware().handler(async () => {
const email = Session.getActiveUser().getEmail();
if (!email) {
throw new Error("Unauthorized access");
}
return { userEmail: email };
});
// 2. Attach middleware to server functions
export const getUserPreferences = createServerFn({
middleware: [authMiddleware],
input: z.void(),
output: z.any(),
handler: async (input, ctx) => {
// ctx is fully typed and contains userEmail:
console.log(`Access by: ${ctx.userEmail}`);
return { theme: "dark" };
},
});TanStack Query Adapters
Every server function includes .queryKey() and .queryOptions() helper methods to integrate with @tanstack/react-query or @tanstack/vue-query. The query key returned is typed as a read-only tuple (as const):
import { useQuery } from "@tanstack/react-query";
import { getGreeting } from "./functions";
// getGreeting.queryKey("Alice") => readonly ["getGreeting", "Alice"]
// getGreeting.queryOptions("Alice") => { queryKey: readonly ["getGreeting", "Alice"], queryFn: ... }
function GreetingComponent() {
const { data, isLoading } = useQuery(getGreeting.queryOptions("Alice"));
if (isLoading) return <div>Loading...</div>;
return <h1>{data}</h1>;
}Structured Error Handling
All thrown errors are caught and reconstructed into GASForgeError instances, providing type-safe code categorizations:
import { GASForgeError } from "@edsallrd/vite-plugin-gasforge";
import { getGreeting } from "./functions";
try {
await getGreeting(123 as any); // Invalid type
} catch (err) {
if (err instanceof GASForgeError) {
console.error(`Error Code: ${err.code}`); // e.g. "INPUT_VALIDATION_FAILED"
console.error(`Issues:`, err.issues); // Validation problems (only for validation failures)
}
}Error Codes
The GASForgeError object contains a code property indicating the type of failure:
| Error Code | Description |
|---|---|
| INPUT_VALIDATION_FAILED | Thrown when client inputs do not conform to the defined input schema. |
| OUTPUT_VALIDATION_FAILED | Thrown when server outputs do not conform to the defined output schema. |
| MIDDLEWARE_ERROR | Thrown when an error occurs inside a middleware handler. |
| SERVER_ERROR | Thrown when an unhandled exception occurs inside your function's handler. |
| RPC_ERROR | Thrown when the called server function is not exported or defined on the Apps Script server (google.script.run). |
Server Entry Point
Your Google Apps Script server-side code should export all discovered endpoints by importing the virtual compilation file:
// src/server/index.ts
export * from "virtual:gas/server-fns";
// Add global triggers or HTML sidebar logic
function onOpen() {
SpreadsheetApp.getUi()
.createMenu("Sidebar Application")
.addItem("Open App", "showSidebar")
.addToUi();
}
function showSidebar() {
const html = HtmlService.createHtmlOutputFromFile("Client");
SpreadsheetApp.getUi().showSidebar(html);
}Production Build
Run the compilation script:
pnpm vite build
# or
npm run buildThis triggers the dual-build pipeline and generates the following flat bundle files:
dist/
Server.js # Deploy directly to Google Apps Script
Client.html # Deploy directly to Google Apps ScriptPlugin Options
| Option | Type | Default | Description |
| ---------------------- | ---------------- | ------------------------- | --------------------------------------------- |
| server | string | "src/server/index.ts" | File path of the server entry-point. |
| client.entry | string | "src/client/index.html" | File path of the client HTML entry-point. |
| client.plugins | PluginOption[] | [] | Additional Vite plugins for the client build. |
| client.rollupOptions | object | {} | Custom Rollup build options for the client. |
