Skip to content

Commit

Permalink
Initial setup
Browse files Browse the repository at this point in the history
  • Loading branch information
enuchi committed Jul 22, 2020
0 parents commit 03b4d2a
Show file tree
Hide file tree
Showing 28 changed files with 16,144 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}
29 changes: 29 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"root": true,
"parser": "babel-eslint",
"extends": ["airbnb-base", "plugin:prettier/recommended"],
"plugins": ["babel", "prettier"],
"env": {
"browser": true,
"es6": true
},
"globals": {
"google": false,
"alert": false,
"css": true
},
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"prettier/prettier": "error",
"camelcase": "warn",
"import/prefer-default-export": "warn",
"import/no-extraneous-dependencies": "warn",
"prefer-object-spread": "warn"
}
}
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.DS_Store
node_modules
build
*.tgz
package*/
build/
yarn-error.log
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
41 changes: 41 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Copyright JS Foundation and other contributors

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Modifications Copyright (C) 2020 Elisha Nuchi

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
127 changes: 127 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Google Apps Script / Webpack Dev Server

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.

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:

**Production environment:**

A. Google Apps Script dialog window is loaded in Google Sheets.

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.

Using this package, here's how it looks for development purposes:

**Development environment:**

A. Google Apps Script dialog window is loaded in Google Sheets.

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.

B. 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.

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.

## Use

