├── src
└── #README.js
├── .gitignore
├── package.json
├── README.md
├── google.script.run.js
└── Gruntfile.js
/src/#README.js:
--------------------------------------------------------------------------------
1 | /**
2 | * MAIN Code is in GIT:
3 | * url://repo
4 | */
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /node_modules/
3 |
4 | /build/src/
5 | /build/config/dev_config.*
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "GAS-shell",
3 | "version": "0.1.0",
4 | "description": "Google Apps Script shell project",
5 | "license": "Apache-2.0",
6 | "repository": {
7 | "type": "git",
8 | "url": ""
9 | },
10 | "dependencies": {
11 | "@scriptaddicts/gas-error-handler": "latest"
12 | },
13 | "devDependencies": {
14 | "gas-lib": "latest",
15 | "grunt": "^1.0.1",
16 | "grunt-contrib-clean": "^1.0.0",
17 | "grunt-contrib-copy": "^1.0.0",
18 | "grunt-preprocess": "^5.1.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | ## How do I get set up?
4 |
5 | #### Summary of set up
6 |
7 | * We assume that a code Editor (Webstorm, Visual Studio Code -> free), Git, and npm are installed on you computer.
8 | * Clone the Git repository
9 | * Run npm install in project root to install **_node_modules_** necessary to run grunt and build the project
10 | * Create a _**/build/config/dev_config.json**_ file
11 | It will contain the built target, and must be of the following form: (see below < config file format >)
12 | ```json
13 | {
14 | "clasp": {"scriptId": "AppsScript file drive ID"},
15 | "script_manifest": {
16 | "timeZone": "Europe/Paris",
17 | "exceptionLogging": "STACKDRIVER"
18 | }
19 | }
20 |
21 | ```
22 | * We use the **@google/clasp** package to get/update apps script file in Google Drive.
23 | Find the setup for clasp on this page: https://www.npmjs.com/package/@google/clasp
24 |
25 |
26 | #### Build command
27 |
28 | 1. Renaming and copy files: Execute ``grunt build --target=$CONFiG_NAME$`` inside the main working directory ($CONFiG_NAME$ = dev, prod, whatever name you choose: it will use the config file named "/build/config/$CONFiG_NAME$_config.json")
29 | 2. Update the script: Execute ``grunt push --target=dev``
30 | 3. Both can be executed with: ``grunt build_push --target=dev``
31 |
32 |
33 | #### Changing target (DEV, PROD for example)
34 |
35 | You can define other target by using the "--target=targetName" grunt option.
36 | For each target, a valid config file named: "/build/config/targetName_config.json" must exist.
37 |
38 |
39 | #### Config file format:
40 |
41 | /build/config/$CONFiG_NAME$_config.json
42 | ```json
43 | {
44 | "clasp": {
45 | "scriptId": "$TARGET_SCRIPT_ID$"
46 | },
47 | "script_manifest": {
48 | "timeZone": "Europe/Paris",
49 | "exceptionLogging": "STACKDRIVER"
50 | }
51 | }
52 | ```
53 | NOTE: fill the "script_manifest" key with this format: https://developers.google.com/apps-script/concepts/manifests#manifest_structure
54 |
55 | NOTE: adjust the timezone to match yours, best is to copy the original from the script
56 |
57 | ## Best practices ###
58 |
59 | Git branches:
60 |
61 | * master: merge Pull requests on master
62 |
63 | * Set **tags** with increasing version on master's commit that are pushed in production (v1, v2, ...)
64 |
65 | **Always merge with no fast forward !** (For major feature and update. Quick fix can be merged by default):
66 | ```
67 | git merge --no-ff branchToMergeHere
68 | ```
69 |
70 | ## I want to publish as an Addon with my build process ##
71 |
72 | It's possible, please check the repository branch:
73 | [buildAddOn](https://github.com/JeanRemiDelteil/gas-starter-kit/tree/buildAddOn)
74 |
75 | ⚠
76 | However be warned that using the command to publish as an addon will FORBID further manual publishing with the GAS interface.
77 | ⚠
78 |
79 | ## Who do I talk to? ###
80 |
81 | * jeanremi.delteil@gmail.com
--------------------------------------------------------------------------------
/google.script.run.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file exist to provide auto-completion and linking between Server side code and Client side code in Apps Script
3 | *
4 | * Add all functions callable from client side to the google.script.run prototype
5 | */
6 |
7 |
8 | //
9 |
10 | GoogleScriptRun = function () {};
11 | // NEVER DO THAT in normal code, this overwrite the prototype, here we do this of auto-completion only
12 | GoogleScriptRun.prototype = {
13 | // PLACE HERE the entryPoint functions (just for listing purpose)
14 | // onInstall: onInstall,
15 | onOpen: onOpen,
16 |
17 | // PLACE HERE functions called from Sidebar/Modal to keep the link and have auto-complete
18 |
19 | };
20 |
21 |
22 | //
23 |
24 | /**
25 | * @param {function(serverResponse: *, userObject: *)} successHandler
26 | * @return {GoogleScriptRun}
27 | */
28 | GoogleScriptRun.prototype.withSuccessHandler = function (successHandler) {
29 | successHandler('serverResponse', this._userObject);
30 | return this;
31 | };
32 | /**
33 | * @param {function(error: Error, userObject: *)} failureHandler
34 | * @return {GoogleScriptRun}
35 | */
36 | GoogleScriptRun.prototype.withFailureHandler = function (failureHandler) {
37 | failureHandler(new Error('errorMessage'), this._userObject);
38 | return this;
39 | };
40 | /**
41 | * @param {*} userObject
42 | * @return {GoogleScriptRun}
43 | */
44 | GoogleScriptRun.prototype.withUserObject = function (userObject) {
45 | this._userObject = userObject;
46 | return this;
47 | };
48 |
49 | // noinspection ES6ConvertVarToLetConst
50 | var google = {
51 | script: {
52 | run: new GoogleScriptRun()
53 | }
54 | };
55 |
56 | //
57 |
58 | //
59 |
60 |
61 | //
62 |
63 |
64 | /**
65 | * @function google.picker.PickerBuilder
66 | * @constructor
67 | *
68 | * NOTE: API is not completely described
69 | * see here for full list (https://developers.google.com/picker/docs/reference#PickerBuilder)
70 | *
71 | * @return PickerBuilder
72 | */
73 | /**
74 | * @function PickerBuilder.prototype.setDeveloperKey
75 | *
76 | * @param {string} developerKey
77 | * @return PickerBuilder
78 | */
79 | /**
80 | * @function PickerBuilder.prototype.setOAuthToken
81 | *
82 | * @param {string} token
83 | * @return PickerBuilder
84 | */
85 | /**
86 | * @function PickerBuilder.prototype.setOrigin
87 | *
88 | * @param {string} origin
89 | * @return PickerBuilder
90 | */
91 | /**
92 | * @function PickerBuilder.prototype.setCallback
93 | *
94 | * @param {function} callBack
95 | * @return PickerBuilder
96 | */
97 | /**
98 | * @function PickerBuilder.prototype.setTitle
99 | *
100 | * @param {string} title
101 | * @return PickerBuilder
102 | */
103 | /**
104 | * @function PickerBuilder.prototype.setSize
105 | *
106 | * @param {number} width
107 | * @param {number} height
108 | * @return PickerBuilder
109 | */
110 | /**
111 | * @function PickerBuilder.prototype.addView
112 | *
113 | * @param {google.picker.View || google.picker.ViewId} view
114 | * @return PickerBuilder
115 | */
116 | /**
117 | * @function PickerBuilder.prototype.enableFeature
118 | *
119 | * @param {google.picker.Feature} feature
120 | * @return PickerBuilder
121 | */
122 | /**
123 | * @function PickerBuilder.prototype.hideTitleBar
124 | *
125 | * @return {PickerBuilder}
126 | */
127 | /**
128 | * @function PickerBuilder.prototype.build
129 | *
130 | * @return {Picker}
131 | */
132 |
133 | /**
134 | * @function google.picker.View
135 | * @constructor
136 | *
137 | * @param {google.picker.ViewId} viewId
138 | *
139 | * @return google.picker.View
140 | */
141 | /**
142 | * @function google.picker.View.prototype.getId
143 | * Returns the ViewId for this view.
144 | *
145 | * @return {string}
146 | */
147 | /**
148 | * @function google.picker.View.prototype.setMimeTypes
149 | *
150 | * @param {string} mimeTypes
151 | */
152 | /**
153 | * @function google.picker.View.prototype.setQuery
154 | *
155 | * @param {string} query
156 | */
157 |
158 |
159 | /**
160 | * @typedef {{}} Picker
161 | */
162 | /**
163 | * @function Picker.prototype.setVisible
164 | *
165 | * @param {boolean} isVisible
166 | */
167 | /**
168 | * @function Picker.prototype.dispose
169 | */
170 |
171 | /**
172 | * @function google.picker.DocsView
173 | * @constructor
174 | *
175 | * @param {google.picker.ViewId} [viewId]
176 | *
177 | * @return DocsView
178 | */
179 | /**
180 | * @function DocsView.prototype.setIncludeFolders
181 | *
182 | * @param {boolean} includeFolders
183 | * @return DocsView
184 | */
185 | /**
186 | * @function DocsView.prototype.setEnableTeamDrives
187 | *
188 | * @param {boolean} enableTeamDrives
189 | * @return DocsView
190 | */
191 | /**
192 | * @function DocsView.prototype.setSelectFolderEnabled
193 | *
194 | * @param {boolean} enableSelectFolder
195 | * @return DocsView
196 | */
197 | /**
198 | * @function DocsView.prototype.setMode
199 | *
200 | * @param {google.picker.DocsViewMode} mode
201 | * @return DocsView
202 | */
203 | /**
204 | * @function DocsView.prototype.setOwnedByMe
205 | *
206 | * @param {boolean} ownedByMe
207 | * @return DocsView
208 | */
209 | /**
210 | * @function DocsView.prototype.setParent
211 | *
212 | * @param {string} parent
213 | * @return DocsView
214 | */
215 | /**
216 | * @function DocsView.prototype.setStarred
217 | *
218 | * @param {boolean} starred
219 | * @return DocsView
220 | */
221 |
222 |
223 | /**
224 | * google.picker
225 | *
226 | * @typedef {{
227 | * Action: {
228 | * PICKED,
229 | * CANCEL,
230 | * LOADED,
231 | * },
232 | * DocsViewMode: {
233 | * GRID,
234 | * LIST,
235 | * },
236 | * ViewId: {
237 | * DOCS,
238 | * DOCS_IMAGES,
239 | * DOCS_IMAGES_AND_VIDEOS,
240 | * DOCS_VIDEOS,
241 | * DOCUMENTS,
242 | * DRAWINGS,
243 | * FOLDERS,
244 | * FORMS,
245 | * IMAGE_SEARCH,
246 | * PDFS,
247 | * PHOTO_ALBUMS,
248 | * PHOTO_UPLOAD,
249 | * PHOTOS,
250 | * PRESENTATIONS,
251 | * RECENTLY_PICKED,
252 | * SPREADSHEETS,
253 | * VIDEO_SEARCH,
254 | * WEBCAM,
255 | * YOUTUBE,
256 | * },
257 | * Feature: {
258 | * NAV_HIDDEN,
259 | * MINE_ONLY,
260 | * MULTISELECT_ENABLED,
261 | * SIMPLE_UPLOAD_ENABLED,
262 | * SUPPORT_TEAM_DRIVES,
263 | * },
264 | * Document: {
265 | * ADDRESS_LINES,
266 | * AUDIENCE,
267 | * DESCRIPTION,
268 | * DURATION,
269 | * EMBEDDABLE_URL,
270 | * ICON_URL,
271 | * ID,
272 | * IS_NEW,
273 | * LAST_EDITED_UTC,
274 | * LATITUDE,
275 | * LONGITUDE,
276 | * MIME_TYPE,
277 | * NAME,
278 | * NUM_CHILDREN,
279 | * PARENT_ID,
280 | * PHONE_NUMBERS,
281 | * SERVICE_ID,
282 | * THUMBNAILS,
283 | * TYPE,
284 | * URL,
285 | * },
286 | * Response: {
287 | * ACTION,
288 | * DOCUMENTS,
289 | * PARENTS,
290 | * VIEW,
291 | * }
292 | * }} google.picker
293 | */
294 |
295 |
296 | //
297 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 | /**
3 | * @typedef {function} grunt.option
4 | * @typedef {function} grunt.initConfig
5 | */
6 |
7 | /** possible build :
8 | *
9 | * > build files to prepare pushing on App Script
10 | * grunt build --target=dev
11 | * grunt build --target=prod
12 | *
13 | * grunt build_push --target=prod
14 | *
15 | *//**/
16 |
17 | // define target and config to run the task with
18 | const TARGET = grunt.option('target') || 'dev';
19 |
20 | /**
21 | * Load config file directly
22 | *
23 | * @type {{
24 | * clasp: {
25 | * scriptId: string
26 | * },
27 | * script_manifest: {},
28 | * context: {}
29 | * }}
30 | */
31 | let CONFIG;
32 |
33 | //
34 | {
35 | try {
36 | CONFIG = require(`./build/config/${TARGET}_config`);
37 | }
38 | catch (e) {
39 | grunt.fail.fatal(`\x1b[31mERROR: "\x1b[0m\x1b[41m\x1b[30m${TARGET}\x1b[31m\x1b[0m\x1b[31m" is not a valid target build\x1b[0m`);
40 |
41 | // not needed, grunt.fail.fatal already exits
42 | return false;
43 | }
44 |
45 |
46 | // Stringify non-string key in .context
47 | let context = JSON.parse(JSON.stringify(CONFIG.context));
48 |
49 | for (let key in context) {
50 | if (typeof context[key] === 'string') continue;
51 |
52 | context[key] = JSON.stringify(context[key]);
53 | }
54 |
55 | CONFIG.context = context;
56 |
57 | }
58 | //
59 |
60 | // init config object
61 | grunt.initConfig({
62 |
63 | // task
64 | clean: {
65 | 'build': {
66 | files: [
67 | {
68 | expand: true,
69 | cwd: 'build/src/', // build source folder
70 | src: [
71 | '**/*', // Wipe everything
72 | ],
73 | },
74 | ],
75 | },
76 | },
77 | copy: {
78 | 'build': {
79 | files: [
80 | {
81 | expand: true,
82 | cwd: 'src/',
83 | src: [
84 | '**/*.gs',
85 | '**/*.js',
86 | '**/*.ts',
87 | '**/*.html',
88 | 'appsscript.json',
89 | ],
90 | dest: 'build/src/',
91 | flatten: false,
92 | filter: 'isFile',
93 | 'rename': (dest, src) => dest + src.replace(/\.gs\.js$/, '.js'),
94 | },
95 | ],
96 | },
97 | 'dependencies': {
98 | get files() {
99 | if (this._cachedFiles) return this._cachedFiles;
100 |
101 | // custom task to copy the package dependencies to build output
102 | let files = [];
103 | let {dependencies} = require('./package');
104 |
105 | // Allows access to saved packages information
106 | this.options.process = this.options.process.bind(this.options);
107 |
108 | // for every dependency package, build src and dest rules
109 | for (let pkgName in dependencies) {
110 | // retrieve installed package version
111 | let {version} = require(`./node_modules/${pkgName}/package`);
112 |
113 | // save package info
114 | this.options._pkg[pkgName] = {
115 | name: pkgName,
116 | version: version,
117 | };
118 |
119 | files.push({
120 | expand: true,
121 | cwd: `node_modules/${pkgName}/src/`,
122 | src: [
123 | '**/*.gs',
124 | '**/*.js',
125 | '**/*.ts',
126 | ],
127 | dest: `build/src/lib/${pkgName}/`,
128 | flatten: false, // set flatten to false once we use clasp folder name
129 | filter: 'isFile',
130 | 'rename': (dest, src) => dest + src.replace(/\.gs\.js$/, '.js'),
131 | });
132 | }
133 |
134 | // Save files to only process them once (they should not change in between calls)
135 | this._cachedFiles = files;
136 |
137 | return files;
138 | },
139 |
140 | options: {
141 | _pkg: {},
142 |
143 | /**
144 | * Add header with library information
145 | *
146 | * @param {string} content
147 | * @param {string} srcPath
148 | */
149 | process: function (content, srcPath) {
150 | // find pkg
151 | let [/*res*/, pkg] = /^node_modules\/(.+?)\/src\//.exec(srcPath) || [];
152 | if (!pkg) return content;
153 |
154 | let {name, version} = this._pkg[pkg];
155 |
156 | let header = `/**
157 | * package: ${name}
158 | * version: ${version}
159 | */`;
160 |
161 | return `${header}\n\n${content}`;
162 | },
163 | },
164 |
165 | },
166 | },
167 | jsonPatch: {
168 | 'build': {
169 | srcFolder: 'build/src/',
170 | destFolder: 'build/src/',
171 |
172 | files: [
173 | {
174 | src: '.clasp.json',
175 | dest: '.clasp.json',
176 | data: CONFIG.clasp,
177 | },
178 | {
179 | src: 'appsscript.json',
180 | dest: 'appsscript.json',
181 | data: CONFIG.script_manifest || {},
182 | },
183 | ],
184 | },
185 | },
186 | preprocess: {
187 | 'js': {
188 | options: {
189 | context: CONFIG.context,
190 | type: 'js',
191 | },
192 | files: [
193 | {
194 | expand: true,
195 | cwd: 'build/src/',
196 | src: [
197 | '**/*.js',
198 | '**/*.js.html',
199 | ],
200 | dest: 'build/src/',
201 | },
202 | ],
203 | },
204 | },
205 | clasp: {
206 | 'push': {
207 | runDir: 'build/src',
208 | command: 'push',
209 | },
210 | 'version': {
211 | runDir: 'build/src',
212 | command: 'version',
213 | },
214 | },
215 | });
216 |
217 |
218 | // load tasks
219 | grunt.loadNpmTasks('grunt-contrib-clean');
220 | grunt.loadNpmTasks('grunt-contrib-copy');
221 | grunt.loadNpmTasks('grunt-preprocess');
222 |
223 | /**
224 | * custom task to patch *.json or multiple *.json
225 | */
226 | grunt.registerMultiTask('jsonPatch', 'Update properties in *.json file or multiple json files', function () {
227 |
228 | // Check if there are files to patch
229 | if (!this || !this.data || (!this.files && (!this.data.src || !this.data.dest))) return;
230 |
231 | let srcFolder = this.data['srcFolder'] || '';
232 | let destFolder = this.data['destFolder'] || '';
233 |
234 | /**
235 | * Load a JSON file, patch it with , save it to
236 | *
237 | * @param {string} src
238 | * @param {string} target
239 | * @param {Object} data
240 | */
241 | function updateJsonFile(src, target, data) {
242 |
243 | let config = {};
244 |
245 | // read config file
246 | try { config = grunt.file.readJSON(srcFolder + src); }
247 | catch (e) {}
248 |
249 | // update the provided parameters
250 | for (let key in data) {
251 | let path = key.split('/'),
252 | configDrillDown = config;
253 |
254 | for (let i = 0; i < path.length - 1; i++) {
255 | // in case the path doesn't exist, create it. ONLY create object
256 | if (configDrillDown[path[i]] === undefined) {
257 |
258 | // Add array element at the end
259 | if (Array.isArray(configDrillDown) && path[i] === '-1') {
260 | path[i] = configDrillDown.push({}) - 1;
261 | }
262 | // Create Array of (-1) is used and object is empty (newly created)
263 | else if (Object.keys(configDrillDown).length === 0 && path[i] === '-1') {
264 | configDrillDown = [{}];
265 | path[i] = 0;
266 | }
267 | else {
268 | configDrillDown[path[i]] = {};
269 | }
270 | }
271 |
272 | configDrillDown = configDrillDown[path[i]];
273 | }
274 |
275 | let i = path.length - 1;
276 | if (configDrillDown[path[i]] === undefined) {
277 |
278 | // Add array element at the end
279 | if (Array.isArray(configDrillDown) && path[i] === '-1') {
280 | path[i] = configDrillDown.push({}) - 1;
281 | }
282 | // Create Array of (-1) is used and object is empty (newly created)
283 | else if (Object.keys(configDrillDown).length === 0 && path[i] === '-1') {
284 | configDrillDown = [{}];
285 | path[i] = 0;
286 | }
287 | }
288 |
289 | configDrillDown[path[i]] = data[key];
290 | }
291 |
292 | // write updated config
293 | grunt.file.write(destFolder + target, JSON.stringify(config, null, '\t'));
294 | }
295 |
296 | // Init files object
297 | let files = this.data.files || [
298 | {
299 | src: this.files[0].src[0],
300 | dest: this.files[0].dest,
301 | data: this.data.data,
302 | },
303 | ];
304 |
305 | // Patch every JSON files
306 | files.forEach(({src, dest, data}) => updateJsonFile(src, dest, data));
307 | });
308 |
309 | /**
310 | * Use clasp
311 | */
312 | grunt.registerMultiTask('clasp', 'push content in script, and create a version', function () {
313 | const child_process = require('child_process');
314 |
315 | /**
316 | * @type {{
317 | * command: string,
318 | * runDir: string
319 | * }}
320 | */
321 | let param = this.data;
322 |
323 | function clasp(cmd) {
324 | let res = child_process.execSync(`clasp ${cmd}`, {
325 | cwd: __dirname + '/' + param.runDir,
326 | });
327 |
328 | // Get string res
329 | return res.toString();
330 | }
331 |
332 | switch (param.command) {
333 | case 'push':
334 | // Push
335 | console.log('Pushing files to the script');
336 | let pushRes = clasp('push');
337 |
338 | // Check result
339 | let resPush = /Pushed\s(\d+)\sfiles\./.exec(pushRes);
340 | if (!resPush) throw 'Error while pushing files to AppsScript';
341 | console.log(`Pushed files: ${resPush[1]}`);
342 |
343 | break;
344 |
345 | case 'version':
346 | // create a new version
347 | console.log('Creating new script version');
348 | let versionRes = clasp('version');
349 |
350 | // Check result and get version num
351 | let resVers = /version\s(\d+)/.exec(versionRes);
352 | if (!resVers) throw 'Error while creating new version';
353 |
354 | let versionNum = +resVers[1];
355 | console.log('New version num: ' + versionNum);
356 |
357 | // Update version value:
358 | !CONFIG.publishing && (CONFIG['publishing'] = {});
359 | CONFIG.publishing.version = versionNum;
360 |
361 | break;
362 | }
363 | });
364 |
365 |
366 | // NEVER name the task as the configurator object
367 | // the default task can be run just by typing "grunt" on the command line
368 | // USE THIS when multiple tasks must be chained
369 | grunt.registerTask('build', [
370 | 'clean:build',
371 | 'copy:build',
372 | 'preprocess:js',
373 | 'jsonPatch:build',
374 | 'copy:dependencies',
375 | ]);
376 |
377 | grunt.registerTask('push', [
378 | 'clasp:push',
379 | ]);
380 |
381 |
382 | grunt.registerTask('build_push', [
383 | 'build',
384 | 'push',
385 | ]);
386 |
387 | // define default task (for grunt alone)
388 | grunt.registerTask('default', ['build']);
389 |
390 | /**
391 | * NOTES:
392 | *
393 | * - Beware of comments in JSON files, no comments can exists in JSON processed by config task
394 | *
395 | */
396 | };
397 |
--------------------------------------------------------------------------------