├── .github ├── FUNDING.yml └── workflows │ └── CI.yml ├── .gitignore ├── test ├── demo │ ├── module.js │ ├── .parcelrc │ ├── package.json │ └── main.js └── index.js ├── lib └── index.js ├── package.json ├── LICENSE ├── ARCHITECTURE.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: vladimirmikulic 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .parcel-cache -------------------------------------------------------------------------------- /test/demo/module.js: -------------------------------------------------------------------------------- 1 | globalThis.customFunction = () => { 2 | console.log('Hello from Google App Script!'); 3 | }; -------------------------------------------------------------------------------- /test/demo/.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "optimizers": { 4 | "*.js": ["...", "../../lib/index.js"] 5 | } 6 | } -------------------------------------------------------------------------------- /test/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "engines": { 5 | "parcel": ">=2.0.0" 6 | }, 7 | "author": "Vladimir Mikulic", 8 | "license": "MIT", 9 | "peerDependencies": { 10 | "parcel": ">=2.0.0" 11 | }, 12 | "devDependencies": { 13 | "parcel": "^2.9.1" 14 | } 15 | } -------------------------------------------------------------------------------- /test/demo/main.js: -------------------------------------------------------------------------------- 1 | require('./module'); 2 | 3 | // Runs when user installs extension 4 | globalThis.onInstall = e => { 5 | onOpen(); 6 | }; 7 | 8 | // Runs as soon as user opens a document 9 | globalThis.onOpen = e => { 10 | DocumentApp.getUi() // Or SlidesApp or FormApp. 11 | .createMenu('My Add-on') 12 | .addItem('Open', 'showSidebar') 13 | .addToUi(); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { Optimizer } = require('@parcel/plugin'); 2 | const generateEntryFunctions = require('gas-entry-generator').generate; 3 | 4 | module.exports = new Optimizer({ 5 | async optimize({ contents, map }) { 6 | const bundleForEntryGen = contents.replace(/globalThis\./g, 'global.'); 7 | const entryPointFunctions = generateEntryFunctions(bundleForEntryGen) 8 | .entryPointFunctions.split('\n') 9 | .join(''); 10 | 11 | const newContents = `${entryPointFunctions}${contents}`; 12 | return { contents: newContents, map }; 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const assert = require('assert'); 3 | const test = require('node:test'); 4 | const { execSync } = require('child_process'); 5 | 6 | test('Building demo project', async () => { 7 | process.chdir('./test/demo'); 8 | execSync('../../node_modules/.bin/parcel build main.js'); 9 | const bundledJS = fs.readFileSync('./dist/main.js', { encoding: 'utf-8' }); 10 | 11 | assert.ok(bundledJS.includes('function customFunction() {}')); 12 | assert.ok(/function onInstall\(.\) {}/.test(bundledJS)) 13 | assert.ok(/function onOpen\(.\) {}/.test(bundledJS)); 14 | }); 15 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Run tests 26 | run: npm test -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-optimizer-gas", 3 | "version": "1.0.6", 4 | "description": "Parcel plugin that enables Google App Script bundling.", 5 | "main": "lib/index.js", 6 | "engines": { 7 | "parcel": ">=2.0.0" 8 | }, 9 | "scripts": { 10 | "test": "node --test" 11 | }, 12 | "keywords": [ 13 | "gas", 14 | "google apps script" 15 | ], 16 | "files": ["lib"], 17 | "homepage": "https://github.com/VladimirMikulic/parcel-optimizer-gas#readme", 18 | "funding": "https://ko-fi.com/vladimirmikulic", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/VladimirMikulic/parcel-optimizer-gas.git" 22 | }, 23 | "author": "Vladimir Mikulic", 24 | "license": "MIT", 25 | "dependencies": { 26 | "gas-entry-generator": "^2.5.1" 27 | }, 28 | "devDependencies": { 29 | "parcel": "^2.9.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vladimir Mikulic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | This guide explains the concepts behind the parcel-plugin-gas. 4 | 5 | ## Part 1 6 | 7 | As already written in README "Usage" section, to prevent uglifying it is 8 | mandatory for a method to be attached to some entity (`globalThis`). The entity 9 | is just a sintatical sugar, thus it can be any arbitrary value (except `global` 10 | since it creates an issue with Parcel)! 11 | 12 | We use `globalThis` to reference the global object and attach methods to it. 13 | 14 | In the browser it's `window` object, in Node.js it's `global` object and in Google App Script environment 15 | it is simply an object! The common misconception is that Google App Script is powered by Node.js, 16 | but that is not true. Both of them use Google's V8 as a JS engine but their runtime environments are different. 17 | 18 | ## Part 2 19 | 20 | Next, we need to generate function declarations at the top of the bundle. If you 21 | open the generated bundle you'll notice this pattern: 22 | `function onInstall(e) {}function onOpen(e) {} ...` at the top of the bundle. 23 | 24 | Why is this necessary? Haven't we already made methods global with `globalThis`? 25 | 26 | Yes and no. As mentioned in the README "Usage" section, Google's App Script runtime 27 | expects explicit `onInstall` method. (not `globaThis.onInstall`) 28 | 29 | Although calling `onInstall()` would be perfectly fine, I suspect 30 | Google doesn't blindly execute your JS. Instead, Google probably uses something like [esprima](https://www.npmjs.com/package/esprima) 31 | to get the names of standalone global functions. 32 | Standalone means that functions are not explicitly attached to an object. (`globaThis.onInstall`) 33 | 34 | That's why we generate the function declarations at the top of the bundle. 35 | 36 | If you are not familiar with how JS works, you might be thinking 37 | "Hey!? Those function declarations bodies are empty. When Google's runtime executes them 38 | nothing will happen!?" 39 | 40 | That's not the case. Since those function declerations are in global scope, they are being overwritten by _real_ functions with bodies (hence `globalThis`). `onInstall() {} -> onInstall(e) { // code }` 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parcel-optimizer-gas 2 | 3 | ![Version](https://img.shields.io/npm/v/parcel-optimizer-gas) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](#) 5 | [![CI](https://github.com/VladimirMikulic/parcel-optimizer-gas/actions/workflows/CI.yml/badge.svg)](https://github.com/VladimirMikulic/parcel-optimizer-gas/actions) 6 | [![Twitter: VladoDev](https://img.shields.io/twitter/follow/VladoDev.svg?style=social)](https://twitter.com/VladoDev) 7 | 8 | > 🌀 Parcel plugin that enables Google App Script bundling. 9 | 10 | _This is the plugin for Parcel v2. The plugin for the first version can be found [here](https://github.com/VladimirMikulic/parcel-plugin-gas)._ 11 | 12 | ## :package: Installation 13 | 14 | ```shell 15 | # Installs the plugin and saves it as a development dependency 16 | npm i parcel-optimizer-gas -D 17 | ``` 18 | 19 | ## 🔌 Configuration 20 | 21 | We need to create `.parcelrc` configuration file and add the plugin to optimizers like this: 22 | 23 | > Syntax "..." instructs Parcel to apply the plugin on top of existing JS optimizations 24 | 25 | ```js 26 | { 27 | "extends": "@parcel/config-default", 28 | "optimizers": { 29 | "*.js": ["...", "parcel-optimizer-gas"] 30 | } 31 | } 32 | ``` 33 | 34 | ## :cloud: Usage 35 | 36 | This plugin modifies the Parcel bundle to be Google App Script compatible. 37 | Parcel uses process called uglifying when generating your production build. 38 | Uglifying is the process of converting long function/variable names into shorter versions 39 | i.e. `onInstall` becomes `j`. This is a problem since Google's runtime expects explicit 40 | `onInstall` method to be available in global scope. This applies to other methods as well. 41 | That's why default Parcel bundle doesn't work on Google's runtime environment. 42 | 43 | By attaching methods to the 44 | [`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) 45 | in source code, you prevent uglifying and with a bit of the plugin's magic your 46 | code works on Google's runtime environment. 47 | 48 | ### 🏠 Single file 49 | 50 | **index.js** 51 | 52 | ```js 53 | // Runs when user installs extension 54 | globalThis.onInstall = e => { 55 | onOpen(); 56 | }; 57 | 58 | // Runs as soon as user opens a document 59 | globalThis.onOpen = e => { 60 | DocumentApp.getUi() // Or SlidesApp or FormApp. 61 | .createMenu('My Add-on') 62 | .addItem('Open', 'showSidebar') 63 | .addToUi(); 64 | }; 65 | ``` 66 | 67 | ### 💫 Multiple files 68 | 69 | Google App Script runtime environment differs from normal JS environment that you are 70 | used to in your Browser or Node.js. There are simply no modules. Everything is loaded in one 71 | global scope/namespace. 72 | 73 | **index.js** 74 | 75 | ```js 76 | require('./my-module'); 77 | 78 | // Runs when user installs extension 79 | globalThis.onInstall = e => { 80 | customFunction(); 81 | }; 82 | ``` 83 | 84 | **my-module.js** 85 | 86 | ```js 87 | globalThis.customFunction = () => { 88 | console.log('Hello from Google App Script!'); 89 | }; 90 | ``` 91 | 92 | ### 🚀 Build 93 | 94 | `parcel build index.js` -> produces bundle which you can manually upload to your Google App Script project or push it from CLI with [clasp](https://developers.google.com/apps-script/guides/clasp). 95 | 96 | 105 | 106 | ## :man: Author 107 | 108 | **Vladimir Mikulic** 109 | 110 | - Twitter: [@VladoDev](https://twitter.com/VladoDev) 111 | - Github: [@VladimirMikulic](https://github.com/VladimirMikulic) 112 | - LinkedIn: [@vladimirmikulic](https://www.linkedin.com/in/vladimir-mikulic/) 113 | 114 | ## :handshake: Contributing 115 | 116 | Contributions, issues and feature requests are welcome! 117 | 118 | ## :pencil: License 119 | 120 | This project is licensed under [MIT](https://opensource.org/licenses/MIT) license. 121 | 122 | ## :man_astronaut: Show your support 123 | 124 | Give a ⭐️ if this project helped you! 125 | --------------------------------------------------------------------------------