@gasboost/vite 1.0.1 → 1.2.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 CHANGED
@@ -603,6 +603,152 @@ URL encoded な RPC 名も decode されます。
603
603
 
604
604
  ---
605
605
 
606
+ ## Client Transport
607
+
608
+ `@gasboost/client` を利用している場合、dev plugin はローカル開発時の transport を自動で Local RPC に切り替えます。
609
+
610
+ アプリケーション側では development / production を判定する必要はありません。
611
+
612
+ ```ts
613
+ import { appsScriptClient } from "@gasboost/client";
614
+ import type { App } from "./server";
615
+
616
+ const { client } = appsScriptClient<App>();
617
+
618
+ const result = await client.sum(1, 2);
619
+ ```
620
+
621
+ Vite Dev Server 上では、`appsScriptClient()` に transport が指定されていない場合、dev plugin が自動的に `FetchTransport` を適用します。
622
+
623
+ ```text
624
+ appsScriptClient()
625
+
626
+ FetchTransport
627
+
628
+ POST /__gasboost/{rpcName}
629
+
630
+ AppsScript.dispatch()
631
+ ```
632
+
633
+ そのため、ローカル開発用に transport を切り替えるコードを書く必要はありません。
634
+
635
+ ```ts
636
+ // 不要
637
+ appsScriptClient({
638
+ transport: import.meta.env.DEV
639
+ ? new FetchTransport({
640
+ endpoint: "/__gasboost",
641
+ })
642
+ : new AppsScriptTransport(),
643
+ });
644
+ ```
645
+
646
+ production build では dev plugin による差し替えは行われません。
647
+
648
+ GAS 上では `appsScriptClient()` のデフォルトである `AppsScriptTransport` がそのまま利用されます。
649
+
650
+ ```text
651
+ appsScriptClient()
652
+
653
+ AppsScriptTransport
654
+
655
+ google.script.run
656
+ ```
657
+
658
+ つまり、同じ application code のまま local development と GAS production の両方で利用できます。
659
+
660
+ ```ts
661
+ const { client } = appsScriptClient<App>();
662
+ ```
663
+
664
+ また、明示的に `transport` を指定した場合はその transport が優先されます。
665
+
666
+ ```ts
667
+ const { client } = appsScriptClient<App>({
668
+ transport: customTransport,
669
+ });
670
+ ```
671
+
672
+ dev plugin が明示指定された transport を上書きすることはありません。
673
+
674
+ ---
675
+
676
+ ## HtmlTemplate variables
677
+
678
+ GAS の `HtmlTemplate` では、`<?= variable ?>` を使用してサーバー側の値を HTML に埋め込めます。
679
+
680
+ ```html
681
+ <script>
682
+ const variables = "<?= variables ?>";
683
+ </script>
684
+ ```
685
+
686
+ GAS 上では `HtmlTemplate` によって評価されますが、Vite Dev Server では GAS のテンプレート処理が実行されません。
687
+
688
+ `@gasboost/vite` では、`template` オプションを指定することで、開発時にこれらの template variables を置換できます。
689
+
690
+ ```ts
691
+ import { gasboost } from "@gasboost/vite";
692
+ import { defineConfig } from "vite";
693
+
694
+ export default defineConfig({
695
+ plugins: [
696
+ gasboost({
697
+ entry: "./src/backend/main.ts",
698
+ template: {
699
+ variables: JSON.stringify({
700
+ scriptId: "local-script-id",
701
+ isSetupCompleted: true,
702
+ isTermsAccepted: true,
703
+ }),
704
+ },
705
+ }).dev,
706
+ ],
707
+ });
708
+ ```
709
+
710
+ 例えば、次の HTML は、
711
+
712
+ ```html
713
+ <script>
714
+ const variables = "<?= variables ?>";
715
+ </script>
716
+ ```
717
+
718
+ Vite の開発環境では次のように変換されます。
719
+
720
+ ```html
721
+ <script>
722
+ const variables =
723
+ '{"scriptId":"local-script-id","isSetupCompleted":true,"isTermsAccepted":true}';
724
+ </script>
725
+ ```
726
+
727
+ 複数の template variable も指定できます。
728
+
729
+ ```ts
730
+ gasboost({
731
+ entry: "./src/backend/main.ts",
732
+ template: {
733
+ environment: "development",
734
+ userName: "Tiger",
735
+ },
736
+ });
737
+ ```
738
+
739
+ ```html
740
+ <p>Environment: <?= environment ?></p>
741
+ <p>User: <?= userName ?></p>
742
+ ```
743
+
744
+ `template` の型は `Record<string, string>` です。
745
+
746
+ 値の serialize は `@gasboost/vite` では行いません。オブジェクトなどを埋め込む場合は、利用側で `JSON.stringify()` などを使用して文字列へ変換してください。
747
+
748
+ `template` による置換は Vite Dev Server でのみ行われます。production build では値を埋め込まず、GAS 上で実際の `HtmlTemplate` が template expression を評価します。
749
+
750
+ ---
751
+
606
752
  # build と dev の責務
