Skip to content

Commit 3ebacfe

Browse files
authored
build: pnpm and enable JSDoc type checking for .gs files via TSC (#580)
1 parent 924e683 commit 3ebacfe

File tree

18 files changed

+1712
-2274
lines changed

18 files changed

+1712
-2274
lines changed

.eslintrc.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,31 @@ module.exports = {
2020
ecmaVersion: 2020
2121
},
2222
env: {
23-
node: true,
23+
'node': true,
2424
'googleappsscript/googleappsscript': true
2525
},
2626
rules: {
2727
'comma-dangle': ['error', 'never'],
28-
'max-len': ['error', { code: 100 }],
29-
'camelcase': ['error', {
30-
'ignoreDestructuring': true,
31-
'ignoreImports': true,
32-
'allow': ['access_type', 'redirect_uris'],
33-
}],
28+
'max-len': ['error', {code: 100}],
29+
'camelcase': [
30+
'error',
31+
{
32+
ignoreDestructuring: true,
33+
ignoreImports: true,
34+
allow: ['access_type', 'redirect_uris']
35+
}
36+
],
3437
'guard-for-in': 'off',
3538
'no-var': 'off', // ES3
3639
'no-unused-vars': 'off' // functions aren't used.
3740
},
38-
plugins: [
39-
'googleappsscript'
41+
plugins: ['googleappsscript'],
42+
overrides: [
43+
{
44+
files: ['scripts/**/*.js'],
45+
parserOptions: {
46+
sourceType: 'module'
47+
}
48+
}
4049
]
41-
}
50+
};

.gemini/settings.json

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
{
2-
"mcpServers": {
3-
"workspace-developer": {
4-
"httpUrl": "https://workspace-developer.goog/mcp",
5-
"trust": true
6-
}
7-
}
2+
"mcpServers": {
3+
"workspace-developer": {
4+
"httpUrl": "https://workspace-developer.goog/mcp",
5+
"trust": true
6+
}
7+
},
8+
"tools": {
9+
"allowed": [
10+
"run_shell_command(pnpm install)",
11+
"run_shell_command(pnpm format)",
12+
"run_shell_command(pnpm lint)",
13+
"run_shell_command(pnpm check)",
14+
"run_shell_command(pnpm test)"
15+
]
16+
}
817
}

