@google/clasp 3.0.1-alpha1 → 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 +63 -18
- package/build/src/auth/auth.js +33 -8
- package/build/src/auth/localhost_auth_code_flow.js +1 -1
- package/build/src/commands/clone-script.js +1 -1
- package/build/src/commands/create-script.js +16 -9
- package/build/src/commands/open-apis.js +8 -2
- package/build/src/commands/open-container.js +8 -2
- package/build/src/commands/open-credentials.js +8 -2
- package/build/src/commands/open-logs.js +9 -2
- package/build/src/commands/open-script.js +7 -2
- package/build/src/commands/open-webapp.js +8 -3
- package/build/src/commands/push.js +2 -1
- package/build/src/commands/run-function.js +1 -1
- package/build/src/commands/tail-logs.js +15 -1
- package/build/src/commands/update-deployment.js +36 -0
- package/build/src/commands/utils.js +5 -4
- package/build/src/core/clasp.js +57 -4
- package/build/src/core/files.js +69 -23
- package/build/src/core/project.js +5 -1
- package/build/src/core/utils.js +24 -1
- package/build/src/experiments.js +12 -0
- package/build/src/index.js +7 -1
- package/build/src/intl.js +2 -0
- package/docs/run.md +8 -7
- package/package.json +4 -19
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,29 +111,30 @@ 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
|
|
120
121
|
|
|
121
|
-
Clasp 3.x
|
|
122
|
+
Clasp 3.x introduces some breaking changes from 2.x. For common use cases these changes should not impact usage, but some lesser used commands have been restructured and renamed to improve consistency.
|
|
122
123
|
|
|
123
124
|
| 2.x | 3.x |
|
|
124
125
|
|----------------------------|----------------------------------------|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
|
136
|
-
|
|
126
|
+
|`open` | `open-script` |
|
|
127
|
+
|`open --web` | `open-web-app` |
|
|
128
|
+
|`open --addon` | `open-container` |
|
|
129
|
+
|`open --creds` | `open-credentials-setup` |
|
|
130
|
+
|`login --creds <file>` | `login -u <name> --creds <file>` |
|
|
131
|
+
|`logs --open` | `open-logs` |
|
|
132
|
+
|`logs --setup` | N/A |
|
|
133
|
+
|`apis --open` | `open-api-console` |
|
|
134
|
+
|`apis enable <api>` | `enable-api <api>` |
|
|
135
|
+
|`apis disable <api>` | `disable-api <api>` |
|
|
136
|
+
|`deploy -i <id>` | `update-deployment <id>` |
|
|
137
|
+
|`settings` | N/A |
|
|
137
138
|
|
|
138
139
|
Other commands have also been renamed but retain aliases for compatibility.
|
|
139
140
|
|
|
@@ -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.
|
|
@@ -263,14 +271,39 @@ You must [associate Google Script project with Google Cloud Platform](https://gi
|
|
|
263
271
|
|
|
264
272
|
Even if you do not set this manually, clasp will ask this via a prompt to you at the required time.
|
|
265
273
|
|
|
266
|
-
### `fileExtension` (optional)
|
|
274
|
+
### `fileExtension` (deprecated, optional)
|
|
267
275
|
|
|
268
276
|
Specifies the file extension for **local** script files in your Apps Script project.
|
|
269
277
|
|
|
278
|
+
### `scriptExtensions` (optional)
|
|
279
|
+
|
|
280
|
+
Specifies the file extensions for **local** script files in your Apps Script project. May be a string or array of strings. Files matching the extension will be considered scripts files.
|
|
281
|
+
|
|
282
|
+
When pulling files, the first extension listed is used to write files.
|
|
283
|
+
|
|
284
|
+
Defaults to `[".js", ".gs"]`
|
|
285
|
+
|
|
286
|
+
### `htmlExtensions` (optional)
|
|
287
|
+
|
|
288
|
+
Specifies the file extensions for **local** HTML files in your Apps Script project. May be a string or array of strings. Files matching the extension will be considered HTML files.
|
|
289
|
+
|
|
290
|
+
When pulling files, the first extension listed is used to write files.
|
|
291
|
+
|
|
292
|
+
Defaults to `[".html"]`
|
|
293
|
+
|
|
270
294
|
### `filePushOrder` (optional)
|
|
271
295
|
|
|
272
|
-
Specifies the files that should be pushed first, useful for scripts that rely on order of execution. All other files are pushed after this list of files.
|
|
296
|
+
Specifies the files that should be pushed first, useful for scripts that rely on order of execution. All other files are pushed after this list of files, sorted by name.
|
|
297
|
+
|
|
298
|
+
Note that file paths are relative to directory containing .clasp.json. If `rootDir` is also set, any files listed should include that path as well.
|
|
273
299
|
|
|
300
|
+
### `skipSubdirectories` (optional)
|
|
301
|
+
|
|
302
|
+
For backwards compatibility with previous behavior where subdirectories
|
|
303
|
+
are ignored if a `.claspignore` file is not present. Clasp provides default
|
|
304
|
+
ignore rules, making the previous warning and behavior confusing. If you
|
|
305
|
+
need to force clasp to ignore subdirectories and do not want to construct
|
|
306
|
+
a `.claspignore` file, set this option to true.
|
|
274
307
|
|
|
275
308
|
## Reference
|
|
276
309
|
|
|
@@ -290,7 +323,6 @@ Logs the user in. Saves the client credentials to a `.clasprc.json` file in the
|
|
|
290
323
|
|
|
291
324
|
- `--no-localhost`: Do not run a local server, manually enter code instead.
|
|
292
325
|
- `--creds <file>`: Use custom credentials used for `clasp run`. Saves a `.clasprc.json` file to current working directory. This file should be private!
|
|
293
|
-
- `--status`: Print who you are currently logged in as, if anyone.
|
|
294
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.
|
|
295
327
|
|
|
296
328
|
#### Examples
|
|
@@ -453,6 +485,19 @@ To update/redeploy an existing deployment, provide the deployment ID.
|
|
|
453
485
|
- `clasp create-deployment --deploymentId abcd1234` (redeploy and create new version)
|
|
454
486
|
- `clasp create-deployment -V 7 -d "Updates sidebar logo." -i abdc1234`
|
|
455
487
|
|
|
488
|
+
### Redeploy
|
|
489
|
+
|
|
490
|
+
Updates an existing deployment. Same as `create-deployment -i id`.
|
|
491
|
+
|
|
492
|
+
#### Options
|
|
493
|
+
|
|
494
|
+
- `-V <version>` `--versionNumber <version>`: The project version to deploy at.
|
|
495
|
+
- `-d <description>` `--description <description>`: The deployment description.
|
|
496
|
+
|
|
497
|
+
#### Examples
|
|
498
|
+
|
|
499
|
+
- `clasp update-deployment abcd1234` (redeploy and create new version)
|
|
500
|
+
|
|
456
501
|
### Undeploy
|
|
457
502
|
|
|
458
503
|
Undeploys a deployment of a script.
|
package/build/src/auth/auth.js
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
|
+
import open from 'open';
|
|
2
3
|
import enableDestroy from 'server-destroy';
|
|
3
4
|
import { intl } from '../intl.js';
|
|
4
5
|
import { AuthorizationCodeFlow, parseAuthResponseUrl } from './auth_code_flow.js';
|
|
5
|
-
import open from 'open';
|
|
6
6
|
export class LocalServerAuthorizationCodeFlow extends AuthorizationCodeFlow {
|
|
7
7
|
constructor(oauth2client) {
|
|
8
8
|
super(oauth2client);
|
|
@@ -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: "
|
|
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);
|
|
@@ -11,7 +11,7 @@ const DRIVE_FILE_MIMETYPES = {
|
|
|
11
11
|
slides: 'application/vnd.google-apps.presentation',
|
|
12
12
|
};
|
|
13
13
|
export const command = new Command('create-script')
|
|
14
|
-
.
|
|
14
|
+
.alias('create')
|
|
15
15
|
.description('Create a script')
|
|
16
16
|
.option('--type <type>', 'Creates a new Apps Script project attached to a new Document, Spreadsheet, Presentation, Form, or as a standalone script, web app, or API.', 'standalone')
|
|
17
17
|
.option('--title <title>', 'The project title.')
|
|
@@ -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
|
|
42
|
-
const
|
|
43
|
-
|
|
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
|
|
51
|
-
const
|
|
52
|
-
|
|
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
|
}
|
|
@@ -60,7 +67,7 @@ export const command = new Command('create-script')
|
|
|
60
67
|
return files;
|
|
61
68
|
});
|
|
62
69
|
files.forEach(f => console.log(`└─ ${f.localPath}`));
|
|
63
|
-
const successMessage = intl.formatMessage({ id: "
|
|
70
|
+
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: "." }] }, {
|
|
64
71
|
count: files.length,
|
|
65
72
|
});
|
|
66
73
|
console.log(successMessage);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { INCLUDE_USER_HINT_IN_URL } from '../experiments.js';
|
|
2
3
|
import { assertGcpProjectConfigured, maybePromptForProjectId, openUrl } from './utils.js';
|
|
3
4
|
export const command = new Command('open-api-console')
|
|
4
5
|
.description('Open the API console for the current project.')
|
|
@@ -6,6 +7,11 @@ export const command = new Command('open-api-console')
|
|
|
6
7
|
const clasp = this.opts().clasp;
|
|
7
8
|
const projectId = await maybePromptForProjectId(clasp);
|
|
8
9
|
assertGcpProjectConfigured(clasp);
|
|
9
|
-
const url =
|
|
10
|
-
|
|
10
|
+
const url = new URL('https://console.developers.google.com/apis/dashboard');
|
|
11
|
+
url.searchParams.set('project', projectId !== null && projectId !== void 0 ? projectId : '');
|
|
12
|
+
if (INCLUDE_USER_HINT_IN_URL) {
|
|
13
|
+
const userHint = await clasp.authorizedUser();
|
|
14
|
+
url.searchParams.set('authUser', userHint !== null && userHint !== void 0 ? userHint : '');
|
|
15
|
+
}
|
|
16
|
+
await openUrl(url.toString());
|
|
11
17
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { INCLUDE_USER_HINT_IN_URL } from '../experiments.js';
|
|
2
3
|
import { intl } from '../intl.js';
|
|
3
4
|
import { openUrl } from './utils.js';
|
|
4
5
|
export const command = new Command('open-container')
|
|
@@ -10,6 +11,11 @@ export const command = new Command('open-container')
|
|
|
10
11
|
const msg = intl.formatMessage({ id: "eXBzoP", defaultMessage: [{ type: 0, value: "Parent ID not set, unable to open document." }] });
|
|
11
12
|
this.error(msg);
|
|
12
13
|
}
|
|
13
|
-
const url =
|
|
14
|
-
|
|
14
|
+
const url = new URL('https://drive.google.com/open');
|
|
15
|
+
url.searchParams.set('id', parentId);
|
|
16
|
+
if (INCLUDE_USER_HINT_IN_URL) {
|
|
17
|
+
const userHint = await clasp.authorizedUser();
|
|
18
|
+
url.searchParams.set('authUser', userHint !== null && userHint !== void 0 ? userHint : '');
|
|
19
|
+
}
|
|
20
|
+
await openUrl(url.toString());
|
|
15
21
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { INCLUDE_USER_HINT_IN_URL } from '../experiments.js';
|
|
2
3
|
import { assertGcpProjectConfigured, maybePromptForProjectId, openUrl } from './utils.js';
|
|
3
4
|
export const command = new Command('open-credentials-setup')
|
|
4
5
|
.description("Open credentials page for the script's GCP project")
|
|
@@ -6,6 +7,11 @@ export const command = new Command('open-credentials-setup')
|
|
|
6
7
|
const clasp = this.opts().clasp;
|
|
7
8
|
const projectId = await maybePromptForProjectId(clasp);
|
|
8
9
|
assertGcpProjectConfigured(clasp);
|
|
9
|
-
const url =
|
|
10
|
-
|
|
10
|
+
const url = new URL('https://console.developers.google.com/apis/credentials');
|
|
11
|
+
url.searchParams.set('project', projectId !== null && projectId !== void 0 ? projectId : '');
|
|
12
|
+
if (INCLUDE_USER_HINT_IN_URL) {
|
|
13
|
+
const userHint = await clasp.authorizedUser();
|
|
14
|
+
url.searchParams.set('authUser', userHint !== null && userHint !== void 0 ? userHint : '');
|
|
15
|
+
}
|
|
16
|
+
await openUrl(url.toString());
|
|
11
17
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { INCLUDE_USER_HINT_IN_URL } from '../experiments.js';
|
|
2
3
|
import { assertGcpProjectConfigured, maybePromptForProjectId, openUrl } from './utils.js';
|
|
3
4
|
export const command = new Command('open-logs')
|
|
4
5
|
.description('Open logs in the developer console')
|
|
@@ -6,6 +7,12 @@ export const command = new Command('open-logs')
|
|
|
6
7
|
const clasp = this.opts().clasp;
|
|
7
8
|
const projectId = await maybePromptForProjectId(clasp);
|
|
8
9
|
assertGcpProjectConfigured(clasp);
|
|
9
|
-
const url =
|
|
10
|
-
|
|
10
|
+
const url = new URL('https://console.cloud.google.com/logs/viewer');
|
|
11
|
+
url.searchParams.set('project', projectId !== null && projectId !== void 0 ? projectId : '');
|
|
12
|
+
url.searchParams.set('resource', 'app_script_function');
|
|
13
|
+
if (INCLUDE_USER_HINT_IN_URL) {
|
|
14
|
+
const userHint = await clasp.authorizedUser();
|
|
15
|
+
url.searchParams.set('authUser', userHint !== null && userHint !== void 0 ? userHint : '');
|
|
16
|
+
}
|
|
17
|
+
await openUrl(url.toString());
|
|
11
18
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { INCLUDE_USER_HINT_IN_URL } from '../experiments.js';
|
|
2
3
|
import { intl } from '../intl.js';
|
|
3
4
|
import { openUrl } from './utils.js';
|
|
4
5
|
export const command = new Command('open-script')
|
|
@@ -13,6 +14,10 @@ export const command = new Command('open-script')
|
|
|
13
14
|
const msg = intl.formatMessage({ id: "RXEA+0", defaultMessage: [{ type: 0, value: "Script ID not set, unable to open IDE." }] });
|
|
14
15
|
this.error(msg);
|
|
15
16
|
}
|
|
16
|
-
const url = `https://script.google.com/d/${scriptId}/edit
|
|
17
|
-
|
|
17
|
+
const url = new URL(`https://script.google.com/d/${scriptId}/edit`);
|
|
18
|
+
if (INCLUDE_USER_HINT_IN_URL) {
|
|
19
|
+
const userHint = await clasp.authorizedUser();
|
|
20
|
+
url.searchParams.set('authUser', userHint !== null && userHint !== void 0 ? userHint : '');
|
|
21
|
+
}
|
|
22
|
+
await openUrl(url.toString());
|
|
18
23
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
|
+
import { INCLUDE_USER_HINT_IN_URL } from '../experiments.js';
|
|
3
4
|
import { intl } from '../intl.js';
|
|
4
5
|
import { ellipsize, isInteractive, openUrl } from './utils.js';
|
|
5
6
|
export const command = new Command('open-web-app')
|
|
@@ -39,7 +40,7 @@ export const command = new Command('open-web-app')
|
|
|
39
40
|
deploymentId = answer.deployment;
|
|
40
41
|
}
|
|
41
42
|
if (!deploymentId) {
|
|
42
|
-
const msg = intl.formatMessage({ id: "
|
|
43
|
+
const msg = intl.formatMessage({ id: "VJZ9X5", defaultMessage: [{ type: 0, value: "Deployment ID is required." }] });
|
|
43
44
|
this.error(msg);
|
|
44
45
|
}
|
|
45
46
|
const entryPoints = (_a = (await clasp.project.entryPoints(deploymentId))) !== null && _a !== void 0 ? _a : [];
|
|
@@ -51,6 +52,10 @@ export const command = new Command('open-web-app')
|
|
|
51
52
|
const msg = intl.formatMessage({ id: "Kfeimc", defaultMessage: [{ type: 0, value: "No web app entry point found." }] });
|
|
52
53
|
this.error(msg);
|
|
53
54
|
}
|
|
54
|
-
const url = webAppEntry.webApp.url;
|
|
55
|
-
|
|
55
|
+
const url = new URL(webAppEntry.webApp.url);
|
|
56
|
+
if (INCLUDE_USER_HINT_IN_URL) {
|
|
57
|
+
const userHint = await clasp.authorizedUser();
|
|
58
|
+
url.searchParams.set('authUser', userHint !== null && userHint !== void 0 ? userHint : '');
|
|
59
|
+
}
|
|
60
|
+
await openUrl(url.toString());
|
|
56
61
|
});
|
|
@@ -18,6 +18,7 @@ export const command = new Command('push')
|
|
|
18
18
|
if (!force) {
|
|
19
19
|
const msg = intl.formatMessage({ id: "TItFfu", defaultMessage: [{ type: 0, value: "Skipping push." }] });
|
|
20
20
|
console.log(msg);
|
|
21
|
+
return;
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
const spinnerMsg = intl.formatMessage({ id: "qUq++d", defaultMessage: [{ type: 0, value: "Pushing files..." }] });
|
|
@@ -47,7 +48,7 @@ export const command = new Command('push')
|
|
|
47
48
|
const msg = intl.formatMessage({ id: "m/C0lF", defaultMessage: [{ type: 0, value: "Waiting for changes..." }] });
|
|
48
49
|
console.log(msg);
|
|
49
50
|
};
|
|
50
|
-
const stopWatching = clasp.files.watchLocalFiles(onReady, async (paths) => {
|
|
51
|
+
const stopWatching = await clasp.files.watchLocalFiles(onReady, async (paths) => {
|
|
51
52
|
if (!(await onChange(paths))) {
|
|
52
53
|
stopWatching();
|
|
53
54
|
}
|
|
@@ -39,7 +39,7 @@ export const command = new Command('run-function')
|
|
|
39
39
|
console.error(`${chalk.red(msg)}`, errorMessage, scriptStackTraceElements || []);
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
-
if (response && response.result) {
|
|
42
|
+
if (response && response.result !== undefined) {
|
|
43
43
|
console.log(response.result);
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
@@ -41,6 +41,7 @@ export const command = new Command('tail-logs')
|
|
|
41
41
|
await maybePromptForProjectId(clasp);
|
|
42
42
|
}
|
|
43
43
|
assertGcpProjectConfigured(clasp);
|
|
44
|
+
console.log('PAST', clasp.project.projectId);
|
|
44
45
|
await fetchAndPrintLogs();
|
|
45
46
|
if (watch) {
|
|
46
47
|
const POLL_INTERVAL = 6000; // 6s
|
|
@@ -62,6 +63,9 @@ function formatEntry(entry, options) {
|
|
|
62
63
|
if (!resource) {
|
|
63
64
|
return undefined;
|
|
64
65
|
}
|
|
66
|
+
if (!timestamp) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
65
69
|
let functionName = (_b = (_a = resource.labels) === null || _a === void 0 ? void 0 : _a['function_name']) !== null && _b !== void 0 ? _b : 'N/A';
|
|
66
70
|
let payloadData = '';
|
|
67
71
|
if (options.json) {
|
|
@@ -85,8 +89,18 @@ function formatEntry(entry, options) {
|
|
|
85
89
|
const coloredSeverity = `${severityColor[severity](severity) || severity}`.padEnd(20);
|
|
86
90
|
functionName = functionName.padEnd(15);
|
|
87
91
|
payloadData = payloadData.padEnd(20);
|
|
92
|
+
const localizedTime = getLocalISODateTime(new Date(timestamp));
|
|
88
93
|
if (options.simplified) {
|
|
89
94
|
return `${coloredSeverity} ${functionName} ${payloadData}`;
|
|
90
95
|
}
|
|
91
|
-
return `${coloredSeverity} ${
|
|
96
|
+
return `${coloredSeverity} ${localizedTime} ${functionName} ${payloadData}`;
|
|
97
|
+
}
|
|
98
|
+
function getLocalISODateTime(date) {
|
|
99
|
+
const year = date.getFullYear();
|
|
100
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
101
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
102
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
103
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
104
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
105
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
92
106
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { intl } from '../intl.js';
|
|
3
|
+
import { withSpinner } from './utils.js';
|
|
4
|
+
export const command = new Command('update-deployment')
|
|
5
|
+
.alias('redeploy')
|
|
6
|
+
.argument('<deploymentId>')
|
|
7
|
+
.description('Updates a deployment for a project to a new version')
|
|
8
|
+
.option('-V, --versionNumber <version>', 'The project version')
|
|
9
|
+
.option('-d, --description <description>', 'The deployment description')
|
|
10
|
+
.action(async function (deploymentId, options) {
|
|
11
|
+
var _a, _b, _c;
|
|
12
|
+
const clasp = this.opts().clasp;
|
|
13
|
+
const description = (_a = options.description) !== null && _a !== void 0 ? _a : '';
|
|
14
|
+
const versionNumber = options.versionNumber ? Number(options.versionNumber) : undefined;
|
|
15
|
+
if (!deploymentId) {
|
|
16
|
+
const msg = intl.formatMessage({ id: "OXJvuR", defaultMessage: [{ type: 0, value: "Deployment ID is required to redeploy." }] });
|
|
17
|
+
this.error(msg);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const spinnerMsg = intl.formatMessage({ id: "oL8t7p", defaultMessage: [{ type: 0, value: "Deploying project..." }] });
|
|
21
|
+
const deployment = await withSpinner(spinnerMsg, async () => {
|
|
22
|
+
return await clasp.project.deploy(description, deploymentId, versionNumber);
|
|
23
|
+
});
|
|
24
|
+
const successMessage = intl.formatMessage({ id: "wWBE7L", defaultMessage: [{ type: 0, value: "Redeployed " }, { type: 1, value: "deploymentId" }, { type: 0, value: " " }, { type: 5, value: "version", options: { undefined: { value: [{ type: 0, value: "@HEAD" }] }, other: { value: [{ type: 0, value: "@" }, { type: 1, value: "version" }] } } }] }, {
|
|
25
|
+
deploymentId: deployment.deploymentId,
|
|
26
|
+
version: (_b = deployment.deploymentConfig) === null || _b === void 0 ? void 0 : _b.versionNumber,
|
|
27
|
+
});
|
|
28
|
+
console.log(successMessage);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (((_c = error.cause) === null || _c === void 0 ? void 0 : _c.code) === 'INVALID_ARGUMENT') {
|
|
32
|
+
this.error(error.cause.message);
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
@@ -3,14 +3,14 @@ import inquirer from 'inquirer';
|
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import { intl } from '../intl.js';
|
|
6
|
-
export
|
|
6
|
+
export function assertScriptConfigured(clasp) {
|
|
7
7
|
if (clasp.project.scriptId) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
10
|
const msg = intl.formatMessage({ id: "2IuvqO", defaultMessage: [{ type: 0, value: "Script ID is not set, unable to continue." }] });
|
|
11
11
|
throw new Error(msg);
|
|
12
12
|
}
|
|
13
|
-
export
|
|
13
|
+
export function assertGcpProjectConfigured(clasp) {
|
|
14
14
|
if (clasp.project.projectId) {
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
@@ -61,13 +61,14 @@ export function ellipsize(value, length) {
|
|
|
61
61
|
}
|
|
62
62
|
// Exporting and wrapping to allow it to be toggled in tests
|
|
63
63
|
export const claspEnv = {
|
|
64
|
-
isInteractive: process.stdout.isTTY
|
|
64
|
+
isInteractive: process.stdout.isTTY,
|
|
65
|
+
isBrowserPresent: process.stdout.isTTY,
|
|
65
66
|
};
|
|
66
67
|
export function isInteractive() {
|
|
67
68
|
return claspEnv.isInteractive;
|
|
68
69
|
}
|
|
69
70
|
export async function openUrl(url) {
|
|
70
|
-
if (!
|
|
71
|
+
if (!claspEnv.isBrowserPresent) {
|
|
71
72
|
const msg = intl.formatMessage({ id: "kvR0OI", defaultMessage: [{ type: 0, value: "Open " }, { type: 1, value: "url" }, { type: 0, value: " in your browser to continue." }] }, {
|
|
72
73
|
url,
|
|
73
74
|
});
|
package/build/src/core/clasp.js
CHANGED
|
@@ -4,11 +4,13 @@ import { findUpSync } from 'find-up';
|
|
|
4
4
|
import fs from 'fs/promises';
|
|
5
5
|
import splitLines from 'split-lines';
|
|
6
6
|
import stripBom from 'strip-bom';
|
|
7
|
+
import { getUserInfo } from '../auth/auth.js';
|
|
7
8
|
import { Files } from './files.js';
|
|
8
9
|
import { Functions } from './functions.js';
|
|
9
10
|
import { Logs } from './logs.js';
|
|
10
11
|
import { Project } from './project.js';
|
|
11
12
|
import { Services } from './services.js';
|
|
13
|
+
import { ensureStringArray } from './utils.js';
|
|
12
14
|
const debug = Debug('clasp:core');
|
|
13
15
|
const DEFAULT_CLASP_IGNORE = [
|
|
14
16
|
'**/**',
|
|
@@ -30,6 +32,19 @@ export class Clasp {
|
|
|
30
32
|
this.logs = new Logs(options);
|
|
31
33
|
this.functions = new Functions(options);
|
|
32
34
|
}
|
|
35
|
+
async authorizedUser() {
|
|
36
|
+
if (!this.options.credentials) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const user = await getUserInfo(this.options.credentials);
|
|
41
|
+
return user === null || user === void 0 ? void 0 : user.id;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
debug('Unable to fetch user info, %O', err);
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
33
48
|
withScriptId(scriptId) {
|
|
34
49
|
if (this.options.project) {
|
|
35
50
|
throw new Error('Science project already set, create new instance instead');
|
|
@@ -65,7 +80,8 @@ export async function initClaspInstance(options) {
|
|
|
65
80
|
ignoreFilePath: ignoreFile,
|
|
66
81
|
ignorePatterns: ignoreRules,
|
|
67
82
|
filePushOrder: [],
|
|
68
|
-
|
|
83
|
+
skipSubdirectories: false,
|
|
84
|
+
fileExtensions: readFileExtensions({}),
|
|
69
85
|
},
|
|
70
86
|
});
|
|
71
87
|
}
|
|
@@ -74,7 +90,7 @@ export async function initClaspInstance(options) {
|
|
|
74
90
|
const ignoreRules = await loadIgnoreFileOrDefaults(ignoreFile);
|
|
75
91
|
const content = await fs.readFile(projectRoot.configPath, { encoding: 'utf8' });
|
|
76
92
|
const config = JSON.parse(content);
|
|
77
|
-
const
|
|
93
|
+
const fileExtensions = readFileExtensions(config);
|
|
78
94
|
const filePushOrder = config.filePushOrder || [];
|
|
79
95
|
const contentDir = path.resolve(projectRoot.rootDir, config.srcDir || config.rootDir || '.');
|
|
80
96
|
return new Clasp({
|
|
@@ -86,15 +102,46 @@ export async function initClaspInstance(options) {
|
|
|
86
102
|
ignoreFilePath: ignoreFile,
|
|
87
103
|
ignorePatterns: ignoreRules,
|
|
88
104
|
filePushOrder: filePushOrder,
|
|
89
|
-
|
|
105
|
+
fileExtensions: fileExtensions,
|
|
106
|
+
skipSubdirectories: config.ignoreSubdirectories,
|
|
90
107
|
},
|
|
91
108
|
project: {
|
|
92
109
|
scriptId: config.scriptId,
|
|
93
110
|
projectId: config.projectId,
|
|
94
|
-
parentId: config.parentId,
|
|
111
|
+
parentId: firstValue(config.parentId),
|
|
95
112
|
},
|
|
96
113
|
});
|
|
97
114
|
}
|
|
115
|
+
function readFileExtensions(config) {
|
|
116
|
+
let scriptExtensions = ['js', 'gs'];
|
|
117
|
+
let htmlExtensions = ['html'];
|
|
118
|
+
let jsonExtensions = ['json'];
|
|
119
|
+
if (config === null || config === void 0 ? void 0 : config.fileExtension) {
|
|
120
|
+
// legacy fileExtension setting
|
|
121
|
+
scriptExtensions = [config.fileExtension];
|
|
122
|
+
}
|
|
123
|
+
if (config === null || config === void 0 ? void 0 : config.scriptExtensions) {
|
|
124
|
+
scriptExtensions = ensureStringArray(config.scriptExtensions);
|
|
125
|
+
}
|
|
126
|
+
if (config === null || config === void 0 ? void 0 : config.htmlExtensions) {
|
|
127
|
+
htmlExtensions = ensureStringArray(config.htmlExtensions);
|
|
128
|
+
}
|
|
129
|
+
if (config === null || config === void 0 ? void 0 : config.jsonExtensions) {
|
|
130
|
+
jsonExtensions = ensureStringArray(config.jsonExtensions);
|
|
131
|
+
}
|
|
132
|
+
const fixupExtension = (ext) => {
|
|
133
|
+
ext = ext.toLowerCase().trim();
|
|
134
|
+
if (!ext.startsWith('.')) {
|
|
135
|
+
ext = `.${ext}`;
|
|
136
|
+
}
|
|
137
|
+
return ext;
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
SERVER_JS: scriptExtensions.map(fixupExtension),
|
|
141
|
+
HTML: htmlExtensions.map(fixupExtension),
|
|
142
|
+
JSON: jsonExtensions.map(fixupExtension),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
98
145
|
async function findProjectRootdDir(configFilePath) {
|
|
99
146
|
debug('Searching for project root');
|
|
100
147
|
if (configFilePath) {
|
|
@@ -169,3 +216,9 @@ async function hasReadAccess(path) {
|
|
|
169
216
|
}
|
|
170
217
|
return true;
|
|
171
218
|
}
|
|
219
|
+
function firstValue(values) {
|
|
220
|
+
if (Array.isArray(values) && values.length > 0) {
|
|
221
|
+
return values[0];
|
|
222
|
+
}
|
|
223
|
+
return values;
|
|
224
|
+
}
|
package/build/src/core/files.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
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
|
}
|
|
@@ -59,28 +66,36 @@ function createFilenameConflictChecker() {
|
|
|
59
66
|
return file;
|
|
60
67
|
};
|
|
61
68
|
}
|
|
62
|
-
function getFileType(fileName) {
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
function getFileType(fileName, fileExtensions) {
|
|
70
|
+
var _a, _b, _c;
|
|
71
|
+
const originalExtension = path.extname(fileName);
|
|
72
|
+
const extension = originalExtension.toLowerCase();
|
|
73
|
+
if ((_a = fileExtensions['SERVER_JS']) === null || _a === void 0 ? void 0 : _a.includes(extension)) {
|
|
65
74
|
return 'SERVER_JS';
|
|
66
75
|
}
|
|
67
|
-
if (
|
|
68
|
-
return 'JSON';
|
|
69
|
-
}
|
|
70
|
-
if (extension === '.HTML') {
|
|
76
|
+
if ((_b = fileExtensions['HTML']) === null || _b === void 0 ? void 0 : _b.includes(extension)) {
|
|
71
77
|
return 'HTML';
|
|
72
78
|
}
|
|
79
|
+
if (((_c = fileExtensions['JSON']) === null || _c === void 0 ? void 0 : _c.includes(extension)) && path.basename(fileName, originalExtension) === 'appsscript') {
|
|
80
|
+
return 'JSON';
|
|
81
|
+
}
|
|
73
82
|
return undefined;
|
|
74
83
|
}
|
|
75
|
-
function getFileExtension(type) {
|
|
84
|
+
function getFileExtension(type, fileExtensions) {
|
|
76
85
|
// TODO - Include project setting override
|
|
86
|
+
const extensionFor = (type, defaultValue) => {
|
|
87
|
+
if (fileExtensions[type] && fileExtensions[type][0]) {
|
|
88
|
+
return fileExtensions[type][0];
|
|
89
|
+
}
|
|
90
|
+
return defaultValue;
|
|
91
|
+
};
|
|
77
92
|
switch (type) {
|
|
78
93
|
case 'SERVER_JS':
|
|
79
|
-
return 'js';
|
|
94
|
+
return extensionFor('SERVER_JS', '.js');
|
|
80
95
|
case 'JSON':
|
|
81
|
-
return 'json';
|
|
96
|
+
return extensionFor('JSON', '.json');
|
|
82
97
|
case 'HTML':
|
|
83
|
-
return 'html';
|
|
98
|
+
return extensionFor('HTML', '.html');
|
|
84
99
|
default:
|
|
85
100
|
throw new Error('Invalid file type', {
|
|
86
101
|
cause: {
|
|
@@ -96,11 +111,14 @@ function debounceFileChanges(callback, delayMs) {
|
|
|
96
111
|
return function (path) {
|
|
97
112
|
// Already tracked as changed, ignore
|
|
98
113
|
if (collectedPaths.includes(path)) {
|
|
114
|
+
debug('Ignoring pending file change for path %s', path);
|
|
99
115
|
return;
|
|
100
116
|
}
|
|
117
|
+
debug('Debouncing change for path %s', path);
|
|
101
118
|
collectedPaths.push(path);
|
|
102
119
|
clearTimeout(timeoutId);
|
|
103
120
|
timeoutId = setTimeout(() => {
|
|
121
|
+
debug('Firing debounced file');
|
|
104
122
|
callback(collectedPaths);
|
|
105
123
|
collectedPaths = [];
|
|
106
124
|
}, delayMs);
|
|
@@ -119,15 +137,16 @@ export class Files {
|
|
|
119
137
|
const contentDir = this.options.files.contentDir;
|
|
120
138
|
const scriptId = this.options.project.scriptId;
|
|
121
139
|
const script = google.script({ version: 'v1', auth: credentials });
|
|
140
|
+
const fileExtensionMap = this.options.files.fileExtensions;
|
|
122
141
|
try {
|
|
123
142
|
const requestOptions = { scriptId, versionNumber };
|
|
124
|
-
debug('
|
|
143
|
+
debug('Fetching script content, request %o', requestOptions);
|
|
125
144
|
const response = await script.projects.getContent(requestOptions);
|
|
126
145
|
const files = (_a = response.data.files) !== null && _a !== void 0 ? _a : [];
|
|
127
146
|
return files.map(f => {
|
|
128
147
|
var _a, _b, _c;
|
|
129
|
-
const ext = getFileExtension(f.type);
|
|
130
|
-
const localPath = path.relative(process.cwd(), path.resolve(contentDir, `${f.name}
|
|
148
|
+
const ext = getFileExtension(f.type, fileExtensionMap);
|
|
149
|
+
const localPath = path.relative(process.cwd(), path.resolve(contentDir, `${f.name}${ext}`));
|
|
131
150
|
const file = {
|
|
132
151
|
localPath: localPath,
|
|
133
152
|
remotePath: (_a = f.name) !== null && _a !== void 0 ? _a : undefined,
|
|
@@ -148,17 +167,18 @@ export class Files {
|
|
|
148
167
|
assertScriptConfigured(this.options);
|
|
149
168
|
const contentDir = this.options.files.contentDir;
|
|
150
169
|
const ignorePatterns = (_a = this.options.files.ignorePatterns) !== null && _a !== void 0 ? _a : [];
|
|
151
|
-
const recursive = this.options.files.
|
|
170
|
+
const recursive = !this.options.files.skipSubdirectories;
|
|
152
171
|
// Read all filenames as a flattened tree
|
|
153
172
|
// Note: filePaths contain relative paths such as "test/bar.ts", "../../src/foo.js"
|
|
154
|
-
const filelist = await getLocalFiles(contentDir, ignorePatterns, recursive);
|
|
173
|
+
const filelist = Array.from(await getLocalFiles(contentDir, ignorePatterns, recursive));
|
|
155
174
|
const checkDuplicate = createFilenameConflictChecker();
|
|
175
|
+
const fileExtensionMap = this.options.files.fileExtensions;
|
|
156
176
|
const files = await Promise.all(filelist.map(async (filename) => {
|
|
157
177
|
const localPath = path.relative(process.cwd(), path.join(contentDir, filename));
|
|
158
178
|
const resolvedPath = path.relative(contentDir, localPath);
|
|
159
179
|
const parsedPath = path.parse(resolvedPath);
|
|
160
180
|
let remotePath = path.format({ dir: normalizePath(parsedPath.dir), name: parsedPath.name });
|
|
161
|
-
const type = getFileType(localPath);
|
|
181
|
+
const type = getFileType(localPath, fileExtensionMap);
|
|
162
182
|
if (!type) {
|
|
163
183
|
debug('Ignoring unsupported file %s', localPath);
|
|
164
184
|
return undefined;
|
|
@@ -178,21 +198,37 @@ export class Files {
|
|
|
178
198
|
const ignorePatterns = (_a = this.options.files.ignorePatterns) !== null && _a !== void 0 ? _a : [];
|
|
179
199
|
const collector = debounceFileChanges(onFilesChanged, 500);
|
|
180
200
|
const onChange = async (path) => {
|
|
201
|
+
debug('Have file changes: %s', path);
|
|
181
202
|
collector(path);
|
|
182
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
|
+
}
|
|
183
215
|
const watcher = chokidar.watch(this.options.files.contentDir, {
|
|
184
216
|
persistent: true,
|
|
185
217
|
ignoreInitial: true,
|
|
186
218
|
cwd: this.options.files.contentDir,
|
|
187
|
-
ignored:
|
|
188
|
-
return micromatch.not([file], ignorePatterns, { dot: true }).length === 0;
|
|
189
|
-
},
|
|
219
|
+
ignored: matcher,
|
|
190
220
|
});
|
|
191
221
|
watcher.on('ready', onReady); // Push on start
|
|
192
222
|
watcher.on('add', onChange);
|
|
193
223
|
watcher.on('change', onChange);
|
|
194
224
|
watcher.on('unlink', onChange);
|
|
195
|
-
|
|
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
|
+
};
|
|
196
232
|
}
|
|
197
233
|
async getChangedFiles() {
|
|
198
234
|
const [localFiles, remoteFiles] = await Promise.all([this.collectLocalFiles(), this.fetchRemote()]);
|
|
@@ -309,6 +345,16 @@ export class Files {
|
|
|
309
345
|
handleApiError(error);
|
|
310
346
|
}
|
|
311
347
|
}
|
|
348
|
+
checkMissingFilesFromPushOrder(pushedFiles) {
|
|
349
|
+
var _a;
|
|
350
|
+
const missingFiles = [];
|
|
351
|
+
for (const path of (_a = this.options.files.filePushOrder) !== null && _a !== void 0 ? _a : []) {
|
|
352
|
+
const wasPushed = pushedFiles.find(f => f.localPath === path);
|
|
353
|
+
if (!wasPushed) {
|
|
354
|
+
missingFiles.push(path);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
312
358
|
async pull(version) {
|
|
313
359
|
debug('Pulling files');
|
|
314
360
|
assertAuthenticated(this.options);
|
|
@@ -285,9 +285,13 @@ 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'],
|
|
291
|
+
htmlExtensions: this.options.files.fileExtensions['HTML'],
|
|
292
|
+
jsonExtensions: this.options.files.fileExtensions['JSON'],
|
|
290
293
|
filePushOrder: [],
|
|
294
|
+
skipSubdirectories: this.options.files.skipSubdirectories,
|
|
291
295
|
};
|
|
292
296
|
await fs.writeFile(this.options.configFilePath, JSON.stringify(settings, null, 2));
|
|
293
297
|
}
|
package/build/src/core/utils.js
CHANGED
|
@@ -100,7 +100,7 @@ export function handleApiError(error) {
|
|
|
100
100
|
throw new Error('Unexpected error', {
|
|
101
101
|
cause: {
|
|
102
102
|
code: 'UNEPECTED_ERROR',
|
|
103
|
-
message: String(error),
|
|
103
|
+
message: new String(error),
|
|
104
104
|
error: error,
|
|
105
105
|
},
|
|
106
106
|
});
|
|
@@ -119,3 +119,26 @@ export function handleApiError(error) {
|
|
|
119
119
|
},
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
+
export function ensureStringArray(value) {
|
|
123
|
+
if (typeof value === 'string') {
|
|
124
|
+
return [value];
|
|
125
|
+
}
|
|
126
|
+
else if (Array.isArray(value)) {
|
|
127
|
+
// Ensure all elements in the array are strings.
|
|
128
|
+
if (value.every(item => typeof item === 'string')) {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Handle cases where the array contains non-string elements.
|
|
133
|
+
// You could throw an error, filter out non-strings, or convert them to strings.
|
|
134
|
+
// Example: filter out non-strings
|
|
135
|
+
return value.filter(item => typeof item === 'string');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Handle cases where the value is neither a string nor an array of strings.
|
|
140
|
+
// You could throw an error or return an empty array.
|
|
141
|
+
// Example: return an empty array
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function isEnabled(experimentName, defaultValue = false) {
|
|
2
|
+
const envVarName = `CLASP_${experimentName.toUpperCase()}`;
|
|
3
|
+
const envVarValue = process.env[envVarName];
|
|
4
|
+
if (envVarValue === undefined || envVarValue === null) {
|
|
5
|
+
return defaultValue;
|
|
6
|
+
}
|
|
7
|
+
if (envVarValue.toLowerCase() === 'true' || envVarValue === '1') {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
export const INCLUDE_USER_HINT_IN_URL = isEnabled('enable_user_hints');
|
package/build/src/index.js
CHANGED
|
@@ -20,8 +20,11 @@
|
|
|
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');
|
|
26
|
+
// Suppress warnings about punycode and other issues caused by dependencies
|
|
27
|
+
process.removeAllListeners('warning');
|
|
25
28
|
// Ensure any unhandled exception won't go unnoticed
|
|
26
29
|
loudRejection();
|
|
27
30
|
const program = makeProgram();
|
|
@@ -32,7 +35,10 @@ try {
|
|
|
32
35
|
}
|
|
33
36
|
catch (error) {
|
|
34
37
|
debug('Error: %O', error);
|
|
35
|
-
if (error instanceof
|
|
38
|
+
if (error instanceof CommanderError) {
|
|
39
|
+
debug('Ignoring commander error, output already logged');
|
|
40
|
+
}
|
|
41
|
+
else if (error instanceof Error) {
|
|
36
42
|
process.exitCode = 1;
|
|
37
43
|
console.error(error.message);
|
|
38
44
|
}
|
package/build/src/intl.js
CHANGED
|
@@ -25,10 +25,12 @@ function loadMessages(_locale) {
|
|
|
25
25
|
}
|
|
26
26
|
const cache = createIntlCache();
|
|
27
27
|
const locale = getLocale();
|
|
28
|
+
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
28
29
|
debug('Using locale: %s', locale);
|
|
29
30
|
export const intl = createIntl({
|
|
30
31
|
// Locale of the application
|
|
31
32
|
locale,
|
|
33
|
+
timeZone: localTimeZone,
|
|
32
34
|
defaultLocale: 'en',
|
|
33
35
|
messages: loadMessages(locale),
|
|
34
36
|
}, cache);
|
package/docs/run.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
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
|
-
- Create an **OAuth Client ID**
|
|
11
|
-
- `clasp login --creds
|
|
10
|
+
- Create an **OAuth Client ID** of type `Desktop Application`. Download as `client_secret.json`.
|
|
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
|
|
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 `
|
|
41
|
-
1.
|
|
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.
|
|
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",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"watch": "tspc --project tsconfig.json --watch",
|
|
22
22
|
"prepare": "npm run compile",
|
|
23
23
|
"lint": "npm run check",
|
|
24
|
-
"test": "
|
|
25
|
-
"coverage": "
|
|
24
|
+
"test": "mocha",
|
|
25
|
+
"test:coverage": "c8 mocha",
|
|
26
26
|
"prettier": "biome format src test --write",
|
|
27
27
|
"check": "biome check src test && npm run compile",
|
|
28
28
|
"clean": "rm -rf build",
|
|
@@ -39,21 +39,6 @@
|
|
|
39
39
|
],
|
|
40
40
|
"outfile": "src/messages/messages.js"
|
|
41
41
|
},
|
|
42
|
-
"nyc": {
|
|
43
|
-
"extends": "@istanbuljs/nyc-config-typescript",
|
|
44
|
-
"include": [
|
|
45
|
-
"src/**/*.ts"
|
|
46
|
-
],
|
|
47
|
-
"extension": [
|
|
48
|
-
".ts"
|
|
49
|
-
],
|
|
50
|
-
"reporter": [
|
|
51
|
-
"text-summary",
|
|
52
|
-
"html"
|
|
53
|
-
],
|
|
54
|
-
"sourceMap": true,
|
|
55
|
-
"instrument": true
|
|
56
|
-
},
|
|
57
42
|
"repository": {
|
|
58
43
|
"type": "git",
|
|
59
44
|
"url": "https://github.com/google/clasp"
|
|
@@ -130,10 +115,10 @@
|
|
|
130
115
|
"@types/sinon": "^17.0.4",
|
|
131
116
|
"@types/tmp": "^0.2.6",
|
|
132
117
|
"@types/wtfnode": "^0.7.3",
|
|
118
|
+
"c8": "^10.1.3",
|
|
133
119
|
"chai": "^5.1.2",
|
|
134
120
|
"chai-as-promised": "^8.0.1",
|
|
135
121
|
"chai-subset": "^1.6.0",
|
|
136
|
-
"coveralls": "^3.1.1",
|
|
137
122
|
"mocha": "^11.1.0",
|
|
138
123
|
"mock-fs": "^5.4.1",
|
|
139
124
|
"nock": "^14.0.0",
|