See the [React / Google Apps Script](https://github.com/enuchi/React-Google-Apps-Script) project for examples (coming soon).

## Background

To enable local development of React projects inside Google Apps Script projects with live reloading, we take the following approach:

1. Instead of loading the actual app's [html page inside a dialog window](<https://developers.google.com/apps-script/reference/html/html-service#createHtmlOutputFromFile(String)>), our Google Apps Script project needs to load an html page that contains an `<iframe>`. That iframe's "src" should point to a local address, e.g. https://localhost:3000/, which is running Webpack Dev Server. If we run our React app locally on this port using Webpack Dev Server, we should be able to see our app within the Google App's dialog window, either in a [sidebar](https://developers.google.com/apps-script/reference/base/ui?hl=en#showsidebaruserinterface) or modal [dialog window](https://developers.google.com/apps-script/reference/base/ui?hl=en#showmodaldialoguserinterface,-title).

2) However, since it is only running in an iframe, our local React app will not have access to any of the available [server-side functions](<https://developers.google.com/apps-script/guides/html/reference/run#myFunction(...)>). To fix this, we use the [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) to exchange messages between the top-level iframe and the React app development iframe, so that server-side calls will work during development.

To achieve this, the package takes advantage of Webpack Dev Server's ["iframe" mode](https://webpack.js.org/configuration/dev-server/#devserverinline). In "iframe" mode, a page is embedded in an iframe under a notification bar with messages about the build. The Webpack Dev Server source code has been modified in this repo to support being embedded in an iframe and pass messages from the top-level Google Apps Script environment down to the embedded React app and vice versa.

## Installing

This package exports a single HTML file as its main export. This file contains the full Webpack Dev Server "iframe" mode capabilities along with the ability to pass messages between the Google Apps Script environment and the locally served embedded app.

You can use this package like this:

1. Install the package.

```bash
yarn add -D google-apps-script-webpack-dev-server
```

```bash
npm install -D google-apps-script-webpack-dev-server
```

2. Modify your project's webpack config's [devServer](https://webpack.js.org/configuration/dev-server/) settings to use this package. Use webpack's [before](https://webpack.js.org/configuration/dev-server/#devserverbefore) configuration to load this custom dev server package.

```javascript
const gasWebpackDevServerHtmlFilePath = require.resolve(
'google-apps-script-webpack-dev-server'
);

module.exports = {
//...
devServer: {
port: 3000,
before: function (app, server, compiler) {
app.get('/gas/*', (req, res) => {
res.setHeader('Content-Type', 'text/html');
createReadStream(gasWebpackDevServerHtmlFilePath).pipe(res);
});
},
},
};
```

Note that we'll direct all traffic at `/gas/*` to our custom dev server package. We use the `/gas/` prefix so that this path doesn't clash with other files in the app that are served locally.

3. We need to create an app that loads an iframe that points to this custom dev server package we are serving. It also needs to send the proper messages using the `window.postMessage` API. See [React / Google Apps Script](https://github.com/enuchi/React-Google-Apps-Script) for examples (coming soon).

### The `postMessage` request body.

Embedded React apps must send a request object to the Dev Server (this package) that follows the following schema:

`type`: must be the string `'REQUEST'` \
`id`: a unique id string \
`functionName`: the name of the publicly exposed Google Apps function \
`args`: an array of arguments with which to call the publicly exposed Google Apps function

The request `targetOrigin` should be a full url that matches /^https?:\/\/localhost:\d+/ or /^https?:\/\/127.0.0.\d+/.

Example request:

```javascript
window.parent.postMessage(
{
type: 'REQUEST',
id: '8f35f5d6-4ffc-4204-8042-4f9a586fc579',
args: [],
functionName: 'getData',
},
'https://localhost:3000'
);
```

### The `postMessage` response body.

Webpack Dev Server (this package) will receive requests through the `postMessage` API described above, and send a request to the parent, which should be a Google Scripts App in a dialog window that can trigger the actual server-side calls. The parent app should then send the response to this package using the following schema. This same schema will also be passed down to the embedded app untouched:

`type`: must be the string `'RESPONSE'`\
`id`: the unique id string that was sent in the request, for identification\
`status`: the string `'SUCCESS'` or `'ERROR'` depending on whether the Google Apps function call was successful\
`response`: the response from the Google Apps function if successful, or the error if not

Example response:

```javascript
{
id: 'e309e66a-8208-4cc5-b6e7-0d8bad97af20';
response: {
rangeData: ['response', 'data'];
}
status: 'SUCCESS';
type: 'RESPONSE';
}
```

The parent app sending requests "down" to the Dev Server app (this package) must have a domain matching /https:\/\/.+.googleusercontent.com\$/, for security.
58 changes: 58 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "google-apps-script-webpack-dev-server",
"version": "1.0.0",
"description": "Webpack Dev Server fork for github.com/enuchi/React-Google-Apps-Script",
"main": "build/index.html",
"files": [
"build/index.html"
],
"scripts": {
"prepack": "npm run build",
"build": "webpack"
},
"repository": {
"type": "git",
"url": "git+https://github.com/enuchi/Google-Apps-Script-Webpack-Dev-Server.git"
},
"keywords": [
"Goole",
"Apps",
"Script",
"GAS",
"Webpack",
"Dev",
"Servr"
],
"author": "Elisha Nuchi",
"license": "MIT",
"bugs": {
"url": "https://github.com/enuchi/Google-Apps-Script-Webpack-Dev-Server/issues"
},
"homepage": "https://github.com/enuchi/Google-Apps-Script-Webpack-Dev-Server#readme",
"devDependencies": {
"@babel/core": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"copy-webpack-plugin": "^6.0.3",
"css-loader": "^3.6.0",
"eslint": "^7.4.0",
"eslint-config-airbnb-base": "^14.2.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^3.1.4",
"html-loader": "^1.1.0",
"html-webpack-inline-source-plugin": "1.0.0-beta.2",
"html-webpack-plugin": "^4.3.0",
"prettier": "2.0.5",
"style-loader": "^1.2.1",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12"
},
"dependencies": {
"jquery": "^3.5.1",
"sockjs-client": "^1.4.0",
"uuid": "^8.2.0"
}
}
10 changes: 10 additions & 0 deletions src/clients/BaseClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

/* eslint-disable
no-unused-vars
*/
module.exports = class BaseClient {
static getClientPath(options) {
throw new Error('Client needs implementation');
}
};
38 changes: 38 additions & 0 deletions src/clients/SockJSClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';

/* eslint-disable
no-unused-vars
*/
const SockJS = require('sockjs-client/dist/sockjs');
const BaseClient = require('./BaseClient');

module.exports = class SockJSClient extends BaseClient {
constructor(url) {
super();
this.sock = new SockJS(url);

this.sock.onerror = (err) => {
// TODO: use logger to log the error event once client and client-src
// are reorganized to have the same directory structure
};
}

static getClientPath(options) {
return require.resolve('./SockJSClient');
}

onOpen(f) {
this.sock.onopen = f;
}

onClose(f) {
this.sock.onclose = f;
}

// call f with the message string as the first argument
onMessage(f) {
this.sock.onmessage = (e) => {
f(e.data);
};
}
};
39 changes: 39 additions & 0 deletions src/clients/WebsocketClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';

/* global WebSocket */

/* eslint-disable
no-unused-vars
*/
const BaseClient = require('./BaseClient');

module.exports = class WebsocketClient extends BaseClient {
constructor(url) {
super();
this.client = new WebSocket(url.replace(/^http/, 'ws'));

this.client.onerror = (err) => {
// TODO: use logger to log the error event once client and client-src
// are reorganized to have the same directory structure
};
}

static getClientPath(options) {
return require.resolve('./WebsocketClient');
}

onOpen(f) {
this.client.onopen = f;
}

onClose(f) {
this.client.onclose = f;
}

// call f with the message string as the first argument
onMessage(f) {
this.client.onmessage = (e) => {
f(e.data);
};
}
};
Loading

0 comments on commit 03b4d2a

Please sign in to comment.