607
753
 
608
754
  ```text
@@ -643,6 +789,58 @@ export default defineConfig(({ mode }) => {
643
789
 
644
790
  `build` を通常の frontend build に含めると、server entry が Vite の build input になるため、frontend と backend の build は分離してください。
645
791
 
792
+ ---
793
+
794
+ ## GAS ランタイム
795
+
796
+ `@gasboost/vite` は、build 時と local RPC 実行時に server entry を実際に評価します。
797
+
798
+ 一般的な GAS API は、デフォルトのローカルランタイムから提供されます。
799
+
800
+ ```ts
801
+ import { gasboost } from "@gasboost/vite";
802
+
803
+ export default gasboost({
804
+ entry: "./src/server.ts",
805
+ });
806
+ ```
807
+
808
+ entry の初期化時に追加の GAS API が必要な場合は、`runtime` から差し込めます。
809
+
810
+ ```ts
811
+ import { SpreadsheetAppStub } from "@gasboost/sheetorm";
812
+ import { gasboost } from "@gasboost/vite";
813
+
814
+ export default gasboost({
815
+ entry: "./src/server.ts",
816
+ runtime: {
817
+ SpreadsheetApp: SpreadsheetAppStub,
818
+ },
819
+ });
820
+ ```
821
+
822
+ `runtime` に渡した値は、デフォルトのローカルランタイムを上書きします。
823
+
824
+ このランタイムは build 時の entry 評価と、local RPC 実行時の両方で利用されます。
825
+
826
+ 基本的には、module 初期化時に必要な API だけを差し込み、実際の GAS API 呼び出しは handler 実行時まで遅延させることを推奨します。
827
+
828
+ ## ランタイム解析
829
+
830
+ GET / POST / RPC の登録状態は、entry ファイルを静的解析するのではなく、実際に評価された `AppsScript` インスタンスから取得します。
831
+
832
+ そのため、import 先、ヘルパー関数、loop、`AppsScript.calls()` 経由の登録にも対応できます。
833
+
834
+ ```ts
835
+ const app = new AppsScript();
836
+
837
+ for (const [name, handler] of Object.entries(handlers)) {
838
+ app.call(name, handler);
839
+ }
840
+
841
+ export default app;
842
+ ```
843
+
646
844
  ## 関連パッケージ
647
845
 
648
846
  - `@gasboost/app` — GAS バックエンドランタイムと RPC 定義
package/dist/dev.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import type { Plugin } from "vite";
2
- export declare function createDevPlugin(entry: string): Plugin;
1
+ import { type Plugin } from "vite";
2
+ import type { GasboostOptions } from "./gasboost";
3
+ export declare function createDevPlugin(options: GasboostOptions): Plugin;
@@ -1,7 +1,10 @@
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;
7
+ template?: Record<string, string>;
5
8
  }
6
9
  export declare function gasboost(options: GasboostOptions): {
7
10
  build: Plugin;
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,75 +23,157 @@ 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
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
117
81
  //#region src/build.ts
118
- function x(e) {
119
- let t;
82
+ function _(e) {
83
+ let t, n;
120
84
  return {
121
85
  name: "gasboost:build",
122
86
  apply: "build",
123
87
  config() {
124
- return _(e);
88
+ return l(e);
89
+ },
90
+ configResolved(e) {
91
+ t = e;
125
92
  },
126
- buildStart() {
127
- t = u(e.entry);
93
+ async buildStart() {
94
+ n = await g(e, t);
128
95
  },
129
- generateBundle(e, n) {
130
- let r = b(t);
131
- 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}`);
132
99
  }
133
100
  };
134
101
  }
135
102
  //#endregion
136
103
  //#region src/dev.ts
