@mcpher/gas-fakes 2.3.17 → 2.5.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.
Files changed (89) hide show
  1. package/README.md +15 -32
  2. package/package.json +1 -2
  3. package/src/cli/app.js +30 -2
  4. package/src/cli/server.js +32 -0
  5. package/src/cli/setup.js +24 -0
  6. package/src/cli/togas.js +176 -0
  7. package/src/index.js +2 -0
  8. package/src/services/common/fakeui.js +45 -0
  9. package/src/services/content/app.js +3 -0
  10. package/src/services/content/contentservice.js +14 -0
  11. package/src/services/content/textoutput.js +45 -0
  12. package/src/services/documentapp/fakedocumentapp.js +1 -1
  13. package/src/services/enums/contentenums.js +15 -0
  14. package/src/services/enums/htmlenums.js +13 -0
  15. package/src/services/enums/scriptenums.js +6 -0
  16. package/src/services/formapp/fakeformapp.js +5 -0
  17. package/src/services/html/app.js +9 -0
  18. package/src/services/html/consumerworker.js +129 -0
  19. package/src/services/html/googlescriptrun.js +91 -0
  20. package/src/services/html/htmloutput.js +127 -0
  21. package/src/services/html/htmloutputmetatag.js +14 -0
  22. package/src/services/html/htmlservice.js +94 -0
  23. package/src/services/html/htmltemplate.js +63 -0
  24. package/src/services/html/serverworker.js +135 -0
  25. package/src/services/html/webapp.js +266 -0
  26. package/src/services/html/worker.js +63 -0
  27. package/src/services/libhandlerapp/fakelibrary.js +2 -2
  28. package/src/services/scriptapp/app.js +44 -0
  29. package/src/services/scriptapp/fakeauthorizationinfo.js +22 -0
  30. package/src/services/slidesapp/fakeslidesapp.js +5 -0
  31. package/src/services/spreadsheetapp/fakebooleancondition.js +14 -2
  32. package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +30 -5
  33. package/src/services/spreadsheetapp/fakegradientcondition.js +1 -1
  34. package/src/services/spreadsheetapp/fakeovergridimage.js +25 -0
  35. package/src/services/spreadsheetapp/fakesheet.js +23 -1
  36. package/src/services/spreadsheetapp/fakespreadsheet.js +68 -11
  37. package/src/services/spreadsheetapp/fakespreadsheetapp.js +70 -9
  38. package/src/services/stores/fakestores.js +5 -0
  39. package/src/support/auth.js +2 -0
  40. package/src/support/proxies.js +1 -1
  41. package/src/support/sxauth.js +20 -12
  42. package/src/support/utils.js +480 -200
  43. package/src/support/workersync/sxhtml.js +8 -0
  44. package/src/support/workersync/synchronizer.js +8 -1
  45. package/src/support/workersync/worker.js +5 -0
  46. package/api-docs/kdrive_api.json +0 -69958
  47. package/appsscript.json +0 -102
  48. package/gf_agent/README.md +0 -101
  49. package/gf_agent/SKILL.md +0 -460
  50. package/gf_agent/documentation.md +0 -105
  51. package/gf_agent/gf-agent-contributor/SKILL.md +0 -56
  52. package/gf_agent/index.md +0 -21
  53. package/gf_agent/knowledge/00-execution-context.md +0 -5
  54. package/gf_agent/knowledge/01-drive.md +0 -12
  55. package/gf_agent/knowledge/02-syntax.md +0 -14
  56. package/gf_agent/knowledge/03-auth.md +0 -15
  57. package/gf_agent/knowledge/04-advanced.md +0 -46
  58. package/gf_agent/knowledge/05-sheets-forms.md +0 -26
  59. package/gf_agent/knowledge/06-jdbc-cloudsql.md +0 -21
  60. package/gf_agent/knowledge/07-jdbc-auth-details.md +0 -30
  61. package/gf_agent/knowledge/08-docs-limitations.md +0 -4
  62. package/gf_agent/knowledge/09-orchestrator-pattern.md +0 -55
  63. package/gf_agent/knowledge/10-sandbox-security.md +0 -62
  64. package/gf_agent/knowledge/11-chart-builder-limitations.md +0 -15
  65. package/gf_agent/knowledge/12-gmail-eventual-consistency.md +0 -13
  66. package/gf_agent/knowledge/13-advanced-services-discovery.md +0 -23
  67. package/gf_agent/knowledge/14-utilities-parity.md +0 -13
  68. package/gf_agent/knowledge/README.md +0 -16
  69. package/gf_agent/scripts/SKILL.template.md +0 -63
  70. package/gf_agent/scripts/builder.js +0 -118
  71. package/gf_agent/skills/base.md +0 -156
  72. package/gf_agent/skills/cache.md +0 -20
  73. package/gf_agent/skills/calendar.md +0 -780
  74. package/gf_agent/skills/charts.md +0 -127
  75. package/gf_agent/skills/document.md +0 -6752
  76. package/gf_agent/skills/drive.md +0 -423
  77. package/gf_agent/skills/forms.md +0 -4036
  78. package/gf_agent/skills/gmail.md +0 -576
  79. package/gf_agent/skills/jdbc.md +0 -3101
  80. package/gf_agent/skills/lock.md +0 -20
  81. package/gf_agent/skills/properties.md +0 -19
  82. package/gf_agent/skills/script.md +0 -50
  83. package/gf_agent/skills/slides.md +0 -5054
  84. package/gf_agent/skills/spreadsheet.md +0 -56075
  85. package/gf_agent/skills/urlfetch.md +0 -28
  86. package/gf_agent/skills/utilities.md +0 -50
  87. package/gf_agent/skills/xml.md +0 -270
  88. package/skills-lock.json +0 -10
  89. package/src/services/documentapp/fakeui.js +0 -27
