@google/clasp 3.0.1-alpha1 → 3.0.2-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
@@ -118,22 +118,22 @@ There are several template projects on GitHub that show how to transform Typescr
118
118
 
119
119
  #### Command renames
120
120
 
121
- Clasp 3.x introdces 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.
121
+ 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
122
 
123
123
  | 2.x | 3.x |
124
124
  |----------------------------|----------------------------------------|
125
- |open | open-script |
126
- |open --web | open-web-app |
127
- |open --addon | open-container |
128
- |open --creds | open-credentials-setup |
129
- |login --creds <file> | login -u <name> --creds <file> |
130
- |logs --open | open-logs |
131
- |logs --setup | N/A |
132
- |apis --open | open-api-console |
133
- |apis enable <api> | enable-api <api> |
134
- |apis disable <api> | disable-api <api> |
135
- |settings | N/A |
136
- |----------------------------|----------------------------------------|
125
+ |`open` | `open-script` |
126
+ |`open --web` | `open-web-app` |
127
+ |`open --addon` | `open-container` |
128
+ |`open --creds` | `open-credentials-setup` |
129
+ |`login --creds <file>` | `login -u <name> --creds <file>` |
130
+ |`logs --open` | `open-logs` |
131
+ |`logs --setup` | N/A |
132
+ |`apis --open` | `open-api-console` |
133
+ |`apis enable <api>` | `enable-api <api>` |
134
+ |`apis disable <api>` | `disable-api <api>` |
135
+ |`deploy -i <id>` | `update-deployment <id>` |
136
+ |`settings` | N/A |
137
137
 
138
138
  Other commands have also been renamed but retain aliases for compatibility.
139
139
 
@@ -263,14 +263,39 @@ You must [associate Google Script project with Google Cloud Platform](https://gi
263
263
 
264
264
  Even if you do not set this manually, clasp will ask this via a prompt to you at the required time.
265
265
 
266
- ### `fileExtension` (optional)
266
+ ### `fileExtension` (deprecated, optional)
267
267
 
268
268
  Specifies the file extension for **local** script files in your Apps Script project.
269
269
 
270
+ ### `scriptExtensions` (optional)
271
+
272
+ 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.
273
+
274
+ When pulling files, the first extension listed is used to write files.
275
+
276
+ Defaults to `[".js", ".gs"]`
277
+
278
+ ### `htmlExtensions` (optional)
279
+
280
+ 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.
281
+
282
+ When pulling files, the first extension listed is used to write files.
283
+
284
+ Defaults to `[".html"]`
285
+
270
286
  ### `filePushOrder` (optional)
271
287
 
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.
288
+ 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.
289
+
290
+ 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.
291
+
292
+ ### `skipSubdirectories` (optional)
273
293
 
294
+ For backwards compatibility with previous behavior where subdirectories
295
+ are ignored if a `.claspignore` file is not present. Clasp provides default
296
+ ignore rules, making the previous warning and behavior confusing. If you
297
+ need to force clasp to ignore subdirectories and do not want to construct
298
+ a `.claspignore` file, set this option to true.
274
299
 
275
300
  ## Reference
276
301
 
@@ -453,6 +478,19 @@ To update/redeploy an existing deployment, provide the deployment ID.
453
478
  - `clasp create-deployment --deploymentId abcd1234` (redeploy and create new version)
454
479
  - `clasp create-deployment -V 7 -d "Updates sidebar logo." -i abdc1234`
455
480
 
481
+ ### Redeploy
482
+
483
+ Updates an existing deployment. Same as `create-deployment -i id`.
484
+
485
+ #### Options
486
+
487
+ - `-V <version>` `--versionNumber <version>`: The project version to deploy at.
488
+ - `-d <description>` `--description <description>`: The deployment description.
489
+
490
+ #### Examples
491
+
492
+ - `clasp update-deployment abcd1234` (redeploy and create new version)
493
+
456
494
  ### Undeploy
457
495
 
458
496
  Undeploys a deployment of a script.
@@ -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);
@@ -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
- .command('create')
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.')
@@ -60,7 +60,7 @@ export const command = new Command('create-script')
60
60
  return files;
61
61
  });
62
62
  files.forEach(f => console.log(`└─ ${f.localPath}`));
63
- 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: "." }] }, {
63
+ 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
64
  count: files.length,
65
65
  });
66
66
  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 = `https://console.developers.google.com/apis/dashboard?project=${projectId}`;
