Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 0 additions & 2 deletions .eslintignore

This file was deleted.

50 changes: 0 additions & 50 deletions .eslintrc.js

This file was deleted.

32 changes: 16 additions & 16 deletions .gemini/settings.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"mcpServers": {
"workspace-developer": {
"httpUrl": "https://workspace-developer.goog/mcp",
"trust": true
}
},
"tools": {
"allowed": [
"run_shell_command(pnpm install)",
"run_shell_command(pnpm format)",
"run_shell_command(pnpm lint)",
"run_shell_command(pnpm check)",
"run_shell_command(pnpm test)"
]
}
}
"mcpServers": {
"workspace-developer": {
"httpUrl": "https://workspace-developer.goog/mcp",
"trust": true
}
},
"tools": {
"allowed": [
"run_shell_command(pnpm install)",
"run_shell_command(pnpm format)",
"run_shell_command(pnpm lint)",
"run_shell_command(pnpm check)",
"run_shell_command(pnpm test)"
]
}
}
130 changes: 73 additions & 57 deletions .github/scripts/check-gs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@

/// <reference types="node" />

import { exec } from "node:child_process";
import {
readdirSync,
statSync,
copyFileSync,
existsSync,
rmSync,
mkdirSync,
copyFileSync,
writeFileSync
} from 'fs';
import {join, relative, dirname, resolve, sep} from 'path';
import {exec} from 'child_process';
import {promisify} from 'util';
readdirSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { dirname, join, relative, resolve, sep } from "node:path";
import { promisify } from "node:util";

const execAsync = promisify(exec);
const TEMP_ROOT = '.tsc_check';
const TEMP_ROOT = ".tsc_check";

