@google/clasp 3.0.2-alpha → 3.0.3-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,7 +81,7 @@ clasp
81
81
  - [`clasp clone-script <scriptId | scriptURL> [versionNumber] [--rootDir <dir>]`](#clone)
82
82
  - [`clasp pull [--versionNumber]`](#pull)
83
83
  - [`clasp push [--watch] [--force]`](#push)
84
- - [`clasp status [--json]`](#status)
84
+ - [`clasp show-file-status [--json]`](#status)
85
85
  - [`clasp open-script](#open)
86
86
  - [`clasp list-deployments`](#deployments)
87
87
  - [`clasp create-deployment [--versionNumber <version>] [--description <description>] [--deploymentId <id>]`](#deploy)
@@ -111,9 +111,10 @@ robust support for Typescript features along with ESM module and NPM package sup
111
111
 
112
112
  There are several template projects on GitHub that show how to transform Typescript code into Apps Script that are all excellent choices.
113
113
 
114
- * https://github.com/sqrrrl/apps-script-typescript-rollup-starter
115
114
  * https://github.com/WildH0g/apps-script-engine-template
116
115
  * https://github.com/tomoyanakano/clasp-typescript-template
116
+ * https://github.com/google/aside
117
+ * https://github.com/sqrrrl/apps-script-typescript-rollup-starter
117
118
 
118
119
 
119
120
  #### Command renames
@@ -182,6 +183,13 @@ https://www.googleapis.com/auth/userinfo.profile
182
183
  https://www.googleapis.com/auth/cloud-platform
183
184
  ```
184
185
 
186
+ ### Allow-list clasp
187
+
188
+ If your organization restricts authorization for third-party apps, you may either:
189
+
190
+ * Request your admin allow-list clasp's client id `1072944905499-vm2v2i5dvn0a0d2o4ca36i1vge8cvbn0.apps.googleusercontent.com`
191
+ * Set up an internal-only GCP project for clasp as described in the previous section.
192
+
185
193
  ### Service accounts
186
194
 
187
195
  Use the `--adc` option on any command to read credentials from the environemtn using Google Cloud's [application default credentials](https://cloud.google.com/docs/authentication/application-default-credentials) mechanism.
@@ -315,7 +323,6 @@ Logs the user in. Saves the client credentials to a `.clasprc.json` file in the
315
323
 
316
324
  - `--no-localhost`: Do not run a local server, manually enter code instead.
317
325
  - `--creds <file>`: Use custom credentials used for `clasp run`. Saves a `.clasprc.json` file to current working directory. This file should be private!
318
- - `--status`: Print who you are currently logged in as, if anyone.
319
326
  - `--redirect-port <port>`: Specify a custom port for the local redirect server during the login process. Useful for environments where a specific port is required.
320
327
 
321
328
  #### Examples
@@ -1,15 +1,18 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
+ import Debug from 'debug';
4
5
  import { GoogleAuth, OAuth2Client } from 'google-auth-library';
5
6
  import { google } from 'googleapis';
6
7
  import { FileCredentialStore } from './file_credential_store.js';
7
8
  import { LocalServerAuthorizationCodeFlow } from './localhost_auth_code_flow.js';
8
9
  import { ServerlessAuthorizationCodeFlow } from './serverless_auth_code_flow.js';
10
+ const debug = Debug('clasp:auth');
9
11
  export async function initAuth(options) {
10
12
  var _a, _b, _c;
11
13
  const authFilePath = (_a = options.authFilePath) !== null && _a !== void 0 ? _a : path.join(os.homedir(), '.clasprc.json');
12
14
  const credentialStore = new FileCredentialStore(authFilePath);
15
+ debug('Initializng auth from %s', options.authFilePath);
13
16
  if (options.useApplicationDefaultCredentials) {
14
17
  const credentials = await createApplicationDefaultCredentials();
15
18
  return {
@@ -26,15 +29,23 @@ export async function initAuth(options) {
26
29
  };
27
30
  }
28
31
  export async function getUserInfo(credentials) {
32
+ debug('Fetching user info');
29
33
  const api = google.oauth2('v2');
30
- const res = await api.userinfo.get({ auth: credentials });
31
- if (res.status !== 200) {
34
+ try {
35
+ const res = await api.userinfo.get({ auth: credentials });
36
+ if (!res.data) {
37
+ debug('No user info returned');
38
+ return undefined;
39
+ }
40
+ return {
41
+ email: res.data.email,
42
+ id: res.data.id,
43
+ };
44
+ }
45
+ catch (err) {
46
+ debug('Error while fetching userinfo: %O', err);
32
47
  return undefined;
33
48
  }
34
- return {
35
- email: res.data.email,
36
- id: res.data.id,
37
- };
38
49
  }
39
50
  /**
40
51
  * Creates an an unauthorized oauth2 client given the client secret file. If no path is provided,
@@ -57,13 +68,16 @@ export async function getAuthorizedOAuth2Client(store, userKey) {
57
68
  if (!userKey) {
58
69
  userKey = 'default';
59
70
  }
71
+ debug('Loading credentials for user %s', userKey);
60
72
  const savedCredentials = await store.load(userKey);
61
73
  if (!savedCredentials) {
74
+ debug('No saved credentials found.');
62
75
  return undefined;
63
76
  }
64
77
  const client = new GoogleAuth().fromJSON(savedCredentials);
65
78
  client.setCredentials(savedCredentials);
66
79
  client.on('tokens', async (tokens) => {
80
+ debug('Saving refreshed token for user %s', userKey);
67
81
  const refreshedCredentials = {
68
82
  ...savedCredentials,
69
83
  expiry_date: tokens.expiry_date,
@@ -85,13 +99,16 @@ export async function getAuthorizedOAuth2Client(store, userKey) {
85
99
  export async function authorize(options) {
86
100
  let flow;
87
101
  if (options.noLocalServer) {
102
+ debug('Starting auth with serverless flow');
88
103
  flow = new ServerlessAuthorizationCodeFlow(options.oauth2Client);
89
104
  }
90
105
  else {
106
+ debug('Starting auth with local server flow');
91
107
  flow = new LocalServerAuthorizationCodeFlow(options.oauth2Client);
92
108
  }
93
109
  const client = await flow.authorize(options.scopes);
94
110
  await saveOauthClientCredentials(options.store, options.userKey, client);
111
+ debug('Auth complete');
95
112
  return client;
96
113
  }
97
114
  async function saveOauthClientCredentials(store, userKey, oauth2Client) {
@@ -110,8 +127,10 @@ async function saveOauthClientCredentials(store, userKey, oauth2Client) {
110
127
  access_token: tokens.access_token,
111
128
  id_token: tokens.access_token,
112
129
  };
130
+ debug('Saving refreshed credentials for user %s', userKey);
113
131
  await store.save(userKey, refreshedCredentials);
114
132
  });
133
+ debug('Saving credentials for user %s', userKey);
115
134
  await store.save(userKey, savedCredentials);
116
135
  }
117
136
  /**
@@ -120,6 +139,7 @@ async function saveOauthClientCredentials(store, userKey, oauth2Client) {
120
139
  * @returns
121
140
  */
122
141
  function createOauthClient(clientSecretPath) {
142
+ debug('Creating new oauth client from %s', clientSecretPath);
123
143
  if (!clientSecretPath) {
124
144
  throw new Error('Invalid credentials');
125
145
  }
@@ -134,11 +154,13 @@ function createOauthClient(clientSecretPath) {
134
154
  throw new Error('No localhost redirect URL found');
135
155
  }
136
156
  // create an oAuth client to authorize the API call
137
- return new OAuth2Client({
157
+ const client = new OAuth2Client({
138
158
  clientId: keys.client_id,
139
159
  clientSecret: keys.client_secret,
140
160
  redirectUri: redirectUrl,
141
161
  });
162
+ debug('Created built-in oauth client, id: %s', client._clientId);
163
+ return client;
142
164
  }
143
165
  /**
144
166
  * Creates an aunthorized oauth2 client using the default id & secret.
@@ -147,11 +169,13 @@ function createOauthClient(clientSecretPath) {
147
169
  */
148
170
  function createDefaultOAuthClient() {
149
171
  // Default client
150
- return new OAuth2Client({
172
+ const client = new OAuth2Client({
151
173
  clientId: '1072944905499-vm2v2i5dvn0a0d2o4ca36i1vge8cvbn0.apps.googleusercontent.com',
152
174
  clientSecret: 'v6V3fKV_zWU7iw1DrpO1rknX',
153
175
  redirectUri: 'http://localhost',
154
176
  });
177
+ debug('Created built-in oauth client, id: %s', client._clientId);
178
+ return client;
155
179
  }
156
180
  export async function createApplicationDefaultCredentials() {
157
181
  const defaultCreds = await new GoogleAuth({
@@ -170,6 +194,7 @@ export async function createApplicationDefaultCredentials() {
170
194
  }).getClient();
171
195
  // Remove this check after https://github.com/googleapis/google-auth-library-nodejs/issues/1677 fixed
172
196
  if (defaultCreds instanceof OAuth2Client) {
197
+ debug('Created service account credentials, id: %s', defaultCreds._clientId);
173
198
  return defaultCreds;
174
199
  }
175
200
  return undefined;
@@ -56,7 +56,7 @@ export const command = new Command('clone-script')
56
56
  return files;
57
57
  });
58
58
  files.forEach(f => console.log(`└─ ${f.localPath}`));
59
- const successMessage = intl.formatMessage({ id: "Hw9Gqn", defaultMessage: [{ type: 0, value: "Warning: files in subfolder are not accounted for unless you set a .claspignore file. Cloned " }, { type: 6, value: "count", options: { "=0": { value: [{ type: 0, value: "no files." }] }, one: { value: [{ type: 0, value: "one file." }] }, other: { value: [{ type: 7 }, { type: 0, value: " files" }] } }, offset: 0, pluralType: "cardinal" }, { type: 0, value: "." }] }, {
59
+ const successMessage = intl.formatMessage({ id: "XABSyD", defaultMessage: [{ type: 0, value: "Cloned " }, { type: 6, value: "count", options: { "=0": { value: [{ type: 0, value: "no files." }] }, one: { value: [{ type: 0, value: "one file." }] }, other: { value: [{ type: 7 }, { type: 0, value: " files" }] } }, offset: 0, pluralType: "cardinal" }, { type: 0, value: "." }] }, {
60
60
  count: files.length,
61
61
  });
62
62
  console.log(successMessage);
@@ -37,19 +37,26 @@ export const command = new Command('create-script')
37
37
  this.error(msg);
38
38
  }
39
39
  const spinnerMsg = intl.formatMessage({ id: "TMfpGK", defaultMessage: [{ type: 0, value: "Creating script..." }] });
40
- const { parentId } = await withSpinner(spinnerMsg, async () => await clasp.project.createWithContainer(name, mimeType));
41
- const url = `https://drive.google.com/open?id=${parentId}`;
42
- const successMessage = intl.formatMessage({ id: "11VZzp", defaultMessage: [{ type: 0, value: "Created new container: [url}" }] }, {
43
- url,
40
+ const { parentId, scriptId } = await withSpinner(spinnerMsg, async () => await clasp.project.createWithContainer(name, mimeType));
41
+ const parentUrl = `https://drive.google.com/open?id=${parentId}`;
42
+ const scriptUrl = `https://script.google.com/d/${scriptId}/edit`;
43
+ const successMessage = intl.formatMessage({ id: "yf9wXJ", defaultMessage: [{ type: 0, value: "Created new document: " }, { type: 1, value: "parentUrl" }, { type: 1, value: "br" }, { type: 0, value: "Created new script: " }, { type: 1, value: "scriptUrl" }] }, {
44
+ parentUrl,
45
+ scriptUrl,
46
+ br: '\n',
44
47
  });
45
48
  console.log(successMessage);
46
49
  }
47
50
  else {
48
51
  const spinnerMsg = intl.formatMessage({ id: "TMfpGK", defaultMessage: [{ type: 0, value: "Creating script..." }] });
49
52
  const scriptId = await withSpinner(spinnerMsg, async () => await clasp.project.createScript(name, parentId));
50
- const url = `https://script.google.com/d/${scriptId}/edit`;
51
- const successMessage = intl.formatMessage({ id: "NUzw0t", defaultMessage: [{ type: 0, value: "Created new script: " }, { type: 1, value: "url" }] }, {
52
- url,
53
+ const parentUrl = `https://drive.google.com/open?id=${parentId}`;
54
+ const scriptUrl = `https://script.google.com/d/${scriptId}/edit`;
55
+ const successMessage = intl.formatMessage({ id: "0a429S", defaultMessage: [{ type: 0, value: "Created new script: " }, { type: 1, value: "scriptUrl" }, { type: 5, value: "parentId", options: { undefined: { value: [] }, other: { value: [{ type: 1, value: "br" }, { type: 0, value: "Bound to document: " }, { type: 1, value: "parentUrl" }] } } }] }, {
56
+ parentId,
57
+ parentUrl,
58
+ scriptUrl,
59
+ br: '\n',
53
60
  });
54
61
  console.log(successMessage);
55
62
  }
@@ -48,7 +48,7 @@ export const command = new Command('push')
48
48
  const msg = intl.formatMessage({ id: "m/C0lF", defaultMessage: [{ type: 0, value: "Waiting for changes..." }] });
49
49
  console.log(msg);
50
50
  };
51
- const stopWatching = clasp.files.watchLocalFiles(onReady, async (paths) => {
51
+ const stopWatching = await clasp.files.watchLocalFiles(onReady, async (paths) => {
52
52
  if (!(await onChange(paths))) {
53
53
  stopWatching();
54
54
  }
@@ -108,7 +108,7 @@ export async function initClaspInstance(options) {
108
108
  project: {
109
109
  scriptId: config.scriptId,
110
110
  projectId: config.projectId,
111
- parentId: config.parentId,
111
+ parentId: firstValue(config.parentId),
112
112
  },
113
113
  });
114
114
  }
@@ -216,3 +216,9 @@ async function hasReadAccess(path) {
216
216
  }
217
217
  return true;
218
218
  }
219
+ function firstValue(values) {
220
+ if (Array.isArray(values) && values.length > 0) {
221
+ return values[0];
222
+ }
223
+ return values;
224
+ }
@@ -28,8 +28,15 @@ async function getLocalFiles(rootDir, ignorePatterns, recursive) {
28
28
  fdirBuilder = fdirBuilder.withMaxDepth(0);
29
29
  }
30
30
  const files = await fdirBuilder.crawl(rootDir).withPromise();
31
- const filteredFiles = micromatch.not(files, ignorePatterns, { dot: true });
32
- debug('Filtered %d files from ignore rules', files.length - filteredFiles.length);
31
+ let filteredFiles;
32
+ if (ignorePatterns && ignorePatterns.length) {
33
+ filteredFiles = micromatch.not(files, ignorePatterns, { dot: true });
34
+ debug('Filtered %d files from ignore rules', files.length - filteredFiles.length);
35
+ }
36
+ else {
37
+ debug('Ignore rules are empty, using all files.');
38
+ filteredFiles = files;
39
+ }
33
40
  filteredFiles.sort((a, b) => a.localeCompare(b));
34
41
  return filteredFiles[Symbol.iterator]();
35
42
  }
@@ -104,11 +111,14 @@ function debounceFileChanges(callback, delayMs) {
104
111
  return function (path) {
105
112
  // Already tracked as changed, ignore
106
113
  if (collectedPaths.includes(path)) {
114
+ debug('Ignoring pending file change for path %s', path);
107
115
  return;
108
116
  }
117
+ debug('Debouncing change for path %s', path);
109
118
  collectedPaths.push(path);
110
119
  clearTimeout(timeoutId);
111
120
  timeoutId = setTimeout(() => {
121
+ debug('Firing debounced file');
112
122
  callback(collectedPaths);
113
123
  collectedPaths = [];
114
124
  }, delayMs);
@@ -188,21 +198,37 @@ export class Files {
188
198
  const ignorePatterns = (_a = this.options.files.ignorePatterns) !== null && _a !== void 0 ? _a : [];
189
199
  const collector = debounceFileChanges(onFilesChanged, 500);
190
200
  const onChange = async (path) => {
201
+ debug('Have file changes: %s', path);
191
202
  collector(path);
192
203
  };
204
+ let matcher;
205
+ if (ignorePatterns && ignorePatterns.length) {
206
+ matcher = (file, stats) => {
207
+ if (!(stats === null || stats === void 0 ? void 0 : stats.isFile())) {
208
+ return false;
209
+ }
210
+ file = path.relative(this.options.files.projectRootDir, file);
211
+ const ignore = micromatch.not([file], ignorePatterns, { dot: true }).length === 0;
212
+ return ignore;
213
+ };
214
+ }
193
215
  const watcher = chokidar.watch(this.options.files.contentDir, {
194
216
  persistent: true,
195
217
  ignoreInitial: true,
196
218
  cwd: this.options.files.contentDir,
197
- ignored: file => {
198
- return micromatch.not([file], ignorePatterns, { dot: true }).length === 0;
199
- },
219
+ ignored: matcher,
200
220
  });
201
221
  watcher.on('ready', onReady); // Push on start
202
222
  watcher.on('add', onChange);
203
223
  watcher.on('change', onChange);
204
224
  watcher.on('unlink', onChange);
205
- return async () => await watcher.close();
225
+ watcher.on('error', err => {
226
+ debug('Unexpected error during watch: %O', err);
227
+ });
228
+ return async () => {
229
+ debug('Stopping watch');
230
+ await watcher.close();
231
+ };
206
232
  }
207
233
  async getChangedFiles() {
208
234
  const [localFiles, remoteFiles] = await Promise.all([this.collectLocalFiles(), this.fetchRemote()]);
@@ -285,6 +285,7 @@ export class Project {
285
285
  const settings = {
286
286
  scriptId: this.options.project.scriptId,
287
287
  rootDir: srcDir,
288
+ parentId: this.options.project.parentId,
288
289
  projectId: this.options.project.projectId,
289
290
  scriptExtensions: this.options.files.fileExtensions['SERVER_JS'],
290
291
  htmlExtensions: this.options.files.fileExtensions['HTML'],
@@ -7,10 +7,6 @@ export function isEnabled(experimentName, defaultValue = false) {
7
7
  if (envVarValue.toLowerCase() === 'true' || envVarValue === '1') {
8
8
  return true;
9
9
  }
10
- if (envVarValue.toLowerCase() === 'false' || envVarValue === '0') {
11
- return false;
12
- }
13
- // If it's not a boolean, return the raw string value (for string experiments)
14
- return envVarValue;
10
+ return false;
15
11
  }
16
12
  export const INCLUDE_USER_HINT_IN_URL = isEnabled('enable_user_hints');
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import Debug from 'debug';
22
22
  import loudRejection from 'loud-rejection';
23
+ import { CommanderError } from 'commander';
23
24
  import { makeProgram } from './commands/program.js';
24
25
  const debug = Debug('clasp:cli');
25
26
  // Suppress warnings about punycode and other issues caused by dependencies
@@ -34,7 +35,10 @@ try {
34
35
  }
35
36
  catch (error) {
36
37
  debug('Error: %O', error);
37
- if (error instanceof Error) {
38
+ if (error instanceof CommanderError) {
39
+ debug('Ignoring commander error, output already logged');
40
+ }
41
+ else if (error instanceof Error) {
38
42
  process.exitCode = 1;
39
43
  console.error(error.message);
40
44
  }
package/docs/run.md CHANGED
@@ -8,7 +8,7 @@ To use `clasp run`, you need to complete 5 steps:
8
8
 
9
9
  - Set up the **Project ID** in your `.clasp.json` if missing.
10
10
  - Create an **OAuth Client ID** of type `Desktop Application`. Download as `client_secret.json`.
11
- - `clasp login --creds client_secret.json` with this downloaded file.
11
+ - `clasp login --creds client_secret.json --user <key>` with this downloaded file.
12
12
  - Add the following to `appsscript.json`:
13
13
  ```json
14
14
  "executionApi": {
@@ -33,12 +33,13 @@ To use `clasp run`, you need to complete 5 steps:
33
33
  - If the `Project Number` is missing,
34
34
  - Click `Change Project`, paste the PROJECT_NUMBER, and click `Set project`
35
35
  1. Use your own OAuth 2 client. Create one by following these instructions:
36
- - `clasp open --creds`
36
+ - `clasp open-credentials-setup`
37
37
  - Press **Create credentials** > **OAuth client ID**
38
38
  - Application type: **Desktop App**
39
39
  - **Create** > **OK**
40
- - Download the file (⬇), move it to your directory, and name it `creds.json`. Please keep this file secret!
41
- 1. Call `clasp login --user <name> --creds creds.json`
40
+ - Download the file (⬇), move it to your directory, and name it `client_secret.json`. Please keep this file secret!
41
+ 1. Ensure that the [scopes required to run the script are listed in `appsscript.json`](https://developers.google.com/apps-script/concepts/scopes#set-explicit).
42
+ 1. Call `clasp login --user <name> --use-project-scopes --creds client_secret.json`
42
43
  1. Add the following to `appsscript.json`:
43
44
  ```json
44
45
  "executionApi": {
@@ -74,6 +75,6 @@ To run functions that use these scopes, you must add the scopes to your Apps Scr
74
75
 
75
76
  - `clasp open`
76
77
  - `File > Project Properties > Scopes`
77
- - Add these scopes to your `appsscript.json`.
78
- - Log in again: `clasp login --creds creds.json`. This will add these scopes to your credentials.
78
+ - Add these [scopes to your `appsscript.json`](https://developers.google.com/apps-script/concepts/scopes#set-explicit).
79
+ - Log in again: `clasp login --user <name> --use-project-scopes --creds creds.json`. This will add these scopes to your credentials.
79
80
  - `clasp run --user <name> sendMail`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google/clasp",
3
- "version": "3.0.2-alpha",
3
+ "version": "3.0.3-alpha",
4
4
  "description": "Develop Apps Script Projects locally",
5
5
  "type": "module",
6
6
  "exports": "./build/src/index.js",