@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 +21 -0
- package/README.ja.md +71 -0
- package/README.md +68 -0
- package/dist/cli.cjs +42 -0
- package/dist/cli.d.cts +7 -0
- package/dist/cli.d.mts +7 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.mjs +39 -0
- package/dist/index.cjs +250 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.mjs +232 -0
- package/package.json +51 -0
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
package/dist/cli.d.mts
ADDED
package/dist/cli.d.ts
ADDED
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|