@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.
- package/README.md +15 -32
- package/package.json +1 -2
- package/src/cli/app.js +30 -2
- package/src/cli/server.js +32 -0
- package/src/cli/setup.js +24 -0
- package/src/cli/togas.js +176 -0
- package/src/index.js +2 -0
- package/src/services/common/fakeui.js +45 -0
- package/src/services/content/app.js +3 -0
- package/src/services/content/contentservice.js +14 -0
- package/src/services/content/textoutput.js +45 -0
- package/src/services/documentapp/fakedocumentapp.js +1 -1
- package/src/services/enums/contentenums.js +15 -0
- package/src/services/enums/htmlenums.js +13 -0
- package/src/services/enums/scriptenums.js +6 -0
- package/src/services/formapp/fakeformapp.js +5 -0
- package/src/services/html/app.js +9 -0
- package/src/services/html/consumerworker.js +129 -0
- package/src/services/html/googlescriptrun.js +91 -0
- package/src/services/html/htmloutput.js +127 -0
- package/src/services/html/htmloutputmetatag.js +14 -0
- package/src/services/html/htmlservice.js +94 -0
- package/src/services/html/htmltemplate.js +63 -0
- package/src/services/html/serverworker.js +135 -0
- package/src/services/html/webapp.js +266 -0
- package/src/services/html/worker.js +63 -0
- package/src/services/libhandlerapp/fakelibrary.js +2 -2
- package/src/services/scriptapp/app.js +44 -0
- package/src/services/scriptapp/fakeauthorizationinfo.js +22 -0
- package/src/services/slidesapp/fakeslidesapp.js +5 -0
- package/src/services/spreadsheetapp/fakebooleancondition.js +14 -2
- package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +30 -5
- package/src/services/spreadsheetapp/fakegradientcondition.js +1 -1
- package/src/services/spreadsheetapp/fakeovergridimage.js +25 -0
- package/src/services/spreadsheetapp/fakesheet.js +23 -1
- package/src/services/spreadsheetapp/fakespreadsheet.js +68 -11
- package/src/services/spreadsheetapp/fakespreadsheetapp.js +70 -9
- package/src/services/stores/fakestores.js +5 -0
- package/src/support/auth.js +2 -0
- package/src/support/proxies.js +1 -1
- package/src/support/sxauth.js +20 -12
- package/src/support/utils.js +480 -200
- package/src/support/workersync/sxhtml.js +8 -0
- package/src/support/workersync/synchronizer.js +8 -1
- package/src/support/workersync/worker.js +5 -0
- package/api-docs/kdrive_api.json +0 -69958
- package/appsscript.json +0 -102
- package/gf_agent/README.md +0 -101
- package/gf_agent/SKILL.md +0 -460
- package/gf_agent/documentation.md +0 -105
- package/gf_agent/gf-agent-contributor/SKILL.md +0 -56
- package/gf_agent/index.md +0 -21
- package/gf_agent/knowledge/00-execution-context.md +0 -5
- package/gf_agent/knowledge/01-drive.md +0 -12
- package/gf_agent/knowledge/02-syntax.md +0 -14
- package/gf_agent/knowledge/03-auth.md +0 -15
- package/gf_agent/knowledge/04-advanced.md +0 -46
- package/gf_agent/knowledge/05-sheets-forms.md +0 -26
- package/gf_agent/knowledge/06-jdbc-cloudsql.md +0 -21
- package/gf_agent/knowledge/07-jdbc-auth-details.md +0 -30
- package/gf_agent/knowledge/08-docs-limitations.md +0 -4
- package/gf_agent/knowledge/09-orchestrator-pattern.md +0 -55
- package/gf_agent/knowledge/10-sandbox-security.md +0 -62
- package/gf_agent/knowledge/11-chart-builder-limitations.md +0 -15
- package/gf_agent/knowledge/12-gmail-eventual-consistency.md +0 -13
- package/gf_agent/knowledge/13-advanced-services-discovery.md +0 -23
- package/gf_agent/knowledge/14-utilities-parity.md +0 -13
- package/gf_agent/knowledge/README.md +0 -16
- package/gf_agent/scripts/SKILL.template.md +0 -63
- package/gf_agent/scripts/builder.js +0 -118
- package/gf_agent/skills/base.md +0 -156
- package/gf_agent/skills/cache.md +0 -20
- package/gf_agent/skills/calendar.md +0 -780
- package/gf_agent/skills/charts.md +0 -127
- package/gf_agent/skills/document.md +0 -6752
- package/gf_agent/skills/drive.md +0 -423
- package/gf_agent/skills/forms.md +0 -4036
- package/gf_agent/skills/gmail.md +0 -576
- package/gf_agent/skills/jdbc.md +0 -3101
- package/gf_agent/skills/lock.md +0 -20
- package/gf_agent/skills/properties.md +0 -19
- package/gf_agent/skills/script.md +0 -50
- package/gf_agent/skills/slides.md +0 -5054
- package/gf_agent/skills/spreadsheet.md +0 -56075
- package/gf_agent/skills/urlfetch.md +0 -28
- package/gf_agent/skills/utilities.md +0 -50
- package/gf_agent/skills/xml.md +0 -270
- package/skills-lock.json +0 -10
- package/src/services/documentapp/fakeui.js +0 -27
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { ServerWorkerContext } from './serverworker.js';
|
|
3
|
+
|
|
4
|
+
const CLIENT_POLYFILL = `
|
|
5
|
+
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
|
|
6
|
+
<script>
|
|
7
|
+
window.google = window.google || {};
|
|
8
|
+
window.google.script = window.google.script || {};
|
|
9
|
+
window.google.script.run = (function() {
|
|
10
|
+
function RunProxy(handlers) {
|
|
11
|
+
this._sh = handlers ? handlers._sh : null;
|
|
12
|
+
this._fh = handlers ? handlers._fh : null;
|
|
13
|
+
this._uo = handlers ? handlers._uo : null;
|
|
14
|
+
|
|
15
|
+
return new Proxy(this, {
|
|
16
|
+
get: (target, prop) => {
|
|
17
|
+
if (prop === 'withSuccessHandler') return (h) => new RunProxy({ ...target, _sh: h });
|
|
18
|
+
if (prop === 'withFailureHandler') return (h) => new RunProxy({ ...target, _fh: h });
|
|
19
|
+
if (prop === 'withUserObject') return (o) => new RunProxy({ ...target, _uo: o });
|
|
20
|
+
|
|
21
|
+
return (...args) => {
|
|
22
|
+
let hasError = false;
|
|
23
|
+
let errorMsg = '';
|
|
24
|
+
|
|
25
|
+
// Validate arguments according to Google Apps Script rules
|
|
26
|
+
const validateArg = (arg) => {
|
|
27
|
+
if (arg === undefined) {
|
|
28
|
+
hasError = true;
|
|
29
|
+
errorMsg = 'google.script.run cannot process undefined arguments.';
|
|
30
|
+
} else if (typeof arg === 'function') {
|
|
31
|
+
hasError = true;
|
|
32
|
+
errorMsg = 'google.script.run cannot process function arguments.';
|
|
33
|
+
} else if (typeof Element !== 'undefined' && arg instanceof Element) {
|
|
34
|
+
hasError = true;
|
|
35
|
+
errorMsg = 'google.script.run cannot process DOM element arguments.';
|
|
36
|
+
} else if (arg === window) {
|
|
37
|
+
hasError = true;
|
|
38
|
+
errorMsg = 'google.script.run cannot process window object arguments.';
|
|
39
|
+
} else if (arg !== null && typeof arg === 'object') {
|
|
40
|
+
// deep validation could go here, but shallow is usually enough for the common mistakes
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
args.forEach(validateArg);
|
|
44
|
+
|
|
45
|
+
if (hasError) {
|
|
46
|
+
if (target._fh) {
|
|
47
|
+
setTimeout(() => target._fh(new Error(errorMsg), target._uo), 0);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fetch(window.location.origin + '/__gas_rpc', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ funcName: prop, args: args })
|
|
56
|
+
})
|
|
57
|
+
.then(r => r.json())
|
|
58
|
+
.then(data => {
|
|
59
|
+
if (data.error) {
|
|
60
|
+
if (target._fh) target._fh(new Error(data.error), target._uo);
|
|
61
|
+
} else {
|
|
62
|
+
if (target._sh) target._sh(data.result, target._uo);
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.catch(err => {
|
|
66
|
+
if (target._fh) target._fh(err, target._uo);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return new RunProxy();
|
|
73
|
+
})();
|
|
74
|
+
window.google.script.host = {
|
|
75
|
+
close: function() {
|
|
76
|
+
if (window.top && window.top !== window) {
|
|
77
|
+
window.top.postMessage({ type: 'gas-fakes-close' }, '*');
|
|
78
|
+
} else {
|
|
79
|
+
document.body.innerHTML = '<h3 style="font-family: sans-serif; text-align: center; margin-top: 50px;">Dialog Closed. You may close this tab.</h3>';
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
origin: window.location.origin,
|
|
83
|
+
setHeight: function(h) { console.log('gas-fakes: Host setHeight called with', h); },
|
|
84
|
+
setWidth: function(w) { console.log('gas-fakes: Host setWidth called with', w); }
|
|
85
|
+
};
|
|
86
|
+
//# sourceURL=gas-fakes:///polyfill.js
|
|
87
|
+
</script>
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
export function startServer(port = 3000, scriptPath = null, entryFunction = 'doGet') {
|
|
91
|
+
const server = http.createServer((req, res) => {
|
|
92
|
+
if (req.method === 'GET' && req.url.startsWith('/')) {
|
|
93
|
+
// 1. Serve GET request
|
|
94
|
+
try {
|
|
95
|
+
const ctx = new ServerWorkerContext(scriptPath);
|
|
96
|
+
// Emulate the event object passed to doGet
|
|
97
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
98
|
+
const params = Object.fromEntries(url.searchParams.entries());
|
|
99
|
+
const getEvent = {
|
|
100
|
+
parameter: params,
|
|
101
|
+
parameters: Object.fromEntries([...url.searchParams.keys()].map(k => [k, url.searchParams.getAll(k)])),
|
|
102
|
+
queryString: url.search.substring(1),
|
|
103
|
+
contextPath: '',
|
|
104
|
+
contentLength: 0
|
|
105
|
+
};
|
|
106
|
+
const funcToRun = params.main || entryFunction || 'doGet';
|
|
107
|
+
const result = ctx.runFunction(funcToRun, [getEvent]);
|
|
108
|
+
|
|
109
|
+
if (result && result.__isHtmlOutput) {
|
|
110
|
+
let html = result.content;
|
|
111
|
+
// Inject polyfill
|
|
112
|
+
if (html.toLowerCase().includes('</head>')) {
|
|
113
|
+
html = html.replace(/<\/head>/i, CLIENT_POLYFILL + '</head>');
|
|
114
|
+
} else {
|
|
115
|
+
html = CLIENT_POLYFILL + html;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (result.__framingType === 'modal' || result.__framingType === 'sidebar') {
|
|
119
|
+
const isSidebar = result.__framingType === 'sidebar';
|
|
120
|
+
const widthStr = isSidebar ? '300px' : '600px';
|
|
121
|
+
const heightStr = isSidebar ? '100vh' : '450px';
|
|
122
|
+
const safeTitle = (result.title || 'gas-fakes Dialog').replace(/"/g, '"');
|
|
123
|
+
|
|
124
|
+
const modalCss = `
|
|
125
|
+
body { background-color: rgba(0,0,0,0.6); margin: 0; display: flex; align-items: center; justify-content: center; height: 100vh; font-family: 'Google Sans', Roboto, Arial, sans-serif; overflow: hidden; }
|
|
126
|
+
.dialog { width: ${widthStr}; height: ${heightStr}; background: #fff; border-radius: 8px; box-shadow: 0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12), 0 11px 15px -7px rgba(0,0,0,0.2); display: flex; flex-direction: column; overflow: hidden; }
|
|
127
|
+
.dialog-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 24px 14px 24px; border-bottom: 1px solid transparent; }
|
|
128
|
+
.dialog-title { font-size: 22px; color: #202124; margin: 0; line-height: 28px; font-weight: 400; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
129
|
+
.dialog-close { background: none; border: none; font-size: 20px; color: #5f6368; cursor: pointer; padding: 0; line-height: 1; }
|
|
130
|
+
.dialog-close:hover { color: #202124; }
|
|
131
|
+
.dialog-content { padding: 0; flex-grow: 1; display: flex; border: none; }
|
|
132
|
+
iframe { border: none; width: 100%; height: 100%; background: #fff; }
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
const sidebarCss = `
|
|
136
|
+
body { background-color: transparent; margin: 0; display: flex; justify-content: flex-end; height: 100vh; font-family: 'Google Sans', Roboto, Arial, sans-serif; overflow: hidden; pointer-events: none; }
|
|
137
|
+
.dialog { width: ${widthStr}; height: ${heightStr}; background: #fff; box-shadow: -1px 0 4px rgba(0,0,0,0.2); display: flex; flex-direction: column; pointer-events: auto; border-left: 1px solid #dadce0; }
|
|
138
|
+
.dialog-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid #dadce0; }
|
|
139
|
+
.dialog-title { font-size: 16px; color: #202124; margin: 0; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
140
|
+
.dialog-close { background: none; border: none; font-size: 20px; color: #5f6368; cursor: pointer; padding: 0; line-height: 1; }
|
|
141
|
+
.dialog-close:hover { color: #202124; }
|
|
142
|
+
.dialog-content { flex-grow: 1; display: flex; border: none; }
|
|
143
|
+
iframe { border: none; width: 100%; height: 100%; background: #fff; }
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
const css = isSidebar ? sidebarCss : modalCss;
|
|
147
|
+
|
|
148
|
+
// We encode the HTML so we can safely inject it into the iframe using document.write
|
|
149
|
+
const encodedHtml = encodeURIComponent(html);
|
|
150
|
+
|
|
151
|
+
html = `
|
|
152
|
+
<!DOCTYPE html>
|
|
153
|
+
<html>
|
|
154
|
+
<head>
|
|
155
|
+
<title>${safeTitle}</title>
|
|
156
|
+
<style>${css}</style>
|
|
157
|
+
</head>
|
|
158
|
+
<body>
|
|
159
|
+
<div class="dialog">
|
|
160
|
+
<div class="dialog-header">
|
|
161
|
+
<h2 class="dialog-title">${safeTitle}</h2>
|
|
162
|
+
<button class="dialog-close" aria-label="Close" onclick="closeDialog()">x</button>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="dialog-content">
|
|
165
|
+
<iframe id="gas-fakes-frame"></iframe>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
<script>
|
|
169
|
+
// Handle programmatic closures from google.script.host.close()
|
|
170
|
+
window.addEventListener('message', function(e) {
|
|
171
|
+
if (e.data && e.data.type === 'gas-fakes-close') {
|
|
172
|
+
document.body.innerHTML = '<h3 style="font-family: sans-serif; text-align: center; margin-top: 50px;">Dialog Closed. You may close this tab.</h3>';
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
// Handle the 'X' button
|
|
176
|
+
function closeDialog() {
|
|
177
|
+
document.body.innerHTML = '<h3 style="font-family: sans-serif; text-align: center; margin-top: 50px;">Dialog Closed. You may close this tab.</h3>';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const iframe = document.getElementById('gas-fakes-frame');
|
|
181
|
+
iframe.contentWindow.document.open();
|
|
182
|
+
iframe.contentWindow.document.write(decodeURIComponent("${encodedHtml}"));
|
|
183
|
+
iframe.contentWindow.document.close();
|
|
184
|
+
</script>
|
|
185
|
+
</body>
|
|
186
|
+
</html>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html);
|
|
190
|
+
} else if (result && result.__isTextOutput) {
|
|
191
|
+
res.writeHead(200, { 'Content-Type': result.mimeType || 'text/plain' });
|
|
192
|
+
res.end(result.content);
|
|
193
|
+
} else {
|
|
194
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
195
|
+
res.end(typeof result === 'string' ? result : JSON.stringify(result));
|
|
196
|
+
}
|
|
197
|
+
} catch (err) {
|
|
198
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
199
|
+
res.end("Error executing " + entryFunction + ": " + err.message);
|
|
200
|
+
}
|
|
201
|
+
} else if (req.method === 'POST' && req.url === '/__gas_rpc') {
|
|
202
|
+
// 2. Handle google.script.run RPC requests
|
|
203
|
+
let body = '';
|
|
204
|
+
req.on('data', chunk => body += chunk.toString());
|
|
205
|
+
req.on('end', () => {
|
|
206
|
+
try {
|
|
207
|
+
const payload = JSON.parse(body);
|
|
208
|
+
const ctx = new ServerWorkerContext(scriptPath);
|
|
209
|
+
// Execute the requested function statelessly
|
|
210
|
+
const result = ctx.runFunction(payload.funcName, payload.args);
|
|
211
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
212
|
+
res.end(JSON.stringify({ result }));
|
|
213
|
+
} catch (err) {
|
|
214
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
215
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
} else if (req.method === 'POST' && req.url === '/') {
|
|
219
|
+
// 3. Serve doPost
|
|
220
|
+
let body = '';
|
|
221
|
+
req.on('data', chunk => body += chunk.toString());
|
|
222
|
+
req.on('end', () => {
|
|
223
|
+
try {
|
|
224
|
+
const ctx = new ServerWorkerContext(scriptPath);
|
|
225
|
+
const doPostEvent = {
|
|
226
|
+
postData: {
|
|
227
|
+
contents: body,
|
|
228
|
+
type: req.headers['content-type']
|
|
229
|
+
},
|
|
230
|
+
parameter: {},
|
|
231
|
+
parameters: {},
|
|
232
|
+
queryString: '',
|
|
233
|
+
contextPath: '',
|
|
234
|
+
contentLength: body.length
|
|
235
|
+
};
|
|
236
|
+
const result = ctx.runFunction('doPost', [doPostEvent]);
|
|
237
|
+
|
|
238
|
+
if (result && result.__isHtmlOutput) {
|
|
239
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
240
|
+
res.end(result.content);
|
|
241
|
+
} else if (result && result.__isTextOutput) {
|
|
242
|
+
res.writeHead(200, { 'Content-Type': result.mimeType || 'text/plain' });
|
|
243
|
+
res.end(result.content);
|
|
244
|
+
} else {
|
|
245
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
246
|
+
res.end(typeof result === 'string' ? result : JSON.stringify(result));
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
250
|
+
res.end("Error executing doPost: " + err.message);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
} else {
|
|
254
|
+
res.writeHead(404);
|
|
255
|
+
res.end('Not found');
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
server.listen(port, () => {
|
|
260
|
+
console.log('\n=================================================');
|
|
261
|
+
console.log('🚀 gas-fakes Web App running at: http://localhost:' + port);
|
|
262
|
+
console.log('=================================================\n');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return server;
|
|
266
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { runInThisContext } from 'vm';
|
|
5
|
+
import { Utils as GasFakesUtils } from '../../support/utils.js';
|
|
6
|
+
|
|
7
|
+
// Initialize the gas-fakes environment in this worker isolate
|
|
8
|
+
import '../../index.js';
|
|
9
|
+
|
|
10
|
+
// The worker is spawned with the path to the main script
|
|
11
|
+
const { mainScriptPath, funcName, args, isTemplate, templateString } = workerData;
|
|
12
|
+
|
|
13
|
+
async function run() {
|
|
14
|
+
try {
|
|
15
|
+
const projectDir = path.dirname(mainScriptPath);
|
|
16
|
+
|
|
17
|
+
// Simulate Apps Script's global execution model:
|
|
18
|
+
// Read all .js and .gs files in the directory and evaluate them in this global context.
|
|
19
|
+
const files = fs.readdirSync(projectDir);
|
|
20
|
+
const scriptFiles = files.filter(f => f.endsWith('.js') || f.endsWith('.gs'));
|
|
21
|
+
|
|
22
|
+
// Sort files to ensure deterministic execution (like clasp does)
|
|
23
|
+
scriptFiles.sort();
|
|
24
|
+
|
|
25
|
+
for (const file of scriptFiles) {
|
|
26
|
+
const fullPath = path.join(projectDir, file);
|
|
27
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
28
|
+
|
|
29
|
+
// Strip ESM keywords to allow evaluation in the raw VM context
|
|
30
|
+
const strippedContent = GasFakesUtils.stripEsmKeywords(content);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// runInThisContext executes the code globally within the current V8 isolate
|
|
34
|
+
runInThisContext(strippedContent, { filename: fullPath });
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(`gas-fakes Error parsing ${file}: ${e.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isTemplate) {
|
|
41
|
+
parentPort.postMessage({ result: "Template evaluation not yet implemented in worker" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const func = globalThis[funcName];
|
|
46
|
+
|
|
47
|
+
if (typeof func !== 'function') {
|
|
48
|
+
throw new Error(`google.script.run: function "${funcName}" is not defined on the server.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Apps Script functions can be async, so we await
|
|
52
|
+
const result = await func(...args);
|
|
53
|
+
|
|
54
|
+
// Apps Script parameters/returns are JSON serialized
|
|
55
|
+
const serializedResult = typeof result === 'undefined' ? undefined : JSON.parse(JSON.stringify(result));
|
|
56
|
+
|
|
57
|
+
parentPort.postMessage({ result: serializedResult });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
parentPort.postMessage({ error: { message: error.message, stack: error.stack } });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
run();
|
|
@@ -60,7 +60,7 @@ class FakeLibrary {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
get combinedJs() {
|
|
63
|
-
return this.libContent.files.filter((f) => f.type === 'server_js').map((f) => `////-- ${f.name} --\n${f.source}
|
|
63
|
+
return this.libContent.files.filter((f) => f.type === 'server_js').map((f) => `////-- ${f.name} --\n${f.source}\n;`).join(`\n\n`)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
get content() {
|
|
@@ -90,7 +90,7 @@ class FakeLibrary {
|
|
|
90
90
|
const functions = makeExports(ast, 'FunctionDeclaration', (f) => f.id.name)
|
|
91
91
|
const variables = makeExports(ast, 'VariableDeclaration', (f) => f.declarations[0].id.name)
|
|
92
92
|
const classes = makeExports(ast, 'ClassDeclaration', (f) => f.id.name)
|
|
93
|
-
const exports = [...functions, ...variables, ...classes];
|
|
93
|
+
const exports = [...functions, ...variables, ...classes].filter(Boolean);
|
|
94
94
|
return `${this.combinedJs};\nreturn { ${exports.join(', ')} };`;
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -6,8 +6,12 @@ import { Syncit } from '../../support/syncit.js'
|
|
|
6
6
|
import { Auth } from '../../support/auth.js'
|
|
7
7
|
import { Proxies } from '../../support/proxies.js'
|
|
8
8
|
import { newFakeBehavior } from './behavior.js'
|
|
9
|
+
import { newFakeAuthorizationInfo } from './fakeauthorizationinfo.js'
|
|
10
|
+
import { ScriptEnums } from '../enums/scriptenums.js'
|
|
9
11
|
import { newCacheDropin } from '@mcpher/gas-flex-cache'
|
|
10
12
|
import { slogger } from "../../support/slogger.js";
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
11
15
|
|
|
12
16
|
// This will eventually hold a proxy for ScriptApp
|
|
13
17
|
let _app = null
|
|
@@ -155,6 +159,41 @@ if (typeof globalThis[name] === typeof undefined) {
|
|
|
155
159
|
_app = {
|
|
156
160
|
getOAuthToken,
|
|
157
161
|
__getSourceOAuthToken: getSourceOAuthToken,
|
|
162
|
+
getResource: (filename) => {
|
|
163
|
+
let mainScriptPath = process.argv[1];
|
|
164
|
+
if (globalThis.__gasFakesMainScriptPath) {
|
|
165
|
+
mainScriptPath = globalThis.__gasFakesMainScriptPath;
|
|
166
|
+
}
|
|
167
|
+
if (!mainScriptPath || mainScriptPath.endsWith('node')) {
|
|
168
|
+
throw new Error("Could not determine project root for getResource. Ensure process.argv[1] is set or __gasFakesMainScriptPath is defined.");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const projectDir = path.dirname(mainScriptPath);
|
|
172
|
+
let fullPath = path.resolve(projectDir, filename);
|
|
173
|
+
|
|
174
|
+
if (!fs.existsSync(fullPath)) {
|
|
175
|
+
// Apps Script allows omitting the extension. Try common ones.
|
|
176
|
+
const exts = ['.js', '.gs', '.html'];
|
|
177
|
+
let found = false;
|
|
178
|
+
for (const ext of exts) {
|
|
179
|
+
if (fs.existsSync(fullPath + ext)) {
|
|
180
|
+
fullPath += ext;
|
|
181
|
+
found = true;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!found) {
|
|
186
|
+
throw new Error(`File not found: ${fullPath}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
190
|
+
// We can't import Utilities easily here without circular deps or breaking the proxy
|
|
191
|
+
// But we can just use the global Utilities object assuming it's loaded.
|
|
192
|
+
if (!globalThis.Utilities) {
|
|
193
|
+
throw new Error("Utilities service not loaded, cannot create Blob for getResource.");
|
|
194
|
+
}
|
|
195
|
+
return globalThis.Utilities.newBlob(content, "text/plain", filename);
|
|
196
|
+
},
|
|
158
197
|
requireAllScopes,
|
|
159
198
|
requireScopes,
|
|
160
199
|
getScriptId: () => {
|
|
@@ -198,6 +237,11 @@ if (typeof globalThis[name] === typeof undefined) {
|
|
|
198
237
|
AuthMode: {
|
|
199
238
|
FULL: 'FULL'
|
|
200
239
|
},
|
|
240
|
+
AuthorizationStatus: ScriptEnums.AuthorizationStatus,
|
|
241
|
+
getAuthorizationInfo: (authMode) => {
|
|
242
|
+
ensureInit();
|
|
243
|
+
return newFakeAuthorizationInfo(authMode);
|
|
244
|
+
},
|
|
201
245
|
// __behavior added below to break recursion
|
|
202
246
|
__newCacheDropin: newCacheDropin,
|
|
203
247
|
__proxies: Proxies,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ScriptEnums } from '../enums/scriptenums.js';
|
|
2
|
+
|
|
3
|
+
export class FakeAuthorizationInfo {
|
|
4
|
+
constructor(authMode) {
|
|
5
|
+
this._authMode = authMode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
getAuthorizationStatus() {
|
|
9
|
+
// gas-fakes always handles auth out-of-band via CLI,
|
|
10
|
+
// so during script execution, we assume auth is not required.
|
|
11
|
+
return ScriptEnums.AuthorizationStatus.NOT_REQUIRED;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getAuthorizationUrl() {
|
|
15
|
+
// Return null since we don't require inline authorization
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const newFakeAuthorizationInfo = (authMode) => {
|
|
21
|
+
return new FakeAuthorizationInfo(authMode);
|
|
22
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Proxies } from '../../support/proxies.js';
|
|
2
2
|
import { newFakeAffineTransformBuilder } from './fakeaffinetransformbuilder.js';
|
|
3
|
+
import { newFakeUi } from '../common/fakeui.js';
|
|
3
4
|
import * as Enums from '../enums/slidesenums.js';
|
|
4
5
|
import { newFakePresentation } from './fakepresentation.js';
|
|
5
6
|
import { Auth } from '../../support/auth.js';
|
|
@@ -85,6 +86,10 @@ class FakeSlidesApp {
|
|
|
85
86
|
return this.openById(id);
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
getUi() {
|
|
90
|
+
return newFakeUi();
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
/**
|
|
89
94
|
* Opens the presentation with the specified ID.
|
|
90
95
|
* @param {string} id The ID of the presentation to open.
|
|
@@ -83,8 +83,20 @@ export class FakeBooleanCondition {
|
|
|
83
83
|
if (!this.__apiCondition.values) return [];
|
|
84
84
|
|
|
85
85
|
// API condition values are typically { userEnteredValue: 'string' } or { relativeDate: 'TODAY' }
|
|
86
|
+
const type = this.getCriteriaType()?.toString() || '';
|
|
87
|
+
const isNumberType = type.startsWith('NUMBER_');
|
|
88
|
+
const isDateType = type.startsWith('DATE_') && !type.endsWith('_RELATIVE');
|
|
89
|
+
|
|
86
90
|
return this.__apiCondition.values.map(v => {
|
|
87
|
-
if (v.userEnteredValue !== undefined)
|
|
91
|
+
if (v.userEnteredValue !== undefined) {
|
|
92
|
+
if (isNumberType && !isNaN(v.userEnteredValue) && v.userEnteredValue !== '') {
|
|
93
|
+
return Number(v.userEnteredValue);
|
|
94
|
+
}
|
|
95
|
+
if (isDateType && !isNaN(Date.parse(v.userEnteredValue))) {
|
|
96
|
+
return new Date(v.userEnteredValue);
|
|
97
|
+
}
|
|
98
|
+
return v.userEnteredValue;
|
|
99
|
+
}
|
|
88
100
|
// In Apps Script, RelativeDate enum values are typically returned as strings matching the enum
|
|
89
101
|
if (v.relativeDate !== undefined) return SpreadsheetApp.RelativeDate[v.relativeDate] || v.relativeDate;
|
|
90
102
|
return null;
|
|
@@ -114,6 +126,6 @@ export class FakeBooleanCondition {
|
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
toString() {
|
|
117
|
-
return '
|
|
129
|
+
return 'ConditionalFormatBooleanCondition';
|
|
118
130
|
}
|
|
119
131
|
}
|
|
@@ -59,15 +59,40 @@ export class FakeEmbeddedChartBuilder {
|
|
|
59
59
|
if (nargs !== 1) matchThrow();
|
|
60
60
|
|
|
61
61
|
const gridRange = makeSheetsGridRange(range);
|
|
62
|
+
const startCol = gridRange.startColumnIndex || 0;
|
|
63
|
+
const endCol = gridRange.endColumnIndex || startCol + 1;
|
|
64
|
+
const numCols = endCol - startCol;
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
// If it's a multi-column range, live GAS splits the first column as domain
|
|
67
|
+
// and the rest as series (unless it's already got a domain)
|
|
68
|
+
if (numCols > 1 && this.__apiChart.spec.basicChart.domains.length === 0) {
|
|
69
|
+
// First column is domain
|
|
70
|
+
const domainRange = JSON.parse(JSON.stringify(gridRange));
|
|
71
|
+
domainRange.endColumnIndex = startCol + 1;
|
|
64
72
|
this.__apiChart.spec.basicChart.domains.push({
|
|
65
|
-
domain: { sourceRange: { sources: [
|
|
73
|
+
domain: { sourceRange: { sources: [domainRange] } },
|
|
66
74
|
});
|
|
75
|
+
|
|
76
|
+
// Rest are series
|
|
77
|
+
for (let i = 1; i < numCols; i++) {
|
|
78
|
+
const seriesRange = JSON.parse(JSON.stringify(gridRange));
|
|
79
|
+
seriesRange.startColumnIndex = startCol + i;
|
|
80
|
+
seriesRange.endColumnIndex = startCol + i + 1;
|
|
81
|
+
this.__apiChart.spec.basicChart.series.push({
|
|
82
|
+
series: { sourceRange: { sources: [seriesRange] } },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
67
85
|
} else {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
86
|
+
// Standard behavior
|
|
87
|
+
if (this.__apiChart.spec.basicChart.domains.length === 0) {
|
|
88
|
+
this.__apiChart.spec.basicChart.domains.push({
|
|
89
|
+
domain: { sourceRange: { sources: [gridRange] } },
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
this.__apiChart.spec.basicChart.series.push({
|
|
93
|
+
series: { sourceRange: { sources: [gridRange] } },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
71
96
|
}
|
|
72
97
|
return this;
|
|
73
98
|
}
|
|
@@ -44,4 +44,29 @@ export class FakeOverGridImage {
|
|
|
44
44
|
getHeight() {
|
|
45
45
|
return this.object.height;
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
setHeight(height) {
|
|
49
|
+
this.object.height = height;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setWidth(width) {
|
|
54
|
+
this.object.width = width;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setAltTextTitle(title) {
|
|
59
|
+
this.object.altTextTitle = title;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setAltTextDescription(description) {
|
|
64
|
+
this.object.altTextDescription = description;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
deleteOverlaidDrawing() {
|
|
69
|
+
// No-op for now as it's a mock
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
47
72
|
}
|
|
@@ -38,7 +38,6 @@ export class FakeSheet {
|
|
|
38
38
|
|
|
39
39
|
const props = [
|
|
40
40
|
// "getImages",
|
|
41
|
-
"insertImage",
|
|
42
41
|
"removeImage",
|
|
43
42
|
// "getNamedRanges",
|
|
44
43
|
// "getRangeByName", // <--- This is a method for Class Spreadsheet.
|
|
@@ -1184,6 +1183,29 @@ export class FakeSheet {
|
|
|
1184
1183
|
return [];
|
|
1185
1184
|
}
|
|
1186
1185
|
|
|
1186
|
+
insertImage(blobSourceOrUrl, column, row, offsetX, offsetY) {
|
|
1187
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "Sheet.insertImage");
|
|
1188
|
+
if (nargs < 3 || nargs > 5) matchThrow();
|
|
1189
|
+
|
|
1190
|
+
// Warn about API limitations
|
|
1191
|
+
console.warn(
|
|
1192
|
+
"Sheet.insertImage: Floating images are not supported via the Google Sheets REST API v4. This is a local mock."
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
const obj = {
|
|
1196
|
+
row: row - 1,
|
|
1197
|
+
col: column - 1,
|
|
1198
|
+
anchorCellXOffset: offsetX || 0,
|
|
1199
|
+
anchorCellYOffset: offsetY || 0,
|
|
1200
|
+
width: 100, // Default width
|
|
1201
|
+
height: 100, // Default height
|
|
1202
|
+
blob: typeof blobSourceOrUrl === "string" ? null : blobSourceOrUrl,
|
|
1203
|
+
url: typeof blobSourceOrUrl === "string" ? blobSourceOrUrl : null,
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
return newFakeOverGridImage(this, obj);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1187
1209
|
getImages() {
|
|
1188
1210
|
const url = `https://docs.google.com/spreadsheets/export?exportFormat=xlsx&id=${this.__parent.__meta.spreadsheetId}`;
|
|
1189
1211
|
const res = UrlFetchApp.fetch(url, {
|