iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🦔

Building the Ultimate clasp Development Environment through OSS

に公開1

Hello 🦔

In the Google Apps Script (GAS) community, I think it has become common to perform local development using the npm library @google/clasp.

However, when building an environment to leverage modern architectures such as front-ends or TypeScript in GAS, you might find yourself discouraged many times by GAS-specific constraints and differences in the ecosystem.

"I want to write in TypeScript."
"I want to use React or Vue."
"I want to deploy with GitHub Actions."

Once you start writing code, you might encounter issues like:

"This is a GAS API, so it results in an undefined error locally..."
"If I export locally and upload to GAS, it causes an error because GAS doesn't support export..."
"The front-end works locally, but for some reason, the Web UI is completely blank in the GAS environment..."

It is not uncommon to spend all day fighting such successive errors and eventually give up.

To solve these painful points of GAS development, the OSS libraries I've been creating have finally come together to cover the entire development cycle.
I will introduce which challenges they solve in which process, along with specific code examples!

[create] Project Generator: @ciderjs/gasbombe

Challenges to Solve

  • TypeScript configuration
  • Bundling tools (Vite, esbuild)
  • clasp configuration
  • Integration with UI frameworks...

When trying to set these up in a modern configuration optimized for the GAS environment, the number of packages to install becomes enormous, and you get exhausted just by the initial setup.
You want best practices from the start.

What the Library Does

https://github.com/luthpg/gasbombe

@ciderjs/gasbombe is a generator that performs all these setups at once via an interactive CLI.

npx @ciderjs/gasbombe

Just run the command and choose the pattern you want to use (React, Vue, vanilla JavaScript, etc.) and the clasp configuration method.
A configured project directory will be generated immediately while benefiting from the modern ecosystem.

Realized DevEx

Just like a propane gas cylinder (gas bombe), you can immediately get the bundle necessary for development and start developing GAS tools right away while enjoying the benefits of the modern ecosystem.

[dev] Connecting Server and Client with Types: @ciderjs/gasnuki

https://github.com/luthpg/gasnuki

Challenges to Solve

Using google.script.run allows you to call server-side functions and manipulate URLs, which are essential features for modern web app development.

However, this google.script.run API is somewhat old-fashioned JavaScript, and there are tough hurdles such as:

  • Types do not work even if you introduce @types/google-apps-script.
  • Naturally, there are no types for the server-side functions you implement yourself.
  • No Promise support (callback format).
  • It doesn't work during local development.

Furthermore, if you pass data through JSON.parse, type information is lost and everything becomes any.
If you have to manually assert these one by one, type mismatches between the server-side and front-end will easily occur.

What the Library Does

@ciderjs/gasnuki analyzes the server-side code to generate type definitions and provides a Promise-based wrapper on the client side.

  • Please refer to the library's README for Vite settings and CLI usage.

Server side (GAS)

server/index.ts
import { serialize } from "@ciderjs/gasnuki/json";

export const getUserData = (id: string) => {
  // Convert to JSON while preserving object type information
  return serialize({ id, name: "Alice", updatedAt: new Date() });
};

Client side (React/Vue, etc.)

src/lib/server.ts
import { serialize } from "@ciderjs/gasnuki/json";
import {
  getPromisedServerScripts,
  type PartialScriptType,
} from '@ciderjs/gasnuki/promise';
import type { ServerScripts } from '~/types/appsscript';

// Define a server mock that works in local development
const mockupFunctions: PartialScriptType<ServerScripts> = {
  // Simulate the getUserData function
  getUserData: async (id) => {
    await new Promise(resolve => setTimeout(resolve, 500)); // Wait to simulate network latency
    return serialize({ id, name: 'test user', updatedAt: new Date() });
  },
};

export const server = getPromisedServerScripts<ServerScripts>({ mockupFunctions, parseJson: true });
src/App.tsx etc.
import { server } from "@/lib/server";

const fetchData = async () => {
  // Function names are autocompleted, and arguments and return values are typed
  // The returned Date object is also automatically restored
  const user = await server.getUserData("001");
  console.log(user.name); 
};

