├── .claspignore ├── .github └── workflows │ └── push.yaml ├── .gitignore ├── .prettierrc ├── README.md ├── frontend ├── .browserslistrc ├── .eslintrc.js ├── babel.config.js ├── package.json ├── public │ └── index.html ├── src │ ├── App.vue │ ├── Loading.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── CopyFile.vue │ │ ├── NavigationDrawer.vue │ │ └── NavigationDrawer │ │ │ └── ListMenu.vue │ ├── google │ │ ├── gapi.ts │ │ ├── picker.ts │ │ └── script.ts │ ├── main.ts │ ├── plugins │ │ ├── gas.ts │ │ └── vuetify.ts │ ├── router-gas-sync.ts │ ├── router.ts │ ├── shims-gas.d.ts │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ └── views │ │ ├── About.vue │ │ └── Home.vue ├── tsconfig.json └── vue.config.js ├── package.json ├── script ├── .eslintrc.js ├── Code.ts ├── appsscript.json ├── functions │ ├── copyFile.ts │ ├── doGet.ts │ ├── getOAuthToken.ts │ └── index.ts ├── package.json ├── tsconfig.json └── webpack.config.js ├── types └── script.d.ts └── yarn.lock /.claspignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !appsscript.json 3 | !Code.js 4 | !index.html 5 | -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: push 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Get yarn cache directory path 10 | id: yarn-cache-dir-path 11 | run: echo "::set-output name=dir::$(yarn cache dir)" 12 | - uses: actions/cache@v2 13 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 14 | with: 15 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 16 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 17 | restore-keys: | 18 | ${{ runner.os }}-yarn- 19 | - run: yarn --frozen-lockfile 20 | - run: yarn lint 21 | 22 | deploy: 23 | runs-on: ubuntu-latest 24 | needs: 25 | - lint 26 | if: github.ref == 'refs/heads/master' 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Get yarn cache directory path 30 | id: yarn-cache-dir-path 31 | run: echo "::set-output name=dir::$(yarn cache dir)" 32 | - uses: actions/cache@v2 33 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 34 | with: 35 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 36 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-yarn- 39 | - run: yarn --frozen-lockfile 40 | - name: Setup .env 41 | env: 42 | PICKER_API_KEY: ${{ secrets.PICKER_API_KEY }} 43 | CLASPRC_JSON: ${{ secrets.CLASPRC_JSON }} 44 | CLASP_JSON: ${{ secrets.CLASP_JSON }} 45 | run: | 46 | echo "VUE_APP_PICKER_DEVELOPER_KEY=${PICKER_API_KEY}" > ./frontend/.env 47 | echo "${CLASPRC_JSON}" > ~/.clasprc.json 48 | echo "${CLASP_JSON}" > ./.clasp.json 49 | - run: yarn build 50 | - run: yarn run clasp push -f 51 | - run: yarn run clasp version "${COMMIT_MESSAGE} ${GITHUB_SHA}" 52 | env: 53 | COMMIT_MESSAGE: ${{ github.event.head_commit.message }} 54 | - run: echo "DEPLOYMENT_ID=$(yarn -s run clasp deployments | tail -n 1 | awk '{ print $2 }')" >> $GITHUB_ENV 55 | - run: echo "VERSION_NUMBER=$(yarn -s run clasp versions | sed -n 2p | awk '{ print $1 }')" >> $GITHUB_ENV 56 | - run: yarn run clasp deploy -i ${DEPLOYMENT_ID} -V ${VERSION_NUMBER} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | 23 | # clasp files 24 | .clasp.json 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gas-vue-typescript 2 | 3 | This is a template repository for Google Apps Script (GAS) WebApp project. 4 | The main use case for this repository is GAS macros and its execution front-end. 5 | 6 | The front-end is preconfigured with: 7 | - TypeScript 8 | - Vue.js 9 | - Vuetify 10 | - Function call from client side 11 | - Google Picker 12 | 13 | ## Project Tree 14 | 15 | ``` 16 | /gas-vue-typescript 17 | ├─ frontend/ # Front-end (Vue.js) project 18 | ├─ script/ # GAS script project 19 | └─ types/ # Type definitions that are used across both projects. 20 | ``` 21 | 22 | ## Setup 23 | 24 | ### Install Clasp 25 | 26 | ```console 27 | % npm -g install @google/clasp 28 | % clasp login 29 | ``` 30 | 31 | ### Create GAS Project 32 | ```console 33 | % clasp create --type webapp --rootDir dist 34 | Created new webapp script: https://script.google.com/d/*****/edit 35 | Warning: files in subfolder are not accounted for unless you set a '.claspignore' file. 36 | Cloned 1 file. 37 | └─ dist/appsscript.json 38 | ``` 39 | 40 | ### Get API Key for Google Picker 41 | See below link and get your API key. 42 | https://developers.google.com/picker/docs/#appreg 43 | 44 | Setup your API key as build environment variables. 45 | ```console 46 | % echo 'VUE_APP_PICKER_DEVELOPER_KEY=' > ./frontend/.env.local 47 | ``` 48 | 49 | ### Install Dependencies 50 | ```console 51 | % yarn 52 | ``` 53 | 54 | ### Build Application 55 | ```console 56 | % yarn build 57 | ``` 58 | 59 | ### Push Built Files to GAS Project 60 | ```console 61 | % clasp push 62 | ? Manifest file has been updated. Do you want to push and overwrite? Yes 63 | └─ dist/Code.js 64 | └─ dist/appsscript.json 65 | └─ dist/index.html 66 | Pushed 3 files. 67 | ``` 68 | 69 | ### Open pushed WebApp 70 | ```console 71 | % clasp open --webapp 72 | ? Open which deployment? 73 | ❯ @HEAD - AKfycbaaaaaaa... 74 | Opening web application: AKfycbaaaaaaa... 75 | ``` 76 | 77 | ### Release WebApp 78 | If the 1st time: 79 | 80 | ```console 81 | % clasp version "First release" 82 | ~ 1 Version ~ 83 | 1 - First release 84 | % clasp deploy -V 1 85 | - AKfycbbbbbbbb... @1. 86 | ``` 87 | 88 | 2nd and after: 89 | 90 | ```console 91 | % clasp version "Another release" 92 | Created version 2. 93 | % clasp deployments 94 | 2 Deployments. 95 | - AKfycbaaaaaaa... @HEAD 96 | - AKfycbbbbbbbb... @1 97 | % clasp deploy -V 2 -i AKfycbbbbbbbb... 98 | - AKfycbbbbbbbb... @2. 99 | ``` 100 | 101 | ### Customize Configuration 102 | See [Configuration Reference](https://cli.vuejs.org/config/). 103 | 104 | ## See Also 105 | - Vue CLI https://cli.vuejs.org/guide/ 106 | - html-webpack-plugin(v3) https://github.com/jantimon/html-webpack-plugin/tree/v3.2.0 107 | - html-webpack-inline-source-plugin https://github.com/DustinJackson/html-webpack-inline-source-plugin/ 108 | - webpack-cdn-plugin https://github.com/van-nguyen/webpack-cdn-plugin 109 | - Google Picker https://developers.google.com/picker/docs/ 110 | - Google Apps Script https://developers.google.com/apps-script/overview 111 | - Clasp https://github.com/google/clasp 112 | - Vuetify https://vuetifyjs.com/ja/ 113 | -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended', 10 | '@vue/prettier', 11 | '@vue/prettier/@typescript-eslint', 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | }, 16 | rules: { 17 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | } 4 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "vue": "^2.6.11", 13 | "vue-router": "^3.4.3", 14 | "vuetify": "^2.3.8" 15 | }, 16 | "devDependencies": { 17 | "@types/gapi": "^0.0.39", 18 | "@types/google.picker": "^0.0.34", 19 | "@types/google.script.client-side": "0.1.0", 20 | "@typescript-eslint/eslint-plugin": "^2.34.0", 21 | "@typescript-eslint/parser": "^2.34.0", 22 | "@vue/cli-plugin-babel": "^4.5.3", 23 | "@vue/cli-plugin-eslint": "^4.5.3", 24 | "@vue/cli-plugin-typescript": "^4.5.3", 25 | "@vue/cli-service": "^4.5.3", 26 | "@vue/eslint-config-prettier": "^6.0.0", 27 | "@vue/eslint-config-typescript": "^5.0.2", 28 | "eslint": "^6.8.0", 29 | "eslint-plugin-prettier": "^3.1.4", 30 | "eslint-plugin-vue": "^6.2.2", 31 | "html-webpack-inline-source-plugin": "0.0.10", 32 | "prettier": "^2.0.5", 33 | "typescript": "~3.9.7", 34 | "vue-cli-plugin-vuetify": "^2.0.7", 35 | "vue-template-compiler": "^2.6.11", 36 | "webpack-cdn-plugin": "^3.3.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | gas-vue-ts 8 | 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 40 | 41 | 46 | -------------------------------------------------------------------------------- /frontend/src/Loading.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clomie/gas-vue-typescript/2ce2da0bbae9c1b03019e29cc655f4464f00262b/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/components/CopyFile.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 105 | -------------------------------------------------------------------------------- /frontend/src/components/NavigationDrawer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/NavigationDrawer/ListMenu.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | -------------------------------------------------------------------------------- /frontend/src/google/gapi.ts: -------------------------------------------------------------------------------- 1 | export function loadGapi(name: string): Promise { 2 | if (typeof gapi === 'undefined') { 3 | return Promise.resolve() 4 | } 5 | 6 | return new Promise((resolve, reject) => { 7 | gapi.load(name, { 8 | callback() { 9 | resolve() 10 | }, 11 | onerror() { 12 | reject(new Error(`${name} failed to load.`)) 13 | }, 14 | timeout: 10000, 15 | ontimeout() { 16 | reject(new Error(`${name} could not load in 10000ms.`)) 17 | }, 18 | }) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/google/picker.ts: -------------------------------------------------------------------------------- 1 | export class GooglePicker { 2 | private readonly developerKey: string 3 | private readonly oauthToken: string 4 | 5 | constructor(developerKey: string, oauthToken: string) { 6 | this.developerKey = developerKey 7 | this.oauthToken = oauthToken 8 | } 9 | 10 | async pickSpreadsheet(): Promise { 11 | return await this.pick(google.picker.ViewId.SPREADSHEETS) 12 | } 13 | 14 | async pickFolder(): Promise { 15 | return await this.pick(google.picker.ViewId.FOLDERS) 16 | } 17 | 18 | private async pick(viewId: string): Promise { 19 | return new Promise((resolve, reject) => { 20 | const view = new google.picker.DocsView(viewId) 21 | .setIncludeFolders(true) 22 | .setSelectFolderEnabled(viewId === google.picker.ViewId.FOLDERS) 23 | .setOwnedByMe(true) 24 | 25 | // See: https://developers.google.com/apps-script/guides/dialogs#file-open_dialogs 26 | new google.picker.PickerBuilder() 27 | .addView(view) 28 | .setOAuthToken(this.oauthToken) 29 | .setDeveloperKey(this.developerKey) 30 | .setOrigin(google.script.host.origin) 31 | .hideTitleBar() 32 | .enableFeature(google.picker.Feature.NAV_HIDDEN) 33 | .enableFeature(google.picker.Feature.MINE_ONLY) 34 | .setSize(1051, 650) 35 | .setCallback((data: any) => { 36 | const action = data[google.picker.Response.ACTION] 37 | if (action === google.picker.Action.PICKED) { 38 | const docs = data[google.picker.Response.DOCUMENTS] 39 | resolve(docs) 40 | } else if (action === google.picker.Action.CANCEL) { 41 | reject(new Error('Canceled picking folder')) 42 | } 43 | }) 44 | .build() 45 | .setVisible(true) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/google/script.ts: -------------------------------------------------------------------------------- 1 | export class GoogleScript { 2 | public getOAuthToken(): Promise { 3 | return this.run('getOAuthToken') 4 | } 5 | 6 | public copyFile(params: CopyFileParams): Promise { 7 | return this.run('copyFile', params) 8 | } 9 | 10 | private run(functionName: string, ...args: any[]): Promise { 11 | if (typeof google === 'undefined' || google.script === undefined) { 12 | return Promise.resolve() 13 | } 14 | return new Promise((resolve, reject) => { 15 | google.script.run 16 | .withSuccessHandler(resolve) 17 | .withFailureHandler(reject) 18 | [functionName](...args) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import vuetify from './plugins/vuetify' 3 | import gasPlugin from './plugins/gas' 4 | import App from './App.vue' 5 | import Loading from './Loading.vue' 6 | import router from './router' 7 | import { syncRouterWithGas } from './router-gas-sync' 8 | 9 | Vue.config.productionTip = false 10 | 11 | new Vue({ 12 | vuetify, 13 | render: (h) => h(Loading), 14 | }).$mount('#loading') 15 | 16 | syncRouterWithGas(router) 17 | ;(async () => { 18 | await gasPlugin(process.env.VUE_APP_PICKER_DEVELOPER_KEY || '') 19 | 20 | new Vue({ 21 | router, 22 | vuetify, 23 | render: (h) => h(App), 24 | }).$mount('#app') 25 | })() 26 | -------------------------------------------------------------------------------- /frontend/src/plugins/gas.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { loadGapi } from '@/google/gapi' 3 | import { GoogleScript } from '@/google/script' 4 | import { GooglePicker } from '@/google/picker' 5 | 6 | export default async (developerKey: string) => { 7 | const script = new GoogleScript() 8 | const [, token] = await Promise.all([ 9 | loadGapi('picker'), 10 | script.getOAuthToken(), 11 | ]) 12 | const picker = new GooglePicker(developerKey, token) 13 | 14 | Vue.prototype.$script = script 15 | Vue.prototype.$picker = picker 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import 'vuetify/dist/vuetify.min.css' 4 | 5 | Vue.use(Vuetify) 6 | 7 | export default new Vuetify({}) 8 | -------------------------------------------------------------------------------- /frontend/src/router-gas-sync.ts: -------------------------------------------------------------------------------- 1 | import Router from 'vue-router' 2 | import { Dictionary } from 'vue-router/types/router' 3 | 4 | export const syncRouterWithGas = (router: Router) => { 5 | if (typeof google === 'undefined' || google.script === undefined) { 6 | return 7 | } 8 | 9 | router.afterEach(({ path, query }) => { 10 | google.script.history.replace( 11 | null, 12 | query as Dictionary, // Suppress compilation error caused by type mismatch (nullable or not) 13 | path 14 | ) 15 | }) 16 | 17 | google.script.url.getLocation(({ hash: path, parameter: query }) => { 18 | router.replace({ path, query }) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home.vue' 4 | import About from './views/About.vue' 5 | 6 | Vue.use(Router) 7 | 8 | export default new Router({ 9 | routes: [ 10 | { 11 | path: '*', 12 | redirect: '/', 13 | }, 14 | { 15 | path: '/', 16 | name: 'home', 17 | component: Home, 18 | }, 19 | { 20 | path: '/about', 21 | name: 'about', 22 | component: About, 23 | }, 24 | ], 25 | }) 26 | -------------------------------------------------------------------------------- /frontend/src/shims-gas.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { GooglePicker } from '@/google/picker' 3 | import { GoogleScript } from '@/google/script' 4 | 5 | declare module 'vue/types/vue' { 6 | interface Vue { 7 | $picker: GooglePicker 8 | $script: GoogleScript 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /frontend/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "sourceMap": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "@/*": ["src/*"] 15 | }, 16 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 17 | }, 18 | "include": [ 19 | "src/**/*.ts", 20 | "src/**/*.tsx", 21 | "src/**/*.vue", 22 | "../types/**/*.ts" 23 | ], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | process.env.VUE_APP_BUILD_TIMESTAMP = new Date().toISOString() 3 | 4 | module.exports = { 5 | outputDir: '../dist', 6 | chainWebpack: (config) => { 7 | // disable prefetch and preload 8 | config.plugins.delete('prefetch') 9 | config.plugins.delete('preload') 10 | 11 | // Make js and css files inline into index.html 12 | config 13 | .plugin('html-inline-source') 14 | .use(require('html-webpack-inline-source-plugin')) 15 | config.plugin('html').tap((args) => { 16 | args[0].inlineSource = '^(/css/.+\\.css|/js/.+\\.js)' 17 | return args 18 | }) 19 | 20 | // make inline images 21 | config.module.rule('images').use('url-loader').options({}) 22 | 23 | // make inline media 24 | config.module.rule('media').use('url-loader').options({}) 25 | 26 | // make inline fonts 27 | config.module.rule('fonts').use('url-loader').options({}) 28 | 29 | // make inline svg 30 | config.module 31 | .rule('svg') 32 | .uses.delete('file-loader') 33 | .end() 34 | .use('url-loader') 35 | .loader('url-loader') 36 | .options({}) 37 | 38 | // Get npm modules from CDN 39 | config.plugin('webpack-cdn').use(require('webpack-cdn-plugin'), [ 40 | { 41 | modules: [ 42 | { 43 | name: 'vue', 44 | var: 'Vue', 45 | path: 'dist/vue.runtime.min.js', 46 | }, 47 | { 48 | name: 'vue-router', 49 | var: 'VueRouter', 50 | path: 'dist/vue-router.min.js', 51 | }, 52 | { 53 | name: 'vuetify', 54 | var: 'Vuetify', 55 | path: 'dist/vuetify.min.js', 56 | style: 'dist/vuetify.min.css', 57 | }, 58 | ], 59 | pathToNodeModules: process.cwd() + '/..', 60 | }, 61 | ]) 62 | 63 | if (process.env.NODE_ENV === 'production') { 64 | // html minify settings for GAS 65 | config.plugin('html').tap((args) => { 66 | args[0].minify.removeAttributeQuotes = false 67 | args[0].minify.removeScriptTypeAttributes = false 68 | return args 69 | }) 70 | } 71 | }, 72 | configureWebpack: { 73 | devtool: 'inline-source-map', 74 | externals: { 75 | 'vuetify/dist/vuetify.min.css': 'undefined', 76 | }, 77 | }, 78 | css: { 79 | sourceMap: true, 80 | }, 81 | productionSourceMap: true, 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gas-vue-typescript", 3 | "private": true, 4 | "workspaces": { 5 | "packages": [ 6 | "frontend", 7 | "script" 8 | ], 9 | "nohoist": [ 10 | "**/@types/google-apps-script" 11 | ] 12 | }, 13 | "scripts": { 14 | "build": "npm run build:frontend && npm run build:script", 15 | "build:frontend": "yarn workspace frontend build", 16 | "build:script": "yarn workspace script build", 17 | "lint": "npm run lint:frontend && npm run lint:script", 18 | "lint:frontend": "yarn workspace frontend lint", 19 | "lint:script": "yarn workspace script lint" 20 | }, 21 | "devDependencies": { 22 | "@google/clasp": "^2.3.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /script/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | es6: true, 6 | }, 7 | parser: '@typescript-eslint/parser', 8 | plugins: ['@typescript-eslint'], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:prettier/recommended', 13 | 'prettier/@typescript-eslint', 14 | ], 15 | parserOptions: {}, 16 | } 17 | -------------------------------------------------------------------------------- /script/Code.ts: -------------------------------------------------------------------------------- 1 | import { doGet, getOAuthToken, copyFile } from './functions' 2 | 3 | declare let global: { [functionName: string]: unknown } 4 | 5 | global.doGet = doGet 6 | global.getOAuthToken = getOAuthToken 7 | global.copyFile = copyFile 8 | -------------------------------------------------------------------------------- /script/appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "Asia/Tokyo", 3 | "dependencies": {}, 4 | "webapp": { 5 | "access": "ANYONE", 6 | "executeAs": "USER_ACCESSING" 7 | }, 8 | "exceptionLogging": "STACKDRIVER", 9 | "runtimeVersion": "V8" 10 | } -------------------------------------------------------------------------------- /script/functions/copyFile.ts: -------------------------------------------------------------------------------- 1 | export function copyFile(params: CopyFileParams): CopyFileResult { 2 | const sourceFile = DriveApp.getFileById(params.source.id) 3 | const targetFolder = DriveApp.getFolderById(params.target.id) 4 | const name = params.fileName 5 | const copy = sourceFile.makeCopy(name, targetFolder) 6 | const file = { 7 | id: copy.getId(), 8 | name: copy.getName(), 9 | url: copy.getUrl(), 10 | } 11 | return { file } 12 | } 13 | -------------------------------------------------------------------------------- /script/functions/doGet.ts: -------------------------------------------------------------------------------- 1 | export function doGet(): GoogleAppsScript.HTML.HtmlOutput { 2 | // Load index.html(embeded css,js) 3 | const output = HtmlService.createHtmlOutputFromFile('index') 4 | 5 | // Set viewport for mobile. 6 | output.addMetaTag('viewport', 'width=device-width, initial-scale=1') 7 | 8 | return output 9 | } 10 | -------------------------------------------------------------------------------- /script/functions/getOAuthToken.ts: -------------------------------------------------------------------------------- 1 | // Get OAuth token for Picker dialog 2 | // See: https://developers.google.com/apps-script/guides/dialogs#file-open_dialogs 3 | export function getOAuthToken(): string { 4 | // Make allow access to GoogleDrive. 5 | DriveApp.getRootFolder() 6 | return ScriptApp.getOAuthToken() 7 | } 8 | -------------------------------------------------------------------------------- /script/functions/index.ts: -------------------------------------------------------------------------------- 1 | export { doGet } from './doGet' 2 | export { getOAuthToken } from './getOAuthToken' 3 | export { copyFile } from './copyFile' 4 | -------------------------------------------------------------------------------- /script/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "script", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "webpack --progress", 7 | "lint": "eslint . --ext .js,.ts" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "@types/google-apps-script": "1.0.14", 12 | "@typescript-eslint/eslint-plugin": "^3.9.0", 13 | "@typescript-eslint/parser": "^3.9.0", 14 | "cache-loader": "^4.1.0", 15 | "copy-webpack-plugin": "^6.0.3", 16 | "es3ify-webpack-plugin": "^0.1.0", 17 | "eslint": "^7.7.0", 18 | "eslint-config-prettier": "^6.11.0", 19 | "eslint-plugin-prettier": "^3.1.4", 20 | "fork-ts-checker-webpack-plugin": "^5.0.14", 21 | "gas-webpack-plugin": "^1.0.4", 22 | "hash-sum": "^2.0.0", 23 | "prettier": "^2.0.5", 24 | "ts-loader": "^8.0.2", 25 | "typescript": "~3.9.7", 26 | "webpack": "^4.44.1", 27 | "webpack-cli": "^3.3.12" 28 | } 29 | } -------------------------------------------------------------------------------- /script/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "ESNext", 5 | "strict": true, 6 | "importHelpers": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "removeComments": true, 11 | "sourceMap": true, 12 | "baseUrl": ".", 13 | "paths": {}, 14 | "lib": ["esnext"] 15 | }, 16 | "include": ["**/*.ts", "../types/**/*.ts"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /script/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const fs = require('fs') 3 | const hash = require('hash-sum') 4 | 5 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') 6 | const GasPlugin = require('gas-webpack-plugin') 7 | const CopyWebpackPlugin = require('copy-webpack-plugin') 8 | 9 | const tsconfigFile = __dirname + '/tsconfig.json' 10 | 11 | const cacheIdentifier = hash([ 12 | require('typescript/package.json').version, 13 | require('ts-loader/package.json').version, 14 | require('cache-loader/package.json').version, 15 | require(tsconfigFile), 16 | fs.readFileSync(__filename, 'utf-8'), 17 | process.env.NODE_ENV, 18 | ]) 19 | 20 | module.exports = { 21 | mode: process.env.NODE_ENV || 'production', 22 | entry: ['./Code.ts'], 23 | output: { 24 | path: __dirname + '/../dist', 25 | filename: 'Code.js', 26 | }, 27 | resolve: { 28 | extensions: ['.js', '.json', '.ts'], 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.[jt]s$/, 34 | use: [ 35 | { 36 | loader: 'cache-loader', 37 | options: { 38 | cacheDirectory: __dirname + '/node_modules/.cache/gas', 39 | cacheIdentifier, 40 | }, 41 | }, 42 | { 43 | loader: 'ts-loader', 44 | options: { 45 | configFile: tsconfigFile, 46 | transpileOnly: true, 47 | }, 48 | }, 49 | ], 50 | }, 51 | ], 52 | }, 53 | plugins: [ 54 | new ForkTsCheckerWebpackPlugin({ 55 | typescript: { 56 | configFile: tsconfigFile, 57 | }, 58 | eslint: { 59 | files: '.', 60 | }, 61 | }), 62 | new GasPlugin(), 63 | // copy appsscript.json to dist dir 64 | new CopyWebpackPlugin({ 65 | patterns: [{ from: 'appsscript.json' }], 66 | }), 67 | ], 68 | optimization: { 69 | minimize: false, 70 | }, 71 | } 72 | -------------------------------------------------------------------------------- /types/script.d.ts: -------------------------------------------------------------------------------- 1 | declare interface PickedObject { 2 | id: string 3 | url: string 4 | name: string 5 | parentId: string 6 | } 7 | 8 | declare interface DriveFile { 9 | id: string 10 | url: string 11 | name: string 12 | } 13 | 14 | declare interface CopyFileParams { 15 | source: PickedObject 16 | target: PickedObject 17 | fileName: string 18 | } 19 | 20 | declare interface CopyFileResult { 21 | file: DriveFile 22 | } 23 | --------------------------------------------------------------------------------