diff --git a/.eslintrc.json b/.eslintrc.json index 98aee79..5875fa8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,19 +1,31 @@ { "root": true, - "parser": "@babel/eslint-parser", - "extends": ["airbnb-base", "plugin:prettier/recommended"], - "plugins": ["prettier"], + "extends": [ + "airbnb-base", + "plugin:prettier/recommended", + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended" + ], + "plugins": ["react-refresh", "prettier"], "rules": { "prettier/prettier": "error", "camelcase": "warn", "import/prefer-default-export": "warn", "import/no-extraneous-dependencies": "warn", "prefer-object-spread": "warn", - "spaced-comment":"off" + "spaced-comment": "off", + "react-refresh/only-export-components": [ + "warn", + { "allowConstantExport": true } + ] }, "parserOptions": { "babelOptions": { "configFile": "./dev/.babelrc" } - } + }, + "env": { "browser": true, "es2020": true }, + "ignorePatterns": ["dist", ".eslintrc.json"], + "parser": "@typescript-eslint/parser" } diff --git a/.github/workflows/integration-tests-basic.yaml b/.github/workflows/integration-tests-basic.yaml index e5fc812..587bbfe 100644 --- a/.github/workflows/integration-tests-basic.yaml +++ b/.github/workflows/integration-tests-basic.yaml @@ -9,18 +9,18 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-11, macos-latest, windows-latest] + os: [macos-12, macos-13, windows-2022] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - node-version: [14, 16, 18] + node-version: [18, 20] timeout-minutes: 8 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - name: Install packages [npm ci] - run: npm ci + - name: Install packages + run: yarn install - name: Allow running mkcert on Mac run: sudo security authorizationdb write com.apple.trust-settings.admin allow if: runner.os == 'MacOS' @@ -30,14 +30,14 @@ jobs: - name: Run mkcert setup [mkcert -install] run: mkcert -install if: runner.os == 'MacOS' - - name: Install https cert [npm setup:https] - run: npm run setup:https + - name: Install https cert [yarn setup:https] + run: yarn setup:https if: runner.os == 'MacOS' - run: | mkdir certs - .\test\generate-cert.ps1 + .\scripts\generate-cert.ps1 shell: pwsh if: runner.os == 'Windows' - name: Run integration tests - run: npm run test:integration + run: yarn test:integration shell: bash \ No newline at end of file diff --git a/.github/workflows/integration-tests-extended.yaml b/.github/workflows/integration-tests-extended.yaml index c4b5a44..c860844 100644 --- a/.github/workflows/integration-tests-extended.yaml +++ b/.github/workflows/integration-tests-extended.yaml @@ -1,25 +1,26 @@ name: Local integration tests - Extended Version on: - push: + pull_request: + branches: [main] jobs: extended-integration-test: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-11, macos-latest, windows-latest] + os: [macos-12, macos-13, windows-2022] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - node-version: [14, 16, 18] + node-version: [18, 20] timeout-minutes: 11 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - name: Install packages [npm ci] - run: npm ci + - name: Install packages + run: yarn install - name: Allow running mkcert on Mac run: sudo security authorizationdb write com.apple.trust-settings.admin allow if: runner.os == 'MacOS' @@ -29,12 +30,12 @@ jobs: - name: Run mkcert setup [mkcert -install] run: mkcert -install if: runner.os == 'MacOS' - - name: Install https cert [npm setup:https] - run: npm run setup:https + - name: Install https cert [yarn setup:https] + run: yarn setup:https if: runner.os == 'MacOS' - run: | mkdir certs - .\test\generate-cert.ps1 + .\scripts\generate-cert.ps1 shell: pwsh if: runner.os == 'Windows' - name: Add .clasprc.json to home folder @@ -65,10 +66,12 @@ jobs: S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - - name: Build and deploy dev setup [npm run deploy:dev] - run: npm run deploy:dev + - name: Build and deploy dev setup [yarn deploy:dev] + run: yarn deploy:dev env: NODE_OPTIONS: '--max_old_space_size=4096' - name: Run integration tests - run: npm run test:integration:extended + # use ci-reporter to publish failing diff images to s3 bucket + # run: yarn test:integration:extended:ci-reporter + run: yarn test:integration:extended shell: bash diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 4fd0219..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..aab0dea --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch spreadsheet with debugger", + "trace": true, + "sourceMaps": true, + "pauseForSourceMap": false, + "skipFiles": ["**/node_modules/**", "!${workspaceFolder}/**"], + "webRoot": "${workspaceFolder}/src/client", + // Need random open port for logging into spreadsheets: + // https://github.com/microsoft/vscode-js-debug/issues/918 + "port": 12345, + } + ] +} diff --git a/README.md b/README.md index fbe0564..19bc87a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ React & Google Apps Script logos

- With support for React v18 and React Fast Refresh + Built with React v18 and Vite for best-in-class frontend development.

@@ -46,62 +46,57 @@ [Google Apps Script](https://developers.google.com/apps-script/overview) is Google's Javascript-based development platform for building applications and add-ons for Google Sheets, Docs, Forms and other Google Apps. -You can add custom [user interfaces inside dialog windows](https://developers.google.com/apps-script/guides/html), but the platform is designed for simple HTML pages built with [templates](https://developers.google.com/apps-script/guides/html/templates) and [jQuery](https://developers.google.com/apps-script/guides/html/best-practices#take_advantage_of_jquery). - -However, using this repo, it's easy to run [React](https://reactjs.org/) apps inside these dialogs, and build everything from small projects to advanced add-ons that can be published on the G Suite Marketplace. +Google Apps Scripts lets you add custom [user interfaces inside dialog windows](https://developers.google.com/apps-script/guides/html). Using this template, it's easy to run [React](https://reactjs.org/) apps inside these dialogs, and build everything from small projects to advanced add-ons that can be published in the Google Workspace Marketplace.

React & Google Apps Script

-This repo is a boilerplate project that uses React and the same development tools that you use for building traditional websites, all inside Google Apps Script projects. - -See below how to get started! +This repo is a boilerplate project for developing React apps with Google Apps Script projects. You can use this starter template to build your own React apps and deploy them inside Google Sheets, Docs, Forms and Slides for use in dialogs and sidebars. Sample code is provided showing how your React app can interact with the underlying Google Apps Script server-side code. +Read on to get started with your own project!
## 🚜 Install -These instructions will get you set up with a copy of the React project code on your local machine. It will also get you logged in to `clasp` so you can manage script projects from the command line. +These instructions will get you set up with a copy of the React project code on your local machine. It will also get you logged in to `clasp`, which lets you manage script projects from the command line. See [deploy](#deploy) for notes on how to deploy the project and see it live in a Google Spreadsheet. ### Prerequisites -- Make sure you're running at least [Node.js](https://nodejs.org/en/download/) v14 and `npm` v6. +- Make sure you're running at least [Node.js](https://nodejs.org/en/download/) v18 and [yarn (classic)](https://classic.yarnpkg.com/lang/en/docs/install/). - You'll need to enable the Google Apps Script API. You can do that by visiting [script.google.com/home/usersettings](https://script.google.com/home/usersettings). -- To use live reload while developing, you'll need to serve your files locally using HTTPS. See [local development](#local-development) below for how to set up your local environment. +- To use live reload while developing, you'll need to serve your files locally using HTTPS. See [local development](#local-development) below for instructions on setting up your local environment. ### 🏁 Getting started -**1.** First, let's clone the repo and install the dependencies. +Full steps to getting your local environment set up, deploying your app, and also running your app locally for local development are shown in the video below: + + + +**1.** First, let's clone the repo and install the dependencies. This project is published as a public template, so you can also fork the repo or select "Use this template" in GitHub. ```bash git clone https://github.com/enuchi/React-Google-Apps-Script.git cd React-Google-Apps-Script -npm install +yarn install ``` - **2.** Next, we'll need to log in to [clasp](https://github.com/google/clasp), which lets us manage our Google Apps Script projects locally. ```bash -npm run login +yarn run login ``` - - - **3.** Now let's run the setup script to create a New spreadsheet and script project from the command line. ```bash -npm run setup +yarn run setup ``` - - Alternatively, you can use an existing Google Spreadsheet and Script file instead of creating a new one.
@@ -134,14 +129,12 @@ Next, let's deploy the app so we can see it live in Google Spreadsheets. Run the deploy command. You may be prompted to update your manifest file. Type 'yes'. ```bash -npm run deploy +yarn run deploy ``` The deploy command will build all necessary files using production settings, including all server code (Google Apps Script code), client code (React bundle), and config files. All bundled files will be outputted to the `dist/` folder, then pushed to the Google Apps Script project. -Now open Google Sheets and navigate to your new spreadsheet (e.g. the file "My React Project"). You can also run `npm run open`. Make sure to refresh the page if you already had it open. You will now see a new menu item appear containing your app! - - +Now open Google Sheets and navigate to your new spreadsheet (e.g. the file "My React Project"). You can also run `yarn run open`. Make sure to refresh the page if you already had it open. You will now see a new menu item appear containing your app!
@@ -149,8 +142,6 @@ Now open Google Sheets and navigate to your new spreadsheet (e.g. the file "My R We can develop our client-side React apps locally, and see our changes directly inside our Google Spreadsheet dialog window. - - There are two steps to getting started: installing a certificate (first time only), and running the start command. 1. Generating a certificate for local development @@ -176,24 +167,18 @@ There are two steps to getting started: installing a certificate (first time onl Create the certs in your repo: ``` - npm run setup:https + yarn run setup:https ``` 2. Now you're ready to start: ```bash - npm run start + yarn run start ``` The start command will create and deploy a development build, and serve your local files. - - After running the start command, navigate to your spreadsheet and open one of the menu items. It should now be serving your local files. When you make and save changes to your React app, your app will reload instantly within the Google Spreadsheet, and have access to any server-side functions! - - -Support for [Fast Refresh](https://github.com/pmmmwh/react-refresh-webpack-plugin) now means that only modified components are refreshed when files are changed, and state is not lost. -
### 🔍 Using React DevTools @@ -210,14 +195,14 @@ You will need to use the "standalone" version of React DevTools since our React 1. In your repo install the React DevTools package as a dev dependency: ```bash - npm install -D react-devtools + yarn add -D react-devtools ``` 2. In a new terminal window run `npx react-devtools` to launch the DevTools standalone app. 3. Add `` to the top of your `` in your React app, e.g. in the [index.html](https://github.com/enuchi/React-Google-Apps-Script/blob/e73e51e56e99903885ef8dd5525986f99038d8bf/src/client/dialog-demo-bootstrap/index.html) file in the sample Bootstrap app. -4. Deploy your app (`npm run deploy:dev`) and you should see DevTools tool running and displaying your app hierarchy. +4. Deploy your app (`yarn run deploy:dev`) and you should see DevTools tool running and displaying your app hierarchy. @@ -238,7 +223,7 @@ The included sample app has five menu items that demonstrate how to load pages i - `Sheet Editor` - This is a basic app that opens in a dialog window that demonstrates how to select, create and delete sheets in a Google Sheets documents through server calls. It uses vanilla React with no component library. - `Sheet Editor (Boostrap)` - The same basic app is included but styled with the Bootstrap library using [`react-bootstrap`](https://react-bootstrap.github.io/). The bootstrap example also contains an example of a page built with typescript (see below). - `Sheet Editor (MUI)` - A similar example using [`Material UI`](https://mui.com/). -- `Sheet Editor (Tailwind CSS)` - Another example, using [`Tailwind`](https://tailwindcss.com/) +- `Sheet Editor (Tailwind CSS)` - Another example, using [`Tailwind CSS`](https://tailwindcss.com/) - `About me` - This is just a simple page that demonstrates the use of a sidebar dialog. Access the dialogs through the new menu item that appears. You may need to refresh the spreadsheet and approve the app's permissions the first time you use it. @@ -248,13 +233,13 @@ Note that if you are choosing to use one framework, for example `Tailwind`, for
Here are some steps to take to clean up the repo if you are only using a single library -1. Uninstall unneeded dependencies (`npm uninstall react-bootstrap ...` etc.) +1. Uninstall unneeded dependencies (`yarn remove react-bootstrap ...` etc.) 2. Remove the unneeded menu bar items from the server code. 3. Remove the unneeded client code. -4. Update the `clientEntrypoints` in the [webpack config file](./webpack.config.js) to only target the relevant apps. +4. Update the `clientEntrypoints` in the [vite config file](./vite.config.ts) to only target the relevant apps.
@@ -262,29 +247,34 @@ Note that if you are choosing to use one framework, for example `Tailwind`, for
-### [New!] Typescript +### Typescript -This project now supports typescript! +This project is built mainly with typescript but also supports Javascript, and examples of both are included here, both in server-side and client-side (React) code. The included sample app has a typescript example using the Bootstrap component library. -To use, simply use a typescript extension in either the client code (.ts/.tsx) or the server code (.ts), and your typescript file will compile to the proper format. +To use typescript, simply use a typescript extension in either the client code (.ts/.tsx) or the server code (.ts), and your typescript file will compile to the proper format. For client-side code, see [FormInput.tsx in the Bootstrap demo](./src/client/dialog-demo-bootstrap/components/FormInput.tsx) for an example file. Note that it is okay to have a mix of javascript and typescript, as seen in the Bootstrap demo. To use typescript in server code, just change the file extension to .ts. The server-side code already utilizes type definitions for Google Apps Script APIs. -A basic typescript configuration is used here, because after code is transpiled from typescript to javascript it is once again transpiled to code that is compatible with Google Apps Script. However, if you want more control over your setup you can modify the included [tsconfig.json file](./tsconfig.json). +A basic typescript configuration is used here that correctly transpiles to code that is compatible with Google Apps Script. However, if you want more control over your setup you can modify the included [tsconfig.json file](./tsconfig.json). ### Adding packages You can add packages to your client-side React app. -For instance, install `react-transition-group` from npm: +For instance, install `react-transition-group`: ```bash -npm install react-transition-group +yarn add react-transition-group ``` -Important: Since Google Apps Scripts projects don't let you easily reference external files, this project will bundle an entire app into one HTML file. This can result in large files if you are importing large packages. To help reduce the size of these large HTML files, you can try to externalize packages by using a CDN to load packages. For packages that can be loaded through a CDN (usually they will have a UMD build), you can configure the CDN details here in the [webpack config file](./webpack.config.js#L187). If set up properly, this will add a script tag that will load packages from a CDN, reducing your overall bundle size. +Important: Since Google Apps Scripts projects don't let you easily reference external files, this project will bundle an entire app into one HTML file. If you are importing large libraries this can result in a large file. To help reduce the size of these large HTML files, you can try to externalize packages by using a CDN to load packages. For packages that can be loaded through a CDN (usually they will have a UMD build), you can configure the externals and globals details in the [vite config file](./vite.config.ts). You will also need to include a script element in the head of the `index.html` file, loading the library from a CDN, and making sure it supports a UMD build, e.g. +``. + +If set up properly, this will load packages from the CDN in production and will reduce your overall bundle size. + +Make sure that you update the script tag with the same version of the package you are installing with yarn, so that you are using the same version in development and production. ### Styles @@ -296,8 +286,6 @@ import './styles.css'; Many external component libraries require a css stylesheet in order to work properly. You can import stylesheets in the HTML template, [as shown here with the Bootstrap stylesheet](./src/client/dialog-demo-bootstrap/index.html). -The webpack.config.js file can also be modified to support scss and other style libraries. - ### Modifying scopes The included app only requires access to Google Spreadsheets and to loading dialog windows. If you make changes to the app's requirements, for instance, if you modify this project to work with Google Forms or Docs, make sure to edit the oauthScopes in the [appscript.json file](./appsscript.json). @@ -309,13 +297,13 @@ See https://developers.google.com/apps-script/manifest for information on the `a This project uses the [gas-client](https://github.com/enuchi/gas-client) package to more easily call server-side functions using promises. ```js -// Google's documentation wants you to do this. Boo. +// Google's client-side google.script.run utility requires calling server-side functions like this: google.script.run .withSuccessHandler((response) => doSomething(response)) .withFailureHandler((err) => handleError(err)) .addSheet(sheetTitle); -// Poof! With a little magic we can now do this: +// Using gas-client we can use more familiar promises style like this: import Server from 'gas-client'; const { serverFunctions } = new Server(); @@ -325,7 +313,7 @@ serverFunctions .then((response) => doSomething(response)) .catch((err) => handleError(err)); -// Or we can equally use async/await style: +// Or with async/await: async () => { try { const response = await serverFunctions.addSheet(sheetTitle); @@ -336,7 +324,7 @@ async () => { }; ``` -In development, `gas-client` will interact with [the custom Webpack Dev Server package](https://github.com/enuchi/Google-Apps-Script-Webpack-Dev-Server) which allows us to run our app within the dialog window and still interact with Google Apps Script functions. +In development, `gas-client` will allow you to call server-side functions from your local environment. In production, it will use Google's underlying `google.script.run` utility. ### Autocomplete diff --git a/dev/.babelrc b/dev/.babelrc deleted file mode 100644 index 749a4ef..0000000 --- a/dev/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "presets": [ - "@babel/react" - ], - "plugins": [ - "@babel/plugin-proposal-object-rest-spread", - "@babel/plugin-proposal-class-properties" - ] -} diff --git a/dev/.eslintrc.json b/dev/.eslintrc.json deleted file mode 100644 index 6478e57..0000000 --- a/dev/.eslintrc.json +++ /dev/null @@ -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"] - } - } - } -} diff --git a/dev/README.md b/dev/README.md deleted file mode 100644 index 7686a6d..0000000 --- a/dev/README.md +++ /dev/null @@ -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. diff --git a/dev/dev-server-wrapper.html b/dev/dev-server-wrapper.html new file mode 100644 index 0000000..c050c05 --- /dev/null +++ b/dev/dev-server-wrapper.html @@ -0,0 +1,82 @@ + + + + + + Dev Server + + + + + + +
+ +
+ + diff --git a/dev/index.html b/dev/index.html deleted file mode 100644 index 64900a9..0000000 --- a/dev/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - -
- - - diff --git a/dev/index.js b/dev/index.js deleted file mode 100644 index fda81d0..0000000 --- a/dev/index.js +++ /dev/null @@ -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 ( -
-