Skip to content

Commit

Permalink
feat(cli): [STENCIL-33] Writing and Reading the Ionic config file (#2963
Browse files Browse the repository at this point in the history
)

- Adds ability to CLI to allow writing and reading to the ~/.ionic/config.json file. When reading, if the file doesn't exist, readConfig will automatically create the file. Satisfies ADR-2
  • Loading branch information
splitinfinities authored Jul 23, 2021
1 parent 8c375bd commit f981812
Show file tree
Hide file tree
Showing 13 changed files with 437 additions and 0 deletions.
41 changes: 41 additions & 0 deletions src/cli/ionic-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getCompilerSystem } from './state/stencil-cli-config';
import { readJson, uuidv4 } from './telemetry/helpers';

export const defaultConfig = () =>
getCompilerSystem().resolvePath(`${getCompilerSystem().homeDir()}/.ionic/config.json`);

export const defaultConfigDirectory = () => getCompilerSystem().resolvePath(`${getCompilerSystem().homeDir()}/.ionic`);

export interface TelemetryConfig {
'telemetry.stencil'?: boolean;
'tokens.telemetry'?: string;
}

export async function readConfig(): Promise<TelemetryConfig> {
let config: TelemetryConfig = await readJson(defaultConfig());

if (!config) {
config = {
'tokens.telemetry': uuidv4(),
'telemetry.stencil': true,
};

await writeConfig(config);
}

return config;
}

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

export async function updateConfig(newOptions: TelemetryConfig): Promise<void> {
const config = await readConfig();
await writeConfig(Object.assign(config, newOptions));
}
12 changes: 12 additions & 0 deletions src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ import { taskInfo } from './task-info';
import { taskPrerender } from './task-prerender';
import { taskServe } from './task-serve';
import { taskTest } from './task-test';
import { initializeStencilCLIConfig } from './state/stencil-cli-config';

