vite-plugin-gas-react 0.1.1

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 Sarfraj Akhtar
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 ADDED
@@ -0,0 +1,221 @@
1
+ # vite-plugin-gas-react
2
+
3
+ **Deploy React apps to Google Apps Script — with code splitting.**
4
+
5
+ > Write a standard React + Vite app. Run one command. Get a working GAS web app with lazy-loaded pages.
6
+
7
+ ---
8
+
9
+ ## Why?
10
+
11
+ Google Apps Script only serves HTML files via `HtmlService`. External `<script>` tags are blocked by the CAJA sanitizer. That means:
12
+
13
+ - No ES modules, no `import()`, no `<script src="...">`
14
+ - You normally have to inline **everything** into a single HTML file
15
+ - Code splitting? Lazy loading? Forget it.
16
+
17
+ This plugin solves all of that. You write a normal React app with `React.lazy()` and Vite's natural code splitting — the plugin transforms the build output into something GAS can actually serve.
18
+
19
+ ## How It Works
20
+
21
+ ```
22
+ React App (Vite) → Build → GAS-compatible output → clasp push → Live web app
23
+ ```
24
+
25
+ The plugin runs at build time and:
26
+
27
+ 1. **Stores all JS server-side** as `.gs` string variables (completely bypasses CAJA)
28
+ 2. **Rewrites `import()`** calls to fetch chunks via `google.script.run.getPage()`
29
+ 3. **Builds a dependency graph** so shared chunks load before the pages that need them
30
+ 4. **Generates `Code.js`** with `doGet()`, `getEntryCode()`, and `getPage()` functions
31
+ 5. **Generates `appsscript.json`** with the correct webapp configuration
32
+
33
+ At runtime, the entry JS is loaded via `google.script.run.getEntryCode()` and injected into a `<script>` tag. When you navigate to a lazy page, the chunk loader fetches its code (and any shared dependencies) the same way.
34
+
35
+ ### Build Output
36
+
37
+ ```
38
+ dist/
39
+ ├── index.html ← Served by doGet() — contains the chunk loader
40
+ ├── __gas_entry__.js ← Entry bundle stored as a .gs string variable
41
+ ├── __gas_chunks__.js ← All lazy + shared chunks as .gs string variables
42
+ ├── Code.js ← Server functions: doGet(), getEntryCode(), getPage()
43
+ └── appsscript.json ← GAS project manifest
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ### 1. Install
49
+
50
+ ```bash
51
+ npm install vite-plugin-gas-react
52
+ npm install -D vite @vitejs/plugin-react
53
+ ```
54
+
55
+ ### 2. Configure Vite
56
+
57
+ ```ts
58
+ // vite.config.ts
59
+ import { createGASViteConfig } from 'vite-plugin-gas-react';
60
+
61
+ export default createGASViteConfig({
62
+ clientRoot: 'src',
63
+ appTitle: 'My App',
64
+ });
65
+ ```
66
+
67
+ That's it. `createGASViteConfig()` sets up React, the GAS plugin, aliases, and dev mode automatically.
68
+
69
+ ### 3. Write Your App
70
+
71
+ Use `React.lazy()` for page-level code splitting — Vite will split them into separate chunks, and the plugin will handle the rest:
72
+
73
+ ```tsx
74
+ // src/App.tsx
75
+ import { useState, Suspense, lazy } from 'react';
76
+
77
+ const Home = lazy(() => import('./pages/Home'));
78
+ const Settings = lazy(() => import('./pages/Settings'));
79
+
80
+ const pages = { Home, Settings };
81
+
82
+ export default function App() {
83
+ const [page, setPage] = useState('Home');
84
+ const Page = pages[page];
85
+
86
+ return (
87
+ <Suspense fallback={<div>Loading...</div>}>
88
+ <Page />
89
+ </Suspense>
90
+ );
91
+ }
92
+ ```
93
+
94
+ ```tsx
95
+ // src/pages/Home.tsx
96
+ export default function Home() {
97
+ return <h1>Home Page</h1>;
98
+ }
99
+ ```
100
+
101
+ ### 4. Set Up Clasp
102
+
103
+ ```bash
104
+ npm install -g @google/clasp
105
+ clasp login
106
+ clasp create --type webapp --rootDir dist
107
+ ```
108
+
109
+ ### 5. Build & Deploy
110
+
111
+ ```bash
112
+ npx vite build
113
+ cd dist && clasp push
114
+ ```
115
+
116
+ Open the GAS web app URL and your React app is live.
117
+
118
+ ## Configuration
119
+
120
+ ### `createGASViteConfig(options?)`
121
+
122
+ Returns a complete Vite config. All options are optional:
123
+
124
+ | Option | Default | Description |
125
+ |---|---|---|
126
+ | `clientRoot` | `'src/client'` | Path to client source directory |
127
+ | `outDir` | `'dist'` | Build output directory |
128
+ | `appTitle` | `'GAS App'` | Title shown in the browser tab |
129
+ | `devServerPort` | `3001` | Port for local dev API server |
130
+ | `devPort` | `5173` | Vite dev server port |
131
+ | `aliases` | `{}` | Additional path aliases (`@` → `src/` is automatic) |
132
+ | `plugins` | `[]` | Additional Vite plugins |
133
+ | `vite` | `{}` | Override/extend any Vite config option |
134
+
135
+ ### `gasPlugin(options?)`
136
+
137
+ Use this if you're building your own Vite config instead of using `createGASViteConfig()`:
138
+
139
+ ```ts
140
+ import { gasPlugin } from 'vite-plugin-gas-react';
141
+ import react from '@vitejs/plugin-react';
142
+
143
+ export default {
144
+ plugins: [react(), gasPlugin({ appTitle: 'My App' })],
145
+ };
146
+ ```
147
+
148
+ | Option | Default | Description |
149
+ |---|---|---|
150
+ | `pagePrefix` | `'page_'` | Prefix for page chunk names |
151
+ | `appTitle` | `'GAS App'` | Web app title |
152
+
153
+ ### `isLocalDev()`
154
+
155
+ Returns `true` when `GAS_LOCAL=true` is set in environment. Use to branch behavior between local development and GAS deployment:
156
+
157
+ ```ts
158
+ if (isLocalDev()) {
159
+ // Local dev: use mock data or local API
160
+ } else {
161
+ // Production: use google.script.run
162
+ }
163
+ ```
164
+
165
+ ## How Code Splitting Works
166
+
167
+ Vite naturally splits your app into chunks:
168
+
169
+ - **Entry chunk** — React, your app shell, shared dependencies
170
+ - **Page chunks** — One per `React.lazy(() => import('./pages/X'))` call
171
+ - **Shared lib chunks** — Common dependencies used by multiple pages (e.g., MUI components)
172
+
173
+ The plugin transforms these into GAS-compatible form:
174
+
175
+ | Vite Output | Plugin Transform | GAS Runtime |
176
+ |---|---|---|
177
+ | `assets/index-abc.js` (entry) | `__GAS_ENTRY_CODE__` string variable | Loaded via `getEntryCode()` |
178
+ | `assets/Home-xyz.js` (page) | `__GAS_CHUNK_page_Home__` string variable | Loaded via `getPage('page_Home')` |
179
+ | `assets/Stack-def.js` (shared lib) | `__GAS_CHUNK_lib_Stack__` string variable | Auto-loaded as dependency |
180
+
181
+ Each chunk type gets its own **isolated namespace** to prevent variable collisions:
182
+
183
+ - Entry exports → `window.__gasEntry__`
184
+ - Shared lib exports → `window.__gasLib_<name>__`
185
+ - Page exports → `window.__gasChunkExports` (per-chunk, cleaned up after load)
186
+
187
+ The plugin automatically builds a dependency graph. When you navigate to `Home`, the loader first loads `lib_Stack` (if not cached), then loads `page_Home`. All subsequent navigations to pages sharing the same libs skip reloading them.
188
+
189
+ ## Local Development
190
+
191
+ Set `GAS_LOCAL=true` to run your app with Vite's dev server instead of deploying to GAS:
192
+
193
+ ```bash
194
+ GAS_LOCAL=true npx vite
195
+ ```
196
+
197
+ In local mode, `createGASViteConfig()`:
198
+ - Skips the GAS plugin entirely
199
+ - Injects `window.__GAS_DEV_MODE__ = true`
200
+ - Injects `window.__GAS_DEV_SERVER__` pointing to your local API server
201
+ - Enables Vite HMR and hot reload
202
+
203
+ You can use these globals in your app to switch between `google.script.run` calls (production) and `fetch()` calls (local dev).
204
+
205
+ ## Requirements
206
+
207
+ - **Node.js** ≥ 18
208
+ - **Vite** ≥ 5
209
+ - **React** 18 or 19 (with `React.lazy()` for code splitting)
210
+ - **clasp** CLI for deployment (`npm install -g @google/clasp`)
211
+
212
+ ## Limitations
213
+
214
+ - **GAS execution time limits** still apply (6 min/execution, 30 sec for web app requests)
215
+ - **Chunk loading** adds a round-trip per chunk on first navigation (chunks are cached after first load)
216
+ - **No SSR** — this is a client-side React app served via `HtmlService`
217
+ - **File size** — GAS has a 50MB total project size limit. Large apps with many dependencies should be fine, but keep an eye on it.
218
+
219
+ ## License
220
+
221
+ MIT
@@ -0,0 +1,308 @@
1
+ // src/vite-plugin-gas.ts
2
+ import path from "path";
3
+ function gasPlugin(options = {}) {
4
+ const { pagePrefix = "page_", appTitle = "GAS App", serverEntry } = options;
5
+ const lazyPageNames = /* @__PURE__ */ new Set();
6
+ const fileToGasName = /* @__PURE__ */ new Map();
7
+ function toGasName(fileName, isLazyPage) {
8
+ const baseName = fileName.split("/").pop();
9
+ const cleanName = baseName.replace(/-.*$/, "").replace(/\.js$/, "");
10
+ return isLazyPage ? `${pagePrefix}${cleanName}` : `lib_${cleanName}`;
11
+ }
12
+ return {
13
+ name: "vite-plugin-gas",
14
+ enforce: "post",
15
+ renderChunk(code, chunk) {
16
+ if (!chunk.isEntry) return null;
17
+ let modified = code;
18
+ let changed = false;
19
+ for (const dynamicImport of chunk.dynamicImports) {
20
+ const baseName = dynamicImport.split("/").pop();
21
+ const pageName = baseName.replace(/-.*$/, "").replace(/\.js$/, "");
22
+ const gasPageName = `${pagePrefix}${pageName}`;
23
+ lazyPageNames.add(pageName);
24
+ const escapedBase = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ const importPattern = new RegExp(
26
+ `import\\(\\s*["']\\.\\/` + escapedBase + `["']\\s*\\)`,
27
+ "g"
28
+ );
29
+ const replacement = `__gasLoadChunk("${gasPageName}")`;
30
+ const newCode = modified.replace(importPattern, replacement);
31
+ if (newCode !== modified) {
32
+ modified = newCode;
33
+ changed = true;
34
+ }
35
+ }
36
+ return changed ? modified : null;
37
+ },
38
+ transformIndexHtml(html) {
39
+ const gasScript = `
40
+ <script>
41
+ (function() {
42
+ var deps = __GAS_DEPS_MAP__;
43
+ var chunkCache = {};
44
+
45
+ window.__gasLoadChunk = function(name) {
46
+ if (chunkCache[name]) return chunkCache[name];
47
+
48
+ var promise = new Promise(function(resolve, reject) {
49
+ var chunkDeps = deps[name] || [];
50
+ var depPromises = chunkDeps.map(function(dep) { return window.__gasLoadChunk(dep); });
51
+
52
+ Promise.all(depPromises).then(function() {
53
+ google.script.run
54
+ .withSuccessHandler(function(js) {
55
+ var script = document.createElement('script');
56
+ script.textContent = js;
57
+ document.head.appendChild(script);
58
+
59
+ var exports = window.__gasChunkExports || {};
60
+ resolve(exports);
61
+ delete window.__gasChunkExports;
62
+ })
63
+ .withFailureHandler(function(err) {
64
+ reject(new Error('Failed to load chunk: ' + name));
65
+ })
66
+ .getPage(name);
67
+ });
68
+ });
69
+
70
+ chunkCache[name] = promise;
71
+ return promise;
72
+ };
73
+
74
+ google.script.run
75
+ .withSuccessHandler(function(js) {
76
+ var s = document.createElement('script');
77
+ s.textContent = js;
78
+ document.head.appendChild(s);
79
+ })
80
+ .withFailureHandler(function(err) {
81
+ document.body.innerHTML = 'Failed to load app: ' + err;
82
+ })
83
+ .getEntryCode();
84
+ })();
85
+ </script>`;
86
+ html = html.replace("</body>", gasScript + "\n</body>");
87
+ return html;
88
+ },
89
+ async generateBundle(_options, bundle) {
90
+ let entryFileName = null;
91
+ let entryCode = null;
92
+ for (const [fileName, item] of Object.entries(bundle)) {
93
+ if (item.type === "chunk" && item.isEntry) {
94
+ entryFileName = fileName;
95
+ entryCode = item.code || "";
96
+ break;
97
+ }
98
+ }
99
+ if (entryFileName && entryCode) {
100
+ const htmlKey = Object.keys(bundle).find((k) => k.endsWith(".html"));
101
+ if (htmlKey) {
102
+ const htmlItem = bundle[htmlKey];
103
+ let html = typeof htmlItem.source === "string" ? htmlItem.source : new TextDecoder().decode(htmlItem.source);
104
+ html = html.replace(
105
+ /<script\b[^>]*src=["'][^"']*["'][^>]*>\s*<\/script>/g,
106
+ ""
107
+ );
108
+ for (const [fileName, item] of Object.entries(bundle)) {
109
+ if (item.type !== "chunk") continue;
110
+ if (fileName === entryFileName) continue;
111
+ const baseName = fileName.split("/").pop();
112
+ const cleanName = baseName.replace(/-.*$/, "").replace(/\.js$/, "");
113
+ const isLazy = lazyPageNames.has(cleanName);
114
+ fileToGasName.set(fileName, toGasName(fileName, isLazy));
115
+ }
116
+ let cleanedEntry = rewriteImportsToGlobals(entryCode, entryFileName, fileToGasName);
117
+ cleanedEntry = rewriteExportsToNamespace(cleanedEntry, "window.__gasEntry__");
118
+ cleanedEntry = `window.__gasEntry__={};
119
+ var __VITE_PRELOAD__=void 0;
120
+ ` + cleanedEntry;
121
+ bundle["__gas_entry__.js"] = {
122
+ type: "asset",
123
+ fileName: "__gas_entry__.js",
124
+ source: `var __GAS_ENTRY_CODE__ = ${JSON.stringify(cleanedEntry)};`
125
+ };
126
+ delete bundle[entryFileName];
127
+ const chunkVars = [];
128
+ const depsMap = {};
129
+ for (const [fileName, item] of Object.entries(bundle)) {
130
+ if (item.type !== "chunk") continue;
131
+ if (fileName === entryFileName) continue;
132
+ const gasName = fileToGasName.get(fileName);
133
+ if (!gasName) continue;
134
+ const chunkInfo = item;
135
+ let code = item.code || "";
136
+ code = rewriteImportsToGlobals(code, fileName, fileToGasName);
137
+ const baseName = fileName.split("/").pop();
138
+ const cleanName = baseName.replace(/-.*$/, "").replace(/\.js$/, "");
139
+ const isLazy = lazyPageNames.has(cleanName);
140
+ if (isLazy) {
141
+ code = rewriteExportsToObject(code);
142
+ const wrappedCode = `(function() {
143
+ var __exports = {};
144
+ ${code}
145
+ window.__gasChunkExports = __exports;
146
+ })();`;
147
+ chunkVars.push(`var __GAS_CHUNK_${gasName}__ = ${JSON.stringify(wrappedCode)};`);
148
+ } else {
149
+ code = rewriteExportsToObject(code);
150
+ const wrappedCode = `(function() {
151
+ var __exports = {};
152
+ ${code}
153
+ window.__gasLib_${gasName}__ = __exports;
154
+ })();`;
155
+ chunkVars.push(`var __GAS_CHUNK_${gasName}__ = ${JSON.stringify(wrappedCode)};`);
156
+ }
157
+ const chunkDeps = [];
158
+ if (chunkInfo.imports) {
159
+ for (const imp of chunkInfo.imports) {
160
+ const depGasName = fileToGasName.get(imp);
161
+ if (depGasName) chunkDeps.push(depGasName);
162
+ }
163
+ }
164
+ if (chunkDeps.length > 0) {
165
+ depsMap[gasName] = chunkDeps;
166
+ }
167
+ delete bundle[fileName];
168
+ }
169
+ if (chunkVars.length > 0) {
170
+ bundle["__gas_chunks__.js"] = {
171
+ type: "asset",
172
+ fileName: "__gas_chunks__.js",
173
+ source: chunkVars.join("\n")
174
+ };
175
+ }
176
+ const depsJson = JSON.stringify(depsMap);
177
+ html = html.replace("__GAS_DEPS_MAP__", depsJson);
178
+ htmlItem.source = html;
179
+ let codeJs = "";
180
+ if (serverEntry) {
181
+ try {
182
+ const esbuild = await import("esbuild");
183
+ const serverPath = path.resolve(process.cwd(), serverEntry);
184
+ const result = await esbuild.build({
185
+ entryPoints: [serverPath],
186
+ bundle: true,
187
+ format: "esm",
188
+ platform: "neutral",
189
+ target: "es2020",
190
+ write: false,
191
+ external: []
192
+ });
193
+ let serverCode = result.outputFiles[0].text;
194
+ serverCode = serverCode.replace(/^export\s*\{[^}]*\};\s*$/gm, "");
195
+ serverCode = serverCode.replace(/^export\s+/gm, "");
196
+ codeJs += serverCode + "\n";
197
+ } catch (err) {
198
+ console.error("Failed to bundle server entry:", err);
199
+ }
200
+ }
201
+ codeJs += `function doGet() {
202
+ return HtmlService.createHtmlOutputFromFile('index')
203
+ .setTitle('${appTitle.replace(/'/g, "\\'")}')
204
+ .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
205
+ }
206
+
207
+ function getPage(name) {
208
+ var varName = '__GAS_CHUNK_' + name + '__';
209
+ var code = globalThis[varName];
210
+ if (!code) throw new Error('Chunk not found: ' + name);
211
+ return code;
212
+ }
213
+
214
+ function getEntryCode() {
215
+ return __GAS_ENTRY_CODE__;
216
+ }
217
+ `;
218
+ bundle["Code.js"] = {
219
+ type: "asset",
220
+ fileName: "Code.js",
221
+ source: codeJs
222
+ };
223
+ bundle["appsscript.json"] = {
224
+ type: "asset",
225
+ fileName: "appsscript.json",
226
+ source: JSON.stringify({
227
+ timeZone: "Asia/Kolkata",
228
+ dependencies: {},
229
+ webapp: {
230
+ access: "ANYONE_ANONYMOUS",
231
+ executeAs: "USER_DEPLOYING"
232
+ },
233
+ exceptionLogging: "STACKDRIVER",
234
+ runtimeVersion: "V8"
235
+ }, null, 2)
236
+ };
237
+ }
238
+ }
239
+ }
240
+ };
241
+ }
242
+ function rewriteExportsToNamespace(code, namespace) {
243
+ let result = code;
244
+ result = result.replace(/export\s*\{([^}]*)\}\s*;?/g, (_match, inner) => {
245
+ return inner.split(",").map((binding) => {
246
+ const parts = binding.trim().split(/\s+as\s+/);
247
+ const local = parts[0].trim();
248
+ const exported = (parts[1] || parts[0]).trim();
249
+ return exported ? `${namespace}.${exported}=${local};` : "";
250
+ }).filter(Boolean).join("");
251
+ });
252
+ result = result.replace(/export\s+default\s+(\w+)\s*;/g, `${namespace}.default=$1;`);
253
+ return result;
254
+ }
255
+ function rewriteImportsToGlobals(code, chunkFileName, fileToGasName) {
256
+ const lastSlash = chunkFileName.lastIndexOf("/");
257
+ const chunkDir = lastSlash >= 0 ? chunkFileName.substring(0, lastSlash) : "";
258
+ return code.replace(
259
+ /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']\s*;?/g,
260
+ (_match, bindings, importPath) => {
261
+ let sourceObj = "window";
262
+ if (importPath.startsWith("./")) {
263
+ const resolved = chunkDir ? `${chunkDir}/${importPath.slice(2)}` : importPath.slice(2);
264
+ const gasName = fileToGasName.get(resolved);
265
+ if (gasName?.startsWith("lib_")) {
266
+ sourceObj = `window.__gasLib_${gasName}__`;
267
+ } else if (!gasName) {
268
+ sourceObj = "window.__gasEntry__";
269
+ }
270
+ }
271
+ return bindings.split(",").map((binding) => {
272
+ const parts = binding.trim().split(/\s+as\s+/);
273
+ const original = parts[0].trim();
274
+ const local = (parts[1] || original).trim();
275
+ if (!original) return "";
276
+ return `var ${local} = ${sourceObj}.${original};`;
277
+ }).filter(Boolean).join("\n");
278
+ }
279
+ );
280
+ }
281
+ function rewriteExportsToObject(code) {
282
+ let result = code;
283
+ result = result.replace(
284
+ /export\s+default\s+(\w+)\s*;/g,
285
+ "__exports.default = $1;"
286
+ );
287
+ result = result.replace(
288
+ /export\s*\{([^}]+)\}\s*;/g,
289
+ (_match, inner) => {
290
+ return inner.split(",").map((binding) => {
291
+ const parts = binding.trim().split(/\s+as\s+/);
292
+ const local = parts[0].trim();
293
+ const exported = (parts[1] || parts[0]).trim();
294
+ return `__exports.${exported} = ${local};`;
295
+ }).join("\n ");
296
+ }
297
+ );
298
+ result = result.replace(
299
+ /export\s+(const|let|var)\s+(\w+)/g,
300
+ "$1 $2"
301
+ );
302
+ return result;
303
+ }
304
+
305
+ export {
306
+ gasPlugin
307
+ };
308
+ //# sourceMappingURL=chunk-DXCNTLXT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite-plugin-gas.ts"],"sourcesContent":["import path from 'node:path';\n\ninterface GASPluginOptions {\n pagePrefix?: string;\n appTitle?: string;\n serverEntry?: string;\n}\n\ninterface VitePlugin {\n name: string;\n enforce?: 'pre' | 'post';\n renderChunk?: (code: string, chunk: ChunkInfo) => string | null;\n transformIndexHtml?: (html: string) => string;\n generateBundle?: (options: unknown, bundle: Record<string, BundleItem>) => void | Promise<void>;\n}\n\ninterface ChunkInfo {\n fileName: string;\n isEntry: boolean;\n dynamicImports: string[];\n imports?: string[];\n exports?: string[];\n facadeModuleId?: string | null;\n}\n\ninterface BundleItem {\n type: 'chunk' | 'asset';\n fileName: string;\n code?: string;\n source?: string | Uint8Array;\n}\n\nexport function gasPlugin(options: GASPluginOptions = {}): VitePlugin {\n const { pagePrefix = 'page_', appTitle = 'GAS App', serverEntry } = options;\n const lazyPageNames = new Set<string>();\n const fileToGasName = new Map<string, string>();\n\n function toGasName(fileName: string, isLazyPage: boolean): string {\n const baseName = fileName.split('/').pop()!;\n const cleanName = baseName.replace(/-.*$/, '').replace(/\\.js$/, '');\n return isLazyPage ? `${pagePrefix}${cleanName}` : `lib_${cleanName}`;\n }\n\n return {\n name: 'vite-plugin-gas',\n enforce: 'post',\n\n renderChunk(code: string, chunk: ChunkInfo): string | null {\n if (!chunk.isEntry) return null;\n\n let modified = code;\n let changed = false;\n\n for (const dynamicImport of chunk.dynamicImports) {\n const baseName = dynamicImport.split('/').pop()!;\n const pageName = baseName.replace(/-.*$/, '').replace(/\\.js$/, '');\n const gasPageName = `${pagePrefix}${pageName}`;\n\n lazyPageNames.add(pageName);\n\n const escapedBase = baseName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const importPattern = new RegExp(\n `import\\\\(\\\\s*[\"']\\\\.\\\\/` + escapedBase + `[\"']\\\\s*\\\\)`,\n 'g'\n );\n const replacement = `__gasLoadChunk(\"${gasPageName}\")`;\n\n const newCode = modified.replace(importPattern, replacement);\n if (newCode !== modified) {\n modified = newCode;\n changed = true;\n }\n }\n\n return changed ? modified : null;\n },\n\n transformIndexHtml(html: string): string {\n const gasScript = `\n<script>\n(function() {\n var deps = __GAS_DEPS_MAP__;\n var chunkCache = {};\n\n window.__gasLoadChunk = function(name) {\n if (chunkCache[name]) return chunkCache[name];\n\n var promise = new Promise(function(resolve, reject) {\n var chunkDeps = deps[name] || [];\n var depPromises = chunkDeps.map(function(dep) { return window.__gasLoadChunk(dep); });\n\n Promise.all(depPromises).then(function() {\n google.script.run\n .withSuccessHandler(function(js) {\n var script = document.createElement('script');\n script.textContent = js;\n document.head.appendChild(script);\n\n var exports = window.__gasChunkExports || {};\n resolve(exports);\n delete window.__gasChunkExports;\n })\n .withFailureHandler(function(err) {\n reject(new Error('Failed to load chunk: ' + name));\n })\n .getPage(name);\n });\n });\n\n chunkCache[name] = promise;\n return promise;\n };\n\n google.script.run\n .withSuccessHandler(function(js) {\n var s = document.createElement('script');\n s.textContent = js;\n document.head.appendChild(s);\n })\n .withFailureHandler(function(err) {\n document.body.innerHTML = 'Failed to load app: ' + err;\n })\n .getEntryCode();\n})();\n</script>`;\n\n html = html.replace('</body>', gasScript + '\\n</body>');\n\n return html;\n },\n\n async generateBundle(_options: unknown, bundle: Record<string, BundleItem>): Promise<void> {\n let entryFileName: string | null = null;\n let entryCode: string | null = null;\n\n for (const [fileName, item] of Object.entries(bundle)) {\n if (item.type === 'chunk' && (item as unknown as ChunkInfo).isEntry) {\n entryFileName = fileName;\n entryCode = item.code || '';\n break;\n }\n }\n\n if (entryFileName && entryCode) {\n const htmlKey = Object.keys(bundle).find(k => k.endsWith('.html'));\n if (htmlKey) {\n const htmlItem = bundle[htmlKey];\n let html = typeof htmlItem.source === 'string'\n ? htmlItem.source\n : new TextDecoder().decode(htmlItem.source as Uint8Array);\n\n html = html.replace(\n /<script\\b[^>]*src=[\"'][^\"']*[\"'][^>]*>\\s*<\\/script>/g,\n ''\n );\n\n for (const [fileName, item] of Object.entries(bundle)) {\n if (item.type !== 'chunk') continue;\n if (fileName === entryFileName) continue;\n const baseName = fileName.split('/').pop()!;\n const cleanName = baseName.replace(/-.*$/, '').replace(/\\.js$/, '');\n const isLazy = lazyPageNames.has(cleanName);\n fileToGasName.set(fileName, toGasName(fileName, isLazy));\n }\n\n let cleanedEntry = rewriteImportsToGlobals(entryCode, entryFileName, fileToGasName);\n cleanedEntry = rewriteExportsToNamespace(cleanedEntry, 'window.__gasEntry__');\n cleanedEntry = `window.__gasEntry__={};\\nvar __VITE_PRELOAD__=void 0;\\n` + cleanedEntry;\n\n bundle['__gas_entry__.js'] = {\n type: 'asset',\n fileName: '__gas_entry__.js',\n source: `var __GAS_ENTRY_CODE__ = ${JSON.stringify(cleanedEntry)};`,\n };\n\n delete bundle[entryFileName];\n\n const chunkVars: string[] = [];\n const depsMap: Record<string, string[]> = {};\n\n for (const [fileName, item] of Object.entries(bundle)) {\n if (item.type !== 'chunk') continue;\n if (fileName === entryFileName) continue;\n\n const gasName = fileToGasName.get(fileName);\n if (!gasName) continue;\n\n const chunkInfo = item as unknown as ChunkInfo;\n let code = item.code || '';\n\n code = rewriteImportsToGlobals(code, fileName, fileToGasName);\n\n const baseName = fileName.split('/').pop()!;\n const cleanName = baseName.replace(/-.*$/, '').replace(/\\.js$/, '');\n const isLazy = lazyPageNames.has(cleanName);\n\n if (isLazy) {\n code = rewriteExportsToObject(code);\n const wrappedCode = `(function() {\n var __exports = {};\n ${code}\n window.__gasChunkExports = __exports;\n})();`;\n chunkVars.push(`var __GAS_CHUNK_${gasName}__ = ${JSON.stringify(wrappedCode)};`);\n } else {\n code = rewriteExportsToObject(code);\n const wrappedCode = `(function() {\n var __exports = {};\n ${code}\n window.__gasLib_${gasName}__ = __exports;\n})();`;\n chunkVars.push(`var __GAS_CHUNK_${gasName}__ = ${JSON.stringify(wrappedCode)};`);\n }\n\n const chunkDeps: string[] = [];\n if (chunkInfo.imports) {\n for (const imp of chunkInfo.imports) {\n const depGasName = fileToGasName.get(imp);\n if (depGasName) chunkDeps.push(depGasName);\n }\n }\n if (chunkDeps.length > 0) {\n depsMap[gasName] = chunkDeps;\n }\n\n delete bundle[fileName];\n }\n\n if (chunkVars.length > 0) {\n bundle['__gas_chunks__.js'] = {\n type: 'asset',\n fileName: '__gas_chunks__.js',\n source: chunkVars.join('\\n'),\n };\n }\n\n const depsJson = JSON.stringify(depsMap);\n html = html.replace('__GAS_DEPS_MAP__', depsJson);\n htmlItem.source = html;\n\n let codeJs = '';\n\n if (serverEntry) {\n try {\n const esbuild = await import('esbuild');\n const serverPath = path.resolve(process.cwd(), serverEntry);\n const result = await esbuild.build({\n entryPoints: [serverPath],\n bundle: true,\n format: 'esm',\n platform: 'neutral',\n target: 'es2020',\n write: false,\n external: [],\n });\n let serverCode = result.outputFiles[0].text;\n serverCode = serverCode.replace(/^export\\s*\\{[^}]*\\};\\s*$/gm, '');\n serverCode = serverCode.replace(/^export\\s+/gm, '');\n codeJs += serverCode + '\\n';\n } catch (err) {\n console.error('Failed to bundle server entry:', err);\n }\n }\n\n codeJs += `function doGet() {\n return HtmlService.createHtmlOutputFromFile('index')\n .setTitle('${appTitle.replace(/'/g, \"\\\\'\")}')\n .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);\n}\n\nfunction getPage(name) {\n var varName = '__GAS_CHUNK_' + name + '__';\n var code = globalThis[varName];\n if (!code) throw new Error('Chunk not found: ' + name);\n return code;\n}\n\nfunction getEntryCode() {\n return __GAS_ENTRY_CODE__;\n}\n`;\n bundle['Code.js'] = {\n type: 'asset',\n fileName: 'Code.js',\n source: codeJs,\n };\n\n bundle['appsscript.json'] = {\n type: 'asset',\n fileName: 'appsscript.json',\n source: JSON.stringify({\n timeZone: 'Asia/Kolkata',\n dependencies: {},\n webapp: {\n access: 'ANYONE_ANONYMOUS',\n executeAs: 'USER_DEPLOYING',\n },\n exceptionLogging: 'STACKDRIVER',\n runtimeVersion: 'V8',\n }, null, 2),\n };\n }\n }\n },\n };\n}\n\n/** Rewrite ESM exports to assignments on a namespace object. */\nfunction rewriteExportsToNamespace(code: string, namespace: string): string {\n let result = code;\n result = result.replace(/export\\s*\\{([^}]*)\\}\\s*;?/g, (_match, inner: string) => {\n return inner\n .split(',')\n .map((binding) => {\n const parts = binding.trim().split(/\\s+as\\s+/);\n const local = parts[0].trim();\n const exported = (parts[1] || parts[0]).trim();\n return exported ? `${namespace}.${exported}=${local};` : '';\n })\n .filter(Boolean)\n .join('');\n });\n result = result.replace(/export\\s+default\\s+(\\w+)\\s*;/g, `${namespace}.default=$1;`);\n return result;\n}\n\n/** Rewrite ESM imports to var declarations reading from the correct namespace. */\nfunction rewriteImportsToGlobals(\n code: string,\n chunkFileName: string,\n fileToGasName: Map<string, string>,\n): string {\n const lastSlash = chunkFileName.lastIndexOf('/');\n const chunkDir = lastSlash >= 0 ? chunkFileName.substring(0, lastSlash) : '';\n\n return code.replace(\n /import\\s*\\{([^}]+)\\}\\s*from\\s*[\"']([^\"']+)[\"']\\s*;?/g,\n (_match, bindings: string, importPath: string) => {\n let sourceObj = 'window';\n\n if (importPath.startsWith('./')) {\n const resolved = chunkDir\n ? `${chunkDir}/${importPath.slice(2)}`\n : importPath.slice(2);\n const gasName = fileToGasName.get(resolved);\n if (gasName?.startsWith('lib_')) {\n sourceObj = `window.__gasLib_${gasName}__`;\n } else if (!gasName) {\n sourceObj = 'window.__gasEntry__';\n }\n }\n\n return bindings\n .split(',')\n .map((binding) => {\n const parts = binding.trim().split(/\\s+as\\s+/);\n const original = parts[0].trim();\n const local = (parts[1] || original).trim();\n if (!original) return '';\n return `var ${local} = ${sourceObj}.${original};`;\n })\n .filter(Boolean)\n .join('\\n');\n }\n );\n}\n\n/** Rewrite ESM exports to assign to an __exports object (used for chunks). */\nfunction rewriteExportsToObject(code: string): string {\n let result = code;\n\n result = result.replace(\n /export\\s+default\\s+(\\w+)\\s*;/g,\n '__exports.default = $1;'\n );\n\n result = result.replace(\n /export\\s*\\{([^}]+)\\}\\s*;/g,\n (_match, inner: string) => {\n return inner\n .split(',')\n .map((binding) => {\n const parts = binding.trim().split(/\\s+as\\s+/);\n const local = parts[0].trim();\n const exported = (parts[1] || parts[0]).trim();\n return `__exports.${exported} = ${local};`;\n })\n .join('\\n ');\n }\n );\n\n result = result.replace(\n /export\\s+(const|let|var)\\s+(\\w+)/g,\n '$1 $2'\n );\n\n return result;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,28 @@
1
+ export declare function isLocalDev(): boolean;
2
+
3
+ export interface GASPluginOptions {
4
+ pagePrefix?: string;
5
+ appTitle?: string;
6
+ serverEntry?: string;
7
+ }
8
+
9
+ export declare function gasPlugin(options?: GASPluginOptions): {
10
+ name: string;
11
+ enforce: 'post';
12
+ };
13
+
14
+ export interface GASViteOptions {
15
+ clientRoot?: string;
16
+ outDir?: string;
17
+ devServerPort?: number;
18
+ devPort?: number;
19
+ aliases?: Record<string, string>;
20
+ plugins?: unknown[];
21
+ appTitle?: string;
22
+ serverEntry?: string;
23
+ vite?: Record<string, unknown>;
24
+ }
25
+
26
+ export declare function createGASViteConfig(
27
+ options?: GASViteOptions
28
+ ): Promise<Record<string, unknown>>;
package/dist/index.js ADDED
@@ -0,0 +1,87 @@
1
+ import {
2
+ gasPlugin
3
+ } from "./chunk-DXCNTLXT.js";
4
+
5
+ // src/index.ts
6
+ import path from "path";
7
+ import { createRequire } from "module";
8
+ import { pathToFileURL } from "url";
9
+ async function importFromProject(specifier) {
10
+ const require2 = createRequire(path.resolve(process.cwd(), "package.json"));
11
+ const resolved = require2.resolve(specifier);
12
+ return import(pathToFileURL(resolved).href);
13
+ }
14
+ function isLocalDev() {
15
+ return process.env.GAS_LOCAL === "true";
16
+ }
17
+ async function createGASViteConfig(options = {}) {
18
+ const {
19
+ clientRoot = "src/client",
20
+ outDir = "dist",
21
+ devServerPort = 3001,
22
+ devPort = 5173,
23
+ aliases = {},
24
+ plugins: extraPlugins = [],
25
+ appTitle,
26
+ serverEntry,
27
+ vite: overrides = {}
28
+ } = options;
29
+ const local = isLocalDev();
30
+ const projectRoot = process.cwd();
31
+ const clientDepth = clientRoot.split("/").filter(Boolean).length;
32
+ const relativeOutDir = "../".repeat(clientDepth) + outDir;
33
+ const plugins = [];
34
+ try {
35
+ const mod = await importFromProject("@vitejs/plugin-react");
36
+ const reactPlugin = mod.default ?? mod;
37
+ plugins.push(typeof reactPlugin === "function" ? reactPlugin() : reactPlugin);
38
+ } catch {
39
+ console.warn(
40
+ "\u26A0\uFE0F @vitejs/plugin-react not found. Install it:\n npm install -D @vitejs/plugin-react\n"
41
+ );
42
+ }
43
+ if (!local) {
44
+ const { gasPlugin: gasPlugin2 } = await import("./vite-plugin-gas-UCZ4473C.js");
45
+ plugins.push(gasPlugin2({ appTitle, serverEntry }));
46
+ }
47
+ plugins.push(...extraPlugins);
48
+ const resolvedAliases = {
49
+ "@": path.resolve(projectRoot, "src"),
50
+ ...Object.fromEntries(
51
+ Object.entries(aliases).map(([key, val]) => [
52
+ key,
53
+ path.resolve(projectRoot, val)
54
+ ])
55
+ )
56
+ };
57
+ const config = {
58
+ root: clientRoot,
59
+ plugins,
60
+ resolve: { alias: resolvedAliases },
61
+ build: { outDir: relativeOutDir, emptyOutDir: true }
62
+ };
63
+ if (local) {
64
+ config.define = {
65
+ "window.__GAS_DEV_MODE__": "true",
66
+ "window.__GAS_DEV_SERVER__": JSON.stringify(`http://localhost:${devServerPort}`)
67
+ };
68
+ config.server = { port: devPort, open: true };
69
+ }
70
+ for (const [key, value] of Object.entries(overrides)) {
71
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && typeof config[key] === "object" && config[key] !== null) {
72
+ config[key] = {
73
+ ...config[key],
74
+ ...value
75
+ };
76
+ } else {
77
+ config[key] = value;
78
+ }
79
+ }
80
+ return config;
81
+ }
82
+ export {
83
+ createGASViteConfig,
84
+ gasPlugin,
85
+ isLocalDev
86
+ };
87
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import path from 'node:path';\nimport { createRequire } from 'node:module';\nimport { pathToFileURL } from 'node:url';\n\nexport { gasPlugin } from './vite-plugin-gas';\n\nasync function importFromProject(specifier: string): Promise<unknown> {\n const require = createRequire(path.resolve(process.cwd(), 'package.json'));\n const resolved = require.resolve(specifier);\n return import(pathToFileURL(resolved).href);\n}\n\nexport function isLocalDev(): boolean {\n return process.env.GAS_LOCAL === 'true';\n}\n\nexport interface GASViteOptions {\n clientRoot?: string;\n outDir?: string;\n devServerPort?: number;\n devPort?: number;\n aliases?: Record<string, string>;\n plugins?: unknown[];\n appTitle?: string;\n serverEntry?: string;\n vite?: Record<string, unknown>;\n}\n\nexport async function createGASViteConfig(\n options: GASViteOptions = {}\n): Promise<Record<string, unknown>> {\n const {\n clientRoot = 'src/client',\n outDir = 'dist',\n devServerPort = 3001,\n devPort = 5173,\n aliases = {},\n plugins: extraPlugins = [],\n appTitle,\n serverEntry,\n vite: overrides = {},\n } = options;\n\n const local = isLocalDev();\n const projectRoot = process.cwd();\n const clientDepth = clientRoot.split('/').filter(Boolean).length;\n const relativeOutDir = '../'.repeat(clientDepth) + outDir;\n\n const plugins: unknown[] = [];\n\n try {\n const mod = await importFromProject('@vitejs/plugin-react') as Record<string, unknown>;\n const reactPlugin = mod.default ?? mod;\n plugins.push(typeof reactPlugin === 'function' ? (reactPlugin as () => unknown)() : reactPlugin);\n } catch {\n console.warn(\n '⚠️ @vitejs/plugin-react not found. Install it:\\n' +\n ' npm install -D @vitejs/plugin-react\\n'\n );\n }\n\n if (!local) {\n const { gasPlugin } = await import('./vite-plugin-gas');\n plugins.push(gasPlugin({ appTitle, serverEntry }));\n }\n\n plugins.push(...extraPlugins);\n\n const resolvedAliases: Record<string, string> = {\n '@': path.resolve(projectRoot, 'src'),\n ...Object.fromEntries(\n Object.entries(aliases).map(([key, val]) => [\n key,\n path.resolve(projectRoot, val),\n ])\n ),\n };\n\n const config: Record<string, unknown> = {\n root: clientRoot,\n plugins,\n resolve: { alias: resolvedAliases },\n build: { outDir: relativeOutDir, emptyOutDir: true },\n };\n\n if (local) {\n config.define = {\n 'window.__GAS_DEV_MODE__': 'true',\n 'window.__GAS_DEV_SERVER__': JSON.stringify(`http://localhost:${devServerPort}`),\n };\n config.server = { port: devPort, open: true };\n }\n\n for (const [key, value] of Object.entries(overrides)) {\n if (\n typeof value === 'object' && value !== null && !Array.isArray(value) &&\n typeof config[key] === 'object' && config[key] !== null\n ) {\n config[key] = {\n ...(config[key] as Record<string, unknown>),\n ...(value as Record<string, unknown>),\n };\n } else {\n config[key] = value;\n }\n }\n\n return config;\n}\n"],"mappings":";;;;;AAAA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAI9B,eAAe,kBAAkB,WAAqC;AACpE,QAAMA,WAAU,cAAc,KAAK,QAAQ,QAAQ,IAAI,GAAG,cAAc,CAAC;AACzE,QAAM,WAAWA,SAAQ,QAAQ,SAAS;AAC1C,SAAO,OAAO,cAAc,QAAQ,EAAE;AACxC;AAEO,SAAS,aAAsB;AACpC,SAAO,QAAQ,IAAI,cAAc;AACnC;AAcA,eAAsB,oBACpB,UAA0B,CAAC,GACO;AAClC,QAAM;AAAA,IACJ,aAAa;AAAA,IACb,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,IACX,SAAS,eAAe,CAAC;AAAA,IACzB;AAAA,IACA;AAAA,IACA,MAAM,YAAY,CAAC;AAAA,EACrB,IAAI;AAEJ,QAAM,QAAQ,WAAW;AACzB,QAAM,cAAc,QAAQ,IAAI;AAChC,QAAM,cAAc,WAAW,MAAM,GAAG,EAAE,OAAO,OAAO,EAAE;AAC1D,QAAM,iBAAiB,MAAM,OAAO,WAAW,IAAI;AAEnD,QAAM,UAAqB,CAAC;AAE5B,MAAI;AACF,UAAM,MAAM,MAAM,kBAAkB,sBAAsB;AAC1D,UAAM,cAAc,IAAI,WAAW;AACnC,YAAQ,KAAK,OAAO,gBAAgB,aAAc,YAA8B,IAAI,WAAW;AAAA,EACjG,QAAQ;AACN,YAAQ;AAAA,MACN;AAAA,IAEF;AAAA,EACF;AAEA,MAAI,CAAC,OAAO;AACV,UAAM,EAAE,WAAAC,WAAU,IAAI,MAAM,OAAO,+BAAmB;AACtD,YAAQ,KAAKA,WAAU,EAAE,UAAU,YAAY,CAAC,CAAC;AAAA,EACnD;AAEA,UAAQ,KAAK,GAAG,YAAY;AAE5B,QAAM,kBAA0C;AAAA,IAC9C,KAAK,KAAK,QAAQ,aAAa,KAAK;AAAA,IACpC,GAAG,OAAO;AAAA,MACR,OAAO,QAAQ,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,GAAG,MAAM;AAAA,QAC1C;AAAA,QACA,KAAK,QAAQ,aAAa,GAAG;AAAA,MAC/B,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,SAAkC;AAAA,IACtC,MAAM;AAAA,IACN;AAAA,IACA,SAAS,EAAE,OAAO,gBAAgB;AAAA,IAClC,OAAO,EAAE,QAAQ,gBAAgB,aAAa,KAAK;AAAA,EACrD;AAEA,MAAI,OAAO;AACT,WAAO,SAAS;AAAA,MACd,2BAA2B;AAAA,MAC3B,6BAA6B,KAAK,UAAU,oBAAoB,aAAa,EAAE;AAAA,IACjF;AACA,WAAO,SAAS,EAAE,MAAM,SAAS,MAAM,KAAK;AAAA,EAC9C;AAEA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACpD,QACE,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK,KACnE,OAAO,OAAO,GAAG,MAAM,YAAY,OAAO,GAAG,MAAM,MACnD;AACA,aAAO,GAAG,IAAI;AAAA,QACZ,GAAI,OAAO,GAAG;AAAA,QACd,GAAI;AAAA,MACN;AAAA,IACF,OAAO;AACL,aAAO,GAAG,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,SAAO;AACT;","names":["require","gasPlugin"]}
@@ -0,0 +1,7 @@
1
+ import {
2
+ gasPlugin
3
+ } from "./chunk-DXCNTLXT.js";
4
+ export {
5
+ gasPlugin
6
+ };
7
+ //# sourceMappingURL=vite-plugin-gas-UCZ4473C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "vite-plugin-gas-react",
3
+ "version": "0.1.1",
4
+ "description": "Vite plugin that deploys React apps to Google Apps Script with automatic code splitting",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Sarfraj Akhtar",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/sarfrajakhtar/gas-react.git",
11
+ "directory": "packages/vite-plugin"
12
+ },
13
+ "homepage": "https://github.com/sarfrajakhtar/gas-react/tree/main/packages/vite-plugin",
14
+ "bugs": {
15
+ "url": "https://github.com/sarfrajakhtar/gas-react/issues"
16
+ },
17
+ "keywords": [
18
+ "vite",
19
+ "vite-plugin",
20
+ "google-apps-script",
21
+ "gas",
22
+ "react",
23
+ "code-splitting",
24
+ "clasp"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "sideEffects": false,
30
+ "main": "./dist/index.js",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ }
38
+ },
39
+ "typesVersions": {
40
+ "*": {
41
+ ".": [
42
+ "dist/index.d.ts"
43
+ ]
44
+ }
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest"
56
+ },
57
+ "peerDependencies": {
58
+ "@vitejs/plugin-react": ">=4.0.0",
59
+ "vite": ">=5.0.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "@vitejs/plugin-react": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^25.3.3",
68
+ "tsup": "^8.0.0",
69
+ "typescript": "^5.7.0",
70
+ "vitest": "^3.2.4"
71
+ }
72
+ }