.github/scripts/check-gs.ts

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/// <reference types="node" />
18+
19+
import {
20+
readdirSync,
21+
statSync,
22+
existsSync,
23+
rmSync,
24+
mkdirSync,
25+
copyFileSync,
26+
writeFileSync
27+
} from 'fs';
28+
import {join, relative, dirname, resolve, sep} from 'path';
29+
import {exec} from 'child_process';
30+
import {promisify} from 'util';
31+
32+
const execAsync = promisify(exec);
33+
const TEMP_ROOT = '.tsc_check';
34+
35+
interface Project {
36+
files: string[];
37+
name: string;
38+
path: string;
39+
}
40+
41+
interface CheckResult {
42+
name: string;
43+
success: boolean;
44+
output: string;
45+
}
46+
47+
// Helper to recursively find all files with a specific extension
48+
function findFiles(dir: string, extension: string, fileList: string[] = []): string[] {
49+
const files = readdirSync(dir);
50+
for (const file of files) {
51+
if (file.endsWith('.js')) continue;
52+
const filePath = join(dir, file);
53+
const stat = statSync(filePath);
54+
if (stat.isDirectory()) {
55+
if (file !== 'node_modules' && file !== '.git' && file !== TEMP_ROOT) {
56+
findFiles(filePath, extension, fileList);
57+
}
58+
} else if (file.endsWith(extension)) {
59+
fileList.push(filePath);
60+
}
61+
}
62+
return fileList;
63+
}
64+
65+
// Find all directories containing appsscript.json
66+
function findProjectRoots(rootDir: string): string[] {
67+
return findFiles(rootDir, 'appsscript.json').map((f) => dirname(f));
68+
}
69+
70+
function createProjects(rootDir: string, projectRoots: string[], allGsFiles: string[]): Project[] {
71+
// Holds files that belong to a formal Apps Script project (defined by the presence of appsscript.json).
72+
const projectGroups = new Map<string, string[]>();
73+
74+
// Holds "orphan" files that do not belong to any defined Apps Script project (no appsscript.json found).
75+
const looseGroups = new Map<string, string[]>();
76+
77+
// Initialize project groups
78+
for (const p of projectRoots) {
79+
projectGroups.set(p, []);
80+
}
81+
82+
for (const file of allGsFiles) {
83+
let assigned = false;
84+
let currentDir = dirname(file);
85+
86+
while (currentDir.startsWith(rootDir) && currentDir !== rootDir) {
87+
if (projectGroups.has(currentDir)) {
88+
projectGroups.get(currentDir)?.push(file);
89+
assigned = true;
90+
break;
91+
}
92+
currentDir = dirname(currentDir);
93+
}
94+
95+
if (!assigned) {
96+
const dir = dirname(file);
97+
if (!looseGroups.has(dir)) {
98+
looseGroups.set(dir, []);
99+
}
100+
looseGroups.get(dir)?.push(file);
101+
}
102+
}
103+
104+
const projects: Project[] = [];
105+
projectGroups.forEach((files, dir) => {
106+
if (files.length > 0) {
107+
projects.push({
108+
files,
109+
name: `Project: ${relative(rootDir, dir)}`,
110+
path: relative(rootDir, dir)
111+
});
112+
}
113+
});
114+
looseGroups.forEach((files, dir) => {
115+
if (files.length > 0) {
116+
projects.push({
117+
files,
118+
name: `Loose Project: ${relative(rootDir, dir)}`,
119+
path: relative(rootDir, dir)
120+
});
121+
}
122+
});
123+
124+
return projects;
125+
}
126+
127+
async function checkProject(project: Project, rootDir: string): Promise<CheckResult> {
128+
const projectNameSafe = project.name.replace(/[^a-zA-Z0-9]/g, '_');
129+
const projectTempDir = join(TEMP_ROOT, projectNameSafe);
130+
131+
// Synchronous setup is fine as it's fast and avoids race conditions on mkdir if we were sharing dirs (we aren't)
132+
mkdirSync(projectTempDir, {recursive: true});
133+
134+
for (const file of project.files) {
135+
const fileRelPath = relative(rootDir, file);
136+
const destPath = join(projectTempDir, fileRelPath.replace(/\.gs$/, '.js'));
137+
const destDir = dirname(destPath);
138+
mkdirSync(destDir, {recursive: true});
139+
copyFileSync(file, destPath);
140+
}
141+
142+
const tsConfig = {
143+
extends: '../../tsconfig.json',
144+
compilerOptions: {
145+
noEmit: true,
146+
allowJs: true,
147+
checkJs: true,
148+
typeRoots: [resolve(rootDir, 'node_modules/@types')]
149+
},
150+
include: ['**/*.js']
151+
};
152+
153+
writeFileSync(
154+
join(projectTempDir, 'tsconfig.json'),
155+
JSON.stringify(tsConfig, null, 2)
156+
);
157+
158+
try {
159+
await execAsync(`tsc -p "${projectTempDir}"`, {cwd: rootDir});
160+
return {name: project.name, success: true, output: ''};
161+
} catch (e: any) {
162+
const rawOutput = (e.stdout || '') + (e.stderr || '');
163+
164+
const rewritten = rawOutput.split('\n').map((line: string) => {
165+
if (line.includes(projectTempDir)) {
166+
let newLine = line.split(projectTempDir + sep).pop();
167+
if (!newLine) {
168+
return line;
169+
}
170+
newLine = newLine.replace(/\.js(:|\()/g, '.gs$1');
171+
return newLine;
172+
}
173+
return line;
174+
}).join('\n');
175+
176+
return {name: project.name, success: false, output: rewritten};
177+
}
178+
}
179+
180+
async function main() {
181+
try {
182+
const rootDir = resolve('.');
183+
const searchArg = process.argv[2];
184+
185+
// 1. Discovery
186+
const projectRoots = findProjectRoots(rootDir);
187+
const allGsFiles = findFiles(rootDir, '.gs');
188+
189+
// 2. Grouping
190+
const projects = createProjects(rootDir, projectRoots, allGsFiles);
191+
192+
// 3. Filtering
193+
const projectsToCheck = projects.filter(p => {
194+
return !searchArg || p.path.startsWith(searchArg);
195+
});
196+
197+
if (projectsToCheck.length === 0) {
198+
console.log('No projects found matching the search path.');
199+
return;
200+
}
201+
202+
// 4. Setup
203+
if (existsSync(TEMP_ROOT)) {
204+
rmSync(TEMP_ROOT, {recursive: true, force: true});
205+
}
206+
mkdirSync(TEMP_ROOT);
207+
208+
console.log(`Checking ${projectsToCheck.length} projects in parallel...`);
209+
210+
// 5. Parallel Execution
211+
const results = await Promise.all(projectsToCheck.map(p => checkProject(p, rootDir)));
212+
213+
// 6. Reporting
214+
let hasError = false;
215+
for (const result of results) {
216+
if (!result.success) {
217+
hasError = true;
218+
console.log(`\n--- Failed: ${result.name} ---`);
219+
console.log(result.output);
220+
}
221+
}
222+
223+
if (hasError) {
224+
console.error('\nOne or more checks failed.');
225+
process.exit(1);
226+
} else {
227+
console.log('\nAll checks passed.');
228+
}
229+
230+
} catch (err) {
231+
console.error('Unexpected error:', err);
232+
process.exit(1);
233+
} finally {
234+
if (existsSync(TEMP_ROOT)) {
235+
rmSync(TEMP_ROOT, {recursive: true, force: true});
236+
}
237+
}
238+
}
239+
240+
main();

.github/workflows/lint.yml

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,22 @@
1313
# limitations under the License.
1414
---
1515
name: Lint
16-
on: [push, pull_request, workflow_dispatch]
16+
on:
17+
push:
18+
branches:
19+
- main
20+
pull_request:
1721
jobs:
1822
lint:
1923
concurrency:
20-
group: ${{ github.head_ref || github.ref }}
24+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.head_ref || github.ref }}
2125
cancel-in-progress: true
22-
runs-on: ubuntu-20.04
26+
runs-on: ubuntu-latest
2327
steps:
24-
- uses: actions/checkout@v3.0.2
28+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
29+
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
30+
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
2531
with:
26-
fetch-depth: 0
27-
- uses: github/super-linter/slim@v4.9.4
28-
env:
29-
ERROR_ON_MISSING_EXEC_BIT: true
30-
VALIDATE_JSCPD: false
31-
VALIDATE_JAVASCRIPT_STANDARD: false
32-
VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' }}
33-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34-
- uses: actions/setup-node@v3
35-
with:
36-
node-version: '14'
37-
- run: npm install
38-
- run: npm run lint
32+
cache: "pnpm"
33+
- run: pnpm i
34+
- run: pnpm lint

