From 60f6ef0b74d7a00b50748ac9ac3ce72855537954 Mon Sep 17 00:00:00 2001 From: Steven Bazyl <sbazyl@google.com> Date: Tue, 3 Aug 2021 11:31:18 -0600 Subject: [PATCH 1/5] fix: Don't require package.json for simple commands (#840) Moves ts2gas to a dynamic import only when transpiling typescript code. --- src/files.ts | 49 +++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/files.ts b/src/files.ts index b350a351..faf6a00a 100644 --- a/src/files.ts +++ b/src/files.ts @@ -4,7 +4,6 @@ import multimatch from 'multimatch'; import path from 'path'; import pMap from 'p-map'; import recursive from 'recursive-readdir'; -import ts2gas from 'ts2gas'; import typescript from 'typescript'; import {loadAPICredentials, script} from './auth.js'; @@ -41,20 +40,22 @@ interface ProjectFile { readonly type: string; } -const projectFileWithContent = (file: ProjectFile, transpileOptions: TranspileOptions): ProjectFile => { - const source = fs.readFileSync(file.name).toString(); - const type = getApiFileType(file.name); - - return type === 'TS' - ? // Transpile TypeScript to Google Apps Script - // @see github.com/grant/ts2gas - { - ...file, - source: ts2gas(source, transpileOptions), - type: 'SERVER_JS', - } - : {...file, source, type}; -}; +async function transpile(source: string, transpileOptions: TranspileOptions): Promise<string> { + const ts2gas = await import('ts2gas'); + return ts2gas.default(source, transpileOptions); +} + +async function projectFileWithContent(file: ProjectFile, transpileOptions: TranspileOptions): Promise<ProjectFile> { + const content = await fs.readFile(file.name); + let source = content.toString(); + let type = getApiFileType(file.name); + + if (type === 'TS') { + source = await transpile(source, transpileOptions); + type = 'SERVER_JS'; + } + return {...file, source, type}; +} const ignoredProjectFile = (file: ProjectFile): ProjectFile => ({...file, source: '', isIgnored: true, type: ''}); @@ -102,7 +103,8 @@ export const getAllProjectFiles = async (rootDir: string = path.join('.', '/')): }); files.sort((a, b) => a.name.localeCompare(b.name)); - return getContentOfProjectFiles(files).map((file: ProjectFile): ProjectFile => { + const filesWithContent = await getContentOfProjectFiles(files); + return filesWithContent.map((file: ProjectFile): ProjectFile => { // Loop through files that are not ignored from `.claspignore` if (!file.isIgnored) { // Prevent node_modules/@types/ @@ -142,14 +144,16 @@ export const splitProjectFiles = (files: ProjectFile[]): [ProjectFile[], Project files.filter(file => file.isIgnored), ]; -const getContentOfProjectFiles = (files: ProjectFile[]) => { +async function getContentOfProjectFiles(files: ProjectFile[]) { const transpileOpttions = getTranspileOptions(); - return files.map(file => (file.isIgnored ? file : projectFileWithContent(file, transpileOpttions))); -}; + const getContent = (file: ProjectFile) => (file.isIgnored ? file : projectFileWithContent(file, transpileOpttions)); + return Promise.all(files.map(getContent)); +} -const getAppsScriptFilesFromProjectFiles = (files: ProjectFile[], rootDir: string) => - getContentOfProjectFiles(files).map((file): AppsScriptFile => { +async function getAppsScriptFilesFromProjectFiles(files: ProjectFile[], rootDir: string) { + const filesWithContent = await getContentOfProjectFiles(files); + return filesWithContent.map(file => { const {name, source, type} = file; return { @@ -158,6 +162,7 @@ const getAppsScriptFilesFromProjectFiles = (files: ProjectFile[], rootDir: strin type, // The file extension }; }); +} // This statement customizes the order in which the files are pushed. // It puts the files in the setting's filePushOrder first. @@ -373,7 +378,7 @@ export const pushFiles = async (silent = false) => { if (toPush.length > 0) { const orderedFiles = getOrderedProjectFiles(toPush, filePushOrder); - const files = getAppsScriptFilesFromProjectFiles(orderedFiles, rootDir ?? path.join('.', '/')); + const files = await getAppsScriptFilesFromProjectFiles(orderedFiles, rootDir ?? path.join('.', '/')); const filenames = orderedFiles.map(file => file.name); // Start pushing. From 07d37185021f700e3ba5d9a242a8b561e533292f Mon Sep 17 00:00:00 2001 From: Steven Bazyl <sbazyl@google.com> Date: Tue, 3 Aug 2021 15:35:53 -0600 Subject: [PATCH 2/5] fix: Shut down server faster during logins. Avoids long pause after login completes. --- package-lock.json | 30 ++++++++++++++++++++++++++++++ package.json | 2 ++ src/auth.ts | 4 +++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 288f4374..f38ab7ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "p-map": "^5.0.0", "read-pkg-up": "^8.0.0", "recursive-readdir": "^2.2.2", + "server-destroy": "^1.0.1", "split-lines": "^3.0.0", "strip-bom": "^5.0.0", "ts2gas": "^4.0.0", @@ -49,6 +50,7 @@ "@types/mocha": "^8.2.2", "@types/node": "^12.20.15", "@types/recursive-readdir": "^2.2.0", + "@types/server-destroy": "^1.0.1", "@types/tmp": "^0.2.0", "@types/wtfnode": "^0.7.0", "chai": "^4.3.4", @@ -710,6 +712,15 @@ "@types/node": "*" } }, + "node_modules/@types/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-77QGr7waZbE0Y0uF+G+uH3H3SmhyA78Jf2r5r7QSrpg0U3kSXduWpGjzP9PvPLR/KCy+kHjjpnugRHsYTnHopg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/through": { "version": "0.0.30", "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz", @@ -6246,6 +6257,11 @@ "randombytes": "^2.1.0" } }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=" + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -7703,6 +7719,15 @@ "@types/node": "*" } }, + "@types/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-77QGr7waZbE0Y0uF+G+uH3H3SmhyA78Jf2r5r7QSrpg0U3kSXduWpGjzP9PvPLR/KCy+kHjjpnugRHsYTnHopg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/through": { "version": "0.0.30", "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz", @@ -11731,6 +11756,11 @@ "randombytes": "^2.1.0" } }, + "server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=" + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", diff --git a/package.json b/package.json index e05c9278..db00874f 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "p-map": "^5.0.0", "read-pkg-up": "^8.0.0", "recursive-readdir": "^2.2.2", + "server-destroy": "^1.0.1", "split-lines": "^3.0.0", "strip-bom": "^5.0.0", "ts2gas": "^4.0.0", @@ -97,6 +98,7 @@ "@types/mocha": "^8.2.2", "@types/node": "^12.20.15", "@types/recursive-readdir": "^2.2.0", + "@types/server-destroy": "^1.0.1", "@types/tmp": "^0.2.0", "@types/wtfnode": "^0.7.0", "chai": "^4.3.4", diff --git a/src/auth.ts b/src/auth.ts index 0d32c2b6..7c7aea14 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -17,6 +17,7 @@ import type {ReadonlyDeep} from 'type-fest'; import type {ClaspToken} from './dotfile'; import type {ClaspCredentials} from './utils'; +import enableDestroy from 'server-destroy'; /** * Authentication with Google's APIs. @@ -217,6 +218,7 @@ const authorizeWithLocalhost = async ( // the server port needed to set up the Oauth2Client. const server = await new Promise<Server>(resolve => { const s = createServer(); + enableDestroy(s); s.listen(0, () => resolve(s)); }); const {port} = server.address() as AddressInfo; @@ -240,7 +242,7 @@ const authorizeWithLocalhost = async ( console.log(LOG.AUTHORIZE(authUrl)); (async () => await open(authUrl))(); }); - server.close(); + server.destroy(); return (await client.getToken(authCode)).tokens; }; From 3dc2ff6e8f03f7677283b28cd91cfea8f54cf7cd Mon Sep 17 00:00:00 2001 From: Steven Bazyl <sbazyl@google.com> Date: Mon, 9 Aug 2021 13:20:43 -0600 Subject: [PATCH 3/5] Rework config paths -- removes PathProxy, fixes a few issues with CLI overrides not being honored. --- package-lock.json | 1 + package.json | 1 + src/apiutils.ts | 2 +- src/commands/clone.ts | 13 ++-- src/commands/create.ts | 16 +++-- src/commands/logout.ts | 36 ++-------- src/commands/logs.ts | 8 +-- src/commands/open.ts | 4 +- src/commands/push.ts | 7 +- src/conf.ts | 155 +++++++++++++++++++++-------------------- src/dotfile.ts | 13 ++-- src/files.ts | 8 ++- src/index.ts | 26 ++++--- src/manifest.ts | 8 +-- src/messages.ts | 26 +++---- src/path-proxy.ts | 131 ---------------------------------- src/utils.ts | 25 ++----- test/test.ts | 6 +- 18 files changed, 174 insertions(+), 312 deletions(-) delete mode 100644 src/path-proxy.ts diff --git a/package-lock.json b/package-lock.json index f38ab7ec..73e38ad7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "commander": "^7.2.0", "debounce": "^1.2.1", "dotf": "^2.0.0", + "find-up": "^5.0.0", "fs-extra": "^10.0.0", "fuzzy": "^0.1.3", "google-auth-library": "^7.1.2", diff --git a/package.json b/package.json index db00874f..93e70c44 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "commander": "^7.2.0", "debounce": "^1.2.1", "dotf": "^2.0.0", + "find-up": "^5.0.0", "fs-extra": "^10.0.0", "fuzzy": "^0.1.3", "google-auth-library": "^7.1.2", diff --git a/src/apiutils.ts b/src/apiutils.ts index 09acd9c3..84d97362 100644 --- a/src/apiutils.ts +++ b/src/apiutils.ts @@ -50,7 +50,7 @@ const getProjectIdOrDie = async (): Promise<string> => { return projectId; } - throw new ClaspError(ERROR.NO_GCLOUD_PROJECT); + throw new ClaspError(ERROR.NO_GCLOUD_PROJECT()); }; // /** diff --git a/src/commands/clone.ts b/src/commands/clone.ts index 7049f046..5cbcd33f 100644 --- a/src/commands/clone.ts +++ b/src/commands/clone.ts @@ -8,6 +8,9 @@ import {ERROR, LOG} from '../messages.js'; import {extractScriptId} from '../urls.js'; import {checkIfOnlineOrDie, saveProject, spinner} from '../utils.js'; import status from './status.js'; +import {Conf} from '../conf.js'; + +const config = Conf.get(); interface CommandOption { readonly rootDir: string; @@ -26,18 +29,20 @@ export default async ( versionNumber: number | undefined, options: CommandOption ): Promise<void> => { + if (options.rootDir) { + config.projectRootDirectory = options.rootDir; + } await checkIfOnlineOrDie(); if (hasProject()) { - throw new ClaspError(ERROR.FOLDER_EXISTS); + throw new ClaspError(ERROR.FOLDER_EXISTS()); } const id = scriptId ? extractScriptId(scriptId) : await getScriptId(); spinner.start(LOG.CLONING); - const {rootDir} = options; - await saveProject({scriptId: id, rootDir}, false); - await writeProjectFiles(await fetchProject(id, versionNumber), rootDir); + await saveProject({scriptId: id, rootDir: config.projectRootDirectory}, false); + await writeProjectFiles(await fetchProject(id, versionNumber), config.projectRootDirectory); await status(); }; diff --git a/src/commands/create.ts b/src/commands/create.ts index 00e5c543..b773a365 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -13,6 +13,9 @@ import { spinner, stopSpinner, } from '../utils.js'; +import {Conf} from '../conf.js'; + +const config = Conf.get(); interface CommandOption { readonly parentId?: string; @@ -30,10 +33,14 @@ interface CommandOption { * If not specified, clasp will default to the current directory. */ export default async (options: CommandOption): Promise<void> => { + if (options.rootDir) { + config.projectRootDirectory = options.rootDir; + } + // Handle common errors. await checkIfOnlineOrDie(); if (hasProject()) { - throw new ClaspError(ERROR.FOLDER_EXISTS); + throw new ClaspError(ERROR.FOLDER_EXISTS()); } await loadAPICredentials(); @@ -98,10 +105,9 @@ export default async (options: CommandOption): Promise<void> => { const scriptId = data.scriptId ?? ''; console.log(LOG.CREATE_PROJECT_FINISH(filetype, scriptId)); - const {rootDir} = options; - await saveProject({scriptId, rootDir, parentId: parentId ? [parentId] : undefined}, false); + await saveProject({scriptId, rootDir: config.projectRootDirectory, parentId: parentId ? [parentId] : undefined}, false); - if (!manifestExists(rootDir)) { - await writeProjectFiles(await fetchProject(scriptId), rootDir); // Fetches appsscript.json, o.w. `push` breaks + if (!manifestExists(config.projectRootDirectory)) { + await writeProjectFiles(await fetchProject(scriptId), config.projectRootDirectory); // Fetches appsscript.json, o.w. `push` breaks } }; diff --git a/src/commands/logout.ts b/src/commands/logout.ts index ba3cbad3..1f0c0d6f 100644 --- a/src/commands/logout.ts +++ b/src/commands/logout.ts @@ -1,40 +1,16 @@ import {Conf} from '../conf.js'; -import {DOTFILE} from '../dotfile.js'; -import {hasOauthClientSettings} from '../utils.js'; +import fs from 'fs'; -const {auth} = Conf.get(); +const config = Conf.get(); /** * Logs out the user by deleting credentials. */ export default async (): Promise<void> => { - let previousPath: string | undefined; - - if (hasOauthClientSettings(true)) { - if (auth.isDefault()) { - // If no local auth defined, try current directory - previousPath = auth.path; - auth.path = '.'; - } - - await DOTFILE.AUTH().delete(); - - if (previousPath) { - auth.path = previousPath; - } + if (config.auth && fs.existsSync(config.auth)) { + fs.unlinkSync(config.auth); } - - if (hasOauthClientSettings()) { - if (!auth.isDefault()) { - // If local auth defined, try with default (global) - previousPath = auth.path; - auth.path = ''; - } - - await DOTFILE.AUTH().delete(); - - if (previousPath) { - auth.path = previousPath; - } + if (config.authLocal && fs.existsSync(config.authLocal)) { + fs.unlinkSync(config.authLocal); } }; diff --git a/src/commands/logs.ts b/src/commands/logs.ts index 16735c37..d9bb681c 100644 --- a/src/commands/logs.ts +++ b/src/commands/logs.ts @@ -153,12 +153,12 @@ const setupLogs = async (projectSettings: ProjectSettings): Promise<string> => { const dotfile = DOTFILE.PROJECT(); if (!dotfile) { - throw new ClaspError(ERROR.SETTINGS_DNE); + throw new ClaspError(ERROR.SETTINGS_DNE()); } const settings = await dotfile.read<ProjectSettings>(); if (!settings.scriptId) { - throw new ClaspError(ERROR.SCRIPT_ID_DNE); + throw new ClaspError(ERROR.SCRIPT_ID_DNE()); } const {projectId} = await projectIdPrompt(); @@ -186,7 +186,7 @@ const fetchAndPrintLogs = async ( ): Promise<void> => { // Validate projectId if (!projectId) { - throw new ClaspError(ERROR.NO_GCLOUD_PROJECT); + throw new ClaspError(ERROR.NO_GCLOUD_PROJECT()); } if (!isValidProjectId(projectId)) { @@ -195,7 +195,7 @@ const fetchAndPrintLogs = async ( const {isLocalCreds} = await loadAPICredentials(); - spinner.start(`${isLocalCreds ? LOG.LOCAL_CREDS : ''}${LOG.GRAB_LOGS}`); + spinner.start(`${isLocalCreds ? LOG.LOCAL_CREDS() : ''}${LOG.GRAB_LOGS}`); // Create a time filter (timestamp >= "2016-11-29T23:00:00Z") // https://cloud.google.com/logging/docs/view/advanced-filters#search-by-time diff --git a/src/commands/open.ts b/src/commands/open.ts index b8d94ce9..3d4c20c4 100644 --- a/src/commands/open.ts +++ b/src/commands/open.ts @@ -42,7 +42,7 @@ export default async (scriptId: string, options: CommandOption): Promise<void> = if (options.creds) { const {projectId} = projectSettings; if (!projectId) { - throw new ClaspError(ERROR.NO_GCLOUD_PROJECT); + throw new ClaspError(ERROR.NO_GCLOUD_PROJECT()); } console.log(LOG.OPEN_CREDS(projectId)); @@ -69,7 +69,7 @@ export default async (scriptId: string, options: CommandOption): Promise<void> = const openAddon = async (projectSettings: ProjectSettings) => { const {parentId: parentIdList = []} = projectSettings; if (parentIdList.length === 0) { - throw new ClaspError(ERROR.NO_PARENT_ID); + throw new ClaspError(ERROR.NO_PARENT_ID()); } if (parentIdList.length > 1) { diff --git a/src/commands/push.ts b/src/commands/push.ts index 65a9bafe..2999394c 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -19,9 +19,10 @@ import type {ProjectSettings} from '../dotfile'; const {debounce} = debouncePkg; const {readFileSync} = fs; -const {project} = Conf.get(); const WATCH_DEBOUNCE_MS = 1000; +const config = Conf.get(); + interface CommandOption { readonly watch?: boolean; readonly force?: boolean; @@ -88,8 +89,8 @@ const confirmManifestUpdate = async (): Promise<boolean> => (await overwriteProm * @returns {Promise<boolean>} */ const manifestHasChanges = async (projectSettings: ProjectSettings): Promise<boolean> => { - const {scriptId, rootDir = project.resolvedDir} = projectSettings; - const localManifest = readFileSync(path.join(rootDir, PROJECT_MANIFEST_FILENAME), FS_OPTIONS); + const {scriptId, rootDir = config.projectRootDirectory} = projectSettings; + const localManifest = readFileSync(path.join(rootDir!, PROJECT_MANIFEST_FILENAME), FS_OPTIONS); const remoteFiles = await fetchProject(scriptId, undefined, true); const remoteManifest = remoteFiles.find(file => file.name === PROJECT_MANIFEST_BASENAME); if (remoteManifest) { diff --git a/src/conf.ts b/src/conf.ts index c95b87ef..db324a8f 100644 --- a/src/conf.ts +++ b/src/conf.ts @@ -2,7 +2,7 @@ import os from 'os'; import path from 'path'; import {PROJECT_NAME} from './constants.js'; -import {PathProxy} from './path-proxy.js'; +import findUp from 'find-up'; /** * supported environment variables @@ -18,62 +18,97 @@ enum ENV { /** * A Singleton class to hold configuration related objects. * Use the `get()` method to access the unique singleton instance. + * + * Resolution order for paths is: + * - Explicitly set paths (via CLI option) + * - Env var + * - Well-known location + * + * */ export class Conf { + private _root: string | undefined; + private _projectConfig: string | undefined; + private _ignore: string | undefined; + private _auth: string | undefined; + private _authLocal: string | undefined; + private static _instance: Conf; - /** - * This dotfile saves clasp project information, local to project directory. - */ - readonly project: PathProxy; - /** - * This dotfile stores information about ignoring files on `push`. Like .gitignore. - */ - readonly ignore: IgnoreFile; - /** - * This dotfile saves auth information. Should never be committed. - * There are 2 types: personal & global: - * - Global: In the $HOME directory. - * - Personal: In the local directory. - * @see {ClaspToken} - */ - readonly auth: AuthFile; - // Local auth for backwards compatibility - readonly authLocal: AuthFile; - // readonly manifest: PathProxy; /** * Private to prevent direct construction calls with the `new` operator. */ - private constructor() { - /** - * Helper to set the PathProxy path if an environment variables is set. - * - * *Note: Empty values (i.e. '') are not accounted for.* - */ - const setPathWithEnvVar = (varName: string, file: PathProxy) => { - const envVar = process.env[varName]; - if (envVar) { - file.path = envVar; + private constructor() {} + + set projectRootDirectory(path: string | undefined) { + this._root = path; + this._projectConfig = undefined; // Force recalculation of path if root chanaged + } + + get projectRootDirectory() { + if (this._root === undefined) { + const configPath = findUp.sync(`.${PROJECT_NAME}.json`); + if (configPath !== undefined) { + this._root = path.dirname(configPath); + } else { + this._root = process.cwd(); } - }; + } + return this._root; + } + + set projectConfig(filePath: string | undefined) { + this._projectConfig = filePath; + if (filePath) { + this._root = path.dirname(filePath); // Root dir must be same dir as config + } + } + + get projectConfig() { + if (this._projectConfig === undefined && this.projectRootDirectory) { + this._projectConfig = this.buildPathOrUseEnv(`.${PROJECT_NAME}.json`, this.projectRootDirectory, ENV.DOT_CLASP_PROJECT); + } + return this._projectConfig; + } + + set ignore(path: string | undefined) { + this._ignore = path; + } + + get ignore() { + if (this._ignore === undefined && this.projectRootDirectory) { + this._ignore = this.buildPathOrUseEnv(`.${PROJECT_NAME}ignore`, this.projectRootDirectory, ENV.DOT_CLASP_IGNORE); + } + return this._ignore; + } - // default `project` path is `./.clasp.json` - this.project = new PathProxy({dir: '.', base: `.${PROJECT_NAME}.json`}); + set auth(path: string | undefined) { + this._auth = path; + } + + get auth() { + if (this._auth === undefined) { + this._auth = this.buildPathOrUseEnv(`.${PROJECT_NAME}rc.json`, os.homedir(), ENV.DOT_CLASP_AUTH); + } + return this._auth; + } - // default `ignore` path is `~/.claspignore` - // IgnoreFile class implements custom `.resolve()` rules - this.ignore = new IgnoreFile({dir: os.homedir(), base: `.${PROJECT_NAME}ignore`}); + set authLocal(path: string | undefined) { + this._authLocal = path; + } - // default `auth` path is `~/.clasprc.json` - // default local auth path is './.clasprc.json' - this.auth = new AuthFile({dir: os.homedir(), base: `.${PROJECT_NAME}rc.json`}); - this.authLocal = new AuthFile({dir: '.', base: `.${PROJECT_NAME}rc.json`}); + get authLocal() { + if (this._authLocal === undefined && this.projectRootDirectory) { + this._authLocal = this.buildPathOrUseEnv(`.${PROJECT_NAME}rc.json`, this.projectRootDirectory, ENV.DOT_CLASP_AUTH); + } + return this._authLocal; + } - // resolve environment variables - setPathWithEnvVar(ENV.DOT_CLASP_PROJECT, this.project); - setPathWithEnvVar(ENV.DOT_CLASP_IGNORE, this.ignore); - setPathWithEnvVar(ENV.DOT_CLASP_AUTH, this.auth); - setPathWithEnvVar(ENV.DOT_CLASP_AUTH, this.authLocal); + private buildPathOrUseEnv(filename: string, root: string, envName?: string): string { + if (envName && process.env[envName] !== undefined) { + return process.env[envName]!; + } + return path.join(root, filename); } /** @@ -89,33 +124,3 @@ export class Conf { return Conf._instance; } } - -class AuthFile extends PathProxy { - /** - * Rules to resolves path: - * - * - if default path, use as is - * - otherwise use super.resolve() - * - * @returns {string} - */ - resolve(): string { - return this.isDefault() ? path.join(this._default.dir, this._default.base) : super.resolve(); - } -} - -class IgnoreFile extends PathProxy { - /** - * Rules to resolves path: - * - * - if default, use the **project** directory and the default base filename - * - otherwise use super.resolve() - * - * @returns {string} - */ - resolve(): string { - return this.isDefault() ? path.join(Conf.get().project.resolvedDir, this._default.base) : super.resolve(); - } -} - -// TODO: add more subclasses if necessary diff --git a/src/dotfile.ts b/src/dotfile.ts index 2f45b91a..0a1d8158 100644 --- a/src/dotfile.ts +++ b/src/dotfile.ts @@ -24,7 +24,7 @@ import type {Credentials, OAuth2ClientOptions} from 'google-auth-library'; export type {Dotfile} from 'dotf'; -const {auth, authLocal, ignore, project} = Conf.get(); +const config = Conf.get(); // Project settings file (Saved in .clasp.json) export interface ProjectSettings { @@ -58,8 +58,9 @@ export const DOTFILE = { * @return {Promise<string[]>} A list of file glob patterns */ IGNORE: async () => { - const ignorePath = ignore.resolve(); - const content = fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, FS_OPTIONS) : defaultClaspignore; + const ignorePath = config.ignore; + const content = + ignorePath && fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, FS_OPTIONS) : defaultClaspignore; return splitLines(stripBom(content)).filter((name: string) => name.length > 0); }, @@ -70,7 +71,7 @@ export const DOTFILE = { */ PROJECT: () => { // ! TODO: currently limited if filename doesn't start with a dot '.' - const {dir, base} = path.parse(project.resolve()); + const {dir, base} = path.parse(config.projectConfig!); if (base[0] === '.') { return dotf(dir || '.', base.slice(1)); } @@ -78,9 +79,9 @@ export const DOTFILE = { }, // Stores {ClaspCredentials} AUTH: (local?: boolean) => { - const configPath = local ? authLocal : auth; + const configPath = local ? config.authLocal : config.auth; // ! TODO: currently limited if filename doesn't start with a dot '.' - const {dir, base} = path.parse(configPath.resolve()); + const {dir, base} = path.parse(configPath!); if (base[0] === '.') { return dotf(dir || '.', base.slice(1)); } diff --git a/src/files.ts b/src/files.ts index faf6a00a..db723930 100644 --- a/src/files.ts +++ b/src/files.ts @@ -24,7 +24,7 @@ import { import type {TranspileOptions} from 'typescript'; const {parseConfigFileTextToJson} = typescript; -const {project} = Conf.get(); +const config = Conf.get(); // An Apps Script API File interface AppsScriptFile { @@ -207,14 +207,16 @@ export const getLocalFileType = (type: string, fileExtension?: string): string = * Returns true if the user has a clasp project. * @returns {boolean} If .clasp.json exists. */ -export const hasProject = (): boolean => fs.existsSync(project.resolve()); +export const hasProject = (): boolean => { + return config.projectConfig !== undefined && fs.existsSync(config.projectConfig); +}; /** * Returns in tsconfig.json. * @returns {TranspileOptions} if tsconfig.json not exists, return an empty object. */ const getTranspileOptions = (): TranspileOptions => { - const tsconfigPath = path.join(project.resolvedDir, 'tsconfig.json'); + const tsconfigPath = path.join(config.projectRootDirectory!, 'tsconfig.json'); return fs.existsSync(tsconfigPath) ? { diff --git a/src/index.ts b/src/index.ts index 4279b122..f772b152 100755 --- a/src/index.ts +++ b/src/index.ts @@ -52,13 +52,14 @@ import versions from './commands/versions.js'; import {Conf} from './conf.js'; import {PROJECT_NAME} from './constants.js'; import {spinner, stopSpinner} from './utils.js'; +import fs from "fs"; const __dirname = dirname(fileURLToPath(import.meta.url)); let beforeExit = () => {}; // instantiate the config singleton (and loads environment variables as a side effect) -const {auth, ignore, project} = Conf.get(); +const config = Conf.get(); // Ensure any unhandled exception won't go unnoticed loudRejection(); @@ -83,8 +84,8 @@ commander.name(PROJECT_NAME).usage('<command> [options]').description(`${PROJECT */ commander .option('-A, --auth <file>', "path to an auth file or a folder with a '.clasprc.json' file.") - .on('option:auth', () => { - auth.path = commander['auth']; + .on('option:auth', (auth) => { + config.auth = auth; }); /** @@ -92,8 +93,8 @@ commander */ commander .option('-I, --ignore <file>', "path to an ignore file or a folder with a '.claspignore' file.") - .on('option:ignore', () => { - ignore.path = commander['ignore']; + .on('option:ignore', (ignore) => { + config.ignore = ignore; }); /** @@ -101,8 +102,13 @@ commander */ commander .option('-P, --project <file>', "path to a project file or to a folder with a '.clasp.json' file.") - .on('option:project', () => { - project.path = commander['project']; + .on('option:project', (path) => { + const stats = fs.lstatSync(path); + if (stats.isDirectory()) { + config.projectRootDirectory = path; + } else { + config.projectConfig = path; + } }); /** @@ -389,9 +395,9 @@ commander .command('paths') .description('List current config files path') .action(() => { - console.log('project', project.path, project.isDefault(), project.resolve()); - console.log('ignore', ignore.path, ignore.isDefault(), ignore.resolve()); - console.log('auth', auth.path, auth.isDefault(), auth.resolve()); + console.log('project', config.projectConfig); + console.log('ignore', config.ignore); + console.log('auth', config.auth); }); const [_bin, _sourcePath, ...args] = process.argv; diff --git a/src/manifest.ts b/src/manifest.ts index 8be6a6a2..c758bc10 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -11,14 +11,14 @@ import {getProjectSettings, parseJsonOrDie} from './utils.js'; import type {AdvancedService} from './apis'; -const {project} = Conf.get(); +const config = Conf.get(); /*** Gets the path to manifest for given `rootDir` */ const getManifestPath = (rootDir: string): string => path.join(rootDir, PROJECT_MANIFEST_FILENAME); /** Gets the `rootDir` from given project */ const getRootDir = (projectSettings: ProjectSettings): string => - typeof projectSettings.rootDir === 'string' ? projectSettings.rootDir : project.resolvedDir; + typeof projectSettings.rootDir === 'string' ? projectSettings.rootDir : config.projectRootDirectory!; /** * Checks if the rootDir appears to be a valid project. @@ -27,8 +27,8 @@ const getRootDir = (projectSettings: ProjectSettings): string => * * @return {boolean} True if valid project, false otherwise */ -export const manifestExists = (rootDir: string = project.resolvedDir): boolean => - fs.existsSync(getManifestPath(rootDir)); +export const manifestExists = (rootDir = config.projectRootDirectory): boolean => + rootDir !== undefined && fs.existsSync(getManifestPath(rootDir)); /** * Reads the appsscript.json manifest file. diff --git a/src/messages.ts b/src/messages.ts index 80c65a8d..0074cf1d 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -4,7 +4,7 @@ import {Conf} from './conf.js'; import {PROJECT_MANIFEST_FILENAME, PROJECT_NAME} from './constants.js'; import {URL} from './urls.js'; -const {auth, ignore, project} = Conf.get(); +const config = Conf.get(); /** Human friendly Google Drive file type name */ const fileTypeName = new Map<string, string>([ @@ -44,7 +44,7 @@ Forgot ${PROJECT_NAME} commands? Get help:\n ${PROJECT_NAME} --help`, DEPLOYMENT_COUNT: 'Unable to deploy; Scripts may only have up to 20 versioned deployments at a time.', DRIVE: 'Something went wrong with the Google Drive API', EXECUTE_ENTITY_NOT_FOUND: 'Script API executable not published/deployed.', - FOLDER_EXISTS: `Project file (${project.resolve()}) already exists.`, + FOLDER_EXISTS: () => `Project file (${config.projectConfig}) already exists.`, FS_DIR_WRITE: 'Could not create directory.', FS_FILE_WRITE: 'Could not write file.', INVALID_JSON: 'Input params not Valid JSON string. Please fix and try again', @@ -58,8 +58,8 @@ Forgot ${PROJECT_NAME} commands? Get help:\n ${PROJECT_NAME} --help`, NO_CREDENTIALS: (local: boolean) => `Could not read API credentials. Are you logged in ${local ? 'locally' : 'globally'}?`, NO_FUNCTION_NAME: 'N/A', - NO_GCLOUD_PROJECT: `No projectId found in your ${project.resolve()} file.`, - NO_PARENT_ID: `No parentId or empty parentId found in your ${project.resolve()} file.`, + NO_GCLOUD_PROJECT: () => `No projectId found in your ${config.projectConfig} file.`, + NO_PARENT_ID: () => `No parentId or empty parentId found in your ${config.projectConfig} file.`, NO_LOCAL_CREDENTIALS: `Requires local crendetials:\n\n ${PROJECT_NAME} login --creds <file.json>`, NO_MANIFEST: (filename: string) => `Manifest: ${filename} invalid. \`create\` or \`clone\` a project first.`, NO_NESTED_PROJECTS: '\nNested clasp projects are not supported.', @@ -76,14 +76,14 @@ Forgot ${PROJECT_NAME} commands? Get help:\n ${PROJECT_NAME} --help`, RATE_LIMIT: 'Rate limit exceeded. Check quota.', RUN_NODATA: 'Script execution API returned no data.', READ_ONLY_DELETE: 'Unable to delete read-only deployment.', - SCRIPT_ID_DNE: `No scriptId found in your ${project.resolve()} file.`, + SCRIPT_ID_DNE: () => `No scriptId found in your ${config.projectConfig} file.`, SCRIPT_ID_INCORRECT: (scriptId: string) => `The scriptId "${scriptId}" looks incorrect. Did you provide the correct scriptId?`, SCRIPT_ID: `Could not find script. Did you provide the correct scriptId? Are you logged in to the correct account with the script?`, - SETTINGS_DNE: ` -No valid ${project.resolve()} project file. You may need to \`create\` or \`clone\` a project first.`, + SETTINGS_DNE: () => ` +No valid ${config.projectConfig} project file. You may need to \`create\` or \`clone\` a project first.`, UNAUTHENTICATED_LOCAL: 'Error: Local client credentials unauthenticated. Check scopes/authorization.', UNAUTHENTICATED: 'Error: Unauthenticated request: Please try again.', UNKNOWN_KEY: (key: string) => `Unknown key "${key}"`, @@ -102,9 +102,9 @@ export const LOG = { AUTH_PAGE_SUCCESSFUL: 'Logged in! You may close this page. ', // HTML Redirect Page AUTH_SUCCESSFUL: 'Authorization successful.', AUTHORIZE: (authUrl: string) => `🔑 Authorize ${PROJECT_NAME} by visiting this url:\n${authUrl}\n`, - CLONE_SUCCESS: ( - fileCount: number - ) => `Warning: files in subfolder are not accounted for unless you set a '${ignore.resolve()}' file. + CLONE_SUCCESS: (fileCount: number) => `Warning: files in subfolder are not accounted for unless you set a '${ + config.ignore + }' file. Cloned ${fileCount} ${fileCount === 1 ? 'file' : 'files'}.`, CLONING: 'Cloning files…', CLONE_SCRIPT_QUESTION: 'Clone which script?', @@ -129,7 +129,7 @@ Cloned ${fileCount} ${fileCount === 1 ? 'file' : 'files'}.`, GET_PROJECT_ID_INSTRUCTIONS: `Go to *Resource > Cloud Platform Project…* and copy your projectId (including "project-id-")`, GIVE_DESCRIPTION: 'Give a description: ', - LOCAL_CREDS: `Using local credentials: ${auth.resolve()} 🔐 `, + LOCAL_CREDS: () =>`Using local credentials: ${config.authLocal} 🔐 `, LOGIN: (isLocal: boolean) => `Logging in ${isLocal ? 'locally' : 'globally'}…`, LOGS_SETUP: 'Finished setting up logs.\n', NO_GCLOUD_PROJECT: `No projectId found. Running ${PROJECT_NAME} logs --setup.`, @@ -148,9 +148,9 @@ Cloned ${fileCount} ${fileCount === 1 ? 'file' : 'files'}.`, PUSHING: 'Pushing files…', SAVED_CREDS: (isLocalCreds: boolean) => isLocalCreds - ? `Local credentials saved to: ${auth.resolve()}. + ? `Local credentials saved to: ${config.authLocal}. *Be sure to never commit this file!* It's basically a password.` - : `Default credentials saved to: ${auth.resolve()}.`, + : `Default credentials saved to: ${config.auth}.`, SCRIPT_LINK: (scriptId: string) => `https://script.google.com/d/${scriptId}/edit`, // SCRIPT_RUN: (functionName: string) => `Executing: ${functionName}`, STACKDRIVER_SETUP: 'Setting up StackDriver Logging.', diff --git a/src/path-proxy.ts b/src/path-proxy.ts deleted file mode 100644 index 196ea020..00000000 --- a/src/path-proxy.ts +++ /dev/null @@ -1,131 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; - -import {ClaspError} from './clasp-error.js'; - -/** A path broken down into a `dir`ectory and a `base` filename */ -type BrokenPath = Pick<path.ParsedPath, 'dir' | 'base'>; - -export class PathProxy { - protected _default: BrokenPath; - protected _userDefined: string | undefined; - - /** - * Handles a path to a file. - * - * - Constructor requires a default path (directory and filename) - * - Path can be overridden with the `path` accessor. - * - The `resolve()` method implements specific rules to define the effective path to the proxied file. - * - * @param {BrokenPath} defaultPath default path - */ - constructor(defaultPath: BrokenPath) { - this._default = defaultPath; - } - - /** - * Returns the current (raw and unresolved) defined path to the proxied file. - * - * *Note: for most uses, prefer the `resolve()` method in order to retreive a file's path* - * - * @returns {string} - */ - get path(): string { - return this._userDefined ?? path.join(this._default.dir, this._default.base); - } - - /** - * Sets the current (raw and unresolved) path to the proxied file. - * - * *Note: passing an empty string restores the default path* - */ - set path(userDefined: string) { - this._userDefined = userDefined === path.join(this._default.dir, this._default.base) ? undefined : userDefined; - } - - /** - * Returns true if current path is the default. - * - * @returns {boolean} - */ - isDefault(): boolean { - return !this._userDefined || this._userDefined === path.join(this._default.dir, this._default.base); - } - - /** - * Returns the resolved directory to the proxied file. - * - * *Note: for most uses, prefer the `.resolve()` method in order to retreive a file's path* - * - * @returns {string} - */ - get resolvedDir(): string { - return path.dirname(this.resolve()); - } - - /** - * Resolves the current active path - * - * @returns {string} - */ - resolve(): string { - return this._userDefined - ? resolvePath(this._userDefined, this._default.base) - : path.join(this._default.dir, this._default.base); - } -} - -/** - * Attempts to resolve a path with the following rules: - * - * - if path exists and points to a file: use it as is - * - if path exists and points to a directory: append the default base filename to the path - * - if path partially resolves to an existing directory but base filename does not exists: use it as is - * - otherwise throw an error - * - * @param {string} pathToResolve the path to resolve - * @param {string} baseFilename the default base filename - * - * @returns {string} - */ -const resolvePath = (pathToResolve: string, baseFilename: string) => { - if (fs.existsSync(pathToResolve)) { - return appendBaseIfIsDirectory(pathToResolve, baseFilename); - } - - const parsedPath = path.parse(pathToResolve); - - if (parsedPath.dir === '' || fs.lstatSync(parsedPath.dir).isDirectory()) { - return pathToResolve; // Assume fullpath to missing file - } - - // TODO: improve support for unresolved paths - throw new ClaspError(`Unrecognized path ${pathToResolve}`); -}; - -/** - * Attempts to resolve an **existing** path using the following rules: - * - * - if path exists and points to a file: use it as is - * - if path exists and points to a directory: append the default base filename to the path - * - otherwise throw an error - * - * @param {string} somePath the path to resolve - * @param {string} baseFilename the default base filename - * - * @returns {string} - */ -const appendBaseIfIsDirectory = (somePath: string, baseFilename: string): string => { - const stats = fs.lstatSync(somePath); - - if (stats.isFile()) { - return somePath; - } - - if (stats.isDirectory()) { - return path.join(somePath, baseFilename); - } - - // TODO: improve support for other stats types (stats.isSymbolicLink() ? ) - throw new ClaspError(`Unrecognized path ${somePath}`); -}; diff --git a/src/utils.ts b/src/utils.ts index 1ec6ed76..2ff543c3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,7 +15,7 @@ import {ERROR, LOG} from './messages.js'; import type {ClaspToken, ProjectSettings} from './dotfile'; -const {auth} = Conf.get(); +const config = Conf.get(); /** * Returns input string with uppercased first character @@ -48,21 +48,10 @@ export interface ClaspCredentials { * @return {boolean} */ export const hasOauthClientSettings = (local = false): boolean => { - let previousPath: string | undefined; - - if (local && auth.isDefault()) { - // if no local auth defined, try current directory - previousPath = auth.path; - auth.path = '.'; + if (local) { + return config.authLocal !== undefined && fs.existsSync(config.authLocal); } - - const result = (local ? !auth.isDefault() : auth.isDefault()) && fs.existsSync(auth.resolve()); - - if (previousPath) { - auth.path = previousPath; - } - - return result; + return config.auth !== undefined && fs.existsSync(config.auth); }; /** @@ -135,7 +124,7 @@ export const getWebApplicationURL = (value: Readonly<scriptV1.Schema$Deployment> * Gets default project name. * @return {string} default project name. */ -export const getDefaultProjectName = (): string => capitalize(path.basename(process.cwd())); +export const getDefaultProjectName = (): string => capitalize(path.basename(config.projectRootDirectory!)); /** * Gets the project settings from the project dotfile. @@ -159,11 +148,11 @@ export const getProjectSettings = async (): Promise<ProjectSettings> => { return settings; } } catch (error) { - throw new ClaspError(ERROR.SETTINGS_DNE); // Never found a dotfile + throw new ClaspError(ERROR.SETTINGS_DNE()); // Never found a dotfile } } - throw new ClaspError(ERROR.SETTINGS_DNE); // Never found a dotfile + throw new ClaspError(ERROR.SETTINGS_DNE()); // Never found a dotfile } catch (error) { if (error instanceof ClaspError) { throw error; diff --git a/test/test.ts b/test/test.ts index b5ed84b2..a6f7e135 100644 --- a/test/test.ts +++ b/test/test.ts @@ -279,20 +279,20 @@ describe('Test all functions while logged out', () => { expect(result.status).to.equal(1); // Should be ERROR.NO_CREDENTIALS // see: https://github.com/google/clasp/issues/278 - expect(result.stderr).to.contain(ERROR.SETTINGS_DNE); + expect(result.stderr).to.contain(ERROR.SETTINGS_DNE()); }); it('should fail to open (no .clasp.json file)', () => { const result = spawnSync(CLASP, ['open'], {encoding: 'utf8'}); expect(result.status).to.equal(1); // Should be ERROR.NO_CREDENTIALS // see: https://github.com/google/clasp/issues/278 - expect(result.stderr).to.contain(ERROR.SETTINGS_DNE); + expect(result.stderr).to.contain(ERROR.SETTINGS_DNE()); }); it('should fail to show logs (no .clasp.json file)', () => { const result = spawnSync(CLASP, ['logs'], {encoding: 'utf8'}); expect(result.status).to.equal(1); // Should be ERROR.NO_CREDENTIALS // see: https://github.com/google/clasp/issues/278 - expect(result.stderr).to.contain(ERROR.SETTINGS_DNE); + expect(result.stderr).to.contain(ERROR.SETTINGS_DNE()); }); }); From 37ae4ae1703bad0adac72848788270eca47e9726 Mon Sep 17 00:00:00 2001 From: Steven Bazyl <sbazyl@google.com> Date: Mon, 9 Aug 2021 13:32:07 -0600 Subject: [PATCH 4/5] Delint/prettify --- src/commands/create.ts | 5 ++++- src/conf.ts | 12 ++++++++++-- src/index.ts | 8 ++++---- src/messages.ts | 2 +- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/commands/create.ts b/src/commands/create.ts index b773a365..ef910fed 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -105,7 +105,10 @@ export default async (options: CommandOption): Promise<void> => { const scriptId = data.scriptId ?? ''; console.log(LOG.CREATE_PROJECT_FINISH(filetype, scriptId)); - await saveProject({scriptId, rootDir: config.projectRootDirectory, parentId: parentId ? [parentId] : undefined}, false); + await saveProject( + {scriptId, rootDir: config.projectRootDirectory, parentId: parentId ? [parentId] : undefined}, + false + ); if (!manifestExists(config.projectRootDirectory)) { await writeProjectFiles(await fetchProject(scriptId), config.projectRootDirectory); // Fetches appsscript.json, o.w. `push` breaks diff --git a/src/conf.ts b/src/conf.ts index db324a8f..9620c7bf 100644 --- a/src/conf.ts +++ b/src/conf.ts @@ -66,7 +66,11 @@ export class Conf { get projectConfig() { if (this._projectConfig === undefined && this.projectRootDirectory) { - this._projectConfig = this.buildPathOrUseEnv(`.${PROJECT_NAME}.json`, this.projectRootDirectory, ENV.DOT_CLASP_PROJECT); + this._projectConfig = this.buildPathOrUseEnv( + `.${PROJECT_NAME}.json`, + this.projectRootDirectory, + ENV.DOT_CLASP_PROJECT + ); } return this._projectConfig; } @@ -99,7 +103,11 @@ export class Conf { get authLocal() { if (this._authLocal === undefined && this.projectRootDirectory) { - this._authLocal = this.buildPathOrUseEnv(`.${PROJECT_NAME}rc.json`, this.projectRootDirectory, ENV.DOT_CLASP_AUTH); + this._authLocal = this.buildPathOrUseEnv( + `.${PROJECT_NAME}rc.json`, + this.projectRootDirectory, + ENV.DOT_CLASP_AUTH + ); } return this._authLocal; } diff --git a/src/index.ts b/src/index.ts index f772b152..139201a1 100755 --- a/src/index.ts +++ b/src/index.ts @@ -52,7 +52,7 @@ import versions from './commands/versions.js'; import {Conf} from './conf.js'; import {PROJECT_NAME} from './constants.js'; import {spinner, stopSpinner} from './utils.js'; -import fs from "fs"; +import fs from 'fs'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -84,7 +84,7 @@ commander.name(PROJECT_NAME).usage('<command> [options]').description(`${PROJECT */ commander .option('-A, --auth <file>', "path to an auth file or a folder with a '.clasprc.json' file.") - .on('option:auth', (auth) => { + .on('option:auth', auth => { config.auth = auth; }); @@ -93,7 +93,7 @@ commander */ commander .option('-I, --ignore <file>', "path to an ignore file or a folder with a '.claspignore' file.") - .on('option:ignore', (ignore) => { + .on('option:ignore', ignore => { config.ignore = ignore; }); @@ -102,7 +102,7 @@ commander */ commander .option('-P, --project <file>', "path to a project file or to a folder with a '.clasp.json' file.") - .on('option:project', (path) => { + .on('option:project', path => { const stats = fs.lstatSync(path); if (stats.isDirectory()) { config.projectRootDirectory = path; diff --git a/src/messages.ts b/src/messages.ts index 0074cf1d..2466bc8c 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -129,7 +129,7 @@ Cloned ${fileCount} ${fileCount === 1 ? 'file' : 'files'}.`, GET_PROJECT_ID_INSTRUCTIONS: `Go to *Resource > Cloud Platform Project…* and copy your projectId (including "project-id-")`, GIVE_DESCRIPTION: 'Give a description: ', - LOCAL_CREDS: () =>`Using local credentials: ${config.authLocal} 🔐 `, + LOCAL_CREDS: () => `Using local credentials: ${config.authLocal} 🔐 `, LOGIN: (isLocal: boolean) => `Logging in ${isLocal ? 'locally' : 'globally'}…`, LOGS_SETUP: 'Finished setting up logs.\n', NO_GCLOUD_PROJECT: `No projectId found. Running ${PROJECT_NAME} logs --setup.`, From e4cd9528f5180db8ca02a44706a9a1b62dbaaa36 Mon Sep 17 00:00:00 2001 From: Steven Bazyl <sbazyl@google.com> Date: Mon, 9 Aug 2021 15:11:07 -0600 Subject: [PATCH 5/5] Standarize using fs-extra instead of fs --- src/commands/logout.ts | 2 +- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/logout.ts b/src/commands/logout.ts index 1f0c0d6f..94887b38 100644 --- a/src/commands/logout.ts +++ b/src/commands/logout.ts @@ -1,5 +1,5 @@ import {Conf} from '../conf.js'; -import fs from 'fs'; +import fs from 'fs-extra'; const config = Conf.get(); diff --git a/src/index.ts b/src/index.ts index 139201a1..2b7488bb 100755 --- a/src/index.ts +++ b/src/index.ts @@ -52,7 +52,7 @@ import versions from './commands/versions.js'; import {Conf} from './conf.js'; import {PROJECT_NAME} from './constants.js'; import {spinner, stopSpinner} from './utils.js'; -import fs from 'fs'; +import fs from 'fs-extra'; const __dirname = dirname(fileURLToPath(import.meta.url));