@gasboost/vite 0.1.1 → 1.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) 2026 gasboost
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.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # @gasboost/vite
2
2
 
3
- `@gasboost/app` で構築したアプリケーションを Google Apps Script 向けにビルドするための Vite プラグインです。
3
+ `@gasboost/app` で構築した Google Apps Script アプリケーションを、Vite でビルド・ローカル開発するためのプラグインです。
4
4
 
5
- アプリケーションの entry file を静的解析し、`AppsScript` に登録されたハンドラを検出して、Google Apps Script が認識するためのグローバル関数宣言を生成します。
5
+ `@gasboost/vite` は用途ごとに2つの Vite Plugin を提供します。
6
+
7
+ - `build` — Google Apps Script 向けの production build
8
+ - `dev` — ローカル開発時の RPC 実行
6
9
 
7
10
  ## インストール
8
11
 
@@ -18,119 +21,175 @@ npm install @gasboost/app
18
21
  npm install -D @gasboost/vite vite
19
22
  ```
20
23
 
21
- ## 使い方
24
+ ## Quick Start
22
25
 
23
- まず通常の Gasboost アプリケーションを作成します。
26
+ まず `@gasboost/app` でバックエンドを定義します。
24
27
 
25
28
  ```ts
26
- // src/main.ts
29
+ // src/server.ts
27
30
 
28
31
  import { AppsScript } from "@gasboost/app";
29
32
 
30
33
  const app = new AppsScript()
