├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── package.json ├── src ├── clients │ ├── BaseClient.js │ ├── SockJSClient.js │ └── WebsocketClient.js ├── default │ ├── index.js │ ├── overlay.js │ ├── socket.js │ ├── utils │ │ ├── createSocketUrl.js │ │ ├── getCurrentScriptSource.js │ │ ├── log.js │ │ ├── reloadApp.js │ │ └── sendMessage.js │ └── webpack.config.js ├── live │ ├── index.js │ ├── live.html │ ├── page.html │ ├── style.css │ └── web_modules │ │ └── jquery │ │ ├── index.js │ │ └── jquery-1.8.1.js └── onSocketMessage.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "extends": ["airbnb-base", "plugin:prettier/recommended"], 5 | "plugins": ["babel", "prettier"], 6 | "env": { 7 | "browser": true, 8 | "es6": true 9 | }, 10 | "globals": { 11 | "google": false, 12 | "alert": false, 13 | "css": true 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 9, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true 20 | } 21 | }, 22 | "rules": { 23 | "prettier/prettier": "error", 24 | "camelcase": "warn", 25 | "import/prefer-default-export": "warn", 26 | "import/no-extraneous-dependencies": "warn", 27 | "prefer-object-spread": "warn" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | *.tgz 5 | package*/ 6 | build/ 7 | yarn-error.log 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | Modifications Copyright (C) 2020 Elisha Nuchi 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining 25 | a copy of this software and associated documentation files (the 26 | 'Software'), to deal in the Software without restriction, including 27 | without limitation the rights to use, copy, modify, merge, publish, 28 | distribute, sublicense, and/or sell copies of the Software, and to 29 | permit persons to whom the Software is furnished to do so, subject to 30 | the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be 33 | included in all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 36 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 37 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 38 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 39 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 40 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 41 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Apps Script / Webpack Dev Server 2 | 3 | This package adapts Webpack Dev Server (https://github.com/webpack/webpack-dev-server) for use with React / Google Apps Script (https://github.com/enuchi/React-Google-Apps-Script) to enable live reloading during development. 4 | 5 | Here's how a deployed [React / Google Apps Script](https://github.com/enuchi/React-Google-Apps-Script) project or published add-on looks in production: 6 | 7 | **Production environment:** 8 | 9 | A. Google Apps Script dialog window is loaded in Google Sheets. 10 | 11 | B. Your React app is an HTML page loaded directly inside the dialog window that can interact with your Google Apps server-side public functions. 12 | 13 | Using this package, here's how it looks for development purposes: 14 | 15 | **Development environment:** 16 | 17 | A. Google Apps Script dialog window is loaded in Google Sheets. 18 | 19 | B. A simple React app is loaded inside the dialog window, which contains an iframe pointing to a locally running Dev Server (this package). The Dev Server loads an iframe that runs your embedded app during development and passes requests between the app and the parent Google Apps Script. 20 | 21 | C. Your React app is an HTML page loaded locally inside our custom Dev Server's iframe that can interact with your Google Apps server-side public functions, because the Dev Server is set up to pass requests to the Google Apps Script environment. 22 | 23 | In short, this package acts as a sort of middle layer, for development purposes, between a Google Apps Script environment and your local environment, so that server functions can be called during development. 24 | 25 | ## Use 26 | 27 | See the [React / Google Apps Script](https://github.com/enuchi/React-Google-Apps-Script) project for examples (coming soon). 28 | 29 | ## Background 30 | 31 | To enable local development of React projects inside Google Apps Script projects with live reloading, we take the following approach: 32 | 33 | 1. Instead of loading the actual app's [html page inside a dialog window](), our Google Apps Script project needs to load an html page that contains an ` 14 | -------------------------------------------------------------------------------- /src/live/style.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | -webkit-box-sizing: border-box; 5 | -moz-box-sizing: border-box; 6 | box-sizing: border-box; 7 | } 8 | 9 | body, 10 | html { 11 | margin: 0; 12 | padding: 0; 13 | height: 100%; 14 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 15 | } 16 | 17 | .header { 18 | width: 100%; 19 | height: 100%; 20 | position: fixed; 21 | z-index: -9999999; 22 | color: rgb(84, 84, 84); 23 | padding: 0; 24 | border-width: 8px; 25 | border-color: transparent; 26 | border-style: solid; 27 | font-size: 10px; 28 | text-align: right; 29 | line-height: 30px; 30 | background: transparent; 31 | overflow: hidden; 32 | transition: border-color 250ms ease-in; 33 | } 34 | 35 | #iframe { 36 | position: absolute; 37 | top: 0; 38 | right: 0; 39 | bottom: 0; 40 | left: 0; 41 | width: 100%; 42 | height: 100%; 43 | border: 0; 44 | } 45 | 46 | #errors { 47 | width: fit-content; 48 | margin: 0; 49 | padding: 23px; 50 | font-family: monospace; 51 | font-size: 12px; 52 | line-height: 1.4; 53 | color: #eff1f5; 54 | background: #b16a71; 55 | overflow: auto; 56 | min-width: 100vw; 57 | min-height: 100vh; 58 | } 59 | 60 | #statuswrapper { 61 | padding-right: 20px; 62 | } 63 | 64 | #okness { 65 | font-weight: bold; 66 | color: rgb(84, 84, 84); 67 | transition: color 200ms ease-in; 68 | } 69 | 70 | #status { 71 | color: rgb(84, 84, 84); 72 | transition: color 200ms ease-in; 73 | } 74 | -------------------------------------------------------------------------------- /src/live/web_modules/jquery/index.js: -------------------------------------------------------------------------------- 1 | require("./jquery-1.8.1"); 2 | module.exports = jQuery; -------------------------------------------------------------------------------- /src/onSocketMessage.js: -------------------------------------------------------------------------------- 1 | const onSocketMsg = { 2 | hot() { 3 | hot = true; 4 | iframe.attr('src', contentPage + window.location.hash); 5 | }, 6 | invalid() { 7 | okness.text(''); 8 | status.text('App updated. Recompiling...'); 9 | header.css({ 10 | borderColor: '#96b5b4', 11 | }); 12 | $errors.hide(); 13 | if (!hot) { 14 | iframe.hide(); 15 | } 16 | }, 17 | hash(hash) { 18 | currentHash = hash; 19 | }, 20 | 'still-ok': function stillOk() { 21 | okness.text(''); 22 | status.text('App ready.'); 23 | header.css({ 24 | borderColor: '', 25 | }); 26 | $errors.hide(); 27 | if (!hot) { 28 | iframe.show(); 29 | } 30 | }, 31 | ok() { 32 | okness.text(''); 33 | $errors.hide(); 34 | reloadApp(); 35 | }, 36 | warnings() { 37 | okness.text('Warnings while compiling.'); 38 | $errors.hide(); 39 | reloadApp(); 40 | }, 41 | errors: function msgErrors(errors) { 42 | status.text('App updated with errors. No reload!'); 43 | okness.text('Errors while compiling.'); 44 | $errors.text(`\n${stripAnsi(errors.join('\n\n\n'))}\n\n`); 45 | header.css({ 46 | borderColor: '#ebcb8b', 47 | }); 48 | $errors.show(); 49 | iframe.hide(); 50 | }, 51 | close() { 52 | status.text(''); 53 | okness.text('Disconnected.'); 54 | $errors.text( 55 | '\n\n\n Lost connection to webpack-dev-server.\n Please restart the server to reestablish connection...\n\n\n\n' 56 | ); 57 | header.css({ 58 | borderColor: '#ebcb8b', 59 | }); 60 | $errors.show(); 61 | iframe.hide(); 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); 5 | 6 | const entry = 'src/live/index.js'; 7 | const destination = 'build'; 8 | 9 | module.exports = { 10 | name: 'Live Reload Frame', 11 | mode: 'production', 12 | entry: path.resolve(__dirname, entry), 13 | output: { 14 | path: path.resolve(__dirname, destination), 15 | filename: 'main.js', 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules|web_modules/, 22 | use: [ 23 | { 24 | loader: 'babel-loader', 25 | }, 26 | ], 27 | }, 28 | { 29 | test: /\.html$/, 30 | use: ['html-loader'], 31 | }, 32 | { 33 | test: /\.css$/, 34 | use: ['style-loader', 'css-loader'], 35 | }, 36 | ], 37 | }, 38 | plugins: [ 39 | // keep the original webpack dev server config here 40 | new webpack.NormalModuleReplacementPlugin( 41 | /^\.\/clients\/SockJSClient$/, 42 | (resource) => { 43 | if (resource.context.startsWith(process.cwd())) { 44 | // eslint-disable-next-line no-param-reassign 45 | resource.request = resource.request.replace( 46 | /^\.\/clients\/SockJSClient$/, 47 | path.resolve(__dirname, 'src/clients/SockJSClient') 48 | ); 49 | } 50 | } 51 | ), 52 | // embed all js and css inline 53 | new HtmlWebpackPlugin({ 54 | inlineSource: '.(js|css)$', 55 | }), 56 | // see https://github.com/DustinJackson/html-webpack-inline-source-plugin/issues/63#issuecomment-515963062 57 | new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin), 58 | ], 59 | }; 60 | --------------------------------------------------------------------------------