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 all 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.

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;
});

splitinfinities marked this conversation as resolved.
Show resolved Hide resolved
expect(result).toBe(false);
});
});
Loading