Realized DevEx

You can call backend functions in a type-safe manner while utilizing IDE autocompletion.
In addition, since it automatically switches to the mock you defined during local development, you can verify the UI operation without deploying.

[dev] Library Type Definitions: @ciderjs/dgs

Challenges to Solve

When trying to use GAS libraries in a local TypeScript environment, the lack of type definitions can make you hesitate to adopt them.
Additionally, manually writing dependency information into appsscript.json—which the web editor's library import feature handles for you—is extremely difficult.

What the Library Does

https://github.com/luthpg/dgs

@ciderjs/dgs is a CLI tool that assists with installing major GAS libraries and introducing their type definitions.

npx @ciderjs/dgs install

When you select a GAS library through the interactive prompt, it checks for the latest version using @google/clasp, updates appsscript.json, and places a type definition file (d.ts) in your local environment.

Realized DevEx

Adding external libraries, which previously depended on GAS editor features, can now be achieved in a local environment.
Furthermore, external libraries can be used in a type-safe manner, significantly reducing the stress of library management.

[dev] Router for GAS Environment: @ciderjs/city-gas

Challenges to Solve

Web apps in GAS do not allow free routing via URL paths. Also, since the APIs differ between the browser during local development (window.history) and the production GAS environment (google.script.history), code commonization is required. As a result, existing router libraries (such as react-router) cannot be used, necessitating manual routing implementation.

What the Library Does

https://github.com/luthpg/city-gas

@ciderjs/city-gas is a query-driven file-based router that absorbs these environmental differences. It includes built-in type definitions and validation for query parameters using Zod schemas. It supports React and Vue3 by default.

src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createRouter } from '@ciderjs/city-gas';
import { RouterProvider } from '@ciderjs/city-gas/react';
// Import automatically generated route definitions
import { pages, specialPages, dynamicRoutes } from './generated/routes';

// Initialize router
const router = createRouter(pages, { specialPages, dynamicRoutes });

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
);
src/pages/search/[id].tsx
import { z } from 'zod';
import { useNavigate, useParams } from '@ciderjs/city-gas/react';

// Schema definition
export const schema = z.object({
  q: z.string(),
  index: z.coerce.number().optional(), // Convert URL string to number
  sort: z.enum(['date', 'relevance']).optional(),
});

export default function SearchPage() {
  // params is inferred as { id: string; q: string; index?: number; sort?: "date" | "relevance" }
  const params = useParams('/search');

  const navigate = useNavigate();

  const handleClick = () => {
    // 1st argument: Route name (with autocomplete)
    // 2nd argument: Parameters (type-checked based on schema)
    navigate('/article/[id]', { id: params.id });
    
    // replace is also possible as an option
    // navigate('/', {}, { replace: true });
  };

  return (
    <div>
      <h1>Search: {params.q}</h1>
      <p>Page: {params.index ?? 0}</p>
      <button onClick={handleClick}>Open article: {params.id}</button>
    </div>
  );
}

Realized DevEx

You can implement routing without worrying about the execution environment, bringing a type-safe router library into the GAS ecosystem.

[test] Mocking GAS-specific objects: @ciderjs/vitest-plugin-gas-mock

Challenges to Solve

Since GAS-specific objects like SpreadsheetApp do not exist in the Node.js environment, even if you want to unit test only the logic of code containing them, it results in an error immediately upon execution. Manually mocking all of these involves enormous effort and incompleteness.

What the Library Does

https://github.com/luthpg/vitest-plugin-gas-mock

@ciderjs/vitest-plugin-gas-mock has pre-analyzed @types/google-apps-script. Therefore, during Vitest execution, it automatically generates and injects mocks for global objects based on highly complete GAS type definition information.

// vitest.config.ts
import { mockGas } from "@ciderjs/vitest-plugin-gas-mock";

export default defineConfig({
  plugins: [mockGas()],
});