.github/workflows/publish.yaml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ on:
2121
jobs:
2222
publish:
2323
concurrency:
24-
group: ${{ github.head_ref || github.ref }}
25-
cancel-in-progress: false
26-
runs-on: ubuntu-24.04
24+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.head_ref || github.ref }}
25+
cancel-in-progress: true
26+
runs-on: ubuntu-latest
2727
steps:
28-
- uses: actions/checkout@v3.0.2
28+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
29+
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
30+
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
2931
with:
30-
fetch-depth: 0
32+
cache: "pnpm"
33+
- run: pnpm i
3134
- name: Get changed files
3235
id: changed-files
3336
uses: tj-actions/changed-files@v23.1
@@ -36,8 +39,5 @@ jobs:
3639
echo "${CLASP_CREDENTIALS}" > "${HOME}/.clasprc.json"
3740
env:
3841
CLASP_CREDENTIALS: ${{secrets.CLASP_CREDENTIALS}}
39-
- uses: actions/setup-node@v3
40-
with:
41-
node-version: '20'
42-
- run: npm install -g @google/clasp
42+
- run: pnpm install -g @google/clasp
4343
- run: ./.github/scripts/clasp_push.sh ${{ steps.changed-files.outputs.all_changed_files }}

0 commit comments

Comments
 (0)