npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

create-gas-app

v0.4.2

Published

The modern CLI to scaffold Google Apps Script apps with React, Vue, Svelte, or SolidJS

Downloads

655

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-addon

Prerequisites

  • Node.js ≥ 18
  • mkcert (optional, for local dev server) — generates trusted local HTTPS certs. Install instructions

Create a project

Running the CLI starts an interactive prompt:

  create-gas-app — Google Apps Script, your way

  What is your project named?
  › my-gas-app

  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
  ◻ ESLint

  Which package manager?
  ● bun   ○ pnpm   ○ npm   ○ yarn

  Install dependencies now?  Yes
  Initialize a git repository? Yes

Note: All command examples below use npm run. Substitute bun run, pnpm run, or yarn depending on what you chose at scaffold time.


Getting started

All project types share the same Vite monorepo structure and the same workflow. Pick your project type during scaffolding — the rest is identical.

Generated structure

my-gas-app/
├── apps/
│   └── my-gas-app/
│       ├── env.ts                    ← Runtime env — 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 scripts

Step 1 — Connect to Google

Authenticate once with your Google account:

npm run clasp:login

Then create a new GAS project and link it to your repo:

npm run clasp:create

This 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:certs

Step 3 — Start the dev server

npm run dev

This pushes lightweight iframe wrappers to GAS, then starts Vite at https://localhost:3000. Open your Google Sheet / Doc / Form → Extensions → My App → 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 deploy

Builds all dialogs to single inlined HTML files, builds the server to a single ES bundle (exports stripped for GAS compatibility), and pushes to GAS.


Project types

All project types share the same structure and workflow. The differences are which GAS service is used server-side and what starter functions are generated.

Sheets Add-on

Extends Google Sheets. Uses SpreadsheetApp.getUi() for the Extensions menu. The generated starter functions:

// Returns spreadsheet name, active sheet name, and row count
export const getSpreadsheetInfo = (): {
  id: string; name: string; activeSheet: string; rowCount: number;
} => { ... };

// Returns headers + first N rows of a sheet
export const getSheetData = (sheetName?: string, maxRows = 20): {
  headers: string[]; rows: string[][];
} => { ... };

Docs Add-on

Extends Google Docs. Uses DocumentApp.getUi() for the Extensions menu. The generated starter function:

export const getDocumentInfo = (): { id: string; name: string } => {
  const doc = DocumentApp.getActiveDocument();
  return { id: doc.getId(), name: doc.getName() };
};

Forms Add-on

Extends the Google Forms editor — adds sidebars, dialogs, and menu items to the form editing interface. It does not modify the form that respondents see. Uses FormApp.getUi() for the Extensions menu.

The generated starter function:

export const getFormInfo = (): { id: string; title: string } => {
  const form = FormApp.getActiveForm();
  return { id: form.getId(), title: form.getTitle() };
};

Forms add-ons also support installable triggers. For example, running a function every time a respondent submits the form:

export const onFormSubmit = (e: GoogleAppsScript.Events.FormsOnFormSubmit): void => {
  const response = e.response;
  // process response...
};

Standalone Script

A standalone script has no container. It is deployed as a web app and responds to HTTP requests via doGet and doPost. There is no Extensions menu and no onOpen trigger.

export const doGet = (_e: GoogleAppsScript.Events.DoGet) => {
  return HtmlService.createHtmlOutputFromFile("sidebar").setTitle("My App");
};

export const doPost = (_e: GoogleAppsScript.Events.DoPost) => {
  return ContentService.createTextOutput(JSON.stringify({ status: "ok" }))
    .setMimeType(ContentService.MimeType.JSON);
};

Deploy via Deploy → New deployment → Web app in the Apps Script editor.


OAuth scopes

Google Apps Script requires explicit OAuth scopes to access Google services. Scopes are declared in appsscript.json at the project root:

{
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/script.external_request"
  ]
}

Common scopes you may need:

| Scope | When to add it | | ----- | -------------- | | https://www.googleapis.com/auth/script.external_request | Calling external APIs with UrlFetchApp | | https://www.googleapis.com/auth/script.scriptapp | Creating or managing installable triggers | | https://www.googleapis.com/auth/script.send_mail | Sending email on behalf of the user via MailApp | | https://www.googleapis.com/auth/spreadsheets | Reading or writing Google Sheets data | | https://www.googleapis.com/auth/documents | Reading or writing Google Docs data | | https://www.googleapis.com/auth/forms | Reading or writing Google Forms data |

Handling granular OAuth

Google OAuth is granular — users are shown each requested scope individually and may choose to grant only some of them. Use ScriptApp.requireScopes() to validate that the user has granted the specific scopes a function needs, or ScriptApp.requireAllScopes() if a function depends on every scope declared in appsscript.json. Both methods end execution immediately and prompt the user for authorization if any required scope is missing.

// Use requireScopes() when a function only needs a subset of your declared scopes.
// Here, fetchAndLog() only needs external request access — not triggers or mail.
export const fetchAndLog = () => {
  ScriptApp.requireScopes(ScriptApp.AuthMode.FULL, [
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/spreadsheets",
  ]);

  const response = UrlFetchApp.fetch("https://api.example.com/data");
  const data = JSON.parse(response.getContentText());

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.getRange(sheet.getLastRow() + 1, 1).setValue(data.value);
};