package/README.md CHANGED
@@ -22,6 +22,16 @@ Collaborators should fork the repo and use the local versions of these files - s
22
22
  ## Usage
23
23
  Just as on Apps Script, everything is executed synchronously so you don't need to bother with handling Promises/async/await. Just write normal Apps Script code. Usually you would have an associated App Script project if that's your eventual target. Just like Apps Script, you need a manifest file (appsscript.json) so you can be sure that the correct scopes are authorized and asked for.
24
24
 
25
+ ### Local Web Development (`doGet` / `doPost`)
26
+ You can develop and test Google Apps Script Web Apps and UI Add-ons entirely on your local machine using the built-in local web server. The `gas-fakes serve` command spins up a local HTTP endpoint that routes requests to your `doGet` or `doPost` functions, perfectly emulating the Apps Script environment.
27
+
28
+ It features complete support for:
29
+ - `HtmlService` templating (`<?!= include() ?>`).
30
+ - Client-side RPC via `google.script.run`.
31
+ - Visual UI emulation for Add-ons: Calling `SpreadsheetApp.getUi().showSidebar(html)` automatically frames your HTML output inside a visually accurate Workspace-style sidebar right in your browser!
32
+
33
+ See the [Local Web Development Guide](notes/local-web-development.md) for full details.
34
+
25
35
  ### Natural Language Automation with `@gf_agent`
26
36
  With the introduction of the `gf_agent` skill for Gemini CLI and the built-in MCP server, you can now automate Google Workspace tasks using natural language. This specialized agent understands the full range of `gas-fakes` services and can generate and execute code locally based on your plain English prompts. Whether it's summarizing emails in a Google Doc or analyzing spreadsheet data, you can now do it directly from your terminal using plain English. See the [gf_agent documentation](gf_agent/README.md) for more details.
27
37
 
@@ -39,20 +49,7 @@ For details see [gas fakes cli](notes/gas-fakes-cli.md)
39
49
 
40
50
  ### Configuration
41
51
 
42
- Configuration for your local Node environment is handled via environment variables, typically stored in a `.env` file and managed by the `gas-fakes init` process.
43
-
44
- | Environment Variable | Default | Description |
45
- |---|---|---|
46
- | `GF_MANIFEST_PATH` | `./appsscript.json` | Path to the `appsscript.json` manifest file. |
47
- | `GF_CLASP_PATH` | `./.clasp.json` | Path to the `.clasp.json` file. |
48
- | `GF_SCRIPT_ID` | from clasp, or random | Discovered from `.clasp.json` or generated as a random UUID during `gas-fakes init`. Used for `ScriptApp.getScriptId()` and partitioning stores. |
49
- | `GF_DOCUMENT_ID` | `null` | A bound document ID for testing container-bound scripts. |
50
- | `GF_CACHE_PATH` | `/tmp/gas-fakes/cache` | Path for `CacheService` local file emulation. |
51
- | `GF_PROPERTIES_PATH` | `/tmp/gas-fakes/properties` | Path for `PropertiesService` local file emulation. |
52
- | `GF_PLATFORM_AUTH` | `google` | Comma-separated list of backends to initialize (`google`, `ksuite`, `msgraph`). |
53
- | `AUTH_TYPE` | `dwd` | Google auth type: `dwd` (Domain-Wide Delegation) or `adc` (Application Default Credentials). |
54
- | `LOG_DESTINATION` | `CONSOLE` | Logging destination: `CONSOLE`, `CLOUD`, `BOTH`, or `NONE`. |
55
- | `STORE_TYPE` | `FILE` | Internal storage type for properties/cache: `FILE` (local) or `UPSTASH` (Redis). |
52
+ Configuration for your local Node environment is handled interactively by the `gas-fakes init` process, which manages the necessary environment variables in your `.env` file.
56
53
 
