@ciderjs/gasnuki 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 luth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,71 @@
1
+ # gasnuki
2
+
3
+ Google Apps Script クライアントサイドAPIの型定義・ユーティリティ
4
+
5
+ ## 概要
6
+
7
+ `gasnuki`は、Google Apps Script のクライアントサイドAPIをTypeScriptで安全に扱うための型定義とユーティリティを提供します。
8
+ Apps Scriptとフロントエンド間の型安全な通信をサポートします。
9
+
10
+ ## インストール
11
+
12
+ ```bash
13
+ npm install gasnuki
14
+ ```
15
+
16
+ または
17
+
18
+ ```bash
19
+ yarn add gasnuki
20
+ ```
21
+
22
+ ## 使い方
23
+
24
+ 1. 型定義ファイルを生成します:
25
+
26
+ ```bash
27
+ npx gasnuki
28
+ ```
29
+
30
+ デフォルトでは `types` ディレクトリに型定義ファイルが生成されます。
31
+
32
+ 2. 生成されたディレクトリ(デフォルト: `types`)を `tsconfig.json` の `include` に追加してください:
33
+
34
+ ```json
35
+ {
36
+ "compilerOptions": {
37
+ // ... your options ...
38
+ },
39
+ "include": [
40
+ "src",
41
+ "types" // 型定義ファイルが 'types' ディレクトリにある場合はこれを追加
42
+ ]
43
+ }
44
+ ```
45
+
46
+ 3. これで型定義付きで `google` を利用できます。
47
+
48
+ ```ts
49
+ // google.script.run への型安全なアクセス
50
+ // 例: サーバーサイド関数 getContent を呼び出す
51
+
52
+ google.script.run
53
+ .withSuccessHandler((result) => {
54
+ console.log(result);
55
+ })
56
+ .getContent('Sheet1');
57
+ ```
58
+
59
+ ## 提供機能
60
+
61
+ - Google Apps Script クライアントAPIの型定義
62
+ - サーバーサイド関数の戻り値型をvoidに変換するユーティリティ型
63
+
64
+ ## コントリビュート
65
+
66
+ バグ報告やプルリクエストは歓迎します。
67
+ `issues`または`pull requests`からご連絡ください。
68
+
69
+ ## ライセンス
70
+
71
+ MIT
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # gasnuki
2
+
3
+ Type definitions and utilities for Google Apps Script client-side API
4
+
5
+ ## Overview
6
+
7
+ `gasnuki` provides TypeScript type definitions and utilities for safely using the Google Apps Script client-side API. It helps ensure type-safe communication between Apps Script and your frontend.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install gasnuki
13
+ ```
14
+
15
+ or
16
+
17
+ ```bash
18
+ yarn add gasnuki
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ 1. Generate type definitions by running:
24
+
25
+ ```bash
26
+ npx gasnuki
27
+ ```
28
+
29
+ This will generate type definition files in the `types` directory by default.
30
+
31
+ 2. Make sure the generated directory (default: `types`) is included in your `tsconfig.json`:
32
+
33
+ ```json
34
+ {
35
+ "compilerOptions": {
36
+ // ... your options ...
37
+ },
38
+ "include": [
39
+ "src",
40
+ "types" // Add this line if your type definitions are in the 'types' directory
41
+ ]
42
+ }
43
+ ```
44
+
45
+ 3. Then, you can use `google` with Type Definitions.
46
+ ```ts
47
+ // Type-safe access to google.script.run
48
+ // Example: Call the server-side function getContent
49
+
50
+ google.script.run
51
+ .withSuccessHandler((result) => {
52
+ console.log(result);
53
+ })
54
+ .getContent('Sheet1');
55
+ ```
56
+
57
+ ## Features
58
+
59
+ - Type definitions for Google Apps Script client-side API
60
+ - Utility type to convert server-side function return types to void
61
+
62
+ ## Contributing
63
+
64
+ Bug reports and pull requests are welcome. Please use the `issues` or `pull requests` section.
65
+
66
+ ## License
67
+
68
+ MIT
package/dist/cli.cjs ADDED
@@ -0,0 +1,42 @@
1
+ #! /usr/bin/env node
2
+ 'use strict';
3
+
4
+ const commander = require('commander');
5
+ const index = require('./index.cjs');
6
+ require('node:path');
7
+ require('chokidar');
8
+ require('consola');
9
+ require('node:fs');
10
+ require('ts-morph');
11
+
12
+ const version = "0.1.0";
13
+
14
+ const parseArgs = async (command) => {
15
+ const { project, srcDir, outDir, outputFile, watch } = command.opts();
16
+ await index.generateTypes({ project, srcDir, outDir, outputFile, watch });
17
+ };
18
+ const cli = async () => {
19
+ const program = new commander.Command();
20
+ program.name("gasnuki").description(
21
+ "Generate type definitions and utilities for Google Apps Script client-side API"
22
+ );
23
+ program.version(version, "-v, --version");
24
+ program.action(async (_param, command) => await parseArgs(command)).option(
25
+ "-p, --project <project>",
26
+ "Project root directory path",
27
+ process.cwd().replace(/\\/g, "/")
28
+ ).option(
29
+ "-s, --srcDir <dir>",
30
+ "Source directory name (relative to project root)",
31
+ "server"
32
+ ).option(
33
+ "-o, --outDir <dir>",
34
+ "Output directory name (relative to project root)",
35
+ "types"
36
+ ).option("-f, --outputFile <file>", "Output file name", "appsscript.ts").option("-w, --watch", "Watch for changes and re-generate types", false);
37
+ await program.parseAsync(process.argv);
38
+ };
39
+ cli();
40
+
41
+ exports.cli = cli;
42
+ exports.parseArgs = parseArgs;
package/dist/cli.d.cts ADDED
@@ -0,0 +1,7 @@
1
+
2
+ import { Command } from 'commander';
3
+
4
+ declare const parseArgs: (command: Command) => Promise<void>;
5
+ declare const cli: () => Promise<void>;
6
+
7
+ export { cli, parseArgs };
package/dist/cli.d.mts ADDED
@@ -0,0 +1,7 @@
1
+
2
+ import { Command } from 'commander';
3
+
4
+ declare const parseArgs: (command: Command) => Promise<void>;
5
+ declare const cli: () => Promise<void>;
6
+
7
+ export { cli, parseArgs };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,7 @@
1
+
2
+ import { Command } from 'commander';
3
+
4
+ declare const parseArgs: (command: Command) => Promise<void>;
5
+ declare const cli: () => Promise<void>;
6
+
7
+ export { cli, parseArgs };
package/dist/cli.mjs ADDED
@@ -0,0 +1,39 @@
1
+ #! /usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { generateTypes } from './index.mjs';
4
+ import 'node:path';
5
+ import 'chokidar';
6
+ import 'consola';
7
+ import 'node:fs';
8
+ import 'ts-morph';
9
+
10
+ const version = "0.1.0";
11
+
12
+ const parseArgs = async (command) => {
13
+ const { project, srcDir, outDir, outputFile, watch } = command.opts();
14
+ await generateTypes({ project, srcDir, outDir, outputFile, watch });
15
+ };
16
+ const cli = async () => {
17
+ const program = new Command();
18
+ program.name("gasnuki").description(
19
+ "Generate type definitions and utilities for Google Apps Script client-side API"
20
+ );
21
+ program.version(version, "-v, --version");
22
+ program.action(async (_param, command) => await parseArgs(command)).option(
23
+ "-p, --project <project>",
24
+ "Project root directory path",
25
+ process.cwd().replace(/\\/g, "/")
26
+ ).option(
27
+ "-s, --srcDir <dir>",
28
+ "Source directory name (relative to project root)",
29
+ "server"
30
+ ).option(
31
+ "-o, --outDir <dir>",
32
+ "Output directory name (relative to project root)",
33
+ "types"
34
+ ).option("-f, --outputFile <file>", "Output file name", "appsscript.ts").option("-w, --watch", "Watch for changes and re-generate types", false);
35
+ await program.parseAsync(process.argv);
36
+ };
37
+ cli();
38
+
39
+ export { cli, parseArgs };
package/dist/index.cjs ADDED
@@ -0,0 +1,250 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const chokidar = require('chokidar');
5
+ const consola = require('consola');
6
+ const fs = require('node:fs');
7
+ const tsMorph = require('ts-morph');
8
+
9
+ function _interopNamespaceCompat(e) {
10
+ if (e && typeof e === 'object' && 'default' in e) return e;
11
+ const n = Object.create(null);
12
+ if (e) {
13
+ for (const k in e) {
14
+ n[k] = e[k];
15
+ }
16
+ }
17
+ n.default = e;
18
+ return n;
19
+ }
20
+
21
+ const path__namespace = /*#__PURE__*/_interopNamespaceCompat(path);
22
+ const chokidar__namespace = /*#__PURE__*/_interopNamespaceCompat(chokidar);
23
+ const fs__namespace = /*#__PURE__*/_interopNamespaceCompat(fs);
24
+
25
+ const getInterfaceMethodDefinition_ = (name, node) => {
26
+ const parameters = node.getParameters().map((param) => {
27
+ const paramName = param.getName();
28
+ const type = param.getTypeNode()?.getText() ?? param.getType().getText(node) ?? "any";
29
+ const questionToken = param.hasQuestionToken() ? "?" : "";
30
+ return `${paramName}${questionToken}: ${type}`;
31
+ }).join(", ");
32
+ const returnTypeNode = node.getReturnTypeNode();
33
+ let returnType;
34
+ if (returnTypeNode != null) {
35
+ returnType = returnTypeNode.getText();
36
+ } else {
37
+ const inferredReturnType = node.getReturnType();
38
+ if (inferredReturnType.isVoid()) {
39
+ returnType = "void";
40
+ } else {
41
+ returnType = inferredReturnType.getText(node);
42
+ }
43
+ }
44
+ let jsDocString = "";
45
+ const jsDocOwner = "getJsDocs" in node ? node : "getParantOrThrow" in node && // @ts-expect-error variable declaration
46
+ node.getParentOrThrow().getKind() === tsMorph.SyntaxKind.VariableDeclaration ? (
47
+ // @ts-expect-error variable declaration
48
+ node.getParentOrThrow()
49
+ ) : null;
50
+ if (jsDocOwner != null) {
51
+ const jsDocs = "getJsDocs" in jsDocOwner ? jsDocOwner.getJsDocs() : null;
52
+ if (jsDocs != null && jsDocs.length > 0) {
53
+ const rawConmmentText = jsDocs.map((doc) => doc.getFullText()).join("\n");
54
+ if (rawConmmentText.includes("@deprecated")) {
55
+ const deprecatedDoc = jsDocs.find(
56
+ (doc) => doc.getFullText().includes("@deprecated")
57
+ );
58
+ jsDocString = `${deprecatedDoc != null ? deprecatedDoc.getFullText().trim() : "/**\n * @deprecated\n */"}
59
+ `;
60
+ } else {
61
+ const firstDoc = jsDocs[0];
62
+ const description = firstDoc.getDescription().trim();
63
+ if (description != null || firstDoc.getTags().length > 0) {
64
+ jsDocString = `${firstDoc.getFullText().trim()}
65
+ `;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ return `${jsDocString}${name}(${parameters}): ${returnType};`;
71
+ };
72
+ const generateAppsScriptTypes = async ({
73
+ project: projectPath,
74
+ srcDir,
75
+ outDir,
76
+ outputFile
77
+ }) => {
78
+ const absoluteSrcDir = path__namespace.resolve(projectPath, srcDir);
79
+ const absoluteOutDir = path__namespace.resolve(projectPath, outDir);
80
+ const absoluteOutputFile = path__namespace.resolve(absoluteOutDir, outputFile);
81
+ consola.consola.info("Starting AppsScript type generation with gasnuki...");
82
+ consola.consola.info(` AppsScript Source Directory: ${absoluteSrcDir}`);
83
+ consola.consola.info(` Output File: ${absoluteOutputFile}`);
84
+ const project = new tsMorph.Project({
85
+ tsConfigFilePath: path__namespace.resolve(projectPath, "tsconfig.json")
86
+ });
87
+ project.addSourceFilesAtPaths(
88
+ path__namespace.join(absoluteSrcDir, "**/*.ts").replace(/\\/g, "/")
89
+ );
90
+ const methodDefinitions = [];
91
+ const sourceFiles = project.getSourceFiles();
92
+ consola.consola.info(`Found ${sourceFiles.length} source file(s).`);
93
+ for (const sourceFile of sourceFiles) {
94
+ for (const funcDecl of sourceFile.getFunctions()) {
95
+ if (!funcDecl.isExported() && !funcDecl.isAmbient()) {
96
+ const name = funcDecl.getName();
97
+ if (name != null) {
98
+ methodDefinitions.push(getInterfaceMethodDefinition_(name, funcDecl));
99
+ }
100
+ }
101
+ }
102
+ for (const varStmt of sourceFile.getVariableStatements()) {
103
+ if (!varStmt.isExported() && !varStmt.isAmbient()) {
104
+ for (const varDecl of varStmt.getDeclarations()) {
105
+ const initializer = varDecl.getInitializer();
106
+ const varName = varDecl.getName();
107
+ if (initializer != null && (initializer.getKind() === tsMorph.SyntaxKind.ArrowFunction || initializer.getKind() === tsMorph.SyntaxKind.FunctionExpression)) {
108
+ methodDefinitions.push(
109
+ getInterfaceMethodDefinition_(
110
+ varName,
111
+ initializer
112
+ )
113
+ );
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ if (!fs__namespace.existsSync(absoluteOutDir)) {
120
+ fs__namespace.mkdirSync(absoluteOutDir, { recursive: true });
121
+ consola.consola.info(`Created output directory: ${absoluteOutDir}`);
122
+ }
123
+ const generatorName = "gasnuki";
124
+ let outputContent = `// Auto-generated by ${generatorName}
125
+ // Do NOT edit this file manually.
126
+ `;
127
+ if (methodDefinitions.length > 0) {
128
+ const formattedMethods = methodDefinitions.map(
129
+ (method) => method.split("\n").map((line) => ` ${line}`).join("\n")
130
+ ).join("\n\n");
131
+ outputContent += `export interface ServerScripts {
132
+ ${formattedMethods}
133
+ }
134
+ `;
135
+ consola.consola.info(
136
+ `Interface 'ServerScript' type definitions written to ${absoluteOutputFile} (${methodDefinitions.length} function(s)).`
137
+ );
138
+ } else {
139
+ outputContent = "export interface ServerScripts {}\n";
140
+ consola.consola.info(
141
+ `Interface 'ServerScript' type definitions written to ${absoluteOutputFile} (no functions found).`
142
+ );
143
+ }
144
+ outputContent += `
145
+ // Auto-generated Types for GoogleAppsScript in client-side code
146
+
147
+ type RemoveReturnType<T> = {
148
+ [P in keyof T]: T[P] extends (...args: infer A) => any
149
+ ? (...args: A) => void
150
+ : T[P];
151
+ };
152
+
153
+ type _AppsScriptRun = RemoveReturnType<ServerScripts> & {
154
+ withSuccessHandler: <T = string | number | boolean | undefined, U = any>(
155
+ callback: (returnValues: T, userObject?: U) => void,
156
+ ) => _AppsScriptRun;
157
+ withFailureHandler: <U = any>(
158
+ callback: (error: Error, userObject?: U) => void,
159
+ ) => _AppsScriptRun;
160
+ withUserObject: <U = any>(userObject: U) => _AppsScriptRun;
161
+ };
162
+
163
+ type _AppsScriptHistoryFunction = (
164
+ stateObject: object,
165
+ params: object,
166
+ hash: string,
167
+ ) => void;
168
+
169
+ interface _WebAppLovacationType {
170
+ hash: string;
171
+ parameter: Record<string, string>;
172
+ parameters: Record<string, string[]>;
173
+ }
174
+
175
+ export declare interface GoogleClientSideApi {
176
+ script: {
177
+ run: _AppsScriptRun;
178
+ url: {
179
+ getLocation: (callback: (location: _WebAppLovacationType) => void) => void;
180
+ };
181
+ history: {
182
+ push: _AppsScriptHistoryFunction;
183
+ replace: _AppsScriptHistoryFunction;
184
+ setChangeHandler: (
185
+ callback: (e: { state: object; location: _WebAppLovacationType }) => void
186
+ ) => void;
187
+ }
188
+ };
189
+ }
190
+
191
+ declare global {
192
+ const google: GoogleClientSideApi;
193
+ }
194
+ `;
195
+ fs__namespace.writeFileSync(absoluteOutputFile, outputContent);
196
+ };
197
+
198
+ const generateTypes = async ({
199
+ project,
200
+ srcDir,
201
+ outDir,
202
+ outputFile,
203
+ watch
204
+ }) => {
205
+ const runGeneration = async (triggeredBy) => {
206
+ const reason = triggeredBy ? ` (${triggeredBy})` : "";
207
+ consola.consola.info(`Generating AppsScript types${reason}...`);
208
+ try {
209
+ await generateAppsScriptTypes({ project, srcDir, outDir, outputFile });
210
+ consola.consola.info("Type generation complete.");
211
+ } catch (e) {
212
+ consola.consola.error(`Type generation failed: ${e.message}`, e);
213
+ }
214
+ };
215
+ await runGeneration();
216
+ if (watch) {
217
+ const sourcePathToWatch = path__namespace.resolve(project, srcDir).replace(/\\/g, "/");
218
+ consola.consola.info(
219
+ `Watching for changes in ${sourcePathToWatch}... (Press Ctrl+C to stop)`
220
+ );
221
+ const watcher = chokidar__namespace.watch(sourcePathToWatch, {
222
+ ignored: ["node_modules", "dist"],
223
+ persistent: true,
224
+ ignoreInitial: true
225
+ });
226
+ const eventHandler = async (filePath, eventName) => {
227
+ consola.consola.info(`Watcher is called triggered on ${eventName}`);
228
+ const relativePath = path__namespace.relative(project, filePath);
229
+ await runGeneration(relativePath);
230
+ };
231
+ watcher.on("ready", async () => {
232
+ console.log("...waiting...");
233
+ watcher.on("all", async (event, path2) => {
234
+ consola.consola.info(`Watcher is called triggered on ${event}: ${path2}`);
235
+ await eventHandler(path2, event);
236
+ });
237
+ });
238
+ for (const signal of ["SIGINT", "SIGTERM"]) {
239
+ process.on(signal, async () => {
240
+ await watcher.close();
241
+ consola.consola.info("Watcher is closed.");
242
+ process.exit(0);
243
+ });
244
+ }
245
+ } else {
246
+ process.exit(0);
247
+ }
248
+ };
249
+
250
+ exports.generateTypes = generateTypes;
@@ -0,0 +1,11 @@
1
+ interface GenerateOptions {
2
+ project: string;
3
+ srcDir: string;
4
+ outDir: string;
5
+ outputFile: string;
6
+ watch: boolean;
7
+ }
8
+ declare const generateTypes: ({ project, srcDir, outDir, outputFile, watch, }: GenerateOptions) => Promise<void>;
9
+
10
+ export { generateTypes };
11
+ export type { GenerateOptions };
@@ -0,0 +1,11 @@
1
+ interface GenerateOptions {
2
+ project: string;
3
+ srcDir: string;
4
+ outDir: string;
5
+ outputFile: string;
6
+ watch: boolean;
7
+ }
8
+ declare const generateTypes: ({ project, srcDir, outDir, outputFile, watch, }: GenerateOptions) => Promise<void>;
9
+
10
+ export { generateTypes };
11
+ export type { GenerateOptions };
@@ -0,0 +1,11 @@
1
+ interface GenerateOptions {
2
+ project: string;
3
+ srcDir: string;
4
+ outDir: string;
5
+ outputFile: string;
6
+ watch: boolean;
7
+ }
8
+ declare const generateTypes: ({ project, srcDir, outDir, outputFile, watch, }: GenerateOptions) => Promise<void>;
9
+
10
+ export { generateTypes };
11
+ export type { GenerateOptions };
package/dist/index.mjs ADDED
@@ -0,0 +1,232 @@
1
+ import * as path from 'node:path';
2
+ import * as chokidar from 'chokidar';
3
+ import { consola } from 'consola';
4
+ import * as fs from 'node:fs';
5
+ import { Project, SyntaxKind } from 'ts-morph';
6
+
7
+ const getInterfaceMethodDefinition_ = (name, node) => {
8
+ const parameters = node.getParameters().map((param) => {
9
+ const paramName = param.getName();
10
+ const type = param.getTypeNode()?.getText() ?? param.getType().getText(node) ?? "any";
11
+ const questionToken = param.hasQuestionToken() ? "?" : "";
12
+ return `${paramName}${questionToken}: ${type}`;
13
+ }).join(", ");
14
+ const returnTypeNode = node.getReturnTypeNode();
15
+ let returnType;
16
+ if (returnTypeNode != null) {
17
+ returnType = returnTypeNode.getText();
18
+ } else {
19
+ const inferredReturnType = node.getReturnType();
20
+ if (inferredReturnType.isVoid()) {
21
+ returnType = "void";
22
+ } else {
23
+ returnType = inferredReturnType.getText(node);
24
+ }
25
+ }
26
+ let jsDocString = "";
27
+ const jsDocOwner = "getJsDocs" in node ? node : "getParantOrThrow" in node && // @ts-expect-error variable declaration
28
+ node.getParentOrThrow().getKind() === SyntaxKind.VariableDeclaration ? (
29
+ // @ts-expect-error variable declaration
30
+ node.getParentOrThrow()
31
+ ) : null;
32
+ if (jsDocOwner != null) {
33
+ const jsDocs = "getJsDocs" in jsDocOwner ? jsDocOwner.getJsDocs() : null;
34
+ if (jsDocs != null && jsDocs.length > 0) {
35
+ const rawConmmentText = jsDocs.map((doc) => doc.getFullText()).join("\n");
36
+ if (rawConmmentText.includes("@deprecated")) {
37
+ const deprecatedDoc = jsDocs.find(
38
+ (doc) => doc.getFullText().includes("@deprecated")
39
+ );
40
+ jsDocString = `${deprecatedDoc != null ? deprecatedDoc.getFullText().trim() : "/**\n * @deprecated\n */"}
41
+ `;
42
+ } else {
43
+ const firstDoc = jsDocs[0];
44
+ const description = firstDoc.getDescription().trim();
45
+ if (description != null || firstDoc.getTags().length > 0) {
46
+ jsDocString = `${firstDoc.getFullText().trim()}
47
+ `;
48
+ }
49
+ }
50
+ }
51
+ }
52
+ return `${jsDocString}${name}(${parameters}): ${returnType};`;
53
+ };
54
+ const generateAppsScriptTypes = async ({
55
+ project: projectPath,
56
+ srcDir,
57
+ outDir,
58
+ outputFile
59
+ }) => {
60
+ const absoluteSrcDir = path.resolve(projectPath, srcDir);
61
+ const absoluteOutDir = path.resolve(projectPath, outDir);
62
+ const absoluteOutputFile = path.resolve(absoluteOutDir, outputFile);
63
+ consola.info("Starting AppsScript type generation with gasnuki...");
64
+ consola.info(` AppsScript Source Directory: ${absoluteSrcDir}`);
65
+ consola.info(` Output File: ${absoluteOutputFile}`);
66
+ const project = new Project({
67
+ tsConfigFilePath: path.resolve(projectPath, "tsconfig.json")
68
+ });
69
+ project.addSourceFilesAtPaths(
70
+ path.join(absoluteSrcDir, "**/*.ts").replace(/\\/g, "/")
71
+ );
72
+ const methodDefinitions = [];
73
+ const sourceFiles = project.getSourceFiles();
74
+ consola.info(`Found ${sourceFiles.length} source file(s).`);
75
+ for (const sourceFile of sourceFiles) {
76
+ for (const funcDecl of sourceFile.getFunctions()) {
77
+ if (!funcDecl.isExported() && !funcDecl.isAmbient()) {
78
+ const name = funcDecl.getName();
79
+ if (name != null) {
80
+ methodDefinitions.push(getInterfaceMethodDefinition_(name, funcDecl));
81
+ }
82
+ }
83
+ }
84
+ for (const varStmt of sourceFile.getVariableStatements()) {
85
+ if (!varStmt.isExported() && !varStmt.isAmbient()) {
86
+ for (const varDecl of varStmt.getDeclarations()) {
87
+ const initializer = varDecl.getInitializer();
88
+ const varName = varDecl.getName();
89
+ if (initializer != null && (initializer.getKind() === SyntaxKind.ArrowFunction || initializer.getKind() === SyntaxKind.FunctionExpression)) {
90
+ methodDefinitions.push(
91
+ getInterfaceMethodDefinition_(
92
+ varName,
93
+ initializer
94
+ )
95
+ );
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ if (!fs.existsSync(absoluteOutDir)) {
102
+ fs.mkdirSync(absoluteOutDir, { recursive: true });
103
+ consola.info(`Created output directory: ${absoluteOutDir}`);
104
+ }
105
+ const generatorName = "gasnuki";
106
+ let outputContent = `// Auto-generated by ${generatorName}
107
+ // Do NOT edit this file manually.
108
+ `;
109
+ if (methodDefinitions.length > 0) {
110
+ const formattedMethods = methodDefinitions.map(
111
+ (method) => method.split("\n").map((line) => ` ${line}`).join("\n")
112
+ ).join("\n\n");
113
+ outputContent += `export interface ServerScripts {
114
+ ${formattedMethods}
115
+ }
116
+ `;
117
+ consola.info(
118
+ `Interface 'ServerScript' type definitions written to ${absoluteOutputFile} (${methodDefinitions.length} function(s)).`
119
+ );
120
+ } else {
121
+ outputContent = "export interface ServerScripts {}\n";
122
+ consola.info(
123
+ `Interface 'ServerScript' type definitions written to ${absoluteOutputFile} (no functions found).`
124
+ );
125
+ }
126
+ outputContent += `
127
+ // Auto-generated Types for GoogleAppsScript in client-side code
128
+
129
+ type RemoveReturnType<T> = {
130
+ [P in keyof T]: T[P] extends (...args: infer A) => any
131
+ ? (...args: A) => void
132
+ : T[P];
133
+ };
134
+
135
+ type _AppsScriptRun = RemoveReturnType<ServerScripts> & {
136
+ withSuccessHandler: <T = string | number | boolean | undefined, U = any>(
137
+ callback: (returnValues: T, userObject?: U) => void,
138
+ ) => _AppsScriptRun;
139
+ withFailureHandler: <U = any>(
140
+ callback: (error: Error, userObject?: U) => void,
141
+ ) => _AppsScriptRun;
142
+ withUserObject: <U = any>(userObject: U) => _AppsScriptRun;
143
+ };
144
+
145
+ type _AppsScriptHistoryFunction = (
146
+ stateObject: object,
147
+ params: object,
148
+ hash: string,
149
+ ) => void;
150
+
151
+ interface _WebAppLovacationType {
152
+ hash: string;
153
+ parameter: Record<string, string>;
154
+ parameters: Record<string, string[]>;
155
+ }
156
+
157
+ export declare interface GoogleClientSideApi {
158
+ script: {
159
+ run: _AppsScriptRun;
160
+ url: {
161
+ getLocation: (callback: (location: _WebAppLovacationType) => void) => void;
162
+ };
163
+ history: {
164
+ push: _AppsScriptHistoryFunction;
165
+ replace: _AppsScriptHistoryFunction;
166
+ setChangeHandler: (
167
+ callback: (e: { state: object; location: _WebAppLovacationType }) => void
168
+ ) => void;
169
+ }
170
+ };
171
+ }
172
+
173
+ declare global {
174
+ const google: GoogleClientSideApi;
175
+ }
176
+ `;
177
+ fs.writeFileSync(absoluteOutputFile, outputContent);
178
+ };
179
+
180
+ const generateTypes = async ({
181
+ project,
182
+ srcDir,
183
+ outDir,
184
+ outputFile,
185
+ watch
186
+ }) => {
187
+ const runGeneration = async (triggeredBy) => {
188
+ const reason = triggeredBy ? ` (${triggeredBy})` : "";
189
+ consola.info(`Generating AppsScript types${reason}...`);
190
+ try {
191
+ await generateAppsScriptTypes({ project, srcDir, outDir, outputFile });
192
+ consola.info("Type generation complete.");
193
+ } catch (e) {
194
+ consola.error(`Type generation failed: ${e.message}`, e);
195
+ }
196
+ };
197
+ await runGeneration();
198
+ if (watch) {
199
+ const sourcePathToWatch = path.resolve(project, srcDir).replace(/\\/g, "/");
200
+ consola.info(
201
+ `Watching for changes in ${sourcePathToWatch}... (Press Ctrl+C to stop)`
202
+ );
203
+ const watcher = chokidar.watch(sourcePathToWatch, {
204
+ ignored: ["node_modules", "dist"],
205
+ persistent: true,
206
+ ignoreInitial: true
207
+ });
208
+ const eventHandler = async (filePath, eventName) => {
209
+ consola.info(`Watcher is called triggered on ${eventName}`);
210
+ const relativePath = path.relative(project, filePath);
211
+ await runGeneration(relativePath);
212
+ };
213
+ watcher.on("ready", async () => {
214
+ console.log("...waiting...");
215
+ watcher.on("all", async (event, path2) => {
216
+ consola.info(`Watcher is called triggered on ${event}: ${path2}`);
217
+ await eventHandler(path2, event);
218
+ });
219
+ });
220
+ for (const signal of ["SIGINT", "SIGTERM"]) {
221
+ process.on(signal, async () => {
222
+ await watcher.close();
223
+ consola.info("Watcher is closed.");
224
+ process.exit(0);
225
+ });
226
+ }
227
+ } else {
228
+ process.exit(0);
229
+ }
230
+ };
231
+
232
+ export { generateTypes };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@ciderjs/gasnuki",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "main": "dist/index.mjs",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "gasnuki": "dist/cli.mjs"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.mjs",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "dev": "jiti src/cli.ts -p playground/react -s src/server",
19
+ "start": "node dist/cli.mjs -p playground/react -s src/server",
20
+ "check": "biome check --write",
21
+ "build": "unbuild",
22
+ "test": "vitest run",
23
+ "prepare": "pnpm run check && pnpm run build"
24
+ },
25
+ "keywords": ["google-apps-script", "typescript", "@google/clasp"],
26
+ "author": "ciderjs/luth",
27
+ "license": "ISC",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+ssh://git@github.com:luthpg/gasnuki.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/luthpg/gasnuki/issues"
34
+ },
35
+ "packageManager": "pnpm@10.11.1",
36
+ "dependencies": {
37
+ "@inquirer/prompts": "^7.5.3",
38
+ "chokidar": "^4.0.3",
39
+ "commander": "^14.0.0",
40
+ "consola": "^3.4.2",
41
+ "ts-morph": "^26.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@biomejs/biome": "^1.9.4",
45
+ "@types/node": "^22.15.29",
46
+ "jiti": "^2.4.2",
47
+ "typescript": "^5.8.3",
48
+ "unbuild": "^3.5.0",
49
+ "vitest": "^3.2.1"
50
+ }
51
+ }