137
- var S = class extends Error {
104
+ var v = class extends Error {
138
105
  constructor(e) {
139
106
  super(e), this.name = "InvalidRpcRequestError";
140
107
  }
141
- };
142
- function C(e) {
108
+ }, y = "@gasboost/client", b = "\0gasboost:client", x = "/__gasboost";
109
+ function S(e) {
110
+ let { runtime: t, template: r } = e, i;
143
111
  return {
144
112
  name: "gasboost:dev",
145
113
  apply: "serve",
146
- configureServer(t) {
147
- t.middlewares.use(async (n, r, i) => {
148
- if (!n.url) {
149
- i();
114
+ enforce: "pre",
115
+ async resolveId(e, t, n) {
116
+ if (e !== y || this.environment.config.consumer !== "client") return null;
117
+ let r = await this.resolve(e, t, {
118
+ ...n,
119
+ skipSelf: !0
120
+ });
121
+ return r ? (i = r.id, b) : null;
122
+ },
123
+ load(e) {
124
+ if (e !== b) return null;
125
+ if (!i) throw Error("@gasboost/client could not be resolved.");
126
+ let t = JSON.stringify(i);
127
+ return `
128
+ import {
129
+ appsScriptClient as originalAppsScriptClient,
130
+ FetchTransport,
131
+ } from ${t};
132
+
133
+ export * from ${t};
134
+
135
+ export function appsScriptClient(options = {}) {
136
+ return originalAppsScriptClient({
137
+ ...options,
138
+ transport:
139
+ options.transport ??
140
+ new FetchTransport({
141
+ endpoint: ${JSON.stringify(x)},
142
+ }),
143
+ });
144
+ }
145
+ `;
146
+ },
147
+ configureServer(r) {
148
+ h(t), r.middlewares.use(async (t, i, a) => {
149
+ if (!t.url) {
150
+ a();
150
151
  return;
151
152
  }
152
- let a = n.url.split("?")[0];
153
- if (!a.startsWith("/__gasboost/")) {
154
- i();
153
+ let o = t.url.split("?")[0], s = `${x}/`;
154
+ if (!o.startsWith(s)) {
155
+ a();
155
156
  return;
156
157
  }
157
- let o = a.slice(12);
158
- if (!o || o.includes("/")) {
159
- i();
158
+ let c = o.slice(s.length);
159
+ if (!c || c.includes("/")) {
160
+ a();
160
161
  return;
161
162
  }
162
- if (n.method !== "POST") {
163
- r.statusCode = 405, r.setHeader("Content-Type", "application/json; charset=utf-8"), r.end(JSON.stringify({ error: {
163
+ if (t.method !== "POST") {
164
+ i.statusCode = 405, i.setHeader("Content-Type", "application/json; charset=utf-8"), i.end(JSON.stringify({ error: {
164
165
  name: "MethodNotAllowedError",
165
166
  message: "Only POST is allowed."
166
167
  } }));
167
168
  return;
168
169
  }
169
170
  try {
170
- let { args: i } = await w(n), a = await (await t.ssrLoadModule(e)).default.dispatch(decodeURIComponent(o), ...i);
171
- r.statusCode = 200, r.setHeader("Content-Type", "application/json; charset=utf-8"), r.end(a.contents);
171
+ let { args: a } = await w(t), o = r.environments.ssr;
172
+ if (!n(o)) throw Error("Vite SSR environment is not runnable.");
173
+ let s = await (await o.runner.import(e.entry)).default.dispatch(decodeURIComponent(c), ...a);
174
+ i.statusCode = 200, i.setHeader("Content-Type", "application/json; charset=utf-8"), i.end(s.contents);
172
175
  } catch (e) {
173
- r.statusCode = e instanceof S ? 400 : 500, r.setHeader("Content-Type", "application/json; charset=utf-8"), r.end(JSON.stringify({ error: e instanceof Error ? {
176
+ i.statusCode = e instanceof v ? 400 : 500, i.setHeader("Content-Type", "application/json; charset=utf-8"), i.end(JSON.stringify({ error: e instanceof Error ? {
174
177
  name: e.name,
175
178
  message: e.message,
176
179
  stack: e.stack
@@ -180,9 +183,21 @@ function C(e) {
180
183
  } }));
181
184
  }
182
185
  });
186
+ },
187
+ transformIndexHtml(e) {
188
+ if (!r) return e;
189
+ let t = e;
190
+ for (let [e, n] of Object.entries(r)) {
191
+ let r = RegExp(`<\\?=\\s*${C(e)}\\s*\\?>`, "g");
192
+ t = t.replace(r, () => n);
193
+ }
194
+ return t;
183
195
  }
184
196
  };
185
197
  }
198
+ function C(e) {
199
+ return e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
200
+ }
186
201
  function w(e) {
187
202
  return new Promise((t, n) => {
188
203
  let r = [];
@@ -198,11 +213,11 @@ function w(e) {
198
213
  try {
199
214
  i = JSON.parse(e);
200
215
  } catch {
201
- n(new S("Invalid RPC request body. Expected valid JSON."));
216
+ n(new v("Invalid RPC request body. Expected valid JSON."));
202
217
  return;
203
218
  }
204
219
  if (typeof i != "object" || !i || !("args" in i) || !Array.isArray(i.args)) {
205
- n(new S("Invalid RPC request body. Expected { args: unknown[] }."));
220
+ n(new v("Invalid RPC request body. Expected { args: unknown[] }."));
206
221
  return;
207
222
  }
208
223
  t({ args: i.args });
@@ -213,8 +228,8 @@ function w(e) {
213
228
  //#region src/gasboost.ts
214
229
  function T(e) {
215
230
  return {
216
- build: x(e),
217
- dev: C(e.entry)
231
+ build: _(e),
232
+ dev: S(e)
218
233
  };
219
234
  }
220
235
  //#endregion
@@ -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": "1.0.1",
3
+ "version": "1.2.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,11 +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": {
40
+ "@gasboost/fake-core": "^0.1.2",
41
+ "@gasboost/fake-node": "^0.1.2",
39
42
  "typescript": "^7.0.2",
40
- "@gasboost/app": "1.0.1"
43
+ "@gasboost/app": "1.3.0"
41
44
  },
42
45
  "publishConfig": {
43
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;