57
54
  ### Note on Consumer Accounts and ADC
58
55
 
@@ -123,21 +120,6 @@ console.log ('....example cloud log link for this session',Logger.__cloudLogLink
123
120
 
124
121
  It contains a cloud logging query that will display any logging done in this session - the filter is based on the scriptId (from the environment), the projectId and userId (from Auth), as well as the start and end time of the session.
125
122
 
126
- #### A note on .env location
127
-
128
- You will have used the gas-fakes init command to create a .env file, containing the LOG_DESTINATION setting. You can change any of the settings in the .env file manually if you want to.
129
-
130
- If you want to set an initial LOG_DESTINATION using that .env file, you have to let gas-fakes know where to find it. Let's assume it's in the same folder as your main script.
131
- ```env
132
- node yourapp.js
133
- # or if your .env is somewhere else
134
- node --env-file pathtoenv yourapp.js
135
- ```
136
-
137
- Alternatively, instead of putting it in an env file, you can export it in your shell environment.
138
- ```sh
139
- export LOG_DESTINATION="BOTH"
140
- ```
141
123
  Finally, another approach is to set it dynamically at the beginning of your app.
142
124
  ```javascript
143
125
  Logger.__logDestination="BOTH"
@@ -168,14 +150,14 @@ There are a couple of syntactical differences between Node and Apps Script. Not
168
150
  // this required on Node but not on Apps Script
169
151
  if (ScriptApp.isFake) testFakes()
170
152
  ````
171
- For inspiration on pushing modified files to the IDE, see the togas.sh bash script I use for the test suite. There's also a complete push pull workflow available - see - [push test pull](notes/pull-test-push.md)
153
+ For pushing modified files back to the Apps Script IDE, use the built-in `gas-fakes togas` CLI command which handles necessary module syntax transformations automatically.
172
154
 
173
155
 
174
156
  ## Help
175
157
 
176
158
  As I mentioned earlier, to take this further, I'm going to need a lot of help to extend the methods and services supported - so if you feel this would be useful to you, and would like to collaborate, please ping me on bruce@mcpher.com and we'll talk.
177
159
 
178
- ## <img src="./pngs/logo.png" alt="gas-fakes logo" width="50" align="top"> Further Reading
160
+ ## <img src="../pngs/logo.png" alt="gas-fakes logo" width="50" align="top"> Further Reading
179
161
 
180
162
  ## Watch the gas-fakes intro video
181
163
 
@@ -187,6 +169,7 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
187
169
 
188
170
  ## Read more docs
189
171
 
172
+ - [release notes](versionnotes/)
190
173
  - [gas fakes intro video](https://youtu.be/oEjpIrkYpEM)
191
174
  - [getting started](GETTING_STARTED.md) - how to handle authentication for Workspace scopes.
192
175
  - [readme](README.md)
@@ -194,6 +177,7 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
194
177
  - [Add agent skills to gf_agent](https://ramblings.mcpher.com/add-skills-gf_agent/)
195
178
  - [gf_agent documentation](../gf_agent/README.md) - instructions for the Gemini CLI automation agent and MCP server.
196
179
  - [gas fakes cli](notes/gas-fakes-cli.md)
180
+ -[local add-on and webapp development with gas-fakes](notes/local-web-development.md)
197
181
  - [github actions using adc](https://github.com/brucemcpherson/gas-fakes-actions-adc)
198
182
  - [github actions using dwd and wif](https://github.com/brucemcpherson/gas-fakes-actions-dwd)
199
183
  - [ksuite as a back end](notes/ksuite_poc.md)
@@ -227,7 +211,6 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
227
211
  - [article:using apps script libraries with gas-fakes](https://ramblings.mcpher.com/how-to-use-apps-script-libraries-directly-from-node/)
228
212
  - [named range identity](notes/named-range-identity.md)
229
213
  - [Workspace scopes with local authentication](notes/workspace_scopes.md)
230
- - [push test pull](notes/pull-test-push.md)
231
214
  - [sharing cache and properties between gas-fakes and live apps script](https://ramblings.mcpher.com/sharing-cache-and-properties-between-gas-fakes-and-live-apps-script/)
232
215
  - [gas-fakes-cli now has built in mcp server and gemini extension](https://ramblings.mcpher.com/gas-fakes-cli-now-has-built-in-mcp-server-and-gemini-extension/)
233
216
  - [gas-fakes CLI: Run apps script code directly from your terminal](https://ramblings.mcpher.com/gas-fakes-cli-run-apps-script-code-directly-from-your-terminal/)
package/package.json CHANGED
@@ -5,7 +5,6 @@
5
5
  "dependencies": {
6
6
  "@azure/identity": "^4.13.1",
7
7
  "@mcpher/fake-gasenum": "^1.0.6",
8
- "@mcpher/gas-fakes": "^2.3.9",
9
8
  "@mcpher/gas-flex-cache": "^1.1.5",
10
9
  "@microsoft/microsoft-graph-client": "^3.0.7",
11
10
  "@modelcontextprotocol/sdk": "^1.28.0",
@@ -40,7 +39,7 @@
40
39
  },
41
40
  "name": "@mcpher/gas-fakes",
42
41
  "author": "bruce mcpherson",
43
- "version": "2.3.17",
42
+ "version": "2.5.1",
44
43
  "license": "MIT",
45
44
  "main": "main.js",
46
45
  "description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
package/src/cli/app.js CHANGED
@@ -9,7 +9,9 @@ import {
9
9
  authenticateUser,
10
10
  enableGoogleAPIs,
11
11
  } from "./setup.js";
12
+ import { startWebApp } from "./server.js";
12
13
  import { startMcpServer } from "./mcp.js";
14
+ import { togas } from "./togas.js";
13
15
  import { Platforms, PlatformDefaults } from "../services/enums/platformenums.js";
14
16
 
15
17
  export async function main() {
@@ -26,7 +28,7 @@ export async function main() {
26
28
 
27
29
  // --- Main Execution Command ---
28
30
  program
29
- .description("Execute a Google Apps Script file or string.")
31
+ .description("Execute a Google Apps Script file or string, or start the server.")
30
32
  .option("-f, --filename <string>", "Path to the Google Apps Script file.")
31
33
  .option(
32
34
  "-s, --script <string>",
@@ -87,7 +89,7 @@ export async function main() {
87
89
  if (!hasEnvFileFlag || !isDefaultEnv) {
88
90
  const envPath = path.resolve(process.cwd(), env);
89
91
  console.log(`...using env file in ${envPath}`);
90
- dotenv.config({ path: envPath, quiet: true });
92
+ dotenv.config({ path: envPath, quiet: true, override: true });
91
93
  }
92
94
 
93
95
  const sandboxConfig = buildSandboxConfig(options);
@@ -175,6 +177,21 @@ export async function main() {
175
177
  .option("-t, --tools <string>", "Path to custom tools file.")
176
178
  .action(startMcpServer);
177
179
 
180
+ // --- Server Command ---
181
+ program
182
+ .command("serve [filename]")
183
+ .description("Starts a local web server to handle doGet and doPost requests.")
184
+ .option("-p, --port <number>", "Port for local web server (overrides .env).")
185
+ .option("-e, --env <path>", "Path to a custom .env file.", "./.env")
186
+ .option("-m, --main <string>", "The entry point function to execute on GET requests.", "doGet")
187
+ .action(async (filename, options) => {
188
+ if (!filename) {
189
+ console.error("error: missing required argument 'filename'");
190
+ process.exit(1);
191
+ }
192
+ await startWebApp({ ...options, filename });
193
+ });
194
+
178
195
  // --- JDBC Command ---
179
196
  program
180
197
  .command("jdbc")
@@ -193,6 +210,17 @@ export async function main() {
193
210
  }
194
211
  });
195
212
 
213
+ // --- Togas Command ---
214
+ program
215
+ .command("togas")
216
+ .description("Prepares and pushes local code to Google Apps Script using clasp.")
217
+ .option("-p, --pattern <string>", "Comma-separated glob patterns for files to include (default: *)")
218
+ .option("-t, --target <string>", "Target directory for clasp project (default: TOGAS_TARGET in .env)")
219
+ .option("-s, --source <string>", "Source directory (default: ./ )")
220
+ .option("-e, --env <path>", "Path to a custom .env file.", "./.env")
221
+ .option("--scriptId <string>", "Script ID for the target clasp project.")
222
+ .action(togas);
223
+
196
224
  program.showHelpAfterError("(add --help for additional information)");
197
225
 
198
226
  await program.parseAsync(process.argv);
@@ -0,0 +1,32 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import dotenv from "dotenv";
4
+ import { startServer } from "../services/html/webapp.js";
5
+
6
+ /**
7
+ * Starts a web server to handle doGet and doPost requests.
8
+ */
9
+ export async function startWebApp(options = {}) {
10
+ const { filename, env, port, main: entryFunction } = options;
11
+
12
+ // Load environment variables
13
+ const envPath = path.resolve(process.cwd(), env || "./.env");
14
+ if (fs.existsSync(envPath)) {
15
+ dotenv.config({ path: envPath, quiet: true, override: true });
16
+ }
17
+
18
+ const serverPort = port || process.env.GF_SERVER_PORT || 8080;
19
+ let targetFilename = filename;
20
+ if (targetFilename && !targetFilename.endsWith('.js')) {
21
+ targetFilename += '.js';
22
+ }
23
+ const scriptPath = targetFilename ? path.resolve(process.cwd(), targetFilename) : null;
24
+
25
+ if (!scriptPath || !fs.existsSync(scriptPath)) {
26
+ console.error(`Error: GAS file not found: ${targetFilename}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ console.log(`...starting web server for ${scriptPath} (default entrypoint: ${entryFunction})`);
31
+ startServer(serverPort, scriptPath, entryFunction);
32
+ }
package/src/cli/setup.js CHANGED
@@ -228,6 +228,30 @@ export async function initializeConfiguration(options = {}) {
228
228
  name: "GF_PROPERTIES_PATH",
229
229
  message: "Properties storage path",
230
230
  initial: existingConfig.GF_PROPERTIES_PATH || "/tmp/gas-fakes/properties",
231
+ },
232
+ {
233
+ type: "number",
234
+ name: "GF_SERVER_PORT",
235
+ message: "Port for local web server (doGet/doPost)",
236
+ initial: parseInt(existingConfig.GF_SERVER_PORT) || 8080,
237
+ },
238
+ {
239
+ type: "text",
240
+ name: "TOGAS_TARGET",
241
+ message: "Togas: Target directory for clasp project (e.g., ../testongas)",
242
+ initial: existingConfig.TOGAS_TARGET || "../testongas",
243
+ },
244
+ {
245
+ type: "text",
246
+ name: "TOGAS_SCRIPT_ID",
247
+ message: "Togas: Script ID for the target clasp project",
248
+ initial: existingConfig.TOGAS_SCRIPT_ID || existingConfig.GF_SCRIPT_ID || "",
249
+ },
250
+ {
251
+ type: "text",
252
+ name: "TOGAS_PATTERN",
253
+ message: "Togas: Comma-separated glob patterns for files to include (e.g. abc*,xyz*)",
254
+ initial: existingConfig.TOGAS_PATTERN || "*",
231
255
  }