// Use requireAllScopes() when a function relies on every scope in appsscript.json.
export const fullSync = () => {
  ScriptApp.requireAllScopes(ScriptApp.AuthMode.FULL);

  const response = UrlFetchApp.fetch("https://api.example.com/data");
  const data = JSON.parse(response.getContentText());

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.getRange(sheet.getLastRow() + 1, 1).setValue(data.value);

  ScriptApp.newTrigger("fullSync").timeBased().everyHours(1).create();
  MailApp.sendEmail(Session.getActiveUser().getEmail(), "Sync complete", "Data updated.");
};

See the Google Apps Script scopes documentation for the full guide on detecting missing scopes and triggering the authorization popup.


Common patterns

Type-safe server calls

Define functions in 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:

import { serverFunctions } from "@my-gas-app/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
console.log(rows.typo); // ✗ Property 'typo' does not exist

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 settings

Register 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.


Customising the Extensions menu

Standalone scripts do not have an Extensions menu — skip this section if you chose Standalone.

The generated onOpen in packages/server/src/ui.ts runs every time the file is opened and builds the add-on menu. The UI service differs per project type:

| Project type | UI service | | ------------ | ---------- | | Sheets | SpreadsheetApp.getUi() | | Docs | DocumentApp.getUi() | | Forms | FormApp.getUi() |

Add an item that opens a dialog:

export const onOpen = () => {
  SpreadsheetApp.getUi() // swap for DocumentApp / FormApp as needed
    .createAddonMenu()
    .addItem("Open", "openSidebar")
    .addItem("Settings", "openSettingsDialog") // ← add
    .addToUi();
};

Add an item that runs a server function directly:

export const onOpen = () => {
  SpreadsheetApp.getUi()
    .createAddonMenu()
    .addItem("Open", "openSidebar")
    .addSeparator()
    .addItem("Import data", "importDataFromSheet") // ← runs directly, no dialog
    .addToUi();
};

Add a submenu:

export const onOpen = () => {
  const ui = SpreadsheetApp.getUi();
  ui.createAddonMenu()
    .addItem("Open", "openSidebar")
    .addSeparator()
    .addSubMenu(
      ui.createMenu("Tools")
        .addItem("Import data", "importDataFromSheet")
        .addItem("Export to CSV", "exportToCsv"),
    )
    .addToUi();
};

Everything added to the menu must be exported from packages/server/src/index.ts so GAS can find it at the top level:

export { onOpen, onInstall, openSidebar, openSettingsDialog, importDataFromSheet } from "./ui";

Tip: Menu items run as server-side functions — they can read/write data directly without going through serverFunctions. Use them for one-shot operations. Use serverFunctions when you need to trigger an action from within a dialog.


Adding fonts

Each dialog's index.html already includes Google Fonts preconnect links. Add your font:

<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"
/>

Then use it in packages/shared/src/styles/global.css:

body { font-family: 'Inter', sans-serif; }

If you're using Tailwind:

@theme inline {
  --font-sans: 'Inter', sans-serif;
}

For self-hosted fonts, drop the files in packages/shared/src/styles/fonts/ and use @font-face in global.css. Vite inlines them into the final HTML at build time via vite-plugin-singlefile.


Keeping bundles small

Each dialog builds as a single inlined HTML file. The scaffolded project already externalizes your framework 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:

<script type="importmap">
  {
    "imports": {
      "react":      "https://esm.sh/[email protected]",
      "react-dom/": "https://esm.sh/[email protected]/",
      "gas-client": "https://esm.sh/[email protected]",
      "recharts":   "https://esm.sh/[email protected]"
    }
  }
</script>

Step 2 — Mark it as external in vite.config.ts:

rollupOptions: {
  external: ["react", "react-dom", "react-dom/client", "gas-client", "recharts"],
  output: { format: "es" },
}

Tip: Check if the library is available on esm.sh before externalizing. Most npm packages work; native addons or Node-specific packages won't.


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-table

Only available with React.

Commitlint + Lefthook

Enforces Conventional Commits with @commitlint/config-conventional. Runs Prettier on staged files via lefthook before each commit.

ESLint

Generates eslint.config.js with ESLint 9 flat config, TypeScript support, and framework-specific rules:

| Framework | Extra plugins | | --------- | ------------- | | React | eslint-plugin-react-hooks, eslint-plugin-react-refresh | | Vue | eslint-plugin-vue | | Svelte | eslint-plugin-svelte | | SolidJS | eslint-plugin-solid |

Adds lint and lint:fix scripts to package.json.


Adding addons to an existing project

Addons can be added after scaffolding with the add addon subcommand:

npx create-gas-app add addon tailwind
npx create-gas-app add addon eslint
npx create-gas-app add addon commitlint
npx create-gas-app add addon shadcn

Run from the project root. The command auto-detects your framework and project name, writes the addon files, and updates package.json with the required dependencies. Then install:

npm install

For tailwind, the command also prints the exact lines to add to vite.config.ts since that file may have been edited.

Note: shadcn requires React and Tailwind to be installed first.


Scripts reference

These scripts are available in every generated project. They are not part of the create-gas-app CLI repo itself.

| 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:login | Authenticate with Google | | 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 | | lint | Run ESLint (only if ESLint addon was selected) | | lint:fix | Run ESLint with auto-fix |

Override the dev port:

PORT=5173 npm run dev

Resources


Acknowledgements

The Sheets add-on architecture is heavily inspired by enuchi/React-Google-Apps-Script — the original template that pioneered bundling React apps into GAS dialogs with a Webpack + Babel setup. Two of his packages are core dependencies of every generated project:

  • gas-client — the promise-based wrapper around google.script.run that powers all type-safe server calls
  • gas-types-detailed — comprehensive TypeScript type definitions for the entire Google Apps Script API

Contributing

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 tests

License

MIT