Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): [STENCIL-33] Writing and Reading the Ionic config file #2963

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions src/cli/ionic-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

import { readJson, uuidv4 } from './telemetry/helpers';

const CONFIG_FILE = 'config.json';
export const DEFAULT_CONFIG_DIRECTORY = path.resolve(os.homedir(), '.ionic', CONFIG_FILE);
splitinfinities marked this conversation as resolved.
Show resolved Hide resolved

export interface TelemetryConfig {
"telemetry.stencil"?: boolean,
"tokens.telemetry"?: string,
splitinfinities marked this conversation as resolved.
Show resolved Hide resolved
}

export async function readConfig(): Promise<TelemetryConfig> {
try {
return await readJson(DEFAULT_CONFIG_DIRECTORY);
} catch (e) {
if (e.code !== 'ENOENT') {
throw e;
}

const config: TelemetryConfig = {
"tokens.telemetry": uuidv4(),
"telemetry.stencil": true,
};

await writeConfig(config);
splitinfinities marked this conversation as resolved.
Show resolved Hide resolved

return config;
}
}

export async function writeConfig(config: TelemetryConfig): Promise<void> {
try {
await fs.promises.mkdir(path.dirname(DEFAULT_CONFIG_DIRECTORY), { recursive: true });
await fs.promises.writeFile(DEFAULT_CONFIG_DIRECTORY, JSON.stringify(config))
} catch (error) {
console.error(`Stencil Telemetry: couldn't write configuration file to ${DEFAULT_CONFIG_DIRECTORY} - ${error}.`)
};
}

export async function updateConfig(newOptions: TelemetryConfig): Promise<void> {
const config = await readConfig();
await writeConfig(Object.assign(config, newOptions));
}
52 changes: 52 additions & 0 deletions src/cli/telemetry/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import fs from "fs";

interface TerminalInfo {
/**
* Whether this is in CI or not.
*/
readonly ci: boolean;
/**
* Path to the user's shell program.
*/
readonly shell: string;
/**
* Whether the terminal is an interactive TTY or not.
*/
readonly tty: boolean;
/**
* Whether this is a Windows shell or not.
*/
readonly windows: boolean;
}

export declare const TERMINAL_INFO: TerminalInfo;

export const tryFn = async <T extends (...args: any[]) => Promise<R>, R>(
fn: T,
...args: any[]
): Promise<R | null> => {
try {
return await fn(...args);
} catch {
// ignore
}

return null;
};

export const isInteractive = (): boolean => TERMINAL_INFO.tty && !TERMINAL_INFO.ci;
splitinfinities marked this conversation as resolved.
Show resolved Hide resolved

// Plucked from https://github.com/ionic-team/capacitor/blob/b893a57aaaf3a16e13db9c33037a12f1a5ac92e0/cli/src/util/uuid.ts
export function uuidv4(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
splitinfinities marked this conversation as resolved.
Show resolved Hide resolved
const r = (Math.random() * 16) | 0;
const v = c == 'x' ? r : (r & 0x3) | 0x8;

return v.toString(16);
});
}

export async function readJson(path: string) {
const rawdata = await fs.promises.readFile(path);
return JSON.parse(rawdata.toString());
}
59 changes: 59 additions & 0 deletions src/cli/telemetry/test/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { isInteractive, TERMINAL_INFO, tryFn, uuidv4 } from '../helpers';

describe('uuidv4', () => {

it('should output a UUID', async () => {
splitinfinities marked this conversation as resolved.
Show resolved Hide resolved
const pattern = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i);
const uuid = uuidv4()
expect(!!uuid.match(pattern)).toBe(true)
});

});

describe('isInteractive', () => {
it('should return false when tty is false', async () => {
TERMINAL_INFO = { ci: true, shell: true, tty: false, windows: true };
splitinfinities marked this conversation as resolved.
Show resolved Hide resolved
const result = isInteractive();
expect(result).toBe(false)
});

it('should return false when ci is true', async () => {
splitinfinities marked this conversation as resolved.
Show resolved Hide resolved
TERMINAL_INFO = { ci: true, shell: true, tty: true, windows: true };
const result = isInteractive();
expect(result).toBe(false)
});

it('should return true when tty is true and ci is false', async () => {
TERMINAL_INFO = { ci: false, shell: true, tty: true, windows: true };
const result = isInteractive();
expect(result).toBe(true)
});
});


describe('tryFn', () => {

it('should handle failures correctly', async () => {
const result = await tryFn(async () => {
throw new Error("Uh oh!")
});

expect(result).toBe(null)
});

it('should handle success correctly', async () => {
const result = await tryFn(async () => {
return true
})

expect(result).toBe(true)
});

splitinfinities marked this conversation as resolved.
Show resolved Hide resolved
it('handles returning false correctly', async () => {
splitinfinities marked this conversation as resolved.
Show resolved Hide resolved
const result = await tryFn(async () => {
return false;
})

expect(result).toBe(false);
});
});
56 changes: 56 additions & 0 deletions src/cli/test/ionic-config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import fs from 'fs-extra';
import { readConfig, writeConfig, updateConfig, DEFAULT_CONFIG_DIRECTORY } from '../ionic-config';

describe('readConfig', () => {

it('should create a file if it does not exist', async () => {
if (await fs.pathExists(DEFAULT_CONFIG_DIRECTORY)) {
fs.rmSync(DEFAULT_CONFIG_DIRECTORY);
}

expect(await fs.pathExists(DEFAULT_CONFIG_DIRECTORY)).toBe(false)

const config = await readConfig();

expect(Object.keys(config).join()).toBe("tokens.telemetry,telemetry.stencil");
});

it('should read a file if it exists', async () => {
await writeConfig({"telemetry.stencil": true, "tokens.telemetry": "12345"})

expect(await fs.pathExists(DEFAULT_CONFIG_DIRECTORY)).toBe(true)

const config = await readConfig();

expect(Object.keys(config).join()).toBe("telemetry.stencil,tokens.telemetry");
expect(config['telemetry.stencil']).toBe(true);
expect(config['tokens.telemetry']).toBe("12345");
});
});

describe('updateConfig', () => {

it('should edit a file', async () => {
await writeConfig({ "telemetry.stencil": true, "tokens.telemetry": "12345" })

expect(await fs.pathExists(DEFAULT_CONFIG_DIRECTORY)).toBe(true)

const configPre = await readConfig();

expect(typeof configPre).toBe("object");
expect(Object.keys(configPre).join()).toBe("telemetry.stencil,tokens.telemetry");
expect(configPre['telemetry.stencil']).toBe(true);
expect(configPre['tokens.telemetry']).toBe("12345");

await updateConfig({ "telemetry.stencil": false, "tokens.telemetry": "67890" });

const configPost = await readConfig();

expect(typeof configPost).toBe("object");
// Should keep the previous order
expect(Object.keys(configPost).join()).toBe("telemetry.stencil,tokens.telemetry");
expect(configPost['telemetry.stencil']).toBe(false);
expect(configPost['tokens.telemetry']).toBe("67890");
});

});