@mcpher/gas-fakes 2.3.18 → 2.5.2
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/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 +7 -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 -484
- 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 -27
- 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 -29
- package/gf_agent/knowledge/14-utilities-parity.md +0 -13
- package/gf_agent/knowledge/15-logging-efficiency.md +0 -15
- 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,129 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { workerData } from 'worker_threads';
|
|
4
|
+
|
|
5
|
+
const { mainScriptPath, funcName, args, isTemplate, templateString, env } = workerData;
|
|
6
|
+
const control = new Int32Array(workerData.controlBuf);
|
|
7
|
+
const dataView = new Uint8Array(workerData.dataBuf);
|
|
8
|
+
const textEncoder = new TextEncoder();
|
|
9
|
+
|
|
10
|
+
// Initialize the Apps Script environment
|
|
11
|
+
if (env) {
|
|
12
|
+
Object.assign(process.env, env);
|
|
13
|
+
}
|
|
14
|
+
globalThis.__gasFakesMainScriptPath = mainScriptPath;
|
|
15
|
+
await import('../../../main.js');
|
|
16
|
+
|
|
17
|
+
// Bootstrap Auth completely via standard initialization
|
|
18
|
+
import { Syncit } from '../../support/syncit.js';
|
|
19
|
+
|
|
20
|
+
// Trigger a fresh authentication flow to ensure Auth.getUserId() and others are populated.
|
|
21
|
+
// We specify the platforms from environment or default to google.
|
|
22
|
+
const platforms = process.env.GF_PLATFORM_AUTH ? process.env.GF_PLATFORM_AUTH.split(',') : ['google'];
|
|
23
|
+
Syncit.fxInit({ platformAuth: platforms });
|
|
24
|
+
|
|
25
|
+
const CONTROL_INDICES = {
|
|
26
|
+
STATUS: 0,
|
|
27
|
+
DATA_SIZE: 1,
|
|
28
|
+
IS_ERROR: 2
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function run() {
|
|
32
|
+
try {
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// Dynamically load the user's module
|
|
36
|
+
const userModule = await import(mainScriptPath);
|
|
37
|
+
|
|
38
|
+
// Expose all exports to globalThis for legacy patterns
|
|
39
|
+
Object.keys(userModule).forEach(key => {
|
|
40
|
+
globalThis[key] = userModule[key];
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let result;
|
|
44
|
+
|
|
45
|
+
if (isTemplate) {
|
|
46
|
+
// Evaluate template
|
|
47
|
+
// We pass the userModule context to allow access to functions like Include
|
|
48
|
+
result = templateString.replace(/<\?!=?\s*([\s\S]+?)\s*\?>/g, (match, expression) => {
|
|
49
|
+
try {
|
|
50
|
+
// We use the userModule exports as the scope for template expressions
|
|
51
|
+
const func = new Function(...Object.keys(userModule), `return ${expression}`);
|
|
52
|
+
const exprResult = func(...Object.values(userModule));
|
|
53
|
+
|
|
54
|
+
if (exprResult && typeof exprResult.getContent === 'function') {
|
|
55
|
+
return exprResult.getContent();
|
|
56
|
+
}
|
|
57
|
+
return typeof exprResult !== 'undefined' ? exprResult : '';
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(`gas-fakes template evaluation error for scriptlet '${expression}':`, e.message);
|
|
60
|
+
return match;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
// Run function
|
|
65
|
+
const func = userModule[funcName] || globalThis[funcName];
|
|
66
|
+
if (typeof func !== 'function') {
|
|
67
|
+
throw new Error(`google.script.run: function "${funcName}" is not defined.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Re-hydrate doPost event object
|
|
71
|
+
if (funcName === 'doPost' && args && args[0] && args[0].postData) {
|
|
72
|
+
args[0].postData.getDataAsString = function() { return this.contents; };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rawResult = await func(...(args || []));
|
|
76
|
+
|
|
77
|
+
// Serialize output if it's a FakeHtmlOutput or FakeTextOutput
|
|
78
|
+
if (rawResult && typeof rawResult.getContent === 'function') {
|
|
79
|
+
result = {
|
|
80
|
+
__isHtmlOutput: !!rawResult.__isHtmlOutput,
|
|
81
|
+
__isTextOutput: !!rawResult.__isTextOutput,
|
|
82
|
+
__framingType: rawResult.__framingType || null,
|
|
83
|
+
content: rawResult.getContent(),
|
|
84
|
+
title: typeof rawResult.getTitle === 'function' ? rawResult.getTitle() : '',
|
|
85
|
+
width: typeof rawResult.getWidth === 'function' ? rawResult.getWidth() : null,
|
|
86
|
+
height: typeof rawResult.getHeight === 'function' ? rawResult.getHeight() : null,
|
|
87
|
+
mimeType: typeof rawResult.getMimeType === 'function' ? rawResult.getMimeType() : null
|
|
88
|
+
};
|
|
89
|
+
} else {
|
|
90
|
+
result = typeof rawResult === 'undefined' ? undefined : JSON.parse(JSON.stringify(rawResult));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
writeResult(result);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('[gas-fakes worker error]', error);
|
|
97
|
+
writeError(error);
|
|
98
|
+
} finally {
|
|
99
|
+
Atomics.store(control, CONTROL_INDICES.STATUS, 0);
|
|
100
|
+
Atomics.notify(control, 0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function writeResult(result) {
|
|
105
|
+
const resultString = JSON.stringify(result === undefined ? null : result);
|
|
106
|
+
const encodedResult = textEncoder.encode(resultString);
|
|
107
|
+
|
|
108
|
+
if (encodedResult.length > dataView.buffer.byteLength) {
|
|
109
|
+
throw new Error('Result exceeds shared buffer size');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
dataView.set(encodedResult);
|
|
113
|
+
Atomics.store(control, CONTROL_INDICES.DATA_SIZE, encodedResult.length);
|
|
114
|
+
Atomics.store(control, CONTROL_INDICES.IS_ERROR, 0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function writeError(error) {
|
|
118
|
+
const message = error?.message || (typeof error === 'string' ? error : JSON.stringify(error) || 'Unknown error');
|
|
119
|
+
const stack = error?.stack || new Error().stack;
|
|
120
|
+
console.error('[gas-fakes worker error details]:', message, stack);
|
|
121
|
+
const errorString = JSON.stringify({ message, stack });
|
|
122
|
+
const encodedError = textEncoder.encode(errorString);
|
|
123
|
+
|
|
124
|
+
dataView.set(encodedError);
|
|
125
|
+
Atomics.store(control, CONTROL_INDICES.DATA_SIZE, encodedError.length);
|
|
126
|
+
Atomics.store(control, CONTROL_INDICES.IS_ERROR, 1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
run();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
|
|
2
|
+
import { ServerWorkerContext } from './serverworker.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Emulates the client-side google.script.run API.
|
|
6
|
+
* In gas-fakes, this runs in the same Node process.
|
|
7
|
+
*/
|
|
8
|
+
export class FakeGoogleScriptRun {
|
|
9
|
+
constructor(handlers = {}) {
|
|
10
|
+
this._successHandler = handlers.successHandler;
|
|
11
|
+
this._failureHandler = handlers.failureHandler;
|
|
12
|
+
this._userObject = handlers.userObject;
|
|
13
|
+
this._serverFunctions = handlers.serverFunctions || null;
|
|
14
|
+
|
|
15
|
+
return new Proxy(this, {
|
|
16
|
+
get: (target, prop) => {
|
|
17
|
+
// Return existing properties (like withSuccessHandler)
|
|
18
|
+
if (prop in target) {
|
|
19
|
+
return target[prop];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Methods for chaining handlers
|
|
23
|
+
if (prop === 'withSuccessHandler') {
|
|
24
|
+
return (handler) => new FakeGoogleScriptRun({ ...target._getHandlers(), successHandler: handler });
|
|
25
|
+
}
|
|
26
|
+
if (prop === 'withFailureHandler') {
|
|
27
|
+
return (handler) => new FakeGoogleScriptRun({ ...target._getHandlers(), failureHandler: handler });
|
|
28
|
+
}
|
|
29
|
+
if (prop === 'withUserObject') {
|
|
30
|
+
return (obj) => new FakeGoogleScriptRun({ ...target._getHandlers(), userObject: obj });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Special method to register functions manually if they aren't global
|
|
34
|
+
// This is extremely useful for modular tests or explicit wiring
|
|
35
|
+
if (prop === '__registerServerFunctions') {
|
|
36
|
+
return (funcs) => {
|
|
37
|
+
this._serverFunctions = { ...this._serverFunctions, ...funcs };
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Otherwise, it's a server-side function call
|
|
43
|
+
return (...args) => {
|
|
44
|
+
// Asynchronous execution simulation
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
try {
|
|
47
|
+
if (prop.endsWith('_') || prop.startsWith('__')) {
|
|
48
|
+
throw new Error(`google.script.run: function "${prop}" is private and cannot be called from the client.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// In Apps Script, parameters are passed by value (JSON serialized)
|
|
52
|
+
const serializedArgs = JSON.parse(JSON.stringify(args));
|
|
53
|
+
|
|
54
|
+
let result;
|
|
55
|
+
|
|
56
|
+
// Fallback for modular tests that explicitly registered functions
|
|
57
|
+
if (this._serverFunctions && typeof this._serverFunctions[prop] === 'function') {
|
|
58
|
+
result = this._serverFunctions[prop](...serializedArgs);
|
|
59
|
+
} else {
|
|
60
|
+
// Main stateless execution path via Synchronous Worker Threads
|
|
61
|
+
const ctx = new ServerWorkerContext();
|
|
62
|
+
result = ctx.runFunction(prop, serializedArgs);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Result is also serialized back
|
|
66
|
+
const serializedResult = typeof result === 'undefined' ? undefined : JSON.parse(JSON.stringify(result));
|
|
67
|
+
|
|
68
|
+
if (this._successHandler) {
|
|
69
|
+
this._successHandler(serializedResult, this._userObject);
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (this._failureHandler) {
|
|
73
|
+
this._failureHandler(err, this._userObject);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, 0);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_getHandlers() {
|
|
83
|
+
return {
|
|
84
|
+
successHandler: this._successHandler,
|
|
85
|
+
failureHandler: this._failureHandler,
|
|
86
|
+
userObject: this._userObject,
|
|
87
|
+
serverFunctions: this._serverFunctions
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { FakeHtmlTemplate } from './htmltemplate.js';
|
|
2
|
+
import { FakeHtmlOutputMetaTag } from './htmloutputmetatag.js';
|
|
3
|
+
|
|
4
|
+
export class FakeHtmlOutput {
|
|
5
|
+
constructor(content = '') {
|
|
6
|
+
this._content = content;
|
|
7
|
+
this._title = '';
|
|
8
|
+
this._width = 600;
|
|
9
|
+
this._height = 450;
|
|
10
|
+
this._metaData = {};
|
|
11
|
+
this._faviconUrl = '';
|
|
12
|
+
this._xFrameOptionsMode = null;
|
|
13
|
+
this._sandboxMode = null;
|
|
14
|
+
this.__isHtmlOutput = true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getContent() {
|
|
18
|
+
return this._content
|
|
19
|
+
.replace(/<script([^>]*)>([\s\S]*?)<\/script>/gi, (match, attrs, body) => {
|
|
20
|
+
if (attrs.includes('src=')) return match;
|
|
21
|
+
if (body.includes('//# sourceURL=')) return match;
|
|
22
|
+
const sourceUrl = `\n//# sourceURL=__gas_fakes_dynamic_script_${Date.now()}.js`;
|
|
23
|
+
return `<script${attrs}>${body}${sourceUrl}</script>`;
|
|
24
|
+
})
|
|
25
|
+
.replace(/<style([^>]*)>([\s\S]*?)<\/style>/gi, (match, attrs, body) => {
|
|
26
|
+
if (body.includes('/*# sourceURL=')) return match;
|
|
27
|
+
const sourceUrl = `\n/*# sourceURL=__gas_fakes_dynamic_style_${Date.now()}.css */`;
|
|
28
|
+
return `<style${attrs}>${body}${sourceUrl}</style>`;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setContent(content) {
|
|
33
|
+
this._content = content;
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
append(content) {
|
|
38
|
+
this._content += content;
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
appendUntrusted(content) {
|
|
43
|
+
this._content += String(content)
|
|
44
|
+
.replace(/&/g, '&')
|
|
45
|
+
.replace(/</g, '<')
|
|
46
|
+
.replace(/>/g, '>')
|
|
47
|
+
.replace(/"/g, '"')
|
|
48
|
+
.replace(/'/g, ''');
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
clear() {
|
|
53
|
+
this._content = '';
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setTitle(title) {
|
|
58
|
+
this._title = title;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getTitle() {
|
|
63
|
+
return this._title;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setWidth(width) {
|
|
67
|
+
this._width = width;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getWidth() {
|
|
72
|
+
return this._width;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setHeight(height) {
|
|
76
|
+
this._height = height;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getHeight() {
|
|
81
|
+
return this._height;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setFaviconUrl(iconUrl) {
|
|
85
|
+
this._faviconUrl = iconUrl;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getFaviconUrl() {
|
|
90
|
+
return this._faviconUrl;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
addMetaTag(name, content) {
|
|
94
|
+
this._metaData[name] = content;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getMetaTags() {
|
|
99
|
+
return Object.keys(this._metaData).map(name => new FakeHtmlOutputMetaTag(name, this._metaData[name]));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setXFrameOptionsMode(mode) {
|
|
103
|
+
this._xFrameOptionsMode = mode;
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setSandboxMode(mode) {
|
|
108
|
+
this._sandboxMode = mode;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
asTemplate() {
|
|
113
|
+
return new FakeHtmlTemplate(this._content);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getBlob() {
|
|
117
|
+
return globalThis.Utilities.newBlob(this._content, 'text/html', (this._title || 'output') + '.html');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getAs(contentType) {
|
|
121
|
+
// In live GAS, getAs performs server-side conversion (e.g. to PDF).
|
|
122
|
+
// Locally, we just return a blob with the new mimeType for parity checking.
|
|
123
|
+
const b = this.getBlob();
|
|
124
|
+
b.setContentType(contentType);
|
|
125
|
+
return b;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
|
|
2
|
+
import { FakeHtmlOutput } from './htmloutput.js';
|
|
3
|
+
import { FakeHtmlTemplate } from './htmltemplate.js';
|
|
4
|
+
import { FakeGoogleScriptRun } from './googlescriptrun.js';
|
|
5
|
+
import { HtmlEnums } from '../enums/htmlenums.js';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
import { startServer } from './webapp.js';
|
|
10
|
+
|
|
11
|
+
// Initialize google.script globals
|
|
12
|
+
if (typeof globalThis.google === 'undefined') {
|
|
13
|
+
globalThis.google = {
|
|
14
|
+
script: {
|
|
15
|
+
run: new FakeGoogleScriptRun(),
|
|
16
|
+
host: {
|
|
17
|
+
close: () => {},
|
|
18
|
+
setHeight: () => {},
|
|
19
|
+
setWidth: () => {},
|
|
20
|
+
origin: ''
|
|
21
|
+
},
|
|
22
|
+
history: {
|
|
23
|
+
push: () => {},
|
|
24
|
+
replace: () => {},
|
|
25
|
+
setChangeHandler: () => {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class FakeHtmlService {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.SandboxMode = HtmlEnums.SandboxMode;
|
|
34
|
+
this.XFrameOptionsMode = HtmlEnums.XFrameOptionsMode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_readLocalFile(filename) {
|
|
38
|
+
// In live Apps Script, `HtmlService.createHtmlOutputFromFile('Index')`
|
|
39
|
+
// implicitly looks for an `Index.html` file in the project.
|
|
40
|
+
let targetFile = filename;
|
|
41
|
+
if (!targetFile.endsWith('.html')) {
|
|
42
|
+
targetFile += '.html';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Resolve relative to the consumer's main script
|
|
46
|
+
let mainScriptPath = process.argv[1];
|
|
47
|
+
|
|
48
|
+
// In gas-fakes serve mode, we use worker threads. The worker passes the main script path via workerData.
|
|
49
|
+
if (globalThis.__gasFakesMainScriptPath) {
|
|
50
|
+
mainScriptPath = globalThis.__gasFakesMainScriptPath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!mainScriptPath || mainScriptPath.endsWith('node')) {
|
|
54
|
+
throw new Error("Could not determine project root. Ensure process.argv[1] is set or __gasFakesMainScriptPath is defined.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const projectDir = path.dirname(mainScriptPath);
|
|
58
|
+
const fullPath = path.resolve(projectDir, targetFile);
|
|
59
|
+
|
|
60
|
+
if (!fs.existsSync(fullPath)) {
|
|
61
|
+
throw new Error(`No HTML file named ${filename} was found.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return fs.readFileSync(fullPath, 'utf8');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
createHtmlOutput(html = '') {
|
|
68
|
+
return new FakeHtmlOutput(html);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
createHtmlOutputFromFile(filename) {
|
|
72
|
+
const content = this._readLocalFile(filename);
|
|
73
|
+
return new FakeHtmlOutput(content);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
createTemplate(html = '') {
|
|
77
|
+
return new FakeHtmlTemplate(html);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
createTemplateFromFile(filename) {
|
|
81
|
+
const content = this._readLocalFile(filename);
|
|
82
|
+
return new FakeHtmlTemplate(content);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getUserAgent() {
|
|
86
|
+
return 'Node.js gas-fakes emulator';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
__startWebApp(port = 3000) {
|
|
90
|
+
return startServer(port);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const newFakeHtmlService = () => new FakeHtmlService();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
|
|
2
|
+
import { FakeHtmlOutput } from './htmloutput.js';
|
|
3
|
+
import { ServerWorkerContext } from './serverworker.js';
|
|
4
|
+
|
|
5
|
+
export class FakeHtmlTemplate {
|
|
6
|
+
constructor(content = '') {
|
|
7
|
+
this._content = content;
|
|
8
|
+
|
|
9
|
+
return new Proxy(this, {
|
|
10
|
+
get: (target, prop, receiver) => {
|
|
11
|
+
if (prop in target) {
|
|
12
|
+
return Reflect.get(target, prop, receiver);
|
|
13
|
+
}
|
|
14
|
+
return target[prop];
|
|
15
|
+
},
|
|
16
|
+
set: (target, prop, value, receiver) => {
|
|
17
|
+
return Reflect.set(target, prop, value, receiver);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
evaluate() {
|
|
23
|
+
let evaluatedContent = this._content;
|
|
24
|
+
|
|
25
|
+
let workerResult = null;
|
|
26
|
+
try {
|
|
27
|
+
const ctx = new ServerWorkerContext();
|
|
28
|
+
workerResult = ctx.evaluateTemplate(this._content);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error(e);
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (workerResult) {
|
|
35
|
+
evaluatedContent = workerResult;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Final pass for explicitly set template properties
|
|
39
|
+
evaluatedContent = evaluatedContent.replace(/<\?=\s*([^?]+)\s*\?>/g, (match, varName) => {
|
|
40
|
+
const trimmedVarName = varName.trim();
|
|
41
|
+
if (typeof this[trimmedVarName] !== 'undefined') {
|
|
42
|
+
return this[trimmedVarName];
|
|
43
|
+
}
|
|
44
|
+
return match;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return new FakeHtmlOutput(evaluatedContent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getRawContent() {
|
|
51
|
+
return this._content;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getCode() {
|
|
55
|
+
return `// Compiled template\n(function() { let output = ""; ${this._content.split('\n').map(line => `output += ${JSON.stringify(line)} + "\\n";`).join('\n')} return output; })()`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getCodeWithComments() {
|
|
59
|
+
return `// Compiled template with comments\n(function() { let output = ""; ${this._content.split('\n').map(line => `// ${line}\noutput += ${JSON.stringify(line)} + "\\n";`).join('\n')} return output; })()`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Auth } from '../../support/auth.js';
|
|
2
|
+
import { Worker } from 'worker_threads';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const workerPath = path.join(__dirname, 'consumerworker.js');
|
|
9
|
+
|
|
10
|
+
const CONTROL_INDICES = {
|
|
11
|
+
STATUS: 0,
|
|
12
|
+
DATA_SIZE: 1,
|
|
13
|
+
IS_ERROR: 2
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class ServerWorkerContext {
|
|
17
|
+
constructor(scriptPath = null) {
|
|
18
|
+
this._mainScriptPath = scriptPath || globalThis.__gasFakesMainScriptPath || process.argv[1];
|
|
19
|
+
|
|
20
|
+
// Fallback for test runners or dynamic imports where process.argv[1] is node itself or undefined
|
|
21
|
+
if (!this._mainScriptPath || this._mainScriptPath.endsWith('node') || this._mainScriptPath.endsWith('gas-fakes.js') || this._mainScriptPath.endsWith('gas-fakes')) {
|
|
22
|
+
const stack = new Error().stack;
|
|
23
|
+
const match = stack.match(/at file:\/\/(.*\.js)/);
|
|
24
|
+
if (match && match[1]) {
|
|
25
|
+
// We want to find the entry point, not this file itself
|
|
26
|
+
const lines = stack.split('\n');
|
|
27
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
28
|
+
const m = lines[i].match(/at (?:async )?file:\/\/(.*\.js)/);
|
|
29
|
+
if (m && m[1] && !m[1].includes('serverworker.js')) {
|
|
30
|
+
this._mainScriptPath = m[1];
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// Create shared buffers for synchronous communication
|
|
39
|
+
// 3 control Int32s
|
|
40
|
+
this._controlBuf = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 3);
|
|
41
|
+
this._control = new Int32Array(this._controlBuf);
|
|
42
|
+
|
|
43
|
+
// 1MB data buffer (should be enough for template substitutions and function returns)
|
|
44
|
+
this._dataBuf = new SharedArrayBuffer(1024 * 1024);
|
|
45
|
+
this._dataView = new Uint8Array(this._dataBuf);
|
|
46
|
+
this._textDecoder = new TextDecoder();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_executeSyncWorker(workerDataPayload) {
|
|
50
|
+
if (!this._mainScriptPath) {
|
|
51
|
+
throw new Error("Could not determine main script path. Ensure process.argv[1] is set.");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Set lock to busy (1)
|
|
55
|
+
Atomics.store(this._control, CONTROL_INDICES.STATUS, 1);
|
|
56
|
+
|
|
57
|
+
const worker = new Worker(workerPath, {
|
|
58
|
+
workerData: {
|
|
59
|
+
...workerDataPayload,
|
|
60
|
+
mainScriptPath: globalThis.__gasFakesMainScriptPath || this._mainScriptPath,
|
|
61
|
+
controlBuf: this._controlBuf,
|
|
62
|
+
dataBuf: this._dataBuf,
|
|
63
|
+
env: process.env // Pass current environment
|
|
64
|
+
},
|
|
65
|
+
stdout: true,
|
|
66
|
+
stderr: true
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
worker.stdout.pipe(process.stdout);
|
|
70
|
+
worker.stderr.pipe(process.stderr);
|
|
71
|
+
|
|
72
|
+
// Handle unexpected worker crashes
|
|
73
|
+
worker.on('error', (err) => {
|
|
74
|
+
console.error("Consumer worker crashed:", err);
|
|
75
|
+
Atomics.store(this._control, CONTROL_INDICES.STATUS, 0);
|
|
76
|
+
Atomics.notify(this._control, 0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
worker.on('exit', (code) => {
|
|
80
|
+
if (code !== 0) {
|
|
81
|
+
Atomics.store(this._control, CONTROL_INDICES.STATUS, 0);
|
|
82
|
+
Atomics.notify(this._control, 0);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Block main thread until worker finishes (sets status to 0)
|
|
87
|
+
Atomics.wait(this._control, CONTROL_INDICES.STATUS, 1);
|
|
88
|
+
|
|
89
|
+
// Read result
|
|
90
|
+
const hasError = Atomics.load(this._control, CONTROL_INDICES.IS_ERROR) === 1;
|
|
91
|
+
const resultSize = Atomics.load(this._control, CONTROL_INDICES.DATA_SIZE);
|
|
92
|
+
|
|
93
|
+
const resultBytes = this._dataView.slice(0, resultSize);
|
|
94
|
+
const resultString = this._textDecoder.decode(resultBytes);
|
|
95
|
+
|
|
96
|
+
const resultData = JSON.parse(resultString);
|
|
97
|
+
|
|
98
|
+
if (hasError) {
|
|
99
|
+
// If the error was thrown as a string, its stack will be artificial (generated in writeError)
|
|
100
|
+
// and won't contain the actual message. Let's make sure the message is always visible.
|
|
101
|
+
const errorMsg = resultData.message || 'Unknown error in worker';
|
|
102
|
+
const errorStack = resultData.stack || '';
|
|
103
|
+
|
|
104
|
+
const err = new Error(errorMsg);
|
|
105
|
+
// Only replace the stack if it contains useful information, otherwise use the standard one
|
|
106
|
+
if (errorStack && !errorStack.startsWith('Error\n at writeError')) {
|
|
107
|
+
err.stack = errorStack;
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return resultData;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Synchronously evaluates a template string in a fresh consumer instance.
|
|
117
|
+
*/
|
|
118
|
+
evaluateTemplate(templateString) {
|
|
119
|
+
return this._executeSyncWorker({
|
|
120
|
+
isTemplate: true,
|
|
121
|
+
templateString
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Synchronously executes a function in a fresh consumer instance.
|
|
127
|
+
*/
|
|
128
|
+
runFunction(funcName, args) {
|
|
129
|
+
return this._executeSyncWorker({
|
|
130
|
+
isTemplate: false,
|
|
131
|
+
funcName,
|
|
132
|
+
args
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|