Skip to content

Commit

Permalink
feat: env & option based config files
Browse files Browse the repository at this point in the history
  • Loading branch information
PopGoesTheWza committed Dec 14, 2020
1 parent d3f809e commit 1b68374
Show file tree
Hide file tree
Showing 11 changed files with 443 additions and 114 deletions.
17 changes: 8 additions & 9 deletions src/auth.ts
@@ -1,20 +1,21 @@
import {Credentials, GenerateAuthUrlOpts, OAuth2Client, OAuth2ClientOptions} from 'google-auth-library';
import {OAuth2Client} from 'google-auth-library';
import {google, script_v1 as scriptV1} from 'googleapis';
import {createServer} from 'http';
import open from 'open';
import readline from 'readline';
import {URL} from 'url';

import {ClaspError} from './clasp-error';
import {ClaspToken, DOTFILE, Dotfile} from './dotfile';
import {DOTFILE} from './dotfile';
import {ERROR, LOG} from './messages';
import {checkIfOnlineOrDie, getOAuthSettings} from './utils';
// import {oauthScopesPrompt} from './inquirer';
// import {readManifest} from './manifest';

import type {Credentials, GenerateAuthUrlOpts, OAuth2ClientOptions} from 'google-auth-library';
import type {IncomingMessage, Server, ServerResponse} from 'http';
import type {AddressInfo} from 'net';
import type {ReadonlyDeep} from 'type-fest';

import type {ClaspToken} from './dotfile';
import type {ClaspCredentials} from './utils';

/**
Expand Down Expand Up @@ -151,9 +152,8 @@ export const authorize = async (options: {

// Save the token and own creds together.
let claspToken: ClaspToken;
let dotfile: Dotfile;
// TODO: deprecate `--creds` option
if (options.creds) {
dotfile = DOTFILE.RC_LOCAL();
const {client_id: clientId, client_secret: clientSecret, redirect_uris: redirectUri} = options.creds.installed;
// Save local ClaspCredentials.
claspToken = {
Expand All @@ -162,7 +162,6 @@ export const authorize = async (options: {
isLocalCreds: true,
};
} else {
dotfile = DOTFILE.RC;
// Save global ClaspCredentials.
claspToken = {
token,
Expand All @@ -171,7 +170,7 @@ export const authorize = async (options: {
};
}

await dotfile.write(claspToken);
await DOTFILE.AUTH().write(claspToken);
console.log(LOG.SAVED_CREDS(Boolean(options.creds)));
} catch (error) {
if (error instanceof ClaspError) {
Expand Down Expand Up @@ -310,7 +309,7 @@ const setOauthClientCredentials = async (rc: ClaspToken) => {
await refreshCredentials(globalOAuth2Client);

// Save the credentials.
await (rc.isLocalCreds ? DOTFILE.RC_LOCAL() : DOTFILE.RC).write(rc);
await DOTFILE.AUTH().write(rc);
} catch (error) {
if (error instanceof ClaspError) {
throw error;
Expand Down
33 changes: 28 additions & 5 deletions src/commands/logout.ts
@@ -1,17 +1,40 @@
import fs from 'fs-extra';

import {DOT} from '../dotfile';
import {Conf} from '../conf';
import {DOTFILE} from '../dotfile';
import {hasOauthClientSettings} from '../utils';

const {auth} = Conf.get();

/**
* Logs out the user by deleting credentials.
*/
export default async (): Promise<void> => {
let previousPath: string | undefined;

if (hasOauthClientSettings(true)) {
fs.unlinkSync(DOT.RC.ABSOLUTE_LOCAL_PATH);
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 (hasOauthClientSettings()) {
fs.unlinkSync(DOT.RC.ABSOLUTE_PATH);
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;
}
}
};
9 changes: 7 additions & 2 deletions src/commands/push.ts
Expand Up @@ -6,14 +6,19 @@ import {watchTree} from 'watch';

import {loadAPICredentials} from '../auth';
import {ClaspError} from '../clasp-error';
import {Conf} from '../conf';
import {FS_OPTIONS, PROJECT_MANIFEST_BASENAME, PROJECT_MANIFEST_FILENAME} from '../constants';
import {DOT, DOTFILE, ProjectSettings} from '../dotfile';
import {DOTFILE} from '../dotfile';
import {fetchProject, pushFiles} from '../files';
import {overwritePrompt} from '../inquirer';
import {isValidManifest} from '../manifest';
import {LOG} from '../messages';
import {checkIfOnlineOrDie, getProjectSettings, spinner} from '../utils';

import type {ProjectSettings} from '../dotfile';

const {project} = Conf.get();

interface CommandOption {
readonly watch?: boolean;
readonly force?: boolean;
Expand Down Expand Up @@ -81,7 +86,7 @@ const confirmManifestUpdate = async (): Promise<boolean> => (await overwriteProm
* @returns {Promise<boolean>}
*/
const manifestHasChanges = async (projectSettings: ProjectSettings): Promise<boolean> => {
const {scriptId, rootDir = DOT.PROJECT.DIR} = projectSettings;
const {scriptId, rootDir = project.resolvedDir} = 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);
Expand Down
117 changes: 117 additions & 0 deletions src/conf.ts
@@ -0,0 +1,117 @@
import os from 'os';
import path from 'path';

import {PROJECT_NAME} from './constants';
import {PathProxy} from './path-proxy';

/**
* supported environment variables
*/
enum ENV {
DOT_CLASP_AUTH = 'clasp_config_auth',
DOT_CLASP_IGNORE = 'clasp_config_ignore',
DOT_CLASP_PROJECT = 'clasp_config_project',
// MANIFEST = 'clasp_config_manifest',
// TSCONFIG = 'clasp_config_tsconfig',
}

/**
* A Singleton class to hold configuration related objects.
* Use the `get()` method to access the unique singleton instance.
*/
export class Conf {
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;
// 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;
}
};

// default `project` path is `./.clasp.json`
this.project = new PathProxy({dir: '.', base: `.${PROJECT_NAME}.json`});

// default `ignore` path is `~/.claspignore`
// IgnoreFile class implements custom `.resolve()` rules
this.ignore = new IgnoreFile({dir: os.homedir(), base: `.${PROJECT_NAME}ignore`});

// default `auth` path is `~/.clasprc.json`
// Default Auth is global. Any other implies local auth
this.auth = new AuthFile({dir: os.homedir(), base: `.${PROJECT_NAME}rc.json`});

// resolve environment variables
setPathWithEnvVar(ENV.DOT_CLASP_PROJECT, this.project);
setPathWithEnvVar(ENV.DOT_CLASP_IGNORE, this.ignore);
setPathWithEnvVar(ENV.DOT_CLASP_AUTH, this.auth);
}

