diff --git a/package-lock.json b/package-lock.json index 44eb6ea412b..30d9e8bcd4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@stencil/core", - "version": "2.6.0", + "version": "2.7.0-0", "license": "MIT", "bin": { "stencil": "bin/stencil" diff --git a/src/cli/ionic-config.ts b/src/cli/ionic-config.ts new file mode 100644 index 00000000000..3a7b8769759 --- /dev/null +++ b/src/cli/ionic-config.ts @@ -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 { + 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 { + 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 { + const config = await readConfig(); + await writeConfig(Object.assign(config, newOptions)); +} diff --git a/src/cli/run.ts b/src/cli/run.ts index 530e38adc7d..6c18ca16746 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -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; @@ -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; @@ -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); diff --git a/src/cli/state/stencil-cli-config.ts b/src/cli/state/stencil-cli-config.ts new file mode 100644 index 00000000000..8faaf947952 --- /dev/null +++ b/src/cli/state/stencil-cli-config.ts @@ -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; +} diff --git a/src/cli/state/test/stencil-cli-config.spec.ts b/src/cli/state/test/stencil-cli-config.spec.ts new file mode 100644 index 00000000000..ec63563854f --- /dev/null +++ b/src/cli/state/test/stencil-cli-config.spec.ts @@ -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()); + }); +}); diff --git a/src/cli/telemetry/helpers.ts b/src/cli/telemetry/helpers.ts new file mode 100644 index 00000000000..b15cfc852f7 --- /dev/null +++ b/src/cli/telemetry/helpers.ts @@ -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 Promise, R>(fn: T, ...args: any[]): Promise => { + 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); +} diff --git a/src/cli/telemetry/test/helpers.spec.ts b/src/cli/telemetry/test/helpers.spec.ts new file mode 100644 index 00000000000..70a8c05e965 --- /dev/null +++ b/src/cli/telemetry/test/helpers.spec.ts @@ -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); + }); +}); diff --git a/src/cli/test/ionic-config.spec.ts b/src/cli/test/ionic-config.spec.ts new file mode 100644 index 00000000000..c1d8f5a9d7f --- /dev/null +++ b/src/cli/test/ionic-config.spec.ts @@ -0,0 +1,75 @@ +import { mockLogger } from '@stencil/core/testing'; +import { getCompilerSystem, initializeStencilCLIConfig } from '../state/stencil-cli-config'; +import { readConfig, writeConfig, updateConfig, defaultConfig } from '../ionic-config'; +import { createSystem } from '../../compiler/sys/stencil-sys'; + +describe('readConfig', () => { + initializeStencilCLIConfig({ + sys: createSystem(), + logger: mockLogger(), + args: [], + }); + + it('should create a file if it does not exist', async () => { + let result = await getCompilerSystem().stat(defaultConfig()); + + if (result.isFile) { + await getCompilerSystem().removeFile(defaultConfig()); + } + + result = await getCompilerSystem().stat(defaultConfig()); + + expect(result.isFile).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' }); + + let result = await getCompilerSystem().stat(defaultConfig()); + + expect(result.isFile).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', () => { + initializeStencilCLIConfig({ + sys: createSystem(), + logger: mockLogger(), + args: [], + }); + + it('should edit a file', async () => { + await writeConfig({ 'telemetry.stencil': true, 'tokens.telemetry': '12345' }); + + let result = await getCompilerSystem().stat(defaultConfig()); + + expect(result.isFile).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'); + }); +}); diff --git a/src/compiler/sys/stencil-sys.ts b/src/compiler/sys/stencil-sys.ts index a98af811d38..60806a909c0 100644 --- a/src/compiler/sys/stencil-sys.ts +++ b/src/compiler/sys/stencil-sys.ts @@ -16,6 +16,8 @@ import type { } from '../../declarations'; import platformPath from 'path-browserify'; import { basename, dirname, join } from 'path'; +import * as process from 'process'; +import * as os from 'os'; import { buildEvents } from '../events'; import { createLogger } from './logger/console-logger'; import { createWebWorkerMainController } from './worker/web-worker-main'; @@ -74,6 +76,14 @@ export const createSystem = (c?: { logger?: Logger }) => { return true; }; + const isTTY = (): boolean => { + return !!process?.stdout?.isTTY; + }; + + const homeDir = () => { + return os.homedir(); + }; + const createDirSync = (p: string, opts?: CompilerSystemCreateDirectoryOptions) => { p = normalize(p); const results: CompilerSystemCreateDirectoryResults = { @@ -523,6 +533,10 @@ export const createSystem = (c?: { logger?: Logger }) => { return results; }; + const getEnvironmentVar = (key: string) => { + return process?.env[key]; + }; + const getLocalModulePath = (opts: { rootDir: string; moduleId: string; path: string }) => join(opts.rootDir, 'node_modules', opts.moduleId, opts.path); @@ -546,6 +560,9 @@ export const createSystem = (c?: { logger?: Logger }) => { copyFile, createDir, createDirSync, + homeDir, + isTTY, + getEnvironmentVar, destroy, encodeToBase64, exit: async exitCode => logger.warn(`exit ${exitCode}`), diff --git a/src/compiler/sys/tests/stencil-sys.spec.ts b/src/compiler/sys/tests/stencil-sys.spec.ts index 2b7ce85346b..330b71981d0 100644 --- a/src/compiler/sys/tests/stencil-sys.spec.ts +++ b/src/compiler/sys/tests/stencil-sys.spec.ts @@ -135,6 +135,11 @@ describe('stencil system', () => { expect(newABCStat.isDirectory).toBe(true); }); + it('get home directory', async () => { + const homedir = sys.homeDir(); + expect(typeof homedir).toBe("string") + }) + it('rename directory, with files/subfolders', async () => { await sys.createDir('/x/y/z', { recursive: true }); await sys.createDir('/x/y/y1-dir', { recursive: true }); diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index 9c2f9fcc932..da803ab50e5 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -902,6 +902,11 @@ export interface CompilerSystem { * SYNC! Does not throw. */ createDirSync(p: string, opts?: CompilerSystemCreateDirectoryOptions): CompilerSystemCreateDirectoryResults; + homeDir(): string; + /** + * Used to determine if the current context of the terminal is TTY. + */ + isTTY(): boolean; /** * Each plaform as a different way to dynamically import modules. */ diff --git a/src/sys/deno/deno-sys.ts b/src/sys/deno/deno-sys.ts index 00b7238acf5..4259829da87 100644 --- a/src/sys/deno/deno-sys.ts +++ b/src/sys/deno/deno-sys.ts @@ -124,6 +124,12 @@ export function createDenoSys(c: { Deno?: any } = {}) { } return results; }, + isTTY() { + return !!deno?.isatty(deno?.stdout?.rid); + }, + homeDir() { + return deno.env.get('HOME'); + }, createDirSync(p, opts) { const results: CompilerSystemCreateDirectoryResults = { basename: basename(p), diff --git a/src/sys/node/node-sys.ts b/src/sys/node/node-sys.ts index 99fabf53256..a5ef96c5cb6 100644 --- a/src/sys/node/node-sys.ts +++ b/src/sys/node/node-sys.ts @@ -17,6 +17,7 @@ import { NodeLazyRequire } from './node-lazy-require'; import { NodeResolveModule } from './node-resolve-module'; import { NodeWorkerController } from './node-worker-controller'; import path from 'path'; +import * as os from 'os'; import type TypeScript from 'typescript'; export function createNodeSys(c: { process?: any } = {}) { @@ -242,6 +243,9 @@ export function createNodeSys(c: { process?: any } = {}) { }); }); }, + isTTY() { + return !!process?.stdout?.isTTY; + }, readDirSync(p) { try { return fs.readdirSync(p).map(f => { @@ -270,6 +274,12 @@ export function createNodeSys(c: { process?: any } = {}) { } catch (e) {} return undefined; }, + homeDir() { + try { + return os.homedir(); + } catch (e) {} + return undefined; + }, realpath(p) { return new Promise(resolve => { fs.realpath(p, 'utf8', (e, data) => { diff --git a/src/testing/testing-sys.ts b/src/testing/testing-sys.ts index 7b7da828cfa..0a4c54d0c6c 100644 --- a/src/testing/testing-sys.ts +++ b/src/testing/testing-sys.ts @@ -36,6 +36,7 @@ export const createTestingSystem = () => { sys.access = wrapRead(sys.access); sys.accessSync = wrapRead(sys.accessSync); + sys.homeDir = wrapRead(sys.homeDir); sys.readFile = wrapRead(sys.readFile); sys.readFileSync = wrapRead(sys.readFileSync); sys.readDir = wrapRead(sys.readDir);