Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions .github/scripts/biome-gs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { exec } from "node:child_process";
import { readdirSync, renameSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
import { promisify } from "node:util";

const execAsync = promisify(exec);

async function findGsFiles(
dir: string,
fileList: string[] = [],
): Promise<string[]> {
const files = readdirSync(dir);
for (const file of files) {
const filePath = join(dir, file);
if (
file === "node_modules" ||
file === ".git" ||
file === "dist" ||
file === "target" ||
file === "pkg"
) {
continue;
}
const stat = statSync(filePath);
if (stat.isDirectory()) {
await findGsFiles(filePath, fileList);
} else if (file.endsWith(".gs") && file !== "moment.gs") {
fileList.push(filePath);
}
}
return fileList;
}
Comment on lines +24 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The findGsFiles function uses synchronous file system operations (readdirSync, statSync) within an async function. This mix of synchronous and asynchronous code is inefficient and can block the Node.js event loop, which can be slow for large repositories.

A better approach is to use the promise-based APIs from node:fs/promises and Promise.all to perform directory traversal concurrently. This will significantly improve performance and make the code more robust.

Please also update your imports from node:fs to use readdir from node:fs/promises and remove the now-unused statSync.

async function findGsFiles(dir: string): Promise<string[]> {
  const ignoredDirs = new Set([
    "node_modules",
    ".git",
    "dist",
    "target",
    "pkg",
  ]);
  try {
    const dirents = await readdir(dir, { withFileTypes: true });
    const files = await Promise.all(
      dirents.map(async (dirent) => {
        const fullPath = resolve(dir, dirent.name);
        if (ignoredDirs.has(dirent.name)) {
          return [];
        }
        if (dirent.isDirectory()) {
          return findGsFiles(fullPath);
        }
        if (dirent.isFile() && dirent.name.endsWith(".gs") && dirent.name !== "moment.gs") {
          return [fullPath];
        }
        return [];
      }),
    );
    return files.flat();
  } catch (error) {
    // Ignore errors for directories that can't be read (e.g. permissions)
    console.error(`Could not read directory ${dir}:`, error);
    return [];
  }
}


async function main() {
const command = process.argv[2]; // 'lint' or 'format'
if (command !== "lint" && command !== "format") {
console.error("Usage: tsx biome-gs.ts [lint|format]");
process.exit(1);
}

const rootDir = resolve(".");
const gsFiles = await findGsFiles(rootDir);
const renamedFiles: { oldPath: string; newPath: string }[] = [];

const restoreFiles = () => {
for (const { oldPath, newPath } of renamedFiles) {
try {
renameSync(newPath, oldPath);
} catch (e) {
console.error(`Failed to restore ${newPath} to ${oldPath}:`, e);
}
}
renamedFiles.length = 0;
};

process.on("SIGINT", () => {
restoreFiles();
process.exit(1);
});
process.on("SIGTERM", () => {
restoreFiles();
process.exit(1);
});
process.on("exit", restoreFiles);

try {
// 1. Rename .gs to .gs.js
for (const gsFile of gsFiles) {
const jsFile = `${gsFile}.js`;
renameSync(gsFile, jsFile);
renamedFiles.push({ oldPath: gsFile, newPath: jsFile });
}

// 2. Run Biome
const biomeArgs = command === "format" ? "check --write ." : "check .";
console.log(`Running biome ${biomeArgs}...`);
try {
const { stdout, stderr } = await execAsync(
`pnpm exec biome ${biomeArgs}`,
{ cwd: rootDir },
);
if (stdout) console.log(stdout.replace(/\.gs\.js/g, ".gs"));
if (stderr) console.error(stderr.replace(/\.gs\.js/g, ".gs"));
} catch (e: unknown) {
const err = e as { stdout?: string; stderr?: string };
if (err.stdout) console.log(err.stdout.replace(/\.gs\.js/g, ".gs"));
if (err.stderr) console.error(err.stderr.replace(/\.gs\.js/g, ".gs"));
// Don't exit yet, we need to restore files
}
} catch (err) {
console.error("An error occurred:", err);
} finally {
restoreFiles();
// Remove listeners to avoid double-running or issues on exit
process.removeAllListeners("exit");
process.removeAllListeners("SIGINT");
process.removeAllListeners("SIGTERM");
}
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
3 changes: 2 additions & 1 deletion GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ This guide outlines best practices for developing Google Apps Script projects, f

* For new sample directories, ensure the top-level folder is included in the [`test.yaml`](.github/workflows/test.yaml) GitHub workflow's matrix configuration.
* Do not move or delete snippet tags: `[END apps_script_... ]` or `[END apps_script_... ]`.

* Keep code within snippet tags self-contained. Avoid depending on helper functions defined outside the snippet tags if the snippet is intended to be copied and pasted.
* Avoid function name collisions (e.g., multiple `onOpen` or `main` functions) by placing separate samples in their own directories or files. Do not append suffixes like `_2`, `_3` to function names. For variables, replace collisions with a more descriptive name.

## Tools

Expand Down
14 changes: 7 additions & 7 deletions adminSDK/directory/quickstart.gs
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,27 @@
*/
function listUsers() {
const optionalArgs = {
customer: 'my_customer',
customer: "my_customer",
maxResults: 10,
orderBy: 'email'
orderBy: "email",
};
if (!AdminDirectory || !AdminDirectory.Users) {
throw new Error('Enable the AdminDirectory Advanced Service.');
throw new Error("Enable the AdminDirectory Advanced Service.");
}
const response = AdminDirectory.Users.list(optionalArgs);
const users = response.users;
if (!users || users.length === 0) {
console.log('No users found.');
console.log("No users found.");
return;
}
// Print the list of user's full name and email
console.log('Users:');
console.log("Users:");
for (const user of users) {
if (user.primaryEmail) {
if (user.name?.fullName) {
console.log('%s (%s)', user.primaryEmail, user.name.fullName);
console.log("%s (%s)", user.primaryEmail, user.name.fullName);
} else {
console.log('%s', user.primaryEmail);
console.log("%s", user.primaryEmail);
}
}
}
Expand Down
31 changes: 21 additions & 10 deletions adminSDK/reports/quickstart.gs
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,38 @@
* @see https://developers.google.com/admin-sdk/reports/reference/rest/v1/activities/list
*/
function listLogins() {
const userKey = 'all';
const applicationName = 'login';
const userKey = "all";
const applicationName = "login";
const optionalArgs = {
maxResults: 10
maxResults: 10,
};
if (!AdminReports || !AdminReports.Activities) {
throw new Error('Enable the AdminReports Advanced Service.');
throw new Error("Enable the AdminReports Advanced Service.");
}
const response = AdminReports.Activities.list(
userKey, applicationName, optionalArgs);
userKey,
applicationName,
optionalArgs,
);
const activities = response.items;
if (!activities || activities.length === 0) {
console.log('No logins found.');
console.log("No logins found.");
return;
}
// Print login events
console.log('Logins:');
console.log("Logins:");
for (const activity of activities) {
if (activity.id?.time && activity.actor?.email && activity.events?.[0]?.name) {
console.log('%s: %s (%s)', activity.id.time, activity.actor.email,
activity.events[0].name);
if (
activity.id?.time &&
activity.actor?.email &&
activity.events?.[0]?.name
) {
console.log(
"%s: %s (%s)",
activity.id.time,
activity.actor.email,
activity.events[0].name,
);
}
}
}
Expand Down
18 changes: 11 additions & 7 deletions adminSDK/reseller/quickstart.gs
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,29 @@
*/
function listSubscriptions() {
const optionalArgs = {
maxResults: 10
maxResults: 10,
};
if (!AdminReseller || !AdminReseller.Subscriptions) {
throw new Error('Enable the AdminReseller Advanced Service.');
throw new Error("Enable the AdminReseller Advanced Service.");
}
const response = AdminReseller.Subscriptions.list(optionalArgs);
const subscriptions = response.subscriptions;
if (!subscriptions || subscriptions.length === 0) {
console.log('No subscriptions found.');
console.log("No subscriptions found.");
return;
}
console.log('Subscriptions:');
console.log("Subscriptions:");
for (const subscription of subscriptions) {
if (subscription.customerId && subscription.skuId) {
if (subscription.plan?.planName) {
console.log('%s (%s, %s)', subscription.customerId, subscription.skuId,
subscription.plan.planName);
console.log(
"%s (%s, %s)",
subscription.customerId,
subscription.skuId,
subscription.plan.planName,
);
} else {
console.log('%s (%s)', subscription.customerId, subscription.skuId);
console.log("%s (%s)", subscription.customerId, subscription.skuId);
}
}
}
Expand Down
Loading
Loading