/**
* The static method that controls the access to the Conf singleton instance.
*
* @returns {Conf}
*/
static get(): Conf {
if (!Conf._instance) {
Conf._instance = new 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
85 changes: 25 additions & 60 deletions src/dotfile.ts
Expand Up @@ -11,17 +11,18 @@
*
* This should be the only file that uses DOTFILE.
*/

import os from 'os';
import path from 'path';
import findUp from 'find-up';
import fs from 'fs-extra';
import {Credentials, OAuth2ClientOptions} from 'google-auth-library';
import stripBom from 'strip-bom';
import dotf from 'dotf';
import fs from 'fs-extra';
import path from 'path';
import splitLines from 'split-lines';
import stripBom from 'strip-bom';

import {Conf} from './conf';
import {FS_OPTIONS} from './constants';

import type {Credentials, OAuth2ClientOptions} from 'google-auth-library';

import {FS_OPTIONS, PROJECT_NAME} from './constants';
const {auth, ignore, project} = Conf.get();

export {Dotfile} from 'dotf';

Expand All @@ -35,42 +36,6 @@ export interface ProjectSettings {
parentId?: string[];
}

// Dotfile names
export const DOT = {
/**
* This dotfile stores information about ignoring files on `push`. Like .gitignore.
*/
IGNORE: {
DIR: '~',
NAME: `${PROJECT_NAME}ignore`,
PATH: `.${PROJECT_NAME}ignore`,
},
/**
* This dotfile saves clasp project information, local to project directory.
*/
PROJECT: {
DIR: path.join('.', '/'), // Relative to where the command is run. See DOTFILE.PROJECT()
NAME: `${PROJECT_NAME}.json`,
PATH: `.${PROJECT_NAME}.json`,
},
/**
* 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}
*/
RC: {
DIR: '~',
LOCAL_DIR: './',
NAME: `${PROJECT_NAME}rc.json`,
LOCAL_PATH: `.${PROJECT_NAME}rc.json`,
PATH: path.join('~', `.${PROJECT_NAME}rc.json`),
ABSOLUTE_PATH: path.join(os.homedir(), `.${PROJECT_NAME}rc.json`),
ABSOLUTE_LOCAL_PATH: path.join('.', `.${PROJECT_NAME}rc.json`),
},
};

const defaultClaspignore = `# ignore all files…
**/**
Expand All @@ -89,16 +54,12 @@ node_modules/**
// Methods for retrieving dotfiles.
export const DOTFILE = {
/**
* Reads DOT.IGNORE.PATH to get a glob pattern of ignored paths.
* Reads ignore.resolve() to get a glob pattern of ignored paths.
* @return {Promise<string[]>} A list of file glob patterns
*/
IGNORE: async () => {
const localPath = await findUp(DOT.PROJECT.PATH);
const usePath = path.join(localPath ? path.dirname(localPath) : DOT.PROJECT.DIR);
const content =
fs.existsSync(usePath) && fs.existsSync(DOT.IGNORE.PATH)
? fs.readFileSync(DOT.IGNORE.PATH, FS_OPTIONS)
: defaultClaspignore;
const ignorePath = ignore.resolve();
const content = fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, FS_OPTIONS) : defaultClaspignore;

return splitLines(stripBom(content)).filter((name: string) => name.length > 0);
},
Expand All @@ -108,17 +69,21 @@ export const DOTFILE = {
* @return {Dotf} A dotf with that dotfile. Null if there is no file
*/
PROJECT: () => {
const localPath = findUp.sync(DOT.PROJECT.PATH);
const usePath = localPath ? path.dirname(localPath) : DOT.PROJECT.DIR;
return dotf(usePath, DOT.PROJECT.NAME);
// ! TODO: currently limited if filename doesn't start with a dot '.'
const {dir, base} = path.parse(project.resolve());
if (base[0] === '.') {
return dotf(dir || '.', base.slice(1));
}
throw new Error('Project file must start with a dot (i.e. .clasp.json)');
},
// Stores {ClaspCredentials}
RC: dotf(DOT.RC.DIR, DOT.RC.NAME),
// Stores {ClaspCredentials}
RC_LOCAL: () => {
const localPath = findUp.sync(DOT.PROJECT.PATH);
const usePath = localPath ? path.dirname(localPath) : DOT.RC.LOCAL_DIR;
return dotf(usePath, DOT.RC.NAME);
AUTH: () => {
// ! TODO: currently limited if filename doesn't start with a dot '.'
const {dir, base} = path.parse(auth.resolve());
if (base[0] === '.') {
return dotf(dir || '.', base.slice(1));
}
throw new Error('Auth file must start with a dot (i.e. .clasp.json)');
},
};

Expand Down

0 comments on commit 1b68374

Please sign in to comment.