export const run = async (init: CliInitOptions) => {
const { args, logger, sys } = init;

// Initialize the singleton so we can use this throughout the lifecycle of the CLI.
const stencilCLIConfig = initializeStencilCLIConfig({ args, logger, sys});

try {
const flags = parseFlags(args, sys);
const task = flags.task;
Expand All @@ -32,6 +36,12 @@ export const run = async (init: CliInitOptions) => {
sys.applyGlobalPatch(sys.getCurrentDirectory());
}

// Update singleton with modifications
stencilCLIConfig.logger = logger;
stencilCLIConfig.task = task;
stencilCLIConfig.sys = sys;
stencilCLIConfig.flags = flags;

if (task === 'help' || flags.help) {
taskHelp(sys, logger);
return;
Expand All @@ -50,12 +60,14 @@ export const run = async (init: CliInitOptions) => {
logger,
dependencies: dependencies as any,
});

if (hasError(ensureDepsResults.diagnostics)) {
logger.printDiagnostics(ensureDepsResults.diagnostics);
return sys.exit(1);
}

const coreCompiler = await loadCoreCompiler(sys);
stencilCLIConfig.coreCompiler = coreCompiler;

if (task === 'version' || flags.version) {
console.log(coreCompiler.version);
Expand Down
98 changes: 98 additions & 0 deletions src/cli/state/stencil-cli-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { Logger, CompilerSystem, ConfigFlags } from '../../declarations';
export type CoreCompiler = typeof import('@stencil/core/compiler');

export interface StencilCLIConfigArgs {
task?: string;
args: string[];
logger: Logger;
sys: CompilerSystem;
flags?: ConfigFlags;
coreCompiler?: CoreCompiler;
}

export default class StencilCLIConfig {
static instance: StencilCLIConfig;

private _args: string[];
private _logger: Logger;
private _sys: CompilerSystem;
private _flags: ConfigFlags | undefined;
private _task: string | undefined;
private _coreCompiler: CoreCompiler | undefined;

private constructor(options: StencilCLIConfigArgs) {
this._args = options?.args || [];
this._logger = options?.logger;
this._sys = options?.sys;
}

public static getInstance(options?: StencilCLIConfigArgs): StencilCLIConfig {
if (!StencilCLIConfig.instance) {
StencilCLIConfig.instance = new StencilCLIConfig(options);
}

return StencilCLIConfig.instance;
}

public get logger() {
return this._logger;
}
public set logger(logger: Logger) {
this._logger = logger;
}

public get sys() {
return this._sys;
}
public set sys(sys: CompilerSystem) {
this._sys = sys;
}

public get args() {
return this._args;
}
public set args(args: string[]) {
this._args = args;
}

public get task() {
return this._task;
}
public set task(task: string) {
this._task = task;
}

public get flags() {
return this._flags;
}
public set flags(flags: ConfigFlags) {
this._flags = flags;
}

public get coreCompiler() {
return this._coreCompiler;
}
public set coreCompiler(coreCompiler: CoreCompiler) {
this._coreCompiler = coreCompiler;
}
}

export function initializeStencilCLIConfig(options: StencilCLIConfigArgs): StencilCLIConfig {
return StencilCLIConfig.getInstance(options);
}

export function getStencilCLIConfig(): StencilCLIConfig {
return StencilCLIConfig.getInstance();
}

export function getCompilerSystem(): CompilerSystem {
return getStencilCLIConfig().sys;
}

export function getLogger(): Logger {
return getStencilCLIConfig().logger;
}

export function getCoreCompiler(): CoreCompiler {
return getStencilCLIConfig().coreCompiler;
}
48 changes: 48 additions & 0 deletions src/cli/state/test/stencil-cli-config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createLogger } from '../../../compiler/sys/logger/console-logger';
import { createSystem } from '../../../compiler/sys/stencil-sys';
import {
getCompilerSystem,
getStencilCLIConfig,
initializeStencilCLIConfig,
getLogger,
getCoreCompiler,
} from '../stencil-cli-config';

describe('StencilCLIConfig', () => {
const config = initializeStencilCLIConfig({
sys: createSystem(),
args: [],
logger: createLogger(),
});

it('should behave as a singleton', () => {
const config2 = initializeStencilCLIConfig({
sys: createSystem(),
args: [],
logger: createLogger(),
});

expect(config2).toBe(config);

const config3 = getStencilCLIConfig();

expect(config3).toBe(config);
});

it('allows updating any item', () => {
config.args = ['nice', 'awesome'];
expect(config.args).toEqual(['nice', 'awesome']);
});

it('getCompilerSystem should return a segment of the singleton', () => {
expect(config.sys).toBe(getCompilerSystem());
});

it('getLogger should return a segment of the singleton', () => {
expect(config.logger).toBe(getLogger());
});

it('getCoreCompiler should return a segment of the singleton', () => {
expect(config.coreCompiler).toBe(getCoreCompiler());
});
});
53 changes: 53 additions & 0 deletions src/cli/telemetry/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { getCompilerSystem, getStencilCLIConfig } from '../state/stencil-cli-config';

interface TerminalInfo {
/**
* Whether this is in CI or not.
*/
readonly ci: boolean;
/**
* Whether the terminal is an interactive TTY or not.
*/
readonly tty: 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 = (object?: TerminalInfo): boolean => {
const terminalInfo =
object ||
Object.freeze({
tty: getCompilerSystem().isTTY() ? true : false,
ci:
['CI', 'BUILD_ID', 'BUILD_NUMBER', 'BITBUCKET_COMMIT', 'CODEBUILD_BUILD_ARN'].filter(
v => !!getCompilerSystem().getEnvironmentVar(v),
).length > 0 || !!getStencilCLIConfig()?.flags?.ci,
});

return terminalInfo.tty && !terminalInfo.ci;
};

// 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 => {
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 file = await getCompilerSystem().readFile(path);
return !!file && JSON.parse(file);
}
66 changes: 66 additions & 0 deletions src/cli/telemetry/test/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { initializeStencilCLIConfig } from '../../state/stencil-cli-config';
import { isInteractive, TERMINAL_INFO, tryFn, uuidv4 } from '../helpers';
import { createSystem } from '../../../compiler/sys/stencil-sys';
import { mockLogger } from '@stencil/core/testing';

describe('uuidv4', () => {
it('outputs a UUID', () => {
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', () => {
initializeStencilCLIConfig({
sys: createSystem(),
logger: mockLogger(),
args: [],
});

it('returns false by default', () => {
const result = isInteractive();
expect(result).toBe(false);
});

it('returns false when tty is false', () => {
const result = isInteractive({ ci: true, tty: false });
expect(result).toBe(false);
});

it('returns false when ci is true', () => {
const result = isInteractive({ ci: true, tty: true });
expect(result).toBe(false);
});

it('returns true when tty is true and ci is false', () => {
const result = isInteractive({ ci: false, tty: true });
expect(result).toBe(true);
});
});

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

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

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

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

it('handles returning false correctly', async () => {
const result = await tryFn(async () => {
return false;
});

expect(result).toBe(false);
});
});
Loading

0 comments on commit f981812

Please sign in to comment.