interface Project {
files: string[];
Expand All @@ -45,14 +45,18 @@ interface CheckResult {
}

// Helper to recursively find all files with a specific extension
function findFiles(dir: string, extension: string, fileList: string[] = []): string[] {
function findFiles(
dir: string,
extension: string,
fileList: string[] = [],
): string[] {
const files = readdirSync(dir);
for (const file of files) {
if (file.endsWith('.js')) continue;
if (file.endsWith(".js")) continue;
const filePath = join(dir, file);
const stat = statSync(filePath);
if (stat.isDirectory()) {
if (file !== 'node_modules' && file !== '.git' && file !== TEMP_ROOT) {
if (file !== "node_modules" && file !== ".git" && file !== TEMP_ROOT) {
findFiles(filePath, extension, fileList);
}
} else if (file.endsWith(extension)) {
Expand All @@ -64,10 +68,14 @@ function findFiles(dir: string, extension: string, fileList: string[] = []): str

// Find all directories containing appsscript.json
function findProjectRoots(rootDir: string): string[] {
return findFiles(rootDir, 'appsscript.json').map((f) => dirname(f));
return findFiles(rootDir, "appsscript.json").map((f) => dirname(f));
}

function createProjects(rootDir: string, projectRoots: string[], allGsFiles: string[]): Project[] {
function createProjects(
rootDir: string,
projectRoots: string[],
allGsFiles: string[],
): Project[] {
// Holds files that belong to a formal Apps Script project (defined by the presence of appsscript.json).
const projectGroups = new Map<string, string[]>();

Expand Down Expand Up @@ -107,7 +115,7 @@ function createProjects(rootDir: string, projectRoots: string[], allGsFiles: str
projects.push({
files,
name: `Project: ${relative(rootDir, dir)}`,
path: relative(rootDir, dir)
path: relative(rootDir, dir),
});
}
});
Expand All @@ -116,100 +124,109 @@ function createProjects(rootDir: string, projectRoots: string[], allGsFiles: str
projects.push({
files,
name: `Loose Project: ${relative(rootDir, dir)}`,
path: relative(rootDir, dir)
path: relative(rootDir, dir),
});
}
});

return projects;
}

async function checkProject(project: Project, rootDir: string): Promise<CheckResult> {
const projectNameSafe = project.name.replace(/[^a-zA-Z0-9]/g, '_');
async function checkProject(
project: Project,
rootDir: string,
): Promise<CheckResult> {
const projectNameSafe = project.name.replace(/[^a-zA-Z0-9]/g, "_");
const projectTempDir = join(TEMP_ROOT, projectNameSafe);

// Synchronous setup is fine as it's fast and avoids race conditions on mkdir if we were sharing dirs (we aren't)
mkdirSync(projectTempDir, {recursive: true});
mkdirSync(projectTempDir, { recursive: true });

for (const file of project.files) {
const fileRelPath = relative(rootDir, file);
const destPath = join(projectTempDir, fileRelPath.replace(/\.gs$/, '.js'));
const destPath = join(projectTempDir, fileRelPath.replace(/\.gs$/, ".js"));
const destDir = dirname(destPath);
mkdirSync(destDir, {recursive: true});
mkdirSync(destDir, { recursive: true });
copyFileSync(file, destPath);
}

const tsConfig = {
extends: '../../tsconfig.json',
extends: "../../tsconfig.json",
compilerOptions: {
noEmit: true,
allowJs: true,
checkJs: true,
typeRoots: [resolve(rootDir, 'node_modules/@types')]
typeRoots: [resolve(rootDir, "node_modules/@types")],
},
include: ['**/*.js']
include: ["**/*.js"],
};

writeFileSync(
join(projectTempDir, 'tsconfig.json'),
JSON.stringify(tsConfig, null, 2)
join(projectTempDir, "tsconfig.json"),
JSON.stringify(tsConfig, null, 2),
);

try {
await execAsync(`tsc -p "${projectTempDir}"`, {cwd: rootDir});
return {name: project.name, success: true, output: ''};
} catch (e: any) {
const rawOutput = (e.stdout || '') + (e.stderr || '');

const rewritten = rawOutput.split('\n').map((line: string) => {
if (line.includes(projectTempDir)) {
let newLine = line.split(projectTempDir + sep).pop();
if (!newLine) {
return line;
await execAsync(`tsc -p \"${projectTempDir}\"`, { cwd: rootDir });
return { name: project.name, success: true, output: "" };
} catch (e) {
const err = e as { stdout?: string; stderr?: string };
const rawOutput = (err.stdout ?? "") + (err.stderr || "");

const rewritten = rawOutput
.split("\n")
.map((line: string) => {
if (line.includes(projectTempDir)) {
let newLine = line.split(projectTempDir + sep).pop();
if (!newLine) {
return line;
}
newLine = newLine.replace(/\.js(:|\()/g, ".gs$1");
return newLine;
}
newLine = newLine.replace(/\.js(:|\()/g, '.gs$1');
return newLine;
}
return line;
}).join('\n');
return line;
})
.join("\n");

return {name: project.name, success: false, output: rewritten};
return { name: project.name, success: false, output: rewritten };
}
}

async function main() {
try {
const rootDir = resolve('.');
const rootDir = resolve(".");
const args = process.argv.slice(2);
const searchArg = args.find(arg => arg !== '--');
const searchArg = args.find((arg) => arg !== "--");

// 1. Discovery
const projectRoots = findProjectRoots(rootDir);
const allGsFiles = findFiles(rootDir, '.gs');
const allGsFiles = findFiles(rootDir, ".gs");

// 2. Grouping
const projects = createProjects(rootDir, projectRoots, allGsFiles);

// 3. Filtering
const projectsToCheck = projects.filter(p => {
const projectsToCheck = projects.filter((p) => {
return !searchArg || p.path.startsWith(searchArg);
});

if (projectsToCheck.length === 0) {
console.log('No projects found matching the search path.');
console.log("No projects found matching the search path.");
return;
}

// 4. Setup
if (existsSync(TEMP_ROOT)) {
rmSync(TEMP_ROOT, {recursive: true, force: true});
rmSync(TEMP_ROOT, { recursive: true, force: true });
}
mkdirSync(TEMP_ROOT);

console.log(`Checking ${projectsToCheck.length} projects in parallel...`);

// 5. Parallel Execution
const results = await Promise.all(projectsToCheck.map(p => checkProject(p, rootDir)));
const results = await Promise.all(
projectsToCheck.map((p) => checkProject(p, rootDir)),
);

// 6. Reporting
let hasError = false;
Expand All @@ -222,18 +239,17 @@ async function main() {
}

if (hasError) {
console.error('\nOne or more checks failed.');
console.error("\nOne or more checks failed.");
process.exit(1);
} else {
console.log('\nAll checks passed.');
console.log("\nAll checks passed.");
}

} catch (err) {
console.error('Unexpected error:', err);
console.error("Unexpected error:", err);
process.exit(1);
} finally {
if (existsSync(TEMP_ROOT)) {
rmSync(TEMP_ROOT, {recursive: true, force: true});
rmSync(TEMP_ROOT, { recursive: true, force: true });
}
}
}
Expand Down
6 changes: 2 additions & 4 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{
"recommendations": [
"google-workspace.google-workspace-developer-tools"
]
}
"recommendations": ["google-workspace.google-workspace-developer-tools"]
}
8 changes: 4 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"files.associations": {
"*.gs": "javascript"
}
}
"files.associations": {
"*.gs": "javascript"
}
}
10 changes: 10 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ 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_... ]`.


## Tools

Lint and format code using [Biome](https://biomejs.dev/).

```bash
pnpm lint
pnpm format
```

## Apps Script Code Best Practices

Apps Script supports the V8 runtime, which enables modern ECMAScript syntax. Using these features makes your code cleaner, more readable, and less error-prone.
Expand Down
2 changes: 1 addition & 1 deletion ai/autosummarize/appsscript.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
}
]
}
}
}
Loading
Loading