V3: Migrate to Vite (#221)
This is version 3 of the boilerplate, which makes significant changes to the build script and local development tooling. It replaces Webpack with Vite, which required some significant changes to how the local development setup works with Google Apps Script. A new dev-server-wrapper.html file is used as the wrapper app in development, which is a standalone html + js file instead of a React app. This reduces the need for a complex build step for the wrapper app. Instead the file is just copied over for each entrypoing and the file location is changed with a "replace" step. Due to the way Vite works, the html templates are changed a bit to support local development. Vite doesn't easily allow completely separate multiple builds, so some Vite plugins and custom plugins are written to support multiple entrypoints (sidebars/dialogs). The externalization for large packages is now handled in the Vite config instead of a plugin, and by manually adding in the script tags into the index.html templates. Additional changes here: - yarn is now used instead of npm as package manager - eslint configs are updated to support vite - GitHub workflows for tests are updated to use yarn and other minor changes such as node versions and OS. - A VS Code launch.json configuration is provided - Dev wrapper setup has been updated (see above description) - ES Modules (import/export) updated throughout - Unneeded packages supporting webpack configurations are removed. Many packages have been upgraded. NPM scripts have been updated to use yarn. - Removed `import React` due to Vite config. - Added script tags to each template since webpack externalization plugin is no longer used - Added <script type="module" src="./index.jsx"></script> to index.html templates to support Vite local development - Changed src/server/sheets.js to .ts typescript as exemplar - Updated tests to support yarn commands, Vite changes, and listening for Vite stdout triggers. - Add Vite-style tsconfig.json - README is updated
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/react"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
]
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@babel/eslint-parser",
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"plugins": ["babel", "react", "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",
|
||||
"react/prop-types": "warn",
|
||||
"camelcase": "warn",
|
||||
"import/prefer-default-export": "warn",
|
||||
"import/no-extraneous-dependencies": "warn",
|
||||
"prefer-object-spread": "warn"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
},
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".js", ".jsx"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# Development App Wrapper
|
||||
|
||||
This directory contains the app needed to run development mode.
|
||||
|
||||
It utilizes special Webpack configurations as well as two packages, [gas-client](https://github.com/enuchi/gas-client) and [Webpack Dev Server for Google Apps Script](https://github.com/enuchi/Google-Apps-Script-Webpack-Dev-Server), in order to achieve hot reloading inside of a dialog window.
|
||||
|
||||
## How it works
|
||||
|
||||
Running `npm run start` will build and deploy the development app, and then serve files locally.
|
||||
|
||||
The simple React app in this directory, found at [index.js](./index.js), is designed to only be used with development builds. It loads an iframe with the source pointing to `https://localhost:${PORT}/gas/${FILENAME}-impl.html`. During the build step, we will replace `FILENAME` with the appropriate name of the HTML file to load.
|
||||
|
||||
As an example, during the development build, the file `dialog-demo-bootstrap.html` will be generated from the app in this directory, using `dialog-demo-bootstrap` as the `FILENAME` for the iframe source of this page. Opening the menu items in the Google Spreadsheet will load this app, and webpack's devServer settings will serve `https://localhost:3000/gas/dialog-demo-bootstrap-impl.html` within the iframe, using the customized Webpack Dev Server build.
|
||||
|
||||
The customized Google Apps Script Webpack Dev Server acts very similarly to Webpack Dev Server's iframe mode, but is able to pass requests to Google Apps Script server functions back and forth between all the iframes being used in development.
|
||||
@@ -0,0 +1,82 @@
|
||||
<!--
|
||||
This is a development server page that serves as a wrapper for Google Apps Script (GAS) client-side development.
|
||||
|
||||
It is meant to be run inside a Google Sheets/Docs/Forms dialog window during local development.
|
||||
|
||||
It loads the gas-client library (as an external), sets up an iframe that points to a local development
|
||||
server (such as running with vite), and establishes a communication bridge between the GAS server functions and the local development server.
|
||||
|
||||
This allows for local development and testing of client-side code while still being able to interact with
|
||||
the GAS server-side functions.
|
||||
|
||||
Two placeholders are used in this file that will need to be replaced in a build step:
|
||||
- _ _PORT_ _: The port number of the local development server. (e.g. 3000)
|
||||
- _ _FILE_NAME_ _: The name of the file being loaded. (e.g. dialog-demo-bootstrap/index.html)
|
||||
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<base target="_top" />
|
||||
<title>Dev Server</title>
|
||||
<!-- Load gas-client as external. Exposed global variable is GASClient. -->
|
||||
<script src="https://unpkg.com/gas-client@1.1.1/dist/index.js"></script>
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// These values need to be replaced during the build process
|
||||
const PORT = '__PORT__';
|
||||
const FILE_NAME = '__FILE_NAME__';
|
||||
|
||||
const iframe = document.getElementById('iframe');
|
||||
iframe.src = 'https://localhost:' + PORT + '/' + FILE_NAME;
|
||||
const { serverFunctions } = new window.GASClient.GASClient({
|
||||
allowedDevelopmentDomains: (origin) =>
|
||||
/https:\/\/.*\.googleusercontent\.com$/.test(origin),
|
||||
});
|
||||
|
||||
const handleRequest = (event) => {
|
||||
const request = event.data;
|
||||
const { type, functionName, id, args } = request;
|
||||
|
||||
if (type !== 'REQUEST') return;
|
||||
|
||||
serverFunctions[functionName](...args)
|
||||
.then((response) => {
|
||||
iframe.contentWindow.postMessage(
|
||||
{ type: 'RESPONSE', id, status: 'SUCCESS', response },
|
||||
'https://localhost:' + PORT
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
id,
|
||||
status: 'ERROR',
|
||||
response: err,
|
||||
},
|
||||
'https://localhost:' + PORT
|
||||
);
|
||||
});
|
||||
};
|
||||
window.addEventListener('message', handleRequest, false);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div style="width: 100%; height: 100%">
|
||||
<iframe
|
||||
id="iframe"
|
||||
style="width: 100%; height: 100%; border: 0; position: absolute"
|
||||
></iframe>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<base target="_top" />
|
||||
<!-- Add any external scripts and stylesheets here -->
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section id="index" />
|
||||
<!-- bundled js and css will get inlined here during build -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,63 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { serverFunctions } from '../src/client/utils/serverFunctions.ts';
|
||||
|
||||
const { FILENAME, PORT } = process.env;
|
||||
|
||||
const DevServer = () => {
|
||||
const iframe = React.useRef(null);
|
||||
useEffect(() => {
|
||||
const handleRequest = (event) => {
|
||||
const request = event.data;
|
||||
const { type, functionName, id, args } = request;
|
||||
|
||||
if (type !== 'REQUEST') return;
|
||||
|
||||
serverFunctions[functionName](...args)
|
||||
.then((response) => {
|
||||
iframe.current.contentWindow.postMessage(
|
||||
{ type: 'RESPONSE', id, status: 'SUCCESS', response },
|
||||
`https://localhost:${PORT}`
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
iframe.current.contentWindow.postMessage(
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
id,
|
||||
status: 'ERROR',
|
||||
response: err,
|
||||
},
|
||||
`https://localhost:${PORT}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleRequest, false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
// we want our dev environment to fill the dialog window
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: '0',
|
||||
position: 'absolute',
|
||||
}}
|
||||
ref={iframe}
|
||||
src={`https://localhost:${PORT}/${FILENAME}-impl.html`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const container = document.getElementById('index');
|
||||
const root = createRoot(container);
|
||||
root.render(<DevServer />);
|
||||
Reference in New Issue
Block a user