232
256
  ];
233
257
 
@@ -0,0 +1,176 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import prompts from "prompts";
4
+ import { execSync } from "child_process";
5
+ import dotenv from "dotenv";
6
+ import { Utils } from "../support/utils.js";
7
+
8
+ /**
9
+ * Prepares and pushes local code to Google Apps Script using clasp.
10
+ * @param {object} options Options object provided by commander.js.
11
+ */
12
+ export async function togas(options) {
13
+ // 1. Load env
14
+ const envPath = path.resolve(process.cwd(), options.env || "./.env");
15
+ if (fs.existsSync(envPath)) {
16
+ dotenv.config({ path: envPath, quiet: true, override: true });
17
+ }
18
+
19
+ const target = options.target || process.env.TOGAS_TARGET;
20
+ const scriptId = options.scriptId || process.env.TOGAS_SCRIPT_ID || process.env.GF_SCRIPT_ID;
21
+ const pattern = options.pattern || process.env.TOGAS_PATTERN || "*";
22
+ const source = options.source || "./";
23
+
24
+ if (!target) {
25
+ console.error("Error: TOGAS_TARGET is not set. Please run 'gas-fakes init' or provide --target.");
26
+ process.exit(1);
27
+ }
28
+
29
+ const absoluteTarget = path.resolve(process.cwd(), target);
30
+ const absoluteSource = path.resolve(process.cwd(), source);
31
+
32
+ if (absoluteTarget === absoluteSource) {
33
+ console.error("Error: Target directory cannot be the same as the source directory.");
34
+ process.exit(1);
35
+ }
36
+
37
+ // 2. Prepare target
38
+ if (!fs.existsSync(absoluteTarget)) {
39
+ fs.mkdirSync(absoluteTarget, { recursive: true });
40
+ }
41
+
42
+ const claspJsonPath = path.join(absoluteTarget, ".clasp.json");
43
+ if (!fs.existsSync(claspJsonPath)) {
44
+ const response = await prompts({
45
+ type: "confirm",
46
+ name: "create",
47
+ message: `No .clasp.json found in ${target}. Create one?`,
48
+ initial: true
49
+ });
50
+
51
+ if (response.create) {
52
+ if (!scriptId) {
53
+ console.error("Error: No Script ID found. Please provide one with --scriptId or in .env.");
54
+ process.exit(1);
55
+ }
56
+ const claspConfig = { scriptId, rootDir: "./" };
57
+ fs.writeFileSync(claspJsonPath, JSON.stringify(claspConfig, null, 2));
58
+ console.log(`Created .clasp.json with scriptId: ${scriptId}`);
59
+ } else {
60
+ console.log("Operation cancelled.");
61
+ return;
62
+ }
63
+ } else {
64
+ // Check scriptId
65
+ try {
66
+ const claspConfig = JSON.parse(fs.readFileSync(claspJsonPath, "utf8"));
67
+ if (scriptId && claspConfig.scriptId !== scriptId) {
68
+ console.error(`Error: .clasp.json scriptId (${claspConfig.scriptId}) does not match configured scriptId (${scriptId}).`);
69
+ process.exit(1);
70
+ }
71
+ } catch (e) {
72
+ console.warn(`Warning: Failed to parse ${claspJsonPath}.`);
73
+ }
74
+ }
75
+
76
+ // 3. Copy files
77
+ console.log(`...scanning source: ${absoluteSource}`);
78
+ console.log(`...matching patterns: ${pattern}`);
79
+
80
+ // Create an array of regexes for the comma-separated patterns
81
+ // We want to match the pattern explicitly ending in .js, .html, or .gs
82
+ const regexPatterns = pattern.split(',').map(p => p.trim()).filter(Boolean).map(p => {
83
+ // Escape regex characters except the * wildcard
84
+ const escapedPattern = p.split('*').map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*');
85
+
86
+ // If the user provided an extension, use it, otherwise match our target extensions
87
+ if (p.endsWith('.js') || p.endsWith('.html') || p.endsWith('.gs')) {
88
+ return new RegExp("^" + escapedPattern + "$");
89
+ } else {
90
+ // Append the target extensions to the pattern
91
+ return new RegExp("^" + escapedPattern + "\\.(js|html|gs)$");
92
+ }
93
+ });
94
+
95
+ const copiedFiles = [];
96
+ const specialFiles = ["appsscript.json"];
97
+
98
+ // Helper to walk directory recursively and efficiently
99
+ function walkSync(dir, relativeDir = "") {
100
+ const absoluteDir = path.join(dir, relativeDir);
101
+
102
+ // Skip target directory if it's inside source
103
+ if (absoluteDir === absoluteTarget) return;
104
+
105
+ const entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
106
+
107
+ for (const entry of entries) {
108
+ const entryRelativePath = path.join(relativeDir, entry.name);
109
+ const entryAbsolutePath = path.join(dir, entryRelativePath);
110
+
111
+ if (entry.isDirectory()) {
112
+ // Skip common ignored directories
113
+ if (entry.name === "node_modules" || entry.name === ".git" || entryAbsolutePath === absoluteTarget) {
114
+ continue;
115
+ }
116
+ walkSync(dir, entryRelativePath);
117
+ } else if (entry.isFile()) {
118
+ const fileName = entry.name;
119
+ const extension = path.extname(fileName).toLowerCase();
120
+
121
+ // Handle special files
122
+ if (specialFiles.includes(entryRelativePath)) {
123
+ const destPath = path.join(absoluteTarget, entryRelativePath);
124
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
125
+ fs.copyFileSync(entryAbsolutePath, destPath);
126
+ if (extension === ".js" || extension === ".gs") copiedFiles.push(destPath);
127
+ continue;
128
+ }
129
+
130
+ // Match patterns for .js, .gs, .html
131
+ if ((extension === ".js" || extension === ".html" || extension === ".gs") &&
132
+ regexPatterns.some(regex => regex.test(fileName))) {
133
+
134
+ const destPath = path.join(absoluteTarget, entryRelativePath);
135
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
136
+ fs.copyFileSync(entryAbsolutePath, destPath);
137
+ if (extension === ".js" || extension === ".gs") copiedFiles.push(destPath);
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ walkSync(absoluteSource);
144
+
145
+ if (copiedFiles.length === 0) {
146
+ console.log("No files matched the patterns. Nothing to do.");
147
+ return;
148
+ }
149
+
150
+ // 4. Transform JS files
151
+ console.log(`...transforming ${copiedFiles.length} files for Apps Script compatibility`);
152
+ for (const filePath of copiedFiles) {
153
+ let content = fs.readFileSync(filePath, "utf8");
154
+ fs.writeFileSync(filePath, Utils.stripEsmKeywords(content));
155
+ }
156
+
157
+ console.log(`Ready for clasp push in ${target}.`);
158
+
159
+ // 5. Clasp push
160
+ const response = await prompts({
161
+ type: "confirm",
162
+ name: "push",
163
+ message: "Push to Apps Script using clasp now?",
164
+ initial: true
165
+ });
166
+
167
+ if (response.push) {
168
+ console.log("Running clasp push...");
169
+ try {
170
+ execSync("clasp push", { cwd: absoluteTarget, stdio: "inherit" });
171
+ console.log("Push successful.");
172
+ } catch (err) {
173
+ console.error("Clasp push failed. Ensure you are logged in (clasp login) and the script exists.");
174
+ }
175
+ }
176
+ }
package/src/index.js CHANGED
@@ -29,4 +29,6 @@ import './services/mimetype/app.js'
29
29
  import './services/lock/app.js'
30
30
  import './services/libhandlerapp/app.js'
31
31
  import './services/jdbc/app.js'
32
+ import './services/html/app.js'
33
+ import './services/content/app.js'
32
34
 
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @file Provides a fake implementation of the Ui class.
3
+ */
4
+ import { Proxies } from '../../support/proxies.js';
5
+ import { notYetImplemented } from '../../support/helpers.js';
6
+
7
+ /**
8
+ * A fake implementation of the Ui class.
9
+ * @class FakeUi
10
+ * @implements {GoogleAppsScript.Base.Ui}
11
+ */
12
+ class FakeUi {
13
+ createAddonMenu() {
14
+ return notYetImplemented('Ui.createAddonMenu');
15
+ }
16
+
17
+ createMenu(caption) {
18
+ return notYetImplemented('Ui.createMenu');
19
+ }
20
+
21
+ showSidebar(userInterface) {
22
+ // In live GAS, this returns void and pushes to the client UI.
23
+ // Locally, we set the dimensions to match a standard Sidebar (300px width).
24
+ if (userInterface && typeof userInterface.setWidth === 'function') {
25
+ userInterface.setWidth(300);
26
+ userInterface.__framingType = 'sidebar';
27
+ }
28
+ // We log it so the developer knows the UI interaction occurred.
29
+ console.log(`[gas-fakes] Ui.showSidebar triggered with Title: "${userInterface.getTitle ? userInterface.getTitle() : ''}"`);
30
+ return this;
31
+ }
32
+
33
+ showModalDialog(userInterface, title) {
34
+ // In live GAS, this returns void and pushes to the client UI.
35
+ // Locally, we set the title and apply standard dialog styling to the HtmlOutput.
36
+ if (userInterface && typeof userInterface.setTitle === 'function') {
37
+ userInterface.setTitle(title);
38
+ userInterface.__framingType = 'modal';
39
+ }
40
+ console.log(`[gas-fakes] Ui.showModalDialog triggered with Title: "${title}"`);
41
+ return this;
42
+ }
43
+ }
44
+
45
+ export const newFakeUi = (...args) => Proxies.guard(new FakeUi(...args));
@@ -0,0 +1,3 @@
1
+ import { newFakeContentService } from "./contentservice.js";
2
+
3
+ globalThis.ContentService = newFakeContentService();
@@ -0,0 +1,14 @@
1
+ import { FakeTextOutput } from "./textoutput.js";
2
+ import { ContentEnums } from "../enums/contentenums.js";
3
+
4
+ export class FakeContentService {
5
+ constructor() {
6
+ this.MimeType = ContentEnums.MimeType;
7
+ }
8
+
9
+ createTextOutput(content = "") {
10
+ return new FakeTextOutput(content);
11
+ }
12
+ }
13
+
14
+ export const newFakeContentService = () => new FakeContentService();
@@ -0,0 +1,45 @@
1
+ export class FakeTextOutput {
2
+ constructor(content = "") {
3
+ this._content = content;
4
+ this._mimeType = "text/plain";
5
+ this._fileName = null;
6
+ this.__isTextOutput = true;
7
+ }
8
+
9
+ append(content) {
10
+ this._content += content;
11
+ return this;
12
+ }
13
+
14
+ clear() {
15
+ this._content = null;
16
+ return this;
17
+ }
18
+
19
+ downloadAsFile(filename) {
20
+ this._fileName = filename;
21
+ return this;
22
+ }
23
+
24
+ getContent() {
25
+ return this._content;
26
+ }
27
+
28
+ getFileName() {
29
+ return this._fileName;
30
+ }
31
+
32
+ getMimeType() {
33
+ return this._mimeType;
34
+ }
35
+
36
+ setContent(content) {
37
+ this._content = content;
38
+ return this;
39
+ }
40
+
41
+ setMimeType(mimeType) {
42
+ this._mimeType = mimeType;
43
+ return this;
44
+ }
45
+ }
@@ -3,7 +3,7 @@ import { newFakeDocument } from './fakedocument.js';
3
3
 
4
4
  import { signatureArgs } from '../../support/helpers.js';
5
5
  import is from '@sindresorhus/is';
6
- import { newFakeUi } from './fakeui.js';
6
+ import { newFakeUi } from '../common/fakeui.js';
7
7
  import { Auth } from '../../support/auth.js';
8
8
  import * as Enums from '../enums/docsenums.js'
9
9
  import { defaultDocumentStyleRequests } from './elementblasters.js';
@@ -0,0 +1,15 @@
1
+ import { newFakeGasenum } from "@mcpher/fake-gasenum";
2
+
3
+ export const ContentEnums = {
4
+ MimeType: newFakeGasenum([
5
+ "ATOM",
6
+ "CSV",
7
+ "ICAL",
8
+ "JAVASCRIPT",
9
+ "JSON",
10
+ "RSS",
11
+ "TEXT",
12
+ "VCARD",
13
+ "XML"
14
+ ])
15
+ }
@@ -0,0 +1,13 @@
1
+ import { newFakeGasenum } from "@mcpher/fake-gasenum";
2
+
3
+ export const HtmlEnums = {
4
+ SandboxMode: newFakeGasenum([
5
+ "EMULATED",
6
+ "IFRAME",
7
+ "NATIVE"
8
+ ]),
9
+ XFrameOptionsMode: newFakeGasenum([
10
+ "ALLOWALL",
11
+ "DEFAULT"
12
+ ])
13
+ }
@@ -0,0 +1,6 @@
1
+ export const ScriptEnums = {
2
+ AuthorizationStatus: {
3
+ REQUIRED: 'REQUIRED',
4
+ NOT_REQUIRED: 'NOT_REQUIRED'
5
+ }
6
+ };
@@ -1,6 +1,7 @@
1
1
  import { Proxies } from '../../support/proxies.js';
2
2
  import * as Enums from '../enums/formsenums.js';
3
3
  import { newFakeForm } from './fakeform.js';
4
+ import { newFakeUi } from '../common/fakeui.js';
4
5
  import { Auth } from '../../support/auth.js';
5
6
  import { Url } from '../../support/url.js';
6
7
  import { signatureArgs } from '../../support/helpers.js'
@@ -74,6 +75,10 @@ class FakeFormApp {
74
75
  return this.openById(id);
75
76
  }
76
77
 
78
+ getUi() {
79
+ return newFakeUi();
80
+ }
81
+
77
82
  /**
78
83
  * Opens the form with the specified ID.
79
84
  * @param {string} id The ID of the form to open.
@@ -0,0 +1,9 @@
1
+
2
+ /**
3
+ * the idea here is to create an empty global entry for the singleton
4
+ * but only load it when it is actually used.
5
+ */
6
+ import { lazyLoaderApp } from '../common/lazyloader.js'
7
+ import { newFakeHtmlService as maker} from './htmlservice.js'
8
+ let _app = null;
9
+ _app = lazyLoaderApp(_app, 'HtmlService', maker)