@gasboost/vite 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/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # @gasboost/vite
2
+
3
+ `@gasboost/app` で構築したアプリケーションを Google Apps Script 向けにビルドするための Vite プラグインです。
4
+
5
+ アプリケーションの entry file を静的解析し、`AppsScript` に登録されたハンドラを検出して、Google Apps Script が認識するためのグローバル関数宣言を生成します。
6
+
7
+ ## インストール
8
+
9
+ ```bash
10
+ pnpm add @gasboost/app
11
+ pnpm add -D @gasboost/vite vite
12
+ ```
13
+
14
+ npm:
15
+
16
+ ```bash
17
+ npm install @gasboost/app
18
+ npm install -D @gasboost/vite vite
19
+ ```
20
+
21
+ ## 使い方
22
+
23
+ まず通常の Gasboost アプリケーションを作成します。
24
+
25
+ ```ts
26
+ // src/main.ts
27
+
28
+ import { AppsScript } from "@gasboost/app";
29
+
30
+ const app = new AppsScript()
31
+ .get((request) => {
32
+ return HtmlService.createHtmlOutput("Hello");
33
+ })
34
+ .post((request) => {
35
+ return ContentService.createTextOutput(request.text());
36
+ })
37
+ .call("sum", (a: number, b: number) => a + b);
38
+
39
+ export default app;
40
+ ```
41
+
42
+ Vite を設定します。
43
+
44
+ ```ts
45
+ // vite.config.ts
46
+
47
+ import { defineConfig } from "vite";
48
+ import { gasboost } from "@gasboost/vite";
49
+
50
+ export default defineConfig({
51
+ plugins: [
52
+ gasboost({
53
+ entry: "src/main.ts",
54
+ }),
55
+ ],
56
+ });
57
+ ```
58
+
59
+ 通常通りビルドできます。
60
+
61
+ ```bash
62
+ vite build
63
+ ```
64
+
65
+ ## オプション
66
+
67
+ ### `entry`
68
+
69
+ 必須です。
70
+
71
+ `AppsScript` アプリケーションを定義している entry file のパスを指定します。
72
+
73
+ ```ts
74
+ gasboost({
75
+ entry: "src/main.ts",
76
+ });
77
+ ```
78
+
79
+ entry file には1つの `AppsScript` インスタンスを定義し、そのインスタンスを default export する必要があります。
80
+
81
+ ```ts
82
+ const app = new AppsScript();
83
+
84
+ export default app;
85
+ ```
86
+
87
+ チェーン形式の登録に対応しています。
88
+
89
+ ```ts
90
+ const app = new AppsScript()
91
+ .get(...)
92
+ .post(...)
93
+ .call("getUser", ...);
94
+
95
+ export default app;
96
+ ```
97
+
98
+ インスタンス生成後に登録する形式にも対応しています。
99
+
100
+ ```ts
101
+ const app = new AppsScript();
102
+
103
+ app.get(...);
104
+ app.call("getUser", ...);
105
+
106
+ export default app;
107
+ ```
108
+
109
+ ### `envDir`
110
+
111
+ 任意です。
112
+
113
+ Vite が環境変数ファイルを読み込むディレクトリを指定します。
114
+
115
+ ```ts
116
+ gasboost({
117
+ entry: "src/main.ts",
118
+ envDir: "config",
119
+ });
120
+ ```
121
+
122
+ 例えば以下の構成を利用できます。
123
+
124
+ ```text
125
+ config/
126
+ ├── .env
127
+ ├── .env.development
128
+ └── .env.production
129
+ ```
130
+
131
+ ## 生成されるグローバル関数
132
+
133
+ Google Apps Script はトップレベルのグローバル関数を entry point として認識します。
134
+
135
+ 例えば次のアプリケーションを定義した場合:
136
+
137
+ ```ts
138
+ const app = new AppsScript()
139
+ .get(...)
140
+ .post(...)
141
+ .call("getUser", ...)
142
+ .call("sum", ...);
143
+
144
+ export default app;
145
+ ```
146
+
147
+ ビルド結果には以下に対応するグローバル関数宣言が生成されます。
148
+
149
+ ```js
150
+ function doGet() {}
151
+ function doPost() {}
152
+ function getUser() {}
153
+ function sum() {}
154
+ ```
155
+
156
+ これらの宣言は Google Apps Script に関数名を認識させるためのものです。
157
+
158
+ 実際の dispatch 処理は行いません。
159
+
160
+ ハンドラの実体は `@gasboost/app` がランタイム上で登録します。
161
+
162
+ 責務は次のように分離されています。
163
+
164
+ ```text
165
+ @gasboost/app
166
+ runtime registration
167
+ handler dispatch
168
+ globalThis implementation
169
+
170
+ @gasboost/vite
171
+ static analysis
172
+ GAS build configuration
173
+ global function declarations
174
+ ```
175
+
176
+ ## 静的解析
177
+
178
+ プラグインは指定された entry file を静的解析します。
179
+
180
+ 対象となる `AppsScript` インスタンスに対する登録を検出します。
181
+
182
+ ```ts
183
+ app.get(...);
184
+ app.post(...);
185
+ app.call("getUser", ...);
186
+ ```
187
+
188
+ 別オブジェクトの同名メソッドは無視されます。
189
+
190
+ ```ts
191
+ other.get(...);
192
+ other.call("something", ...);
193
+ ```
194
+
195
+ ### RPC 名
196
+
197
+ RPC 名は文字列リテラルで指定する必要があります。
198
+
199
+ 対応:
200
+
201
+ ```ts
202
+ app.call("getUser", handler);
203
+ ```
204
+
205
+ 非対応:
206
+
207
+ ```ts
208
+ const name = "getUser";
209
+
210
+ app.call(name, handler);
211
+ ```
212
+
213
+ ビルド時に生成する GAS グローバル関数名を静的に確定するため、この制約があります。
214
+
215
+ ### 重複登録
216
+
217
+ 以下の曖昧な登録はエラーになります。
218
+
219
+ - GET ハンドラの重複
220
+ - POST ハンドラの重複
221
+ - RPC 名の重複
222
+ - entry 内に複数の `AppsScript` インスタンスが存在する場合
223
+
224
+ `AppsScript` インスタンスは default export されている必要があります。
225
+
226
+ ## GAS 向けビルド設定
227
+
228
+ `gasboost()` が Google Apps Script 向けの Vite build configuration を提供します。
229
+
230
+ 現在は以下の設定を利用します。
231
+
232
+ - target: ECMAScript 2019
233
+ - output format: CommonJS
234
+ - output directory: `dist`
235
+ - `entry` で指定されたファイルを build input として利用
236
+
237
+ 利用側の Vite config で GAS 固有の build setting を重複して定義する必要はありません。
238
+
239
+ ## 環境変数
240
+
241
+ 環境変数は Vite 標準の仕組みを利用します。
242
+
243
+ 例えば:
244
+
245
+ ```text
246
+ config/.env
247
+ ```
248
+
249
+ ```env
250
+ VITE_API_URL=https://example.com
251
+ ```
252
+
253
+ `envDir` を設定します。
254
+
255
+ ```ts
256
+ gasboost({
257
+ entry: "src/main.ts",
258
+ envDir: "config",
259
+ });
260
+ ```
261
+
262
+ アプリケーションから通常通り参照できます。
263
+
264
+ ```ts
265
+ const apiUrl = import.meta.env.VITE_API_URL;
266
+ ```
267
+
268
+ mode ごとの環境変数ファイルにも対応します。
269
+
270
+ ```bash
271
+ vite build --mode production
272
+ ```
273
+
274
+ `envDir: "config"` の場合、例えば以下が読み込まれます。
275
+
276
+ ```text
277
+ config/.env.production
278
+ ```
279
+
280
+ ## 責務
281
+
282
+ `@gasboost/vite` はビルド時の処理のみを担当します。
283
+
284
+ 主な責務:
285
+
286
+ - `AppsScript` 登録内容の静的解析
287
+ - Google Apps Script 向け Vite configuration
288
+ - GAS グローバル関数宣言の生成
289
+ - Vite 環境変数との統合
290
+
291
+ 以下は担当しません。
292
+
293
+ - GET ハンドラの実行
294
+ - POST ハンドラの実行
295
+ - RPC dispatch
296
+ - ハンドラ実体のランタイム登録
297
+ - Google Apps Script API 自体の抽象化
298
+
299
+ これらの runtime responsibility は `@gasboost/app` が担当します。
300
+
301
+ ## 現在の制約
302
+
303
+ Analyzer は、意図的にシンプルな entry file 構造のみを対象としています。
304
+
305
+ 以下のような alias を介した default export は現在対象外です。
306
+
307
+ ```ts
308
+ const app = new AppsScript();
309
+ const exported = app;
310
+
311
+ export default exported;
312
+ ```
313
+
314
+ 別関数の内部に登録処理を隠す形式も対象外です。
315
+
316
+ ```ts
317
+ registerHandlers(app);
318
+ ```
319
+
320
+ 別ファイルに登録処理を分散する形式も現在対象外です。
321
+
322
+ ```ts
323
+ import { registerHandlers } from "./handlers";
324
+
325
+ registerHandlers(app);
326
+ ```
327
+
328
+ 登録内容を entry file から静的に確定できるようにすることで、生成する GAS グローバル関数を決定的にしています。
329
+
330
+ ## 関連パッケージ
331
+
332
+ アプリケーションの runtime と handler registration には `@gasboost/app` を利用してください。
@@ -0,0 +1,6 @@
1
+ export interface AppsScriptAnalysis {
2
+ hasGet: boolean;
3
+ hasPost: boolean;
4
+ calls: string[];
5
+ }
6
+ export declare function analyzeAppsScript(entry: string): AppsScriptAnalysis;
@@ -0,0 +1,3 @@
1
+ import type { UserConfig } from "vite";
2
+ import type { GasboostOptions } from "./gasboost";
3
+ export declare function createGasboostConfig(options: GasboostOptions): UserConfig;
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from "vite";
2
+ export interface GasboostOptions {
3
+ entry: string;
4
+ envDir?: string;
5
+ }
6
+ export declare function gasboost(options: GasboostOptions): Plugin;
@@ -0,0 +1,2 @@
1
+ import type { AppsScriptAnalysis } from "./analyzer";
2
+ export declare function createGlobalCode(analysis: AppsScriptAnalysis): string;
@@ -0,0 +1 @@
1
+ export { gasboost, type GasboostOptions } from "./gasboost";
package/dist/index.js ADDED
@@ -0,0 +1,135 @@
1
+ import e from "node:fs";
2
+ import t from "node:path";
3
+ import { isCallExpression as n, isIdentifier as r, isNewExpression as i, isPropertyAccessExpression as a, isStringLiteral as o, isVariableDeclaration as s } from "typescript/unstable/ast/is";
4
+ import { createVirtualFileSystem as c } from "typescript/unstable/fs";
5
+ import { API as l } from "typescript/unstable/sync";
6
+ //#region src/analyzer.ts
7
+ function u(r) {
8
+ let i = t.resolve(r);
9
+ if (!e.existsSync(i)) throw Error(`Entry file not found: ${i}`);
10
+ let s = d(i, e.readFileSync(i, "utf8")), c = f(s);
11
+ if (c.length === 0) throw Error("AppsScript instance not found.");
12
+ if (c.length > 1) throw Error("Multiple AppsScript instances found in entry.");
13
+ let l = c[0];
14
+ g(s, l);
15
+ let u = {
16
+ hasGet: !1,
17
+ hasPost: !1,
18
+ calls: []
19
+ }, p = /* @__PURE__ */ new Set();
20
+ function m(e) {
21
+ if (!a(e.expression)) return;
22
+ let t = e.expression, n = t.name.text;
23
+ if (!h(t.expression, l)) return;
24
+ if (n === "get") {
25
+ if (u.hasGet) throw Error("Duplicate GET handler registration.");
26
+ u.hasGet = !0;
27
+ return;
28
+ }
29
+ if (n === "post") {
30
+ if (u.hasPost) throw Error("Duplicate POST handler registration.");
31
+ u.hasPost = !0;
32
+ return;
33
+ }
34
+ if (n !== "call") return;
35
+ let r = e.arguments[0];
36
+ if (!r) throw Error(".call() requires a function name.");
37
+ if (!o(r)) throw Error(".call() function name must be a string literal.");
38
+ if (p.has(r.text)) throw Error(`Duplicate RPC registration: "${r.text}".`);
39
+ p.add(r.text), u.calls.unshift(r.text);
40
+ }
41
+ function _(e) {
42
+ n(e) && m(e), e.forEachChild(_);
43
+ }
44
+ return _(s), u;
45
+ }
46
+ function d(e, t) {
47
+ let n = "/entry.ts", r = "/tsconfig.json", i = c({
48
+ [r]: JSON.stringify({ files: [n] }),
49
+ [n]: t
50
+ }), a = new l({
51
+ cwd: "/",
52
+ fs: i
53
+ }).updateSnapshot({ openProject: r }).getProject(r);
54
+ if (!a) throw Error("Failed to create TypeScript project.");
55
+ let o = a.program.getSourceFile(n);
56
+ if (!o) throw Error(`Failed to parse entry: ${e}`);
57
+ return o;
58
+ }
59
+ function f(e) {
60
+ let t = [];
61
+ function n(e) {
62
+ s(e) && p(e) && t.push(e.name.text), e.forEachChild(n);
63
+ }
64
+ return n(e), t;
65
+ }
66
+ function p(e) {
67
+ return !r(e.name) || !e.initializer ? !1 : m(e.initializer);
68
+ }
69
+ function m(e) {
70
+ return i(e) && r(e.expression) && e.expression.text === "AppsScript" ? !0 : n(e) && a(e.expression) ? m(e.expression.expression) : !1;
71
+ }
72
+ function h(e, t) {
73
+ return r(e) && e.text === t || i(e) && r(e.expression) && e.expression.text === "AppsScript" ? !0 : n(e) && a(e.expression) ? h(e.expression.expression, t) : !1;
74
+ }
75
+ function g(e, t) {
76
+ let n = !1;
77
+ for (let r of e.statements) if (r.getText() === `export default ${t};`) {
78
+ n = !0;
79
+ break;
80
+ }
81
+ if (!n) throw Error("AppsScript instance must be default exported.");
82
+ }
83
+ //#endregion
84
+ //#region src/config.ts
85
+ function _(e) {
86
+ let n = t.resolve(e.entry);
87
+ return {
88
+ envDir: e.envDir,
89
+ build: {
90
+ target: "es2019",
91
+ outDir: "dist",
92
+ emptyOutDir: !1,
93
+ rollupOptions: {
94
+ input: n,
95
+ output: {
96
+ format: "cjs",
97
+ entryFileNames: t.basename(n, t.extname(n)) + ".js"
98
+ }
99
+ }
100
+ }
101
+ };
102
+ }
103
+ //#endregion
104
+ //#region src/globals.ts
105
+ var v = /* @__PURE__ */ new Set(["doGet", "doPost"]);
106
+ function y(e) {
107
+ if (v.has(e)) throw Error(`RPC name "${e}" is reserved by Google Apps Script.`);
108
+ if (!/^[$A-Z_a-z][$\w]*$/u.test(e)) throw Error(`RPC name "${e}" is not a valid JavaScript identifier.`);
109
+ }
110
+ function b(e) {
111
+ let t = [];
112
+ e.hasGet && t.push("function doGet() {}"), e.hasPost && t.push("function doPost() {}");
113
+ for (let n of e.calls) y(n), t.push(`function ${n}() {}`);
114
+ return t.join("\n");
115
+ }
116
+ //#endregion
117
+ //#region src/gasboost.ts
118
+ function x(e) {
119
+ let t;
120
+ return {
121
+ name: "gasboost",
122
+ config() {
123
+ return _(e);
124
+ },
125
+ buildStart() {
126
+ t = u(e.entry);
127
+ },
128
+ generateBundle(e, n) {
129
+ let r = b(t);
130
+ for (let e of Object.values(n)) e.type === "chunk" && e.isEntry && (e.code = `${r}\n\n${e.code}`);
131
+ }
132
+ };
133
+ }
134
+ //#endregion
135
+ export { x as gasboost };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@gasboost/vite",
3
+ "version": "0.1.0",
4
+ "description": "A Vite plugin for building @gasboost/app applications for Google Apps Script.",
5
+ "keywords": [
6
+ "google-apps-script",
7
+ "gas",
8
+ "vite",
9
+ "typescript",
10
+ "plugin",
11
+ "build",
12
+ "bundler",
13
+ "serverless",
14
+ "gasboost"
15
+ ],
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/gasboost/app.git",
20
+ "directory": "packages/vite"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "default": "./dist/index.js",
31
+ "types": "./dist/index.d.ts"
32
+ }
33
+ },
34
+ "type": "module",
35
+ "devDependencies": {
36
+ "vite": "^8.2.2"
37
+ },
38
+ "dependencies": {
39
+ "typescript": "^7.0.2"
40
+ },
41
+ "scripts": {
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "test": "vitest run",
44
+ "build": "rm -rf dist && vite build --config vite.package.config.ts && tsc -p tsconfig.build.json"
45
+ }
46
+ }