10
- await openUrl(url);
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 = `https://drive.google.com/open?id=${parentId}`;
14
- await openUrl(url);
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 = `https://console.developers.google.com/apis/credentials?project=${projectId}`;
10
- await openUrl(url);
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 = `https://console.cloud.google.com/logs/viewer?project=${projectId}&resource=app_script_function`;
10
- await openUrl(url);
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
- await openUrl(url);
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: "j847fH", defaultMessage: [{ type: 0, value: "Deployment ID is requrired." }] });
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
- await openUrl(url);
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..." }] });
@@ -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} ${timestamp} ${functionName} ${payloadData}`;
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 async function assertScriptConfigured(clasp) {
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 async function assertGcpProjectConfigured(clasp) {
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 (!isInteractive()) {
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
  });
@@ -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
- fileExtension: 'js',
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 fileExtension = config.fileExtension || 'js';
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,7 +102,8 @@ export async function initClaspInstance(options) {
86
102
  ignoreFilePath: ignoreFile,
87
103
  ignorePatterns: ignoreRules,
88
104
  filePushOrder: filePushOrder,
89
- fileExtension: fileExtension,
105
+ fileExtensions: fileExtensions,
106
+ skipSubdirectories: config.ignoreSubdirectories,
90
107
  },
91
108
  project: {
92
109
  scriptId: config.scriptId,
@@ -95,6 +112,36 @@ export async function initClaspInstance(options) {
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) {
@@ -59,28 +59,36 @@ function createFilenameConflictChecker() {
59
59
  return file;
60
60
  };
61
61
  }
62
- function getFileType(fileName) {
63
- const extension = path.extname(fileName).toUpperCase();
64
- if (['.GS', '.JS'].includes(extension)) {
62
+ function getFileType(fileName, fileExtensions) {
63
+ var _a, _b, _c;
64
+ const originalExtension = path.extname(fileName);
65
+ const extension = originalExtension.toLowerCase();
66
+ if ((_a = fileExtensions['SERVER_JS']) === null || _a === void 0 ? void 0 : _a.includes(extension)) {
65
67
  return 'SERVER_JS';
66
68
  }
67
- if (extension === '.JSON' && path.basename(fileName) === 'appsscript.json') {
68
- return 'JSON';
69
- }
70
- if (extension === '.HTML') {
69
+ if ((_b = fileExtensions['HTML']) === null || _b === void 0 ? void 0 : _b.includes(extension)) {
71
70
  return 'HTML';
72
71
  }
72
+ if (((_c = fileExtensions['JSON']) === null || _c === void 0 ? void 0 : _c.includes(extension)) && path.basename(fileName, originalExtension) === 'appsscript') {
73
+ return 'JSON';
74
+ }
73
75
  return undefined;
74
76
  }
75
- function getFileExtension(type) {
77
+ function getFileExtension(type, fileExtensions) {
76
78
  // TODO - Include project setting override
79
+ const extensionFor = (type, defaultValue) => {
80
+ if (fileExtensions[type] && fileExtensions[type][0]) {
81
+ return fileExtensions[type][0];
82
+ }
83
+ return defaultValue;
84
+ };
77
85
  switch (type) {
78
86
  case 'SERVER_JS':
79
- return 'js';
87
+ return extensionFor('SERVER_JS', '.js');
80
88
  case 'JSON':
81
- return 'json';
89
+ return extensionFor('JSON', '.json');
82
90
  case 'HTML':
83
- return 'html';
91
+ return extensionFor('HTML', '.html');
84
92
  default:
85
93
  throw new Error('Invalid file type', {
86
94
  cause: {
@@ -119,15 +127,16 @@ export class Files {
119
127
  const contentDir = this.options.files.contentDir;
120
128
  const scriptId = this.options.project.scriptId;
121
129
  const script = google.script({ version: 'v1', auth: credentials });
130
+ const fileExtensionMap = this.options.files.fileExtensions;
122
131
  try {
123
132
  const requestOptions = { scriptId, versionNumber };
124
- debug('Fetchign script content, request %o', requestOptions);
133
+ debug('Fetching script content, request %o', requestOptions);
125
134
  const response = await script.projects.getContent(requestOptions);
126
135
  const files = (_a = response.data.files) !== null && _a !== void 0 ? _a : [];
127
136
  return files.map(f => {
128
137
  var _a, _b, _c;
129
- const ext = getFileExtension(f.type);
130
- const localPath = path.relative(process.cwd(), path.resolve(contentDir, `${f.name}.${ext}`));
138
+ const ext = getFileExtension(f.type, fileExtensionMap);
139
+ const localPath = path.relative(process.cwd(), path.resolve(contentDir, `${f.name}${ext}`));
131
140
  const file = {
132
141
  localPath: localPath,
133
142
  remotePath: (_a = f.name) !== null && _a !== void 0 ? _a : undefined,
@@ -148,17 +157,18 @@ export class Files {
148
157
  assertScriptConfigured(this.options);
149
158
  const contentDir = this.options.files.contentDir;
150
159
  const ignorePatterns = (_a = this.options.files.ignorePatterns) !== null && _a !== void 0 ? _a : [];
151
- const recursive = this.options.files.ignoreFilePath !== undefined;
160
+ const recursive = !this.options.files.skipSubdirectories;
152
161
  // Read all filenames as a flattened tree
153
162
  // Note: filePaths contain relative paths such as "test/bar.ts", "../../src/foo.js"
154
- const filelist = await getLocalFiles(contentDir, ignorePatterns, recursive);
163
+ const filelist = Array.from(await getLocalFiles(contentDir, ignorePatterns, recursive));
155
164
  const checkDuplicate = createFilenameConflictChecker();
165
+ const fileExtensionMap = this.options.files.fileExtensions;
156
166
  const files = await Promise.all(filelist.map(async (filename) => {
157
167
  const localPath = path.relative(process.cwd(), path.join(contentDir, filename));
158
168
  const resolvedPath = path.relative(contentDir, localPath);
159
169
  const parsedPath = path.parse(resolvedPath);
160
170
  let remotePath = path.format({ dir: normalizePath(parsedPath.dir), name: parsedPath.name });
161
- const type = getFileType(localPath);
171
+ const type = getFileType(localPath, fileExtensionMap);
162
172
  if (!type) {
163
173
  debug('Ignoring unsupported file %s', localPath);
164
174
  return undefined;
@@ -309,6 +319,16 @@ export class Files {
309
319
  handleApiError(error);
310
320
  }
311
321
  }
322
+ checkMissingFilesFromPushOrder(pushedFiles) {
323
+ var _a;
324
+ const missingFiles = [];
325
+ for (const path of (_a = this.options.files.filePushOrder) !== null && _a !== void 0 ? _a : []) {
326
+ const wasPushed = pushedFiles.find(f => f.localPath === path);
327
+ if (!wasPushed) {
328
+ missingFiles.push(path);
329
+ }
330
+ }
331
+ }
312
332
  async pull(version) {
313
333
  debug('Pulling files');
314
334
  assertAuthenticated(this.options);
@@ -286,8 +286,11 @@ export class Project {
286
286
  scriptId: this.options.project.scriptId,
287
287
  rootDir: srcDir,
288
288
  projectId: this.options.project.projectId,
289
- fileExtension: this.options.files.fileExtension,
289
+ scriptExtensions: this.options.files.fileExtensions['SERVER_JS'],
290
+ htmlExtensions: this.options.files.fileExtensions['HTML'],
291
+ jsonExtensions: this.options.files.fileExtensions['JSON'],
290
292
  filePushOrder: [],
293
+ skipSubdirectories: this.options.files.skipSubdirectories,
291
294
  };
292
295
  await fs.writeFile(this.options.configFilePath, JSON.stringify(settings, null, 2));
293
296
  }
@@ -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,16 @@
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
+ 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;
15
+ }
16
+ export const INCLUDE_USER_HINT_IN_URL = isEnabled('enable_user_hints');
@@ -22,6 +22,8 @@ import Debug from 'debug';
22
22
  import loudRejection from 'loud-rejection';
23
23
  import { makeProgram } from './commands/program.js';
24
24
  const debug = Debug('clasp:cli');
25
+ // Suppress warnings about punycode and other issues caused by dependencies
26
+ process.removeAllListeners('warning');
25
27
  // Ensure any unhandled exception won't go unnoticed
26
28
  loudRejection();
27
29
  const program = makeProgram();
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** (Other). Download as `creds.json`.
11
- - `clasp login --creds creds.json` with this downloaded file.
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.
12
12
  - Add the following to `appsscript.json`:
13
13
  ```json
14
14
  "executionApi": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google/clasp",
3
- "version": "3.0.1-alpha1",
3
+ "version": "3.0.2-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": "nyc mocha",
25
- "coverage": "nyc --cache false report --reporter=text-lcov | coveralls",
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",