31
- .get((request) => {
34
+ .get(() => {
32
35
  return HtmlService.createHtmlOutput("Hello");
33
36
  })
34
37
  .post((request) => {
35
38
  return ContentService.createTextOutput(request.text());
36
39
  })
37
- .call("sum", (a: number, b: number) => a + b);
40
+ .call("sum", (a: number, b: number) => a + b)
41
+ .call("getUser", async (id: string) => ({
42
+ id,
43
+ name: "Taro",
44
+ }));
38
45
 
39
46
  export default app;
40
47
  ```
41
48
 
42
- Vite を設定します。
49
+ `gasboost()` から `build` と `dev` を取得し、1つの `vite.config.ts` の中で mode に応じて使い分けます。
50
+
51
+ frontend と backend は成果物と build graph が異なるため、同時に1つの Vite build へ混在させません。
43
52
 
44
53
  ```ts
45
54
  // vite.config.ts
46
55
 
56
+ import react from "@vitejs/plugin-react";
47
57
  import { defineConfig } from "vite";
58
+ import { viteSingleFile } from "vite-plugin-singlefile";
48
59
  import { gasboost } from "@gasboost/vite";
49
60
 
50
- export default defineConfig({
51
- plugins: [
52
- gasboost({
53
- entry: "src/main.ts",
54
- }),
55
- ],
61
+ const { build, dev } = gasboost({
62
+ entry: "src/server.ts",
63
+ envDir: ".env",
56
64
  });
57
- ```
58
65
 
59
- 通常通りビルドできます。
66
+ export default defineConfig(({ mode }) => {
67
+ if (mode === "server") {
68
+ return {
69
+ plugins: [build],
70
+ build: {
71
+ outDir: "dist",
72
+ emptyOutDir: false,
73
+ },
74
+ };
75
+ }
76
+
77
+ return {
78
+ plugins: [react(), viteSingleFile(), dev],
79
+ build: {
80
+ outDir: "dist",
81
+ },
82
+ };
83
+ });
84
+ ```
60
85
 
61
- ```bash
62
- vite build
86
+ この例では、frontend build と backend build を mode で分離します。
87
+
88
+ ```json
89
+ {
90
+ "scripts": {
91
+ "dev": "vite",
92
+ "build:client": "vite build --mode client",
93
+ "build:server": "vite build --mode server",
94
+ "build": "pnpm build:client && pnpm build:server"
95
+ }
96
+ }
63
97
  ```
64
98
 
65
- ## オプション
99
+ ```text
100
+ vite
101
+
102
+ gasboost:dev
66
103
 
67
- ### `entry`
104
+ vite build --mode client
105
+
106
+ frontend build
68
107
 
69
- 必須です。
108
+ vite build --mode server
109
+
110
+ gasboost:build
111
+ ```
112
+
113
+ frontend 側の React、single-file 化などの設定は利用側プロジェクトの責務です。
70
114
 
71
- `AppsScript` アプリケーションを定義している entry file のパスを指定します。
115
+ ## gasboost()
72
116
 
73
117
  ```ts
74
- gasboost({
75
- entry: "src/main.ts",
118
+ const { build, dev } = gasboost({
119
+ entry: "src/server.ts",
120
+ envDir: "config",
76
121
  });
77
122
  ```
78
123
 
79
- entry file には1つの `AppsScript` インスタンスを定義し、そのインスタンスを default export する必要があります。
124
+ 戻り値:
80
125
 
81
126
  ```ts
82
- const app = new AppsScript();
83
-
84
- export default app;
127
+ {
128
+ build: Plugin;
129
+ dev: Plugin;
130
+ }
85
131
  ```
86
132
 
87
- チェーン形式の登録に対応しています。
88
-
89
- ```ts
90
- const app = new AppsScript()
91
- .get(...)
92
- .post(...)
93
- .call("getUser", ...);
133
+ ### `entry`
94
134
 
95
- export default app;
96
- ```
135
+ 必須です。
97
136
 
98
- インスタンス生成後に登録する形式にも対応しています。
137
+ `AppsScript` を定義して default export している entry file を指定します。
99
138
 
100
139
  ```ts
101
- const app = new AppsScript();
102
-
103
- app.get(...);
104
- app.call("getUser", ...);
105
-
106
- export default app;
140
+ gasboost({
141
+ entry: "src/server.ts",
142
+ });
107
143
  ```
108
144
 
109
145
  ### `envDir`
110
146
 
111
147
  任意です。
112
148
 
113
- Vite が環境変数ファイルを読み込むディレクトリを指定します。
149
+ build 時に Vite が環境変数ファイルを読み込むディレクトリを指定します。
114
150
 
115
151
  ```ts
116
152
  gasboost({
117
- entry: "src/main.ts",
153
+ entry: "src/server.ts",
118
154
  envDir: "config",
119
155
  });
120
156
  ```
121
157
 
122
- 例えば以下の構成を利用できます。
158
+ ---
123
159
 
124
- ```text
125
- config/
126
- ├── .env
127
- ├── .env.development
128
- └── .env.production
160
+ # Build Plugin
161
+
162
+ `build` は `vite build` のときだけ動作し、GAS backend build を担当します。
163
+
164
+ frontend / backend を同じ `vite.config.ts` で扱う場合は、server 用 mode のときだけ `plugins` に含めてください。
165
+
166
+ ```ts
167
+ const { build } = gasboost({
168
+ entry: "src/server.ts",
169
+ });
170
+
171
+ export default defineConfig(({ mode }) => {
172
+ if (mode === "server") {
173
+ return {
174
+ plugins: [build],
175
+ };
176
+ }
177
+
178
+ return {
179
+ plugins: [],
180
+ };
181
+ });
129
182
  ```
130
183
 
131
- ## 生成されるグローバル関数
184
+ `build` plugin は次の処理を担当します。
132
185
 
133
- Google Apps Script はトップレベルのグローバル関数を entry point として認識します。
186
+ - entry file の静的解析
187
+ - `AppsScript` に登録された GET / POST / RPC の検出
188
+ - GAS が認識するグローバル関数宣言の生成
189
+ - GAS 向け Vite build configuration
190
+ - 環境変数の読み込み
191
+
192
+ ## グローバル関数の生成
134
193
 
135
194
  例えば次のアプリケーションを定義した場合:
136
195
 
@@ -144,7 +203,7 @@ const app = new AppsScript()
144
203
  export default app;
145
204
  ```
146
205
 
147
- ビルド結果には以下に対応するグローバル関数宣言が生成されます。
206
+ build plugin は GAS が認識するためのグローバル関数宣言を生成します。
148
207
 
149
208
  ```js
150
209
  function doGet() {}
@@ -153,56 +212,51 @@ function getUser() {}
153
212
  function sum() {}
154
213
  ```
155
214
 
156
- これらの宣言は Google Apps Script に関数名を認識させるためのものです。
157
-
158
- 実際の dispatch 処理は行いません。
159
-
160
- ハンドラの実体は `@gasboost/app` がランタイム上で登録します。
215
+ これらは関数名を Google Apps Script に認識させるための宣言です。
161
216
 
162
- 責務は次のように分離されています。
217
+ 実際のハンドラ登録と dispatch は `@gasboost/app` がランタイム上で行います。
163
218
 
164
219
  ```text
165
- @gasboost/app
166
- runtime registration
167
- handler dispatch
168
- globalThis implementation
169
-
170
220
  @gasboost/vite
171
221
  static analysis
172
- GAS build configuration
173
222
  global function declarations
223
+
224
+ @gasboost/app
225
+ runtime registration
226
+ dispatch
174
227
  ```
175
228
 
176
229
  ## 静的解析
177
230
 
178
- プラグインは指定された entry file を静的解析します。
179
-
180
- 対象となる `AppsScript` インスタンスに対する登録を検出します。
231
+ entry file には1つの `AppsScript` インスタンスを定義し、default export します。
181
232
 
182
233
  ```ts
234
+ const app = new AppsScript();
235
+
183
236
  app.get(...);
184
- app.post(...);
185
237
  app.call("getUser", ...);
238
+
239
+ export default app;
186
240
  ```
187
241
 
188
- 別オブジェクトの同名メソッドは無視されます。
242
+ チェーン形式にも対応しています。
189
243
 
190
244
  ```ts
191
- other.get(...);
192
- other.call("something", ...);
193
- ```
245
+ const app = new AppsScript()
246
+ .get(...)
247
+ .post(...)
248
+ .call("getUser", ...);
194
249
 
195
- ### RPC
250
+ export default app;
251
+ ```
196
252
 
197
253
  RPC 名は文字列リテラルで指定する必要があります。
198
254
 
199
- 対応:
200
-
201
255
  ```ts
202
256
  app.call("getUser", handler);
203
257
  ```
204
258
 
205
- 非対応:
259
+ 次のような動的な名前は静的解析の対象外です。
206
260
 
207
261
  ```ts
208
262
  const name = "getUser";
@@ -210,123 +264,442 @@ const name = "getUser";
210
264
  app.call(name, handler);
211
265
  ```
212
266
 
213
- ビルド時に生成する GAS グローバル関数名を静的に確定するため、この制約があります。
267
+ build 時に GAS のグローバル関数名を確定する必要があるためです。
214
268
 
215
- ### 重複登録
269
+ ## 検証
216
270
 
217
- 以下の曖昧な登録はエラーになります。
271
+ 静的解析時には、曖昧なアプリケーション定義をエラーとして扱います。
218
272
 
219
273
  - GET ハンドラの重複
220
274
  - POST ハンドラの重複
221
275
  - RPC 名の重複
222
- - entry 内に複数の `AppsScript` インスタンスが存在する場合
223
-
224
- `AppsScript` インスタンスは default export されている必要があります。
276
+ - entry 内の複数 `AppsScript` インスタンス
277
+ - default export されていない `AppsScript`
225
278
 
226
- ## GAS 向けビルド設定
279
+ ## GAS 向け build configuration
227
280
 
228
- `gasboost()` が Google Apps Script 向けの Vite build configuration を提供します。
229
-
230
- 現在は以下の設定を利用します。
281
+ 現在の build 設定:
231
282
 
232
283
  - target: ECMAScript 2019
233
284
  - output format: CommonJS
234
285
  - output directory: `dist`
235
- - `entry` で指定されたファイルを build input として利用
236
-
237
- 利用側の Vite config で GAS 固有の build setting を重複して定義する必要はありません。
286
+ - `entry` build input として利用
238
287
 
239
288
  ## 環境変数
240
289
 
241
- 環境変数は Vite 標準の仕組みを利用します。
242
-
243
- 例えば:
290
+ Vite 標準の環境変数機構を利用します。
244
291
 
245
292
  ```text
246
- config/.env
293
+ config/
294
+ ├── .env
295
+ ├── .env.development
296
+ └── .env.production
247
297
  ```
248
298
 
249
- ```env
250
- VITE_API_URL=https://example.com
299
+ ```ts
300
+ const { build } = gasboost({
301
+ entry: "src/server.ts",
302
+ envDir: "config",
303
+ });
251
304
  ```
252
305
 
253
- `envDir` を設定します。
306
+ アプリケーションでは通常の Vite と同様に参照できます。
254
307
 
255
308
  ```ts
256
- gasboost({
257
- entry: "src/main.ts",
258
- envDir: "config",
309
+ const apiUrl = import.meta.env.VITE_API_URL;
310
+ ```
311
+
312
+ ---
313
+
314
+ # Dev Plugin
315
+
316
+ `dev` は Vite Dev Server 上でのみ動作します。
317
+
318
+ frontend の開発用 Vite config に追加して利用します。
319
+
320
+ ```ts
321
+ const { dev } = gasboost({
322
+ entry: "src/server.ts",
323
+ });
324
+
325
+ export default defineConfig({
326
+ plugins: [dev],
259
327
  });
260
328
  ```
261
329
 
262
- アプリケーションから通常通り参照できます。
330
+ `dev` plugin は、GAS にデプロイしなくてもローカル環境から `AppsScript` の RPC を実行できるエンドポイントを Vite Dev Server に追加します。
331
+
332
+ ## Local RPC
333
+
334
+ ローカル RPC のエンドポイントは次の形式です。
335
+
336
+ ```text
337
+ POST /__gasboost/{rpcName}
338
+ ```
339
+
340
+ 例えば、
263
341
 
264
342
  ```ts
265
- const apiUrl = import.meta.env.VITE_API_URL;
343
+ const app = new AppsScript().call("sum", (a: number, b: number) => a + b);
344
+
345
+ export default app;
266
346
  ```
267
347
 
268
- mode ごとの環境変数ファイルにも対応します。
348
+ に対して、
269
349
 
270
- ```bash
271
- vite build --mode production
350
+ ```http
351
+ POST /__gasboost/sum
352
+ Content-Type: application/json
353
+ ```
354
+
355
+ ```json
356
+ {
357
+ "args": [1, 2]
358
+ }
359
+ ```
360
+
361
+ を送信すると、内部では次の dispatch が実行されます。
362
+
363
+ ```ts
364
+ app.dispatch("sum", 1, 2);
365
+ ```
366
+
367
+ レスポンス:
368
+
369
+ ```json
370
+ 3
371
+ ```
372
+
373
+ ## RPC Request
374
+
375
+ Request Body は次の形式です。
376
+
377
+ ```ts
378
+ {
379
+ args: unknown[];
380
+ }
381
+ ```
382
+
383
+ 例えば複数の引数を持つ RPC:
384
+
385
+ ```ts
386
+ app.call(
387
+ "example",
388
+ (id: number, name: string, active: boolean, options: { value: number }) => {
389
+ // ...
390
+ },
391
+ );
392
+ ```
393
+
394
+ に対して、
395
+
396
+ ```json
397
+ {
398
+ "args": [
399
+ 1,
400
+ "Taro",
401
+ true,
402
+ {
403
+ "value": 4
404
+ }
405
+ ]
406
+ }
407
+ ```
408
+
409
+ のように送信できます。
410
+
411
+ ### 引数なし RPC
412
+
413
+ Request Body が空の場合は、引数なし RPC として扱われます。
414
+
415
+ ```http
416
+ POST /__gasboost/noArgs
417
+ ```
418
+
419
+ 内部では次のように dispatch されます。
420
+
421
+ ```ts
422
+ app.dispatch("noArgs");
423
+ ```
424
+
425
+ 明示的に送る場合は次でも構いません。
426
+
427
+ ```json
428
+ {
429
+ "args": []
430
+ }
431
+ ```
432
+
433
+ ## RPC Response
434
+
435
+ `@gasboost/app` の `dispatch()` が返す `AppsScriptResponse.contents` を、そのまま HTTP Response Body として返します。
436
+
437
+ 例えば、
438
+
439
+ ```ts
440
+ app.call("getUser", () => ({
441
+ id: 1,
442
+ name: "Taro",
443
+ }));
444
+ ```
445
+
446
+ に対するレスポンスは:
447
+
448
+ ```json
449
+ {
450
+ "id": 1,
451
+ "name": "Taro"
452
+ }
453
+ ```
454
+
455
+ となります。
456
+
457
+ Response の Content-Type は:
458
+
459
+ ```text
460
+ application/json; charset=utf-8
461
+ ```
462
+
463
+ です。
464
+
465
+ ## Async RPC
466
+
467
+ 非同期 RPC にも対応しています。
468
+
469
+ ```ts
470
+ app.call("loadUser", async (id: string) => {
471
+ const user = await loadUser(id);
472
+
473
+ return user;
474
+ });
475
+ ```
476
+
477
+ dev plugin は `dispatch()` の完了を待ってからレスポンスを返します。
478
+
479
+ ## 開発中の module loading
480
+
481
+ RPC リクエストごとに、指定された `entry` を Vite の `ssrLoadModule()` で読み込みます。
482
+
483
+ ```text
484
+ POST /__gasboost/sum
485
+
486
+ ssrLoadModule("src/server.ts")
487
+
488
+ AppsScript.dispatch("sum", ...)
489
+ ```
490
+
491
+ そのため、Vite Dev Server 上の最新のサーバーコードを利用して RPC を実行できます。
492
+
493
+ ## Error Response
494
+
495
+ ### 不正な Request Body
496
+
497
+ JSON として不正な body や、`args` が配列でない body は `400` を返します。
498
+
499
+ ```json
500
+ {
501
+ "args": "invalid"
502
+ }
503
+ ```
504
+
505
+ レスポンス例:
506
+
507
+ ```json
508
+ {
509
+ "error": {
510
+ "name": "InvalidRpcRequestError",
511
+ "message": "Invalid RPC request body. Expected { args: unknown[] }."
512
+ }
513
+ }
514
+ ```
515
+
516
+ JSON 自体が不正な場合も `400` です。
517
+
518
+ ### POST 以外
519
+
520
+ Local RPC endpoint は POST のみ受け付けます。
521
+
522
+ ```http
523
+ GET /__gasboost/sum
524
+ ```
525
+
526
+ は `405` になります。
527
+
528
+ ```json
529
+ {
530
+ "error": {
531
+ "name": "MethodNotAllowedError",
532
+ "message": "Only POST is allowed."
533
+ }
534
+ }
272
535
  ```
273
536
 
274
- `envDir: "config"` の場合、例えば以下が読み込まれます。
537
+ ### 未登録 RPC
538
+
539
+ 存在しない RPC を呼び出した場合は `500` を返します。
275
540
 
276
541
  ```text
277
- config/.env.production
542
+ POST /__gasboost/unknown
278
543
  ```
279
544
 
280
- ## 責務
545
+ ```json
546
+ {
547
+ "error": {
548
+ "name": "Error",
549
+ "message": "Function unknown is not registered."
550
+ }
551
+ }
552
+ ```
281
553
 
282
- `@gasboost/vite` はビルド時の処理のみを担当します。
554
+ ### Handler Error
283
555
 
284
- 主な責務:
556
+ RPC handler 内で例外が発生した場合も `500` として JSON で返されます。
285
557
 
286
- - `AppsScript` 登録内容の静的解析
287
- - Google Apps Script 向け Vite configuration
288
- - GAS グローバル関数宣言の生成
289
- - Vite 環境変数との統合
558
+ ```json
559
+ {
560
+ "error": {
561
+ "name": "TypeError",
562
+ "message": "handler failed"
563
+ }
564
+ }
565
+ ```
566
+
567
+ ## Local RPC の対象 path
290
568
 
291
- 以下は担当しません。
569
+ 次の形式だけを RPC として扱います。
292
570
 
293
- - GET ハンドラの実行
294
- - POST ハンドラの実行
295
- - RPC dispatch
296
- - ハンドラ実体のランタイム登録
297
- - Google Apps Script API 自体の抽象化
571
+ ```text
572
+ /__gasboost/{rpcName}
573
+ ```
574
+
575
+ 例えば以下は対象です。
576
+
577
+ ```text
578
+ /__gasboost/getUser
579
+ /__gasboost/sum
580
+ ```
581
+
582
+ 一方、次のような path は RPC として扱いません。
583
+
584
+ ```text
585
+ /__gasboost/
586
+ /__gasboost/foo/bar
587
+ /api/users
588
+ ```
589
+
590
+ 通常の Vite middleware chain に処理を渡します。
591
+
592
+ query string が付いていても RPC 名は正しく解決されます。
593
+
594
+ ```text
595
+ /__gasboost/hello?foo=bar
596
+ ```
597
+
598
+ URL encoded な RPC 名も decode されます。
599
+
600
+ ```text
601
+ /__gasboost/hello%20world
602
+ ```
298
603
 
299
- これらの runtime responsibility は `@gasboost/app` が担当します。
604
+ ---
300
605
 
301
- ## 現在の制約
606
+ # build と dev の責務
302
607
 
303
- Analyzer は、意図的にシンプルな entry file 構造のみを対象としています。
608
+ ```text
609
+ gasboost()
610
+
611
+ ├─ build
612
+ │ ├─ AppsScript の静的解析
613
+ │ ├─ GAS グローバル関数生成
614
+ │ ├─ GAS 向け build config
615
+ │ └─ backend production build
616
+
617
+ └─ dev
618
+ ├─ Vite Dev Server middleware
619
+ ├─ Local RPC endpoint
620
+ ├─ entry の ssrLoadModule
621
+ └─ AppsScript.dispatch
622
+ ```
304
623
 
305
- 以下のような alias を介した default export は現在対象外です。
624
+ frontend backend を1つの `vite.config.ts` で扱う場合は、mode で分岐します。
306
625
 
307
626
  ```ts
308
- const app = new AppsScript();
309
- const exported = app;
627
+ const { build, dev } = gasboost({
628
+ entry: "src/server.ts",
629
+ });
310
630
 
311
- export default exported;
631
+ export default defineConfig(({ mode }) => {
632
+ if (mode === "server") {
633
+ return {
634
+ plugins: [build],
635
+ };
636
+ }
637
+
638
+ return {
639
+ plugins: [dev],
640
+ };
641
+ });
312
642
  ```
313
643
 
314
- 別関数の内部に登録処理を隠す形式も対象外です。
644
+ `build` を通常の frontend build に含めると、server entry が Vite の build input になるため、frontend と backend の build は分離してください。
645
+
646
+ ---
647
+
648
+ ## GAS ランタイム
649
+
650
+ `@gasboost/vite` は、build 時と local RPC 実行時に server entry を実際に評価します。
651
+
652
+ 一般的な GAS API は、デフォルトのローカルランタイムから提供されます。
315
653
 
316
654
  ```ts
317
- registerHandlers(app);
655
+ import { gasboost } from "@gasboost/vite";
656
+
657
+ export default gasboost({
658
+ entry: "./src/server.ts",
659
+ });
318
660
  ```
319
661
 
320
- 別ファイルに登録処理を分散する形式も現在対象外です。
662
+ entry の初期化時に追加の GAS API が必要な場合は、`runtime` から差し込めます。
321
663
 
322
664
  ```ts
323
- import { registerHandlers } from "./handlers";
665
+ import { SpreadsheetAppStub } from "@gasboost/sheetorm";
666
+ import { gasboost } from "@gasboost/vite";
324
667
 
325
- registerHandlers(app);
668
+ export default gasboost({
669
+ entry: "./src/server.ts",
670
+ runtime: {
671
+ SpreadsheetApp: SpreadsheetAppStub,
672
+ },
673
+ });
326
674
  ```
327
675
 
328
- 登録内容を entry file から静的に確定できるようにすることで、生成する GAS グローバル関数を決定的にしています。
676
+ `runtime` に渡した値は、デフォルトのローカルランタイムを上書きします。
677
+
678
+ このランタイムは build 時の entry 評価と、local RPC 実行時の両方で利用されます。
679
+
680
+ 基本的には、module 初期化時に必要な API だけを差し込み、実際の GAS API 呼び出しは handler 実行時まで遅延させることを推奨します。
681
+
682
+ ## ランタイム解析
683
+
684
+ GET / POST / RPC の登録状態は、entry ファイルを静的解析するのではなく、実際に評価された `AppsScript` インスタンスから取得します。
685
+
686
+ そのため、import 先、ヘルパー関数、loop、`AppsScript.calls()` 経由の登録にも対応できます。
687
+
688
+ ```ts
689
+ const app = new AppsScript();
690
+
691
+ for (const [name, handler] of Object.entries(handlers)) {
692
+ app.call(name, handler);
693
+ }
694
+
695
+ export default app;
696
+ ```
329
697
 
330
698
  ## 関連パッケージ
331
699
 
332
- アプリケーションの runtime と handler registration には `@gasboost/app` を利用してください。
700
+ - `@gasboost/app` — GAS バックエンドランタイムと RPC 定義
701
+ - `@gasboost/client` — フロントエンド側の型安全 RPC クライアント
702
+
703
+ ## License
704
+
705
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "vite";
2
+ import type { GasboostOptions } from "./gasboost";
3
+ export declare function createBuildPlugin(options: GasboostOptions): Plugin;
package/dist/dev.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type Plugin } from "vite";
2
+ import type { GasboostOptions } from "./gasboost";
3
+ export declare function createDevPlugin(options: GasboostOptions): Plugin;
@@ -1,6 +1,11 @@
1
1
  import type { Plugin } from "vite";
2
+ export type GasRuntime = Readonly<Record<string, unknown>>;
2
3
  export interface GasboostOptions {
3
4
  entry: string;
4
5
  envDir?: string;
6
+ runtime?: GasRuntime;
5
7
  }
6
- export declare function gasboost(options: GasboostOptions): Plugin;
8
+ export declare function gasboost(options: GasboostOptions): {
9
+ build: Plugin;
10
+ dev: Plugin;
11
+ };
package/dist/globals.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import type { AppsScriptAnalysis } from "./analyzer";
2
- export declare function createGlobalCode(analysis: AppsScriptAnalysis): string;
1
+ import type { AppsScriptDescription } from "@gasboost/app";
2
+ export declare function createGlobalCode(analysis: AppsScriptDescription): string;
package/dist/index.js CHANGED
@@ -1,91 +1,12 @@
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
1
+ import e from "node:path";
2
+ import { createServer as t, isRunnableDevEnvironment as n } from "vite";
3
+ import { InMemoryCacheService as r, InMemoryContext as i, InMemoryPropertiesService as a, InMemorySession as o, SecurityPolicy as s } from "@gasboost/fake-core";
4
+ import { NodeUtilities as c } from "@gasboost/fake-node";
84
5
  //#region src/config.ts
85
- function _(e) {
86
- let n = t.resolve(e.entry);
6
+ function l(t) {
7
+ let n = e.resolve(t.entry);
87
8
  return {
88
- envDir: e.envDir,
9
+ envDir: t.envDir,
89
10
  build: {
90
11
  target: "es2019",
91
12
  outDir: "dist",
@@ -94,7 +15,7 @@ function _(e) {
94
15
  input: n,
95
16
  output: {
96
17
  format: "cjs",
97
- entryFileNames: t.basename(n, t.extname(n)) + ".js"
18
+ entryFileNames: e.basename(n, e.extname(n)) + ".js"
98
19
  }
99
20
  }
100
21
  }
@@ -102,34 +23,169 @@ function _(e) {
102
23
  }
103
24
  //#endregion
104
25
  //#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.`);
26
+ var u = /* @__PURE__ */ new Set(["doGet", "doPost"]);
27
+ function d(e) {
28
+ if (u.has(e)) throw Error(`RPC name "${e}" is reserved by Google Apps Script.`);
108
29
  if (!/^[$A-Z_a-z][$\w]*$/u.test(e)) throw Error(`RPC name "${e}" is not a valid JavaScript identifier.`);
109
30
  }
110
- function b(e) {
31
+ function f(e) {
111
32
  let t = [];
112
33
  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}() {}`);
34
+ for (let n of e.calls) d(n), t.push(`function ${n}() {}`);
114
35
  return t.join("\n");
115
36
  }
116
37
  //#endregion
117
- //#region src/gasboost.ts
118
- function x(e) {
119
- let t;
38
+ //#region src/runtime.ts
39
+ var p = new i("", "", {
40
+ type: "WEB_APP",
41
+ executeAs: "USER"
42
+ }, new s([]), "en", "UTC"), m = {
43
+ CacheService: new r(),
44
+ PropertiesService: new a(),
45
+ Session: new o(p),
46
+ Utilities: new c()
47
+ };
48
+ function h(e = {}) {
49
+ Object.assign(globalThis, {
50
+ ...m,
51
+ ...e
52
+ });
53
+ }
54
+ //#endregion
55
+ //#region src/loadAppsScript.ts
56
+ async function g(e, r) {
57
+ h(e.runtime);
58
+ let i = await t({
59
+ configFile: !1,
60
+ root: r.root,
61
+ mode: r.mode,
62
+ envDir: e.envDir,
63
+ appType: "custom",
64
+ ssr: { external: ["@gasboost/app"] },
65
+ server: {
66
+ middlewareMode: !0,
67
+ hmr: !1
68
+ }
69
+ });
70
+ try {
71
+ let t = i.environments.ssr;
72
+ if (!n(t)) throw Error("Vite SSR environment is not runnable.");
73
+ let r = await t.runner.import(e.entry);
74
+ if (!r.default) throw Error("AppsScript entry must have a default export.");
75
+ return r.default.describe();
76
+ } finally {
77
+ await i.close();
78
+ }
79
+ }
80
+ //#endregion
81
+ //#region src/build.ts
82
+ function _(e) {
83
+ let t, n;
120
84
  return {
121
- name: "gasboost",
85
+ name: "gasboost:build",
86
+ apply: "build",
122
87
  config() {
123
- return _(e);
88
+ return l(e);
89
+ },
90
+ configResolved(e) {
91
+ t = e;
124
92
  },
125
- buildStart() {
126
- t = u(e.entry);
93
+ async buildStart() {
94
+ n = await g(e, t);
127
95
  },
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}`);
96
+ generateBundle(e, t) {
97
+ let r = f(n);
98
+ for (let e of Object.values(t)) e.type === "chunk" && e.isEntry && (e.code = `${r}\n\n${e.code}`);
131
99
  }
132
100
  };
133
101
  }
134
102
  //#endregion
103
+ //#region src/dev.ts
104
+ var v = class extends Error {
105
+ constructor(e) {
106
+ super(e), this.name = "InvalidRpcRequestError";
107
+ }
108
+ };
109
+ function y(e) {
110
+ let { runtime: t } = e;
111
+ return {
112
+ name: "gasboost:dev",
113
+ apply: "serve",
114
+ configureServer(r) {
115
+ h(t), r.middlewares.use(async (t, i, a) => {
116
+ if (!t.url) {
117
+ a();
118
+ return;
119
+ }
120
+ let o = t.url.split("?")[0];
121
+ if (!o.startsWith("/__gasboost/")) {
122
+ a();
123
+ return;
124
+ }
125
+ let s = o.slice(12);
126
+ if (!s || s.includes("/")) {
127
+ a();
128
+ return;
129
+ }
130
+ if (t.method !== "POST") {
131
+ i.statusCode = 405, i.setHeader("Content-Type", "application/json; charset=utf-8"), i.end(JSON.stringify({ error: {
132
+ name: "MethodNotAllowedError",
133
+ message: "Only POST is allowed."
134
+ } }));
135
+ return;
136
+ }
137
+ try {
138
+ let { args: a } = await b(t), o = r.environments.ssr;
139
+ if (!n(o)) throw Error("Vite SSR environment is not runnable.");
140
+ let c = await (await o.runner.import(e.entry)).default.dispatch(decodeURIComponent(s), ...a);
141
+ i.statusCode = 200, i.setHeader("Content-Type", "application/json; charset=utf-8"), i.end(c.contents);
142
+ } catch (e) {
143
+ i.statusCode = e instanceof v ? 400 : 500, i.setHeader("Content-Type", "application/json; charset=utf-8"), i.end(JSON.stringify({ error: e instanceof Error ? {
144
+ name: e.name,
145
+ message: e.message,
146
+ stack: e.stack
147
+ } : {
148
+ name: "UnknownError",
149
+ message: String(e)
150
+ } }));
151
+ }
152
+ });
153
+ }
154
+ };
155
+ }
156
+ function b(e) {
157
+ return new Promise((t, n) => {
158
+ let r = [];
159
+ e.on("data", (e) => {
160
+ r.push(typeof e == "string" ? Buffer.from(e) : e);
161
+ }), e.on("error", n), e.on("end", () => {
162
+ let e = Buffer.concat(r).toString("utf-8");
163
+ if (!e) {
164
+ t({ args: [] });
165
+ return;
166
+ }
167
+ let i;
168
+ try {
169
+ i = JSON.parse(e);
170
+ } catch {
171
+ n(new v("Invalid RPC request body. Expected valid JSON."));
172
+ return;
173
+ }
174
+ if (typeof i != "object" || !i || !("args" in i) || !Array.isArray(i.args)) {
175
+ n(new v("Invalid RPC request body. Expected { args: unknown[] }."));
176
+ return;
177
+ }
178
+ t({ args: i.args });
179
+ });
180
+ });
181
+ }
182
+ //#endregion
183
+ //#region src/gasboost.ts
184
+ function x(e) {
185
+ return {
186
+ build: _(e),
187
+ dev: y(e)
188
+ };
189
+ }
190
+ //#endregion
135
191
  export { x as gasboost };
@@ -0,0 +1,4 @@
1
+ import type { AppsScriptDescription } from "@gasboost/app";
2
+ import { type ResolvedConfig } from "vite";
3
+ import type { GasboostOptions } from "./gasboost";
4
+ export declare function loadAppsScript(options: GasboostOptions, config: ResolvedConfig): Promise<AppsScriptDescription>;
@@ -0,0 +1,3 @@
1
+ import type { GasRuntime } from "./gasboost";
2
+ export declare const defaultGasRuntime: GasRuntime;
3
+ export declare function installGasRuntime(runtime?: GasRuntime): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gasboost/vite",
3
- "version": "0.1.1",
3
+ "version": "1.1.0",
4
4
  "description": "A Vite plugin for building @gasboost/app applications for Google Apps Script.",
5
5
  "keywords": [
6
6
  "google-apps-script",
@@ -33,10 +33,14 @@
33
33
  },
34
34
  "type": "module",
35
35
  "devDependencies": {
36
+ "@types/google-apps-script": "^2.0.12",
36
37
  "vite": "^8.2.2"
37
38
  },
38
39
  "dependencies": {
39
- "typescript": "^7.0.2"
40
+ "@gasboost/fake-core": "^0.1.2",
41
+ "@gasboost/fake-node": "^0.1.2",
42
+ "typescript": "^7.0.2",
43
+ "@gasboost/app": "1.3.0"
40
44
  },
41
45
  "publishConfig": {
42
46
  "access": "public"
@@ -1,6 +0,0 @@
1
- export interface AppsScriptAnalysis {
2
- hasGet: boolean;
3
- hasPost: boolean;
4
- calls: string[];
5
- }
6
- export declare function analyzeAppsScript(entry: string): AppsScriptAnalysis;