// Test code
test("Getting the spreadsheet name", () => {
  // Mocks are automatically connected even for deep method chains
  const name = SpreadsheetApp.getActive().getName();
  expect(name).toBeDefined();
});

Realized DevEx

It makes it possible to inspect only the logic even if it includes GAS APIs, helping to guarantee code quality. Additionally, since deep method chains are automatically resolved, there is no need to define mocks in detail on the test code side.

[build] Countermeasures for GAS-specific errors: vite-plugin-google-apps-script

Challenges to Solve

The GAS runtime exhibits special behavior in interpreting newlines within template literals and URL strings, which can cause the built code to crash. Also, if GAS scriptlets (<?!= ... ?>) are enclosed in double quotes, the internal JSON data gets corrupted, which also causes crashes.

What the Library Does

https://github.com/luthpg/vite-plugin-google-apps-script

This plugin forces the use of Terser, which is available for compression within Vite, to replace newlines in template literals with the newline code \n. It also automatically escapes and cleans up strings and symbols that cause errors during deployment at build time.

Realized DevEx

You can focus on front-end development that doesn't crash in the GAS environment, using fast build tools like Vite with peace of mind and without being conscious of GAS-specific runtime limitations.

[build] Remove unnecessary export clauses from the backend: rolldown-plugin-remove-export

Challenges to Solve

The GAS server-side is designed to share the global scope between files, but without using export, you cannot use tools like vitest for testing. However, if you include export clauses for that purpose, the export statements will remain in the build file even after passing through a bundling tool, which causes syntax errors at runtime.

What the Library Does

https://github.com/luthpg/rolldown-plugin-remove-export

This plugin removes export statements that are unnecessary for GAS from the code after bundling with rolldown (or rollup), and formats it into a shape that works as-is in GAS.

Realized DevEx

While developing with a module system, you can automatically generate code tailored to the specific execution environment of GAS.

[push] Realizing CI/CD even in GAS: @ciderjs/clasp-auth

Challenges to Solve

In automated deployments using GitHub Actions or similar tools, clasp login assumes interactive authentication through a browser, which means it cannot be executed directly in a headless CI environment. As a result, uploading to a GAS environment with clasp required manually logging in and pushing from a local machine.

What the Library Does

https://github.com/ciderjs/clasp-auth

@ciderjs/clasp-auth provides a mechanism to encrypt local authentication credentials, upload them to GitHub Secrets, and restore them during CI execution.

# Run locally to register with Secrets
npx @ciderjs/clasp-auth upload owner/repo
.github/workflows/push.yaml
name: Push to GAS
 on:
   push:
     branches: [ "main" ]

 jobs:
   deploy:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-node@v4
         with:
           node-version: 20
 
       - name: Setup clasp auth
         uses: ciderjs/clasp-auth@v0.2.0
         with:
           json: ${{ secrets.CLASPRC_JSON }}

       - name: Install clasp
         run: npm install -g @google/clasp

       - name: Push to GAS
         run: clasp push

Realized DevEx

You are freed from the hassle of managing authentication credentials and can safely and easily build an automated deployment environment for GAS. This makes it possible to implement workflows such as "automatically deploy to GAS when merging into the main branch" even during team development.


By combining this series of tools, we have established an environment where you can focus on the core logic development for GAS without sacrificing a modern development experience. This is achieved by systematically resolving the inconveniences that were once considered "standard" in GAS development—from environment setup to development, testing, and deployment.

In other words, you don't have to be a "GAS developer" with niche knowledge; as an "engineer," you can bring ordinary best practices from web development—such as CI/CD via GitHub Actions and type-safe development with TypeScript—directly into the GAS world.

I hope these tools serve as a catalyst to make your development even a little bit lighter.

Discussion

luthluth

記事には記載してなかったですが、以下は十分な機能を持つものがあるため、開発してないです〜

  • 画像をフロント側で利用するときはどうすればいいのか: GoogleDrive公開リンクか、vite-plugin-singlefile
  • GAS関数をローカルから実行してログを見る: @google/claspの持つrunコマンド (実行可能APIとしてのデプロイが必要ですが)