diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68502d5217..949712832a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,8 +57,8 @@ The Ionic CLI is organized into a monorepo. Here are the packages: #### Toolset -* npm 5 is required. -* Node 8+ is required. +* npm 6+ is required. +* Node 10+ is required. * The codebase is written in [TypeScript](https://www.typescriptlang.org/). If you're unfamiliar with TypeScript, we recommend using [VS Code](https://code.visualstudio.com/) and finding a tutorial to familiarize diff --git a/README.md b/README.md index b916ab7e09..44a0309be9 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ See the CLI's [`CHANGELOG.md`](https://github.com/ionic-team/ionic-cli/blob/deve --- :book: **Documentation**: -[https://beta.ionicframework.com/docs/cli/overview][ionic-cli-docs] +[https://beta.ionicframework.com/docs/cli][ionic-cli-docs] :mega: **Support/Questions?** Please see our [Support Page][ionic-support] for general support questions. The issues on GitHub should be reserved for bug @@ -29,7 +29,7 @@ reports and feature requests. [CONTRIBUTING.md](https://github.com/ionic-team/ionic-cli/blob/develop/CONTRIBUTING.md). [ionic-homepage]: https://ionicframework.com -[ionic-cli-docs]: https://beta.ionicframework.com/docs/cli/overview +[ionic-cli-docs]: https://beta.ionicframework.com/docs/cli [ionic-support]: https://ionicframework.com/support [circle-badge]: https://circleci.com/gh/ionic-team/ionic-cli.svg?style=shield diff --git a/jest.config.base.js b/jest.config.base.js index cf52ef51d3..a51134bb8a 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -1,13 +1,16 @@ module.exports = { - transform: { - "^.+\\.tsx?$": "ts-jest" + preset: 'ts-jest', + globals: { + 'ts-jest': { + diagnostics: { + // warnOnly: true, + }, + tsConfig: { + types: [ + "node", + "jest", + ], + }, + }, }, - testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", - moduleFileExtensions: [ - "ts", - "tsx", - "js", - "jsx", - "json" - ], }; diff --git a/package.json b/package.json index ed3f70fd55..a47772e109 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "scripts": { "bootstrap": "lerna bootstrap && npm run build", "clean": "lerna run clean", - "lint": "lerna run lint --parallel", + "lint": "lerna run lint", "lint:fix": "lerna run lint -- -- --fix", "link": "lerna exec npm link", "unlink": "lerna exec npm unlink", @@ -24,7 +24,7 @@ }, "husky": { "hooks": { - "pre-commit": "node packages/cli-scripts/bin/ionic-cli-scripts pre-commit" + "pre-commit": "lerna exec --concurrency 1 --stream -- lint-staged" } } } diff --git a/packages/@ionic/cli-framework/CHANGELOG.md b/packages/@ionic/cli-framework/CHANGELOG.md index bb45835b74..ac7c093857 100644 --- a/packages/@ionic/cli-framework/CHANGELOG.md +++ b/packages/@ionic/cli-framework/CHANGELOG.md @@ -3,6 +3,26 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [1.2.0](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.1.1...@ionic/cli-framework@1.2.0) (2018-10-31) + + +### Bug Fixes + +* **config:** write json file with ending newline ([dc3f2a5](https://github.com/ionic-team/ionic-cli/commit/dc3f2a5)) +* **help:** filter out unnecessary global options ([7809c99](https://github.com/ionic-team/ionic-cli/commit/7809c99)) +* **process:** keep node running for `sleepForever()` ([ea08f8d](https://github.com/ionic-team/ionic-cli/commit/ea08f8d)) + + +### Features + +* **namespace:** add `useAliases` option to `locate()` ([489617b](https://github.com/ionic-team/ionic-cli/commit/489617b)) +* **object:** add `keysWithoutAliases()` method to `AliasedMap` ([f4807f4](https://github.com/ionic-team/ionic-cli/commit/f4807f4)) +* **terminal:** detect windows shell ([dcc912d](https://github.com/ionic-team/ionic-cli/commit/dcc912d)) + + + + ## [1.1.1](https://github.com/ionic-team/ionic-cli/compare/@ionic/cli-framework@1.1.0...@ionic/cli-framework@1.1.1) (2018-10-05) diff --git a/packages/@ionic/cli-framework/package.json b/packages/@ionic/cli-framework/package.json index fc417c68bc..c5661824e7 100644 --- a/packages/@ionic/cli-framework/package.json +++ b/packages/@ionic/cli-framework/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/cli-framework", - "version": "1.1.1", + "version": "1.2.0", "description": "The foundation framework of the Ionic CLI", "homepage": "https://ionicframework.com/", "author": "Ionic Team (https://ionicframework.com) ", @@ -14,9 +14,8 @@ "lint": "tslint --config tslint.js --project tsconfig.json", "build": "npm run clean && tsc", "watch": "tsc -w --preserveWatchOutput", - "test": "jest", - "prepublishOnly": "npm run build", - "cli-scripts:pre-commit": "lint-staged" + "test": "jest --maxWorkers=4", + "prepublishOnly": "npm run build" }, "lint-staged": { "linters": { @@ -28,14 +27,14 @@ }, "license": "MIT", "dependencies": { - "@ionic/utils-fs": "0.0.3", + "@ionic/utils-fs": "0.0.4", "chalk": "^2.3.0", "debug": "^4.0.0", "inquirer": "^6.0.0", "lodash": "^4.17.5", "minimist": "^1.2.0", "rimraf": "^2.6.2", - "slice-ansi": "^1.0.0", + "slice-ansi": "^2.0.0", "stream-combiner2": "^1.1.1", "string-width": "^2.1.1", "strip-ansi": "^5.0.0", @@ -49,6 +48,7 @@ "@types/cross-spawn": "^6.0.0", "@types/debug": "0.0.31", "@types/inquirer": "0.0.43", + "@types/jest": "^23.3.5", "@types/lodash": "^4.14.104", "@types/minimist": "^1.2.0", "@types/node": "^6.0.101", @@ -60,8 +60,8 @@ "@types/write-file-atomic": "^2.1.1", "jest": "^23.0.1", "jest-cli": "^23.0.1", - "lint-staged": "^7.2.0", - "ts-jest": "~23.1.0", + "lint-staged": "^8.0.0", + "ts-jest": "^23.10.1", "tslint": "^5.9.1", "typescript": "~3.1.1" } diff --git a/packages/@ionic/cli-framework/src/definitions.ts b/packages/@ionic/cli-framework/src/definitions.ts index 2084446e9d..40b66b4be8 100644 --- a/packages/@ionic/cli-framework/src/definitions.ts +++ b/packages/@ionic/cli-framework/src/definitions.ts @@ -69,11 +69,15 @@ export type NamespaceMapGetter, N extends INam export type ICommandMap, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> = ζobject.AliasedMap>; export type INamespaceMap, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> = ζobject.AliasedMap>; +export interface NamespaceLocateOptions { + useAliases?: boolean; +} + export interface INamespace, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { root: N; parent: N | undefined; - locate(argv: ReadonlyArray): Promise>; + locate(argv: ReadonlyArray, options?: NamespaceLocateOptions): Promise>; getMetadata(): Promise; getNamespaces(): Promise>; diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/command.ts b/packages/@ionic/cli-framework/src/lib/__tests__/command.ts index 9855994ec6..7226d3b25c 100644 --- a/packages/@ionic/cli-framework/src/lib/__tests__/command.ts +++ b/packages/@ionic/cli-framework/src/lib/__tests__/command.ts @@ -1,18 +1,15 @@ -import { - Command, - CommandMap, - CommandMapDefault, - Namespace, - NamespaceMap, - generateCommandPath, -} from '../command'; +import { CommandMetadata } from '../../definitions'; +import { Command, CommandMap, CommandMapDefault, Namespace, NamespaceMap, generateCommandPath } from '../command'; class MyNamespace extends Namespace { - async getMetadata() { - return { name: 'my' }; + async getMetadata(): Promise { + return { + name: 'my', + summary: '', + }; } - async getNamespaces() { + async getNamespaces(): Promise { return new NamespaceMap([ ['foo', async () => new FooNamespace(this)], ['defns', async () => new NamespaceWithDefault(this)], @@ -22,11 +19,14 @@ class MyNamespace extends Namespace { } class NamespaceWithDefault extends Namespace { - async getMetadata() { - return { name: 'defns' }; + async getMetadata(): Promise { + return { + name: 'defns', + summary: '', + }; } - async getCommands() { + async getCommands(): Promise { return new CommandMap([ [CommandMapDefault, async () => new DefaultCommand(this)], ]); @@ -34,11 +34,14 @@ class NamespaceWithDefault extends Namespace { } class FooNamespace extends Namespace { - async getMetadata() { - return { name: 'foo' }; + async getMetadata(): Promise { + return { + name: 'foo', + summary: '', + }; } - async getCommands() { + async getCommands(): Promise { return new CommandMap([ ['bar', async () => new BarCommand(this)], ['baz', async () => new BazCommand(this)], @@ -48,37 +51,45 @@ class FooNamespace extends Namespace { } class EmptyNamespace extends Namespace { - async getMetadata() { - return { name: 'empty' }; + async getMetadata(): Promise { + return { + name: 'empty', + summary: '', + }; } } class DefaultCommand extends Command { - async getMetadata() { - return { name: 'def', description: '' }; + async getMetadata(): Promise { + return { + name: 'def', + summary: '', + }; } + + async run() {} } class BarCommand extends Command { - async getMetadata() { - return { name: 'bar', description: '' }; + async getMetadata(): Promise { + return { + name: 'bar', + summary: '', + }; } -} -class BazCommand extends Command { - async getMetadata() { - return { name: 'baz', description: '' }; - } + async run() {} } -class FooCommand extends Command { - async getMetadata() { +class BazCommand extends Command { + async getMetadata(): Promise { return { - name: 'foo', - type: 'global', - description: '', + name: 'baz', + summary: '', }; } + + async run() {} } describe('@ionic/cli-framework', () => { @@ -90,7 +101,7 @@ describe('@ionic/cli-framework', () => { describe('root and parent', () => { it('should have root attribute', async () => { - const testNamespace = async ns => { + const testNamespace = async (ns: Namespace) => { const namespaces = await ns.getNamespaces(); for (let [ , nsgetter ] of namespaces.entries()) { @@ -107,7 +118,7 @@ describe('@ionic/cli-framework', () => { }); it('should have parent attribute', async () => { - const testNamespace = async ns => { + const testNamespace = async (ns: Namespace) => { const namespaces = await ns.getNamespaces(); for (let [ , nsgetter ] of namespaces.entries()) { @@ -128,7 +139,7 @@ describe('@ionic/cli-framework', () => { describe('getNamespaces', () => { it('should get subnamespaces', async () => { - const testNamespace = async ns => { + const testNamespace = async (ns: Namespace) => { const commands = await ns.getCommands(); const namespaces = await ns.getNamespaces(); @@ -297,7 +308,9 @@ describe('@ionic/cli-framework', () => { const ns = new MyNamespace(); const result = await ns.getCommandMetadataList(); const bar = result.find(c => c.command instanceof BarCommand); - expect(bar).toBeDefined(); + if (!bar) { + throw new Error('bar not defined'); + } expect(bar.path.length).toEqual(3); expect(bar.path[0][0]).toEqual('my'); expect(bar.path[0][1]).toBe(ns); @@ -312,11 +325,17 @@ describe('@ionic/cli-framework', () => { const result = await ns.getCommandMetadataList(); expect(result.length).toEqual(3); const bar = result.find(c => c.command instanceof BarCommand); - expect(bar).toBeDefined(); + if (!bar) { + throw new Error('bar not defined'); + } const baz = result.find(c => c.command instanceof BazCommand); - expect(baz).toBeDefined(); + if (!baz) { + throw new Error('baz not defined'); + } const def = result.find(c => c.command instanceof DefaultCommand); - expect(def).toBeDefined(); + if (!def) { + throw new Error('def not defined'); + } expect(bar.aliases).toEqual(['my foo b']); expect(def.aliases).toEqual([]); expect(baz.aliases).toEqual([]); @@ -333,7 +352,7 @@ describe('@ionic/cli-framework', () => { it('should get path for single namespace and command', async () => { const ns = new FooNamespace(); const { obj } = await ns.locate(['bar']); - const result = await generateCommandPath(obj); + const result = await generateCommandPath(obj as any); expect(result.length).toEqual(2); const [ foons, barcmd ] = result; expect(foons).toBeDefined(); @@ -347,7 +366,7 @@ describe('@ionic/cli-framework', () => { it('should work back through nested namespace', async () => { const ns = new MyNamespace(); const { obj } = await ns.locate(['foo', 'bar']); - const result = await generateCommandPath(obj); + const result = await generateCommandPath(obj as any); expect(result.length).toEqual(3); const [ rootns, foons, barcmd ] = result; expect(rootns).toEqual(['my', ns]); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/config.ts b/packages/@ionic/cli-framework/src/lib/__tests__/config.ts index 03cda61eea..dff10328f3 100644 --- a/packages/@ionic/cli-framework/src/lib/__tests__/config.ts +++ b/packages/@ionic/cli-framework/src/lib/__tests__/config.ts @@ -2,9 +2,12 @@ describe('@ionic/cli-framework', () => { describe('BaseConfig', () => { - let Config, mockReadFileSync, mockMakeDirSync, mockWriteFileAtomicSync; + let Config: any; + let mockReadFileSync: jest.Mock; + let mockMakeDirSync: jest.Mock; + let mockWriteFileAtomicSync: jest.Mock; - beforeEach(() => { + beforeEach(async () => { mockReadFileSync = jest.fn(); mockMakeDirSync = jest.fn(); mockWriteFileAtomicSync = jest.fn(); @@ -13,10 +16,10 @@ describe('@ionic/cli-framework', () => { jest.mock('@ionic/utils-fs', () => ({ mkdirpSync: mockMakeDirSync })); jest.mock('write-file-atomic', () => ({ sync: mockWriteFileAtomicSync })); - const { BaseConfig } = require('../config'); + const { BaseConfig } = await import('../config'); - Config = class extends BaseConfig { - provideDefaults(c) { + Config = class extends BaseConfig { + provideDefaults() { return { defaulted: 5, }; @@ -40,7 +43,7 @@ describe('@ionic/cli-framework', () => { const config = new Config('/path/to/file'); config.c = {}; expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{}'); + expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{}\n'); }); it('should call readFileSync upon getting c', () => { @@ -141,7 +144,7 @@ describe('@ionic/cli-framework', () => { jest.spyOn(config, 'provideDefaults').mockImplementation(() => ({})); config.set('foo', 5); expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{\n "foo": 5\n}'); + expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{\n "foo": 5\n}\n'); }); it('should unset value in file', () => { @@ -150,16 +153,19 @@ describe('@ionic/cli-framework', () => { jest.spyOn(config, 'provideDefaults').mockImplementation(() => ({})); config.unset('foo'); expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{}'); + expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{}\n'); }); }); describe('BaseConfig with pathPrefix', () => { - let Config, mockReadFileSync, mockMakeDirSync, mockWriteFileAtomicSync; + let Config: any; + let mockReadFileSync: jest.Mock; + let mockMakeDirSync: jest.Mock; + let mockWriteFileAtomicSync: jest.Mock; - beforeEach(() => { + beforeEach(async () => { mockReadFileSync = jest.fn(); mockMakeDirSync = jest.fn(); mockWriteFileAtomicSync = jest.fn(); @@ -168,10 +174,10 @@ describe('@ionic/cli-framework', () => { jest.mock('@ionic/utils-fs', () => ({ mkdirpSync: mockMakeDirSync })); jest.mock('write-file-atomic', () => ({ sync: mockWriteFileAtomicSync })); - const { BaseConfig } = require('../config'); + const { BaseConfig } = await import('../config'); - Config = class extends BaseConfig { - provideDefaults(c) { + Config = class extends BaseConfig { + provideDefaults() { return { defaulted: 5, }; @@ -283,7 +289,7 @@ describe('@ionic/cli-framework', () => { jest.spyOn(config, 'provideDefaults').mockImplementation(() => ({})); config.set('foo', 5); expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{\n "sub": {\n "foo": 5\n }\n}'); + expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{\n "sub": {\n "foo": 5\n }\n}\n'); }); it('should unset value in file', () => { @@ -292,7 +298,7 @@ describe('@ionic/cli-framework', () => { jest.spyOn(config, 'provideDefaults').mockImplementation(() => ({})); config.unset('foo'); expect(mockWriteFileAtomicSync).toHaveBeenCalledTimes(1); - expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{\n "sub": {}\n}'); + expect(mockWriteFileAtomicSync).toHaveBeenCalledWith('/path/to/file', '{\n "sub": {}\n}\n'); }); }); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/executor.ts b/packages/@ionic/cli-framework/src/lib/__tests__/executor.ts index 4342c12b23..ae59524c0d 100644 --- a/packages/@ionic/cli-framework/src/lib/__tests__/executor.ts +++ b/packages/@ionic/cli-framework/src/lib/__tests__/executor.ts @@ -1,21 +1,19 @@ -import { - Command, - CommandMap, - CommandMapDefault, - Namespace, - NamespaceMap, -} from '../command'; +import { Command, CommandMap, CommandMapDefault, Namespace, NamespaceMap } from '../command'; +import { CommandMetadata, NamespaceMetadata } from '../../definitions'; import { Executor } from '../executor'; -export const superspy = () => {}; +export const superspy = jest.fn(); class MyNamespace extends Namespace { - async getMetadata() { - return { name: 'my' }; + async getMetadata(): Promise { + return { + name: 'my', + summary: '', + }; } - async getNamespaces() { + async getNamespaces(): Promise { return new NamespaceMap([ ['foo', async () => new FooNamespace(this)], ['defns', async () => new NamespaceWithDefault(this)], @@ -24,11 +22,14 @@ class MyNamespace extends Namespace { } class NamespaceWithDefault extends Namespace { - async getMetadata() { - return { name: 'defns' }; + async getMetadata(): Promise { + return { + name: 'defns', + summary: '', + }; } - async getCommands() { + async getCommands(): Promise { return new CommandMap([ [CommandMapDefault, async () => new DefaultCommand(this)], ]); @@ -36,11 +37,14 @@ class NamespaceWithDefault extends Namespace { } class FooNamespace extends Namespace { - async getMetadata() { - return { name: 'foo' }; + async getMetadata(): Promise { + return { + name: 'foo', + summary: '', + }; } - async getCommands() { + async getCommands(): Promise { return new CommandMap([ ['bar', async () => new BarCommand(this)], ['baz', async () => new BazCommand(this)], @@ -50,14 +54,22 @@ class FooNamespace extends Namespace { } class DefaultCommand extends Command { - async getMetadata() { - return { name: 'def', description: '' }; + async getMetadata(): Promise { + return { + name: 'def', + summary: '', + }; } + + async run() {} } class BarCommand extends Command { - async getMetadata() { - return { name: 'bar', description: '' }; + async getMetadata(): Promise { + return { + name: 'bar', + summary: '', + }; } async validate() { @@ -70,19 +82,14 @@ class BarCommand extends Command { } class BazCommand extends Command { - async getMetadata() { - return { name: 'baz', description: '' }; - } -} - -class FooCommand extends Command { - async getMetadata() { + async getMetadata(): Promise { return { - name: 'foo', - type: 'global', - description: '', + name: 'baz', + summary: '', }; } + + async run() {} } describe('@ionic/cli-framework', () => { @@ -112,8 +119,8 @@ describe('@ionic/cli-framework', () => { delete runArgs[1]._; expect(runArgs[1]).toEqual({ '--': ['c'] }); expect(runArgs[2].location.obj).toBeInstanceOf(BarCommand); - const runPath = runArgs[2].location.path.map(([n]) => n); - const runPathObjs = runArgs[2].location.path.map(([, o]) => o); + const runPath = runArgs[2].location.path.map(([n]: any) => n); + const runPathObjs = runArgs[2].location.path.map(([, o]: any) => o); expect(runPath).toEqual(['my', 'foo', 'bar']); expect(runPathObjs[0]).toBeInstanceOf(MyNamespace); expect(runPathObjs[1]).toBeInstanceOf(FooNamespace); @@ -128,7 +135,7 @@ describe('@ionic/cli-framework', () => { const namespace = new MyNamespace(); const executor = new Executor({ namespace }); const location = await namespace.locate(['foo', 'bar']); - await executor.run(location.obj, []); + await executor.run(location.obj as any, []); expect(spy.mock.calls.length).toEqual(2); const [ [ validateId, validateArgs ], [ runId, runArgs ] ] = spy.mock.calls; expect(validateId).toEqual('bar:validate'); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/help.ts b/packages/@ionic/cli-framework/src/lib/__tests__/help.ts index 59f1a57012..c0adf8c5ed 100644 --- a/packages/@ionic/cli-framework/src/lib/__tests__/help.ts +++ b/packages/@ionic/cli-framework/src/lib/__tests__/help.ts @@ -1,6 +1,5 @@ import { Command, CommandMap, CommandMapDefault, Namespace, NamespaceMap } from '../command'; import { CommandGroup, NamespaceGroup, OptionGroup } from '../options'; -import { DISABLED_COLORS } from '../colors'; import { CommandStringHelpFormatter, NamespaceStringHelpFormatter, NamespaceSchemaHelpFormatter } from '../help'; import { stripAnsi } from '../../utils/format'; @@ -12,7 +11,7 @@ class MyNamespace extends Namespace { }; } - async getNamespaces() { + async getNamespaces(): Promise { return new NamespaceMap([ ['foo', async () => new FooNamespace(this)], ['defns', async () => new NamespaceWithDefault(this)], @@ -20,7 +19,7 @@ class MyNamespace extends Namespace { ]); } - async getCommands() { + async getCommands(): Promise { return new CommandMap([ ['bar', async () => new BarCommand(this)], ]); @@ -36,7 +35,7 @@ class NamespaceWithDefault extends Namespace { }; } - async getCommands() { + async getCommands(): Promise { return new CommandMap([ [CommandMapDefault, async () => new DefaultCommand(this)], ]); @@ -52,7 +51,7 @@ class FooNamespace extends Namespace { }; } - async getCommands() { + async getCommands(): Promise { return new CommandMap([ ['bar', async () => new BarCommand(this)], ['baz', async () => new BazCommand(this)], @@ -110,18 +109,6 @@ class BazCommand extends Command { async validate() {} } -class FooCommand extends Command { - async getMetadata() { - return { - name: 'foo', - summary: 'the foo command', - }; - } - - async run() {} - async validate() {} -} - describe('@ionic/cli-framework', () => { describe('lib/help', () => { @@ -131,7 +118,7 @@ describe('@ionic/cli-framework', () => { it('should format a command appropriately', async () => { const myns = new MyNamespace(); const location = await myns.locate(['foo', 'bar']); - const formatter = new CommandStringHelpFormatter({ location, command: location.obj }); + const formatter = new CommandStringHelpFormatter({ location, command: location.obj as any }); const result = await formatter.format(); expect(stripAnsi(result)).toEqual(` @@ -169,7 +156,7 @@ describe('@ionic/cli-framework', () => { it('should format a namespace appropriately', async () => { const myns = new MyNamespace(); const location = await myns.locate([]); - const formatter = new NamespaceStringHelpFormatter({ location, namespace: location.obj }); + const formatter = new NamespaceStringHelpFormatter({ location, namespace: location.obj as any }); const result = await formatter.format(); expect(stripAnsi(result)).toEqual(` @@ -191,7 +178,7 @@ describe('@ionic/cli-framework', () => { it('should format a subnamespace appropriately', async () => { const myns = new MyNamespace(); const location = await myns.locate(['foo']); - const formatter = new NamespaceStringHelpFormatter({ location, namespace: location.obj }); + const formatter = new NamespaceStringHelpFormatter({ location, namespace: location.obj as any }); const result = await formatter.format(); expect(stripAnsi(result)).toEqual(` @@ -356,7 +343,7 @@ describe('@ionic/cli-framework', () => { }; const myns = new MyNamespace(); const location = await myns.locate([]); - const formatter = new NamespaceSchemaHelpFormatter({ location, namespace: location.obj }); + const formatter = new NamespaceSchemaHelpFormatter({ location, namespace: location.obj as any }); const result = await formatter.serialize(); expect(result).toEqual(expected); }); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/logger.ts b/packages/@ionic/cli-framework/src/lib/__tests__/logger.ts index c74c213b82..1821cb62af 100644 --- a/packages/@ionic/cli-framework/src/lib/__tests__/logger.ts +++ b/packages/@ionic/cli-framework/src/lib/__tests__/logger.ts @@ -1,10 +1,8 @@ -import { Writable } from 'stream'; -import { WritableStreamBuffer } from '../../utils/streams'; - import stripAnsi = require('strip-ansi'); import { wordWrap } from '../../utils/format'; +import { WritableStreamBuffer } from '../../utils/streams'; -import { DEFAULT_OUTPUT, LOGGER_LEVELS, LOGGER_OUTPUTS, Logger, StreamHandler, createPrefixedFormatter, createTaggedFormatter } from '../logger'; +import { LOGGER_LEVELS, Logger, StreamHandler, createPrefixedFormatter, createTaggedFormatter } from '../logger'; describe('@ionic/cli-framework', () => { @@ -41,7 +39,8 @@ describe('@ionic/cli-framework', () => { describe('msg', () => { - let stream, logger; + let stream: WritableStreamBuffer; + let logger: Logger; beforeEach(() => { stream = new WritableStreamBuffer(); @@ -57,7 +56,8 @@ describe('@ionic/cli-framework', () => { describe('nl', () => { - let stream, logger; + let stream: WritableStreamBuffer; + let logger: Logger; beforeEach(() => { stream = new WritableStreamBuffer(); @@ -78,7 +78,9 @@ describe('@ionic/cli-framework', () => { describe('log', () => { - let stream, logger, handler; + let stream: WritableStreamBuffer; + let handler: StreamHandler; + let logger: Logger; beforeEach(() => { stream = new WritableStreamBuffer(); @@ -119,7 +121,9 @@ describe('@ionic/cli-framework', () => { describe('debug', () => { - let stream, logger, handler; + let stream: WritableStreamBuffer; + let handler: StreamHandler; + let logger: Logger; beforeEach(() => { stream = new WritableStreamBuffer(); @@ -142,7 +146,9 @@ describe('@ionic/cli-framework', () => { describe('info', () => { - let stream, logger, handler; + let stream: WritableStreamBuffer; + let handler: StreamHandler; + let logger: Logger; beforeEach(() => { stream = new WritableStreamBuffer(); @@ -165,7 +171,9 @@ describe('@ionic/cli-framework', () => { describe('warn', () => { - let stream, logger, handler; + let stream: WritableStreamBuffer; + let handler: StreamHandler; + let logger: Logger; beforeEach(() => { stream = new WritableStreamBuffer(); @@ -188,7 +196,9 @@ describe('@ionic/cli-framework', () => { describe('error', () => { - let stream, logger, handler; + let stream: WritableStreamBuffer; + let handler: StreamHandler; + let logger: Logger; beforeEach(() => { stream = new WritableStreamBuffer(); @@ -226,40 +236,50 @@ describe('@ionic/cli-framework', () => { describe('createTaggedFormatter', () => { + let stream: WritableStreamBuffer; + let handler: StreamHandler; + let logger: Logger; + + beforeEach(() => { + stream = new WritableStreamBuffer(); + handler = new StreamHandler({ stream }); + logger = new Logger({ handlers: new Set([handler]) }); + }); + it('should not format if requested', () => { const formatter = createTaggedFormatter(); - const result = formatter({ msg: 'hi', level: LOGGER_LEVELS.INFO, format: false }); + const result = formatter({ msg: 'hi', level: LOGGER_LEVELS.INFO, format: false, logger }); expect(result).toEqual('hi'); }); it('should not tag non-leveled outputs', () => { const formatter = createTaggedFormatter(); - const result = formatter({ msg: 'hi' }); + const result = formatter({ msg: 'hi', logger }); expect(result).toEqual('hi'); }); it('should log multi-line message properly for non-leveled outputs', () => { const formatter = createTaggedFormatter({ wrap: false }); - const result = formatter({ msg: 'hello world!\nThis is a message.' }); + const result = formatter({ msg: 'hello world!\nThis is a message.', logger }); expect(result).toEqual('hello world!\nThis is a message.'); }); it('should log multi-line message properly for wrapped, non-leveled outputs', () => { const formatter = createTaggedFormatter({ wrap: true }); - const result = formatter({ msg: 'hello world!\nThis is a message.' }); + const result = formatter({ msg: 'hello world!\nThis is a message.', logger }); expect(result).toEqual('hello world!\nThis is a message.'); }); it('should tag leveled outputs', () => { const formatter = createTaggedFormatter(); - const result = formatter({ msg: 'hi', level: LOGGER_LEVELS.INFO }); + const result = formatter({ msg: 'hi', level: LOGGER_LEVELS.INFO, logger }); expect(stripAnsi(result)).toEqual('[INFO] hi'); }); it('should not wrap by default', () => { const formatter = createTaggedFormatter(); const msg = 'A '.repeat(1000); - const result = formatter({ msg, level: LOGGER_LEVELS.INFO }); + const result = formatter({ msg, level: LOGGER_LEVELS.INFO, logger }); expect(stripAnsi(result)).toEqual(`[INFO] ${msg}`); }); @@ -267,31 +287,31 @@ describe('@ionic/cli-framework', () => { const wordWrapOpts = { width: 50 }; const formatter = createTaggedFormatter({ wrap: wordWrapOpts }); const msg = 'A '.repeat(1000); - const result = formatter({ msg, level: LOGGER_LEVELS.INFO }); + const result = formatter({ msg, level: LOGGER_LEVELS.INFO, logger }); expect(stripAnsi(result)).toEqual(`[INFO] ${wordWrap(msg, { indentation: 7, ...wordWrapOpts })}`); }); it('should not titleize by default', () => { const formatter = createTaggedFormatter(); - const result = formatter({ msg: `Hello!\nThis is a message.\nHere's another.`, level: LOGGER_LEVELS.INFO }); + const result = formatter({ msg: `Hello!\nThis is a message.\nHere's another.`, level: LOGGER_LEVELS.INFO, logger }); expect(stripAnsi(result)).toEqual(`[INFO] Hello!\n This is a message.\n Here's another.`); }); it('should not titleize for single line', () => { const formatter = createTaggedFormatter({ titleize: true }); - const result = formatter({ msg: 'Hello!', level: LOGGER_LEVELS.INFO }); + const result = formatter({ msg: 'Hello!', level: LOGGER_LEVELS.INFO, logger }); expect(stripAnsi(result)).toEqual(`[INFO] Hello!`); }); it('should titleize properly for double newline after first line', () => { const formatter = createTaggedFormatter({ titleize: true }); - const result = formatter({ msg: `Hello!\n\nThis is a message.\n\nHere's another.`, level: LOGGER_LEVELS.INFO }); + const result = formatter({ msg: `Hello!\n\nThis is a message.\n\nHere's another.`, level: LOGGER_LEVELS.INFO, logger }); expect(stripAnsi(result)).toEqual(`[INFO] Hello!\n\n This is a message.\n\n Here's another.`); }); it('should titleize if wanted', () => { const formatter = createTaggedFormatter({ titleize: true }); - const result = formatter({ msg: `Hello!\nThis is a message.\nHere's another.`, level: LOGGER_LEVELS.INFO }); + const result = formatter({ msg: `Hello!\nThis is a message.\nHere's another.`, level: LOGGER_LEVELS.INFO, logger }); expect(stripAnsi(result)).toEqual(`[INFO] Hello!\n\n This is a message.\n Here's another.`); }); @@ -299,14 +319,14 @@ describe('@ionic/cli-framework', () => { const wordWrapOpts = { width: 50 }; const formatter = createTaggedFormatter({ titleize: true, wrap: wordWrapOpts }); const msg = 'A '.repeat(1000); - const result = formatter({ msg, level: LOGGER_LEVELS.INFO }); + const result = formatter({ msg, level: LOGGER_LEVELS.INFO, logger }); expect(stripAnsi(result)).toEqual(`[INFO] ${wordWrap(msg, { indentation: 7, ...wordWrapOpts })}`); }); it('should prefix single line without level', () => { const now = new Date().toISOString(); const formatter = createTaggedFormatter({ prefix: `[${now}]` }); - const result = formatter({ msg: 'hello world!' }); + const result = formatter({ msg: 'hello world!', logger }); expect(result).toEqual(`[${now}] hello world!`); }); @@ -314,8 +334,8 @@ describe('@ionic/cli-framework', () => { let count = 0; const spy = jest.fn(() => `[${++count}]`); const formatter = createTaggedFormatter({ prefix: spy }); - const result1 = formatter({ msg: 'hello world!' }); - const result2 = formatter({ msg: 'hello world!' }); + const result1 = formatter({ msg: 'hello world!', logger }); + const result2 = formatter({ msg: 'hello world!', logger }); expect(result1).toEqual('[1] hello world!'); expect(result2).toEqual('[2] hello world!'); expect(spy).toHaveBeenCalledTimes(2); @@ -325,15 +345,25 @@ describe('@ionic/cli-framework', () => { describe('createPrefixedFormatter', () => { + let stream: WritableStreamBuffer; + let handler: StreamHandler; + let logger: Logger; + + beforeEach(() => { + stream = new WritableStreamBuffer(); + handler = new StreamHandler({ stream }); + logger = new Logger({ handlers: new Set([handler]) }); + }); + it('should not format if requested', () => { const formatter = createPrefixedFormatter('[prefix]'); - const result = formatter({ msg: 'hello world!', format: false }); + const result = formatter({ msg: 'hello world!', format: false, logger }); expect(result).toEqual('hello world!'); }); it('should prefix message', () => { const formatter = createPrefixedFormatter('[prefix]'); - const result = formatter({ msg: 'hello world!' }); + const result = formatter({ msg: 'hello world!', logger }); expect(result).toEqual('[prefix] hello world!'); }); @@ -341,8 +371,8 @@ describe('@ionic/cli-framework', () => { let count = 0; const spy = jest.fn(() => `[${++count}]`); const formatter = createPrefixedFormatter(spy); - const result1 = formatter({ msg: 'hello world!' }); - const result2 = formatter({ msg: 'hello world!' }); + const result1 = formatter({ msg: 'hello world!', logger }); + const result2 = formatter({ msg: 'hello world!', logger }); expect(result1).toEqual('[1] hello world!'); expect(result2).toEqual('[2] hello world!'); expect(spy).toHaveBeenCalledTimes(2); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/options.ts b/packages/@ionic/cli-framework/src/lib/__tests__/options.ts index 15e4fc8159..ead5ba8a06 100644 --- a/packages/@ionic/cli-framework/src/lib/__tests__/options.ts +++ b/packages/@ionic/cli-framework/src/lib/__tests__/options.ts @@ -1,14 +1,7 @@ import * as minimist from 'minimist'; -import { - OptionFilters, - filterCommandLineOptions, - filterCommandLineOptionsByGroup, - metadataOptionsToParseArgsOptions, - separateArgv, - stripOptions, - unparseArgs, -} from '../options'; +import { CommandMetadata } from '../../definitions'; +import { OptionFilters, filterCommandLineOptions, filterCommandLineOptionsByGroup, metadataOptionsToParseArgsOptions, separateArgv, stripOptions, unparseArgs } from '../options'; describe('@ionic/cli-framework', () => { @@ -77,29 +70,38 @@ describe('@ionic/cli-framework', () => { }); - const metadata1 = { + const metadata1: Required = { name: 'bar', + summary: '', + description: '', + exampleCommands: [], + groups: [], inputs: [ { name: 'input1', + summary: '', }, { name: 'input2', + summary: '', }, ], options: [ { name: 'foo', + summary: '', aliases: ['f'], groups: ['a'], }, { name: 'bar', + summary: '', default: 'soup', groups: ['b'], }, { name: 'flag1', + summary: '', type: Boolean, }, ], @@ -268,7 +270,7 @@ describe('@ionic/cli-framework', () => { }); it('should exclude options that do not match the predicate using opt', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], unknown: 'yep', foo: 'wow', bar: 'hi', flag1: true }, opt => opt.groups && opt.groups.includes('a')); + const result = filterCommandLineOptions(metadata1.options, { _: [], unknown: 'yep', foo: 'wow', bar: 'hi', flag1: true }, opt => opt.groups ? opt.groups.includes('a') : false); expect(result).toEqual({ _: [], foo: 'wow' }); }); @@ -278,8 +280,8 @@ describe('@ionic/cli-framework', () => { }); it('should include separated args from original parsed args', () => { - const result = filterCommandLineOptions(metadata1.options, { _: [], '--': 'some more --args' }); - expect(result).toEqual({ _: [], '--': 'some more --args' }); + const result = filterCommandLineOptions(metadata1.options, { _: [], '--': ['some', 'more', '--args'] }); + expect(result).toEqual({ _: [], '--': ['some', 'more', '--args'] }); }); describe('OptionFilters.includesGroups', () => { diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/prompts.ts b/packages/@ionic/cli-framework/src/lib/__tests__/prompts.ts index 1284b24d89..9ce2d49b80 100644 --- a/packages/@ionic/cli-framework/src/lib/__tests__/prompts.ts +++ b/packages/@ionic/cli-framework/src/lib/__tests__/prompts.ts @@ -8,9 +8,9 @@ describe('@ionic/cli-framework', () => { const mockClose = jest.fn(); const mockLogStream = {}; - function setupPromptMocks({ value, tty }: { value: string; tty: boolean; }) { + function setupPromptMocks({ value, tty }: { value?: any; tty: boolean; }) { const mocktty = tty; - const mockCreatePromptModule = () => async (question) => ({ [question.name]: value }); + const mockCreatePromptModule = () => async (question: { name: string; }) => ({ [question.name]: value }); mockMute.mockReset(); mockClose.mockReset(); jest.resetModules(); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/tasks.ts b/packages/@ionic/cli-framework/src/lib/__tests__/tasks.ts index a435bd4b15..d17cc8768b 100644 --- a/packages/@ionic/cli-framework/src/lib/__tests__/tasks.ts +++ b/packages/@ionic/cli-framework/src/lib/__tests__/tasks.ts @@ -6,7 +6,14 @@ describe('@ionic/cli-framework', () => { describe('Task', () => { - let task, handlers; + let task: Task; + let handlers: { + success: jest.Mock; + failure: jest.Mock; + clear: jest.Mock; + tick: jest.Mock; + end: jest.Mock; + }; beforeEach(() => { jest.useFakeTimers(); @@ -161,7 +168,13 @@ describe('@ionic/cli-framework', () => { describe('TaskChain', () => { - let chain, createTaskSpy, handlers; + let chain: TaskChain; + let createTaskSpy: jest.SpyInstance; + let handlers: { + end: jest.Mock; + failure: jest.Mock; + next: jest.Mock; + }; beforeEach(() => { chain = new TaskChain(); @@ -177,33 +190,33 @@ describe('@ionic/cli-framework', () => { }); it('should set current task upon first next', () => { - expect(chain.current).toBeUndefined(); + expect((chain as any).current).toBeUndefined(); const msg = 'hello world!'; const task = chain.next(msg); expect(task.msg).toEqual(msg); expect(task.running).toEqual(true); - expect(chain.current).toBe(task); + expect((chain as any).current).toBe(task); }); it('should remove current task when complete', () => { - expect(chain.current).toBeUndefined(); + expect((chain as any).current).toBeUndefined(); const task = chain.next('hello world!'); const spy = jest.fn(); task.on('success', spy); - expect(chain.current).toBe(task); + expect((chain as any).current).toBe(task); chain.end(); - expect(chain.current).toBeUndefined(); + expect((chain as any).current).toBeUndefined(); expect(spy).toHaveBeenCalledTimes(1); }); it('should fail current task', () => { - expect(chain.current).toBeUndefined(); + expect((chain as any).current).toBeUndefined(); const task = chain.next('hello world!'); const spy = jest.fn(); task.on('failure', spy); - expect(chain.current).toBe(task); + expect((chain as any).current).toBe(task); chain.fail(); - expect(chain.current).toBe(task); + expect((chain as any).current).toBe(task); expect(spy).toHaveBeenCalledTimes(1); }); diff --git a/packages/@ionic/cli-framework/src/lib/__tests__/validators.ts b/packages/@ionic/cli-framework/src/lib/__tests__/validators.ts index 5bc13ae0e9..4d7cadf12b 100644 --- a/packages/@ionic/cli-framework/src/lib/__tests__/validators.ts +++ b/packages/@ionic/cli-framework/src/lib/__tests__/validators.ts @@ -1,5 +1,3 @@ -import chalk from 'chalk'; - import { contains, validators } from '../validators'; import stripAnsi = require('strip-ansi'); @@ -24,12 +22,12 @@ describe('@ionic/cli-framework', () => { it('should return a name-specific message if the input is not given and a name is provided', () => { const result = validators.required(undefined, 'my_key'); - expect(stripAnsi(result)).toEqual('my_key must not be empty.'); + expect(stripAnsi(result as any)).toEqual('my_key must not be empty.'); }); it('should return a name-specific message if the input is an empty string and a name is provided', () => { const result = validators.required('', 'my_key'); - expect(stripAnsi(result)).toEqual('my_key must not be empty.'); + expect(stripAnsi(result as any)).toEqual('my_key must not be empty.'); }); it('should validate falsy input', () => { @@ -57,14 +55,14 @@ describe('@ionic/cli-framework', () => { it('should return a name-specific message if the email is empty and a name is provided', () => { const result = validators.email('', 'my_key'); - expect(stripAnsi(result)).toEqual('my_key is an invalid email address.'); + expect(stripAnsi(result as any)).toEqual('my_key is an invalid email address.'); }); it('should return a string if email address is invalid', () => { const r3 = validators.email('asdf'); const r4 = validators.email('foo@foo'); - expect(stripAnsi(r3)).toEqual('Invalid email address.'); - expect(stripAnsi(r4)).toEqual('Invalid email address.'); + expect(stripAnsi(r3 as any)).toEqual('Invalid email address.'); + expect(stripAnsi(r4 as any)).toEqual('Invalid email address.'); }); it('should validate an email', () => { @@ -86,7 +84,7 @@ describe('@ionic/cli-framework', () => { it('should return a name-specific message if the slug is invalid and a name is provided', () => { const result = validators.slug('A', 'my_key'); - expect(stripAnsi(result)).toEqual('my_key is an invalid slug (machine name).'); + expect(stripAnsi(result as any)).toEqual('my_key is an invalid slug (machine name).'); }); it('should validate a slug', () => { @@ -103,61 +101,61 @@ describe('@ionic/cli-framework', () => { it('should generate a validator that handles input not given', () => { const validator = contains(['foo', 'bar', 'baz'], {}); const result = validator(); - expect(stripAnsi(result)).toEqual('Must be one of: foo, bar, baz (not unset)'); + expect(stripAnsi(result as any)).toEqual('Must be one of: foo, bar, baz (not unset)'); }); it('should generate a validator that handles input not given with the given key', () => { const validator = contains(['foo', 'bar', 'baz'], {}); const result = validator(undefined, 'my_key'); - expect(stripAnsi(result)).toEqual('my_key must be one of: foo, bar, baz (not unset)'); + expect(stripAnsi(result as any)).toEqual('my_key must be one of: foo, bar, baz (not unset)'); }); it('should generate a validator that handles input not in collection', () => { const validator = contains(['foo', 'bar', 'baz'], {}); const result = validator('my input'); - expect(stripAnsi(result)).toEqual('Must be one of: foo, bar, baz (not my input)'); + expect(stripAnsi(result as any)).toEqual('Must be one of: foo, bar, baz (not my input)'); }); it('should generate a validator that handles input not in collection with the given key', () => { const validator = contains(['foo', 'bar', 'baz'], {}); const result = validator('my input', 'my_key'); - expect(stripAnsi(result)).toEqual('my_key must be one of: foo, bar, baz (not my input)'); + expect(stripAnsi(result as any)).toEqual('my_key must be one of: foo, bar, baz (not my input)'); }); it('should generate a validator that handles empty input', () => { const validator = contains(['foo', 'bar', 'baz'], {}); const result = validator(''); - expect(stripAnsi(result)).toEqual('Must be one of: foo, bar, baz (not empty)'); + expect(stripAnsi(result as any)).toEqual('Must be one of: foo, bar, baz (not empty)'); }); it('should generate a validator that handles empty input with the given key', () => { const validator = contains(['foo', 'bar', 'baz'], {}); const result = validator('', 'my_key'); - expect(stripAnsi(result)).toEqual('my_key must be one of: foo, bar, baz (not empty)'); + expect(stripAnsi(result as any)).toEqual('my_key must be one of: foo, bar, baz (not empty)'); }); it('should generate a validator that handles empty input in collection that allows unset input', () => { const validator = contains(['foo', 'bar', 'baz', undefined], {}); const result = validator(''); - expect(stripAnsi(result)).toEqual('Must be unset or one of: foo, bar, baz (not empty)'); + expect(stripAnsi(result as any)).toEqual('Must be unset or one of: foo, bar, baz (not empty)'); }); it('should generate a validator that handles empty input in collection that allows unset input with the given key', () => { const validator = contains(['foo', 'bar', 'baz', undefined], {}); const result = validator('', 'my_key'); - expect(stripAnsi(result)).toEqual('my_key must be unset or one of: foo, bar, baz (not empty)'); + expect(stripAnsi(result as any)).toEqual('my_key must be unset or one of: foo, bar, baz (not empty)'); }); it('should generate a validator that validates input in the collection', () => { const validator = contains(['foo', 'bar', 'baz'], {}); const result = validator('foo'); - expect(stripAnsi(result)).toBe(true); + expect(stripAnsi(result as any)).toBe(true); }); it('should generate a validator that validates input that is not given', () => { const validator = contains(['foo', 'bar', 'baz', undefined], {}); const result = validator(); - expect(stripAnsi(result)).toBe(true); + expect(stripAnsi(result as any)).toBe(true); }); }); diff --git a/packages/@ionic/cli-framework/src/lib/command.ts b/packages/@ionic/cli-framework/src/lib/command.ts index 8ce1537153..c10bdd4ad3 100644 --- a/packages/@ionic/cli-framework/src/lib/command.ts +++ b/packages/@ionic/cli-framework/src/lib/command.ts @@ -1,10 +1,11 @@ import * as lodash from 'lodash'; -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMapGetter, CommandMetadata, CommandMetadataInput, CommandMetadataOption, CommandPathItem, HydratedCommandMetadata, HydratedNamespaceMetadata, ICommand, ICommandMap, INamespace, INamespaceMap, NamespaceLocateResult, NamespaceMapGetter, NamespaceMetadata, ValidationError } from '../definitions'; +import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMapGetter, CommandMetadata, CommandMetadataInput, CommandMetadataOption, CommandPathItem, HydratedCommandMetadata, HydratedNamespaceMetadata, ICommand, ICommandMap, INamespace, INamespaceMap, NamespaceLocateOptions, NamespaceLocateResult, NamespaceMapGetter, NamespaceMetadata, ValidationError } from '../definitions'; import { InputValidationError } from '../errors'; import { AliasedMap } from '../utils/object'; import { strcmp } from '../utils/string'; +import { CommandGroup, NamespaceGroup } from './options'; import { validate } from './validators'; export abstract class BaseCommand, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption> { @@ -96,17 +97,17 @@ export abstract class BaseNamespace, N extends * * @param argv The set of command-line arguments to use to locate. */ - async locate(argv: ReadonlyArray): Promise> { + async locate(argv: ReadonlyArray, { useAliases = true }: NamespaceLocateOptions = {}): Promise> { const _locate = async (inputs: ReadonlyArray, parent: N, path: CommandPathItem[]): Promise> => { const [ key ] = inputs; const children = await parent.getNamespaces(); - const nsgetter = children.resolveAliases(key); + const nsgetter = useAliases ? children.resolveAlias(key) : children.get(key); - if (!nsgetter) { + if (!nsgetter || typeof nsgetter === 'string' || typeof nsgetter === 'symbol') { const commands = await parent.getCommands(); - const cmdgetter = commands.resolveAliases(key); + const cmdgetter = useAliases ? commands.resolveAlias(key) : commands.get(key); - if (cmdgetter) { + if (cmdgetter && typeof cmdgetter !== 'string' && typeof cmdgetter !== 'symbol') { const cmd = await cmdgetter(); return { args: inputs.slice(1), obj: cmd, path: [...path, [key, cmd]] }; } @@ -268,3 +269,8 @@ export async function generateCommandPath, N e return [...(await _cmdpath(ns)), [cmdmeta.name, cmd]]; } + +export async function isCommandVisible, N extends INamespace, M extends CommandMetadata, I extends CommandMetadataInput, O extends CommandMetadataOption>(cmd: HydratedCommandMetadata): Promise { + const ns = await cmd.namespace.getMetadata(); + return (!cmd.groups || !cmd.groups.includes(CommandGroup.Hidden)) && (!ns.groups || !ns.groups.includes(NamespaceGroup.Hidden)); +} diff --git a/packages/@ionic/cli-framework/src/lib/config.ts b/packages/@ionic/cli-framework/src/lib/config.ts index 998e6316df..b128580951 100644 --- a/packages/@ionic/cli-framework/src/lib/config.ts +++ b/packages/@ionic/cli-framework/src/lib/config.ts @@ -52,7 +52,7 @@ export abstract class BaseConfig { const v = this.pathPrefix.length === 0 ? value : lodash.set(this.file, [...this.pathPrefix], value); mkdirpSync(path.dirname(this.p)); - writeFileAtomic.sync(this.p, JSON.stringify(v, undefined, 2)); + writeFileAtomic.sync(this.p, JSON.stringify(v, undefined, 2) + '\n'); } get

(property: P): T[P]; diff --git a/packages/@ionic/cli-framework/src/lib/help.ts b/packages/@ionic/cli-framework/src/lib/help.ts index 96b7a8b707..a3df017c09 100644 --- a/packages/@ionic/cli-framework/src/lib/help.ts +++ b/packages/@ionic/cli-framework/src/lib/help.ts @@ -7,6 +7,8 @@ import { filter, map } from '../utils/array'; import { generateFillSpaceStringList, stringWidth, wordWrap } from '../utils/format'; import { Colors, DEFAULT_COLORS } from './colors'; +import { isCommandVisible } from './command'; +import { formatOptionName, isOptionVisible } from './options'; import { validators } from './validators'; const DEFAULT_DOTS_WIDTH = 25; @@ -48,7 +50,9 @@ export abstract class NamespaceHelpFormatter, * @param meta: The metadata of the command. * @return `true` to keep, `false` to discard */ - filterCommandCallback?(meta: HydratedCommandMetadata): Promise; + async filterCommandCallback(meta: HydratedCommandMetadata): Promise { + return isCommandVisible(meta); + } async getNamespaceMetadata(): Promise { if (!this._metadata) { @@ -130,7 +134,7 @@ export class NamespaceStringHelpFormatter, N e return wordWrap(metadata.description.trim(), { indentation: 4 }); } - async getExtraOptions(): Promise { + async getGlobalOptions(): Promise { return []; } @@ -138,7 +142,7 @@ export class NamespaceStringHelpFormatter, N e const { strong, weak, input } = this.colors; const fullName = await this.getNamespaceFullName(); - const options = ['--help', ...(await this.getExtraOptions())]; + const options = ['--help', ...(await this.getGlobalOptions())]; const usageLines = [ ` ${weak('[]')} ${options.map(opt => weak('[' + opt + ']')).join(' ')} ${weak('[options]')}`, ]; @@ -158,8 +162,7 @@ export class NamespaceStringHelpFormatter, N e async formatCommandGroup(title: string, commands: ReadonlyArray>): Promise { const { strong } = this.colors; - const filterCallback = this.filterCommandCallback; - const filteredCommands = filterCallback ? await filter(commands, async cmd => filterCallback(cmd)) : commands; + const filteredCommands = await filter(commands, async cmd => this.filterCommandCallback(cmd)); const [ cmdDetails, nsDetails ] = await Promise.all([ this.getListOfCommandDetails(filteredCommands.filter(cmd => cmd.namespace === this.namespace)), @@ -293,7 +296,9 @@ export abstract class CommandHelpFormatter, N * * @return `true` to keep, `false` to discard */ - filterOptionCallback?(option: O): Promise; + async filterOptionCallback(option: O): Promise { + return isOptionVisible(option); + } async getCommandMetadata(): Promise> { if (this._hydratedMetadata) { @@ -385,9 +390,8 @@ export class CommandStringHelpFormatter, N ext const fullName = await this.getCommandFullName(); const metadata = await this.getCommandMetadata(); - const filterCallback = this.filterOptionCallback; const options = metadata.options ? metadata.options : []; - const filteredOptions = filterCallback ? await filter(options, async opt => filterCallback(opt)) : options; + const filteredOptions = await filter(options, async opt => this.filterOptionCallback(opt)); const formattedInputs = metadata.inputs ? await Promise.all(metadata.inputs.map(async i => this.formatInlineInput(i))) : []; return ( @@ -460,8 +464,7 @@ export class CommandStringHelpFormatter, N ext async formatOptionsGroup(title: string, options: O[]): Promise { const { strong } = this.colors; - const filterCallback = this.filterOptionCallback; - const filteredOptions = filterCallback ? await filter(options, async opt => filterCallback(opt)) : options; + const filteredOptions = await filter(options, async opt => this.filterOptionCallback(opt)); if (filteredOptions.length === 0) { return ''; @@ -539,8 +542,7 @@ export class NamespaceSchemaHelpFormatter, N e } async formatCommandGroup(commands: ReadonlyArray>): Promise { - const filterCallback = this.filterCommandCallback; - const filteredCommands = filterCallback ? await filter(commands, async cmd => filterCallback(cmd)) : commands; + const filteredCommands = await filter(commands, async cmd => this.filterCommandCallback(cmd)); return map(filteredCommands, async cmd => this.formatCommand(cmd)); } @@ -609,8 +611,7 @@ export class CommandSchemaHelpFormatter, N ext } async formatOptions(options: ReadonlyArray): Promise> { - const filterCallback = this.filterOptionCallback; - const filteredOptions = filterCallback ? await filter(options, async opt => filterCallback(opt)) : options; + const filteredOptions = await filter(options, async opt => this.filterOptionCallback(opt)); return Promise.all(filteredOptions.map(async opt => this.formatOption(opt))); } @@ -641,19 +642,6 @@ export class CommandSchemaHelpFormatter, N ext } } -export function formatOptionName(opt: O, { showAliases = true, colors = DEFAULT_COLORS }: { showAliases?: boolean; colors?: Colors; } = {}): string { - const { input } = colors; - const showInverse = opt.type === Boolean && opt.default === true && opt.name.length > 1; - - return ( - (showInverse ? input(`--no-${opt.name}`) : input(`-${opt.name.length > 1 ? '-' : ''}${opt.name}`)) + - (showAliases ? - (!showInverse && opt.aliases && opt.aliases.length > 0 ? ', ' + opt.aliases - .map(alias => input(`-${alias}`)) - .join(', ') : '') : '') - ); -} - export function createCommandMetadataFromSchema(schema: CommandHelpSchema): Required { return { name: schema.name, diff --git a/packages/@ionic/cli-framework/src/lib/options.ts b/packages/@ionic/cli-framework/src/lib/options.ts index 4ffeab85a6..05adaa3726 100644 --- a/packages/@ionic/cli-framework/src/lib/options.ts +++ b/packages/@ionic/cli-framework/src/lib/options.ts @@ -3,6 +3,8 @@ import * as minimist from 'minimist'; import { CommandLineOptions, CommandMetadataOption, HydratedCommandMetadataOption, HydratedParseArgsOptions, ParsedArg } from '../definitions'; +import { Colors, DEFAULT_COLORS } from './colors'; + export const parseArgs = minimist; export { ParsedArgs } from 'minimist'; @@ -86,6 +88,28 @@ export function hydrateCommandMetadataOption(op }); } +export interface FormatOptionNameOptions { + readonly showAliases?: boolean; + readonly colors?: Colors | false; +} + +export function formatOptionName(opt: O, { showAliases = true, colors = DEFAULT_COLORS }: FormatOptionNameOptions = {}): string { + const colorfn: (input: string) => string = colors ? colors.input : lodash.identity; + const showInverse = opt.type === Boolean && opt.default === true && opt.name.length > 1; + + return ( + (showInverse ? colorfn(`--no-${opt.name}`) : colorfn(`-${opt.name.length > 1 ? '-' : ''}${opt.name}`)) + + (showAliases ? + (!showInverse && opt.aliases && opt.aliases.length > 0 ? ', ' + opt.aliases + .map(alias => colorfn(`-${alias}`)) + .join(', ') : '') : '') + ); +} + +export async function isOptionVisible(opt: O): Promise { + return !opt.groups || !opt.groups.includes(OptionGroup.Hidden); +} + export function metadataOptionsToParseArgsOptions(commandOptions: ReadonlyArray): HydratedParseArgsOptions { const options: HydratedParseArgsOptions = { string: ['_'], diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/array.ts b/packages/@ionic/cli-framework/src/utils/__tests__/array.ts index 3c27978966..615f2574f6 100644 --- a/packages/@ionic/cli-framework/src/utils/__tests__/array.ts +++ b/packages/@ionic/cli-framework/src/utils/__tests__/array.ts @@ -51,7 +51,7 @@ describe('@ionic/cli-framework', () => { describe('concurrentFilter', () => { it('should return new empty array', async () => { - const initial = []; + const initial: number[] = []; const result = await concurrentFilter(initial, async () => true); expect(result).not.toBe(initial); expect(result).toEqual([]); @@ -81,7 +81,7 @@ describe('@ionic/cli-framework', () => { describe('filter', () => { it('should return new empty array', async () => { - const initial = []; + const initial: number[] = []; const result = await filter(initial, async () => true); expect(result).not.toBe(initial); expect(result).toEqual([]); @@ -114,7 +114,7 @@ describe('@ionic/cli-framework', () => { it('should pass array into callback with each iteration', async () => { const initial = [1, 2, 3]; - const result = await filter(initial, async (v, i, arr) => { expect(arr).toBe(initial); }); + const result = await filter(initial, async (v, i, arr) => { expect(arr).toBe(initial); return false; }); expect(result).toEqual([]); }); @@ -123,7 +123,7 @@ describe('@ionic/cli-framework', () => { describe('map', () => { it('should return new empty array', async () => { - const initial = []; + const initial: number[] = []; const result = await map(initial, async () => {}); expect(result).not.toBe(initial); expect(result).toEqual([]); @@ -166,12 +166,13 @@ describe('@ionic/cli-framework', () => { }); it('should reduce to initial value with empty array', async () => { - const result = await reduce([], async () => {}, 10); + const result = await reduce([], async value => value, 10); expect(result).toEqual(10); }); it('should reduce to undefined if callback returns nothing', async () => { - const result = await reduce([1, 2, 3], async () => {}, 0); + const cb = async () => undefined; + const result = await reduce([1, 2, 3], cb as any, 0); expect(result).toBeUndefined(); }); @@ -196,12 +197,12 @@ describe('@ionic/cli-framework', () => { }); it('should reduce via returned values using accumulator and current index', async () => { - const result = await reduce([1, 2, 3], async (acc, v, i) => acc + String(v + i)); + const result = await reduce([1, 2, 3], async (acc, v, i) => String(acc) + String(v + i)); expect(result).toEqual('135'); }); it('should reduce via accumulator', async () => { - const result = await reduce([1, 2, 3], async (acc, v) => { acc.push('call: ' + v); return acc; }, []); + const result = await reduce([1, 2, 3], async (acc, v) => { acc.push('call: ' + v); return acc; }, []); expect(result).toEqual(['call: 1', 'call: 2', 'call: 3']); }); diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/format.ts b/packages/@ionic/cli-framework/src/utils/__tests__/format.ts index 358f9b9170..7b6351fa56 100644 --- a/packages/@ionic/cli-framework/src/utils/__tests__/format.ts +++ b/packages/@ionic/cli-framework/src/utils/__tests__/format.ts @@ -9,17 +9,18 @@ describe('@ionic/cli-framework', () => { describe('prettyPath', () => { + let originalCwd: () => string; const mock_os = os; jest.resetModules(); beforeEach(() => { - this.originalCwd = process.cwd; + originalCwd = process.cwd; process.cwd = () => '/home/user/dir1/dir2/dir3'; }); afterEach(() => { - process.cwd = this.originalCwd; + process.cwd = originalCwd; }); describe('posix', () => { @@ -105,6 +106,7 @@ describe('@ionic/cli-framework', () => { describe('windows', () => { + let originalCwd: () => string; jest.resetModules(); const mock_path_win32 = path.win32; const mock_homedir = () => 'C:\\home\\user'; @@ -113,12 +115,12 @@ describe('@ionic/cli-framework', () => { const { prettyPath } = require('../format'); beforeEach(() => { - this.originalCwd = process.cwd; + originalCwd = process.cwd; process.cwd = () => 'C:\\home\\user\\dir1\\dir2\\dir3'; }); afterEach(() => { - process.cwd = this.originalCwd; + process.cwd = originalCwd; }); it('should pretty print file in cwd', () => { diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/node.ts b/packages/@ionic/cli-framework/src/utils/__tests__/node.ts index 30fa5c40e2..279c4aa964 100644 --- a/packages/@ionic/cli-framework/src/utils/__tests__/node.ts +++ b/packages/@ionic/cli-framework/src/utils/__tests__/node.ts @@ -1,5 +1,4 @@ import * as path from 'path' -import { compileNodeModulesPaths } from '../node'; describe('@ionic/cli-framework', () => { diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/object.ts b/packages/@ionic/cli-framework/src/utils/__tests__/object.ts index c3db679817..096778cfe6 100644 --- a/packages/@ionic/cli-framework/src/utils/__tests__/object.ts +++ b/packages/@ionic/cli-framework/src/utils/__tests__/object.ts @@ -47,7 +47,7 @@ describe('@ionic/cli-framework', () => { it('should work for property keys that are symbols', () => { const o = createCaseInsensitiveObject(); - const s = Symbol('key'); + const s: any = Symbol('key'); o[s] = 'val'; expect(o[s]).toEqual('val'); }); @@ -101,22 +101,49 @@ describe('@ionic/cli-framework', () => { }); - describe('resolveAliases', () => { + describe('resolveAlias', () => { it('should return undefined for unknown command', () => { const m = new MyAliasedMap([]); - expect(m.resolveAliases('bar')).toBeUndefined(); + expect(m.resolveAlias('bar')).toBeUndefined(); }); it('should return command when immediately found', async () => { const obj = { foo: 'bar' }; const m = new MyAliasedMap([['foo', obj]]); - const result = m.resolveAliases('foo'); + const result = m.resolveAlias('foo'); expect(result).toBe(obj); }); }); + describe('keysWithoutAliases', () => { + + it('should return empty array', () => { + const m = new MyAliasedMap([]); + expect(m.keysWithoutAliases()).toEqual([]); + }); + + it('should return non-aliased keys for a plain map', async () => { + const m = new MyAliasedMap([['foo', { foo: 'bar' }], ['bar', { foo: 'bar' }]]); + const result = m.keysWithoutAliases(); + expect(result).toEqual(['foo', 'bar']); + }); + + it('should return non-aliased keys for a map with aliases', async () => { + const m = new MyAliasedMap([['foo', { foo: 'bar' }], ['bar', { foo: 'bar' }], ['f', 'foo'], ['b', 'bar']]); + const result = m.keysWithoutAliases(); + expect(result).toEqual(['foo', 'bar']); + }); + + it('should return non-aliased keys for a map with invalid aliases', async () => { + const m = new MyAliasedMap([['foo', { foo: 'bar' }], ['f', 'foo'], ['g', 'garbage']]); + const result = m.keysWithoutAliases(); + expect(result).toEqual(['foo']); + }); + + }); + }); }); diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/process.ts b/packages/@ionic/cli-framework/src/utils/__tests__/process.ts index d3b41b5ba3..ab51689ea8 100644 --- a/packages/@ionic/cli-framework/src/utils/__tests__/process.ts +++ b/packages/@ionic/cli-framework/src/utils/__tests__/process.ts @@ -1,67 +1,55 @@ -import { createProcessEnv } from '../process'; - describe('@ionic/cli-framework', () => { describe('utils/process', () => { describe('createProcessEnv', () => { - describe('linux', () => { - - const originalPlatform = process.platform; + describe('non-windows', () => { - beforeAll(() => { - Object.defineProperty(process, 'platform', { value: 'linux' }); - }); + const mock_terminal = { TERMINAL_INFO: { windows: false } }; + jest.resetModules(); + jest.mock('../terminal', () => mock_terminal); + const proc = require('../process'); it('should be case sensitive', () => { - const o = createProcessEnv(); + const o = proc.createProcessEnv(); o['asdf'] = 'val1'; o['ASDF'] = 'val2'; expect(o).toEqual({ 'asdf': 'val1', 'ASDF': 'val2' }); }); it('should use sources and be case sensitive', () => { - const o = createProcessEnv({ 'hello': 'you' }, { 'hello': 'world' }); + const o = proc.createProcessEnv({ 'hello': 'you' }, { 'hello': 'world' }); o['asdf'] = 'val1'; o['ASDF'] = 'val2'; expect(o['hello']).toEqual('world'); expect(o).toEqual({ 'asdf': 'val1', 'ASDF': 'val2', 'hello': 'world' }); }); - afterAll(() => { - Object.defineProperty(process, 'platform', { value: originalPlatform }); - }); - }); describe('windows', () => { - const originalPlatform = process.platform; - - beforeAll(() => { - Object.defineProperty(process, 'platform', { value: 'win32' }); - }); + const mock_terminal = { TERMINAL_INFO: { windows: true } }; + jest.resetModules(); + jest.mock('../terminal', () => mock_terminal); + const proc = require('../process'); it('should be case insensitive', () => { - const o = createProcessEnv(); + const o = proc.createProcessEnv(); o['asdf'] = 'val1'; o['ASDF'] = 'val2'; expect(o).toEqual({ 'asdf': 'val2' }); }); it('should use sources and be case insensitive', () => { - const o = createProcessEnv({ 'HELLO': 'YOU' }, { 'hello': 'world' }); + const o = proc.createProcessEnv({ 'HELLO': 'YOU' }, { 'hello': 'world' }); o['asdf'] = 'val1'; o['ASDF'] = 'val2'; expect(o['hello']).toEqual('world'); expect(o).toEqual({ 'asdf': 'val2', 'hello': 'world' }); }); - afterAll(() => { - Object.defineProperty(process, 'platform', { value: originalPlatform }); - }); - }); }); diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/shell.ts b/packages/@ionic/cli-framework/src/utils/__tests__/shell.ts index 8d49b3910f..1d11e7df50 100644 --- a/packages/@ionic/cli-framework/src/utils/__tests__/shell.ts +++ b/packages/@ionic/cli-framework/src/utils/__tests__/shell.ts @@ -1,9 +1,11 @@ import { EventEmitter } from 'events'; -import { createProcessEnv } from '../process'; +import * as path from 'path'; + +import { ShellCommandError } from '../../errors'; -import { PromiseUtil, promisifyEvent } from '../promise'; +import { createProcessEnv } from '../process'; +import { promisifyEvent } from '../promise'; import { ReadableStreamBuffer, WritableStreamBuffer } from '../streams'; -import * as path from 'path'; describe('@ionic/cli-framework', () => { @@ -49,7 +51,7 @@ describe('@ionic/cli-framework', () => { }); - describe('ShellCommand', () => { + describe('ShellCommand', async () => { beforeEach(() => { jest.useFakeTimers(); @@ -66,7 +68,7 @@ describe('@ionic/cli-framework', () => { jest.mock('os', () => mock_os); const mock_path_posix = path.posix; jest.mock('path', () => mock_path_posix); - const { ShellCommand } = require('../shell'); + const { ShellCommand } = await import('../shell'); beforeEach(() => { jest.resetAllMocks(); @@ -77,7 +79,7 @@ describe('@ionic/cli-framework', () => { const args = ['foo', 'bar', 'baz']; const cmd = new ShellCommand(name, args); expect(cmd.name).toEqual(name); - expect(cmd.path).not.toBeDefined(); + expect((cmd as any).path).not.toBeDefined(); expect(cmd.args).toEqual(args); }); @@ -86,7 +88,7 @@ describe('@ionic/cli-framework', () => { const args = ['foo', 'bar', 'baz']; const cmd = new ShellCommand(name, args); expect(cmd.name).toEqual('cmd'); - expect(cmd.path).toEqual(name); + expect((cmd as any).path).toEqual(name); expect(cmd.args).toEqual(args); }); @@ -217,8 +219,7 @@ describe('@ionic/cli-framework', () => { const mockSpawnStderr = new ReadableStreamBuffer(); const cp = new class extends EventEmitter { stdout = mockSpawnStdout; stderr = mockSpawnStderr; }; mockCrossSpawn.mockImplementation(() => cp); - const buf = new WritableStreamBuffer(); - const promise = cmd.run(buf, buf); + const promise = cmd.run(); expect(promise.p).toBe(cp); }); @@ -265,6 +266,14 @@ describe('@ionic/cli-framework', () => { try { await p; } catch (e) { + if (!(e instanceof ShellCommandError)) { + throw new Error('not ShellCommandError'); + } + + if (!e.output) { + throw new Error('no output'); + } + expect(e.output.length).toEqual(outinput.length + errinput.length); expect(e.output.split('').filter(l => l !== outletter).join('')).toEqual(errinput); expect(e.output.split('').filter(l => l !== errletter).join('')).toEqual(outinput); @@ -292,6 +301,14 @@ describe('@ionic/cli-framework', () => { try { await p; } catch (e) { + if (!(e instanceof ShellCommandError)) { + throw new Error('not ShellCommandError'); + } + + if (!e.output) { + throw new Error('no output'); + } + expect(e.output.length).toEqual(outinput.length + errinput.length); expect(e.output.split('').filter(l => l !== outletter).join('')).toEqual(errinput); expect(e.output.split('').filter(l => l !== errletter).join('')).toEqual(outinput); diff --git a/packages/@ionic/cli-framework/src/utils/__tests__/streams.ts b/packages/@ionic/cli-framework/src/utils/__tests__/streams.ts index fc0e058821..17e4887549 100644 --- a/packages/@ionic/cli-framework/src/utils/__tests__/streams.ts +++ b/packages/@ionic/cli-framework/src/utils/__tests__/streams.ts @@ -42,7 +42,7 @@ describe('@ionic/cli-framework', () => { it('should be fed a string', () => { const msg = 'hello world!'; const rsb = new ReadableStreamBuffer(); - const spy = jest.spyOn(rsb.buffer, 'write'); + const spy = jest.spyOn((rsb as any).buffer, 'write'); expect(rsb.size).toEqual(0); rsb.feed(msg); rsb.stop(); @@ -82,7 +82,7 @@ describe('@ionic/cli-framework', () => { it('should be pipeable', done => { let result = ''; - const ws = new class extends Writable { _write(chunk, enc, cb) { result += chunk.toString(); cb(); } }; + const ws = new class extends Writable { _write(chunk: any, enc: string, cb: () => void) { result += chunk.toString(); cb(); } }; const rsb = new ReadableStreamBuffer(); ws.on('pipe', src => { expect(src).toBe(rsb); diff --git a/packages/@ionic/cli-framework/src/utils/object.ts b/packages/@ionic/cli-framework/src/utils/object.ts index 55f067dedd..646613e991 100644 --- a/packages/@ionic/cli-framework/src/utils/object.ts +++ b/packages/@ionic/cli-framework/src/utils/object.ts @@ -38,13 +38,19 @@ export class AliasedMap extends Map return aliasmap; } - resolveAliases(key: AliasedMapKey | K): V | undefined { + resolveAlias(key: AliasedMapKey | K): V | undefined { const r = this.get(key); if (typeof r !== 'string' && typeof r !== 'symbol') { return r; } - return this.resolveAliases(r); + return this.resolveAlias(r); + } + + keysWithoutAliases(): K[] { + return [...this.entries()] + .filter((entry): entry is [K, V] => typeof entry[1] !== 'string' && typeof entry[1] !== 'symbol') + .map(([k, v]) => k); } } diff --git a/packages/@ionic/cli-framework/src/utils/process.ts b/packages/@ionic/cli-framework/src/utils/process.ts index 3e38624367..b69f87bf20 100644 --- a/packages/@ionic/cli-framework/src/utils/process.ts +++ b/packages/@ionic/cli-framework/src/utils/process.ts @@ -3,6 +3,7 @@ import * as lodash from 'lodash'; import * as kill from 'tree-kill'; import { createCaseInsensitiveObject } from './object'; +import { TERMINAL_INFO } from './terminal'; const debug = Debug('ionic:cli-framework:utils:process'); @@ -24,13 +25,13 @@ export function killProcessTree(pid: number, signal: string | number = 'SIGTERM' /** * Creates an alternative implementation of `process.env` object. * - * On Windows, `process.env` is a magic object that offers case-insensitive - * environment variable access. On other platforms, case sensitivity matters. - * This method creates an empty "`process.env`" object type that works for all - * platforms. + * On a Windows shell, `process.env` is a magic object that offers + * case-insensitive environment variable access. On other platforms, case + * sensitivity matters. This method creates an empty "`process.env`" object + * type that works for all platforms. */ export function createProcessEnv(...sources: { [key: string]: string | undefined; }[]): { [key: string]: string; } { - return lodash.assign(process.platform === 'win32' ? createCaseInsensitiveObject() : {}, ...sources); + return lodash.assign(TERMINAL_INFO.windows ? createCaseInsensitiveObject() : {}, ...sources); } /** @@ -65,10 +66,12 @@ export async function sleepUntil(predicate: () => boolean, { interval = 30, time } /** - * Never resolves. + * Never resolves and keeps Node running. */ export async function sleepForever(): Promise { - return new Promise(() => { /* never resolves */ }); + return new Promise(() => { + setInterval(() => { /* do nothing */ }, 1000); + }); } /** diff --git a/packages/@ionic/cli-framework/src/utils/terminal.ts b/packages/@ionic/cli-framework/src/utils/terminal.ts index e07d3b76f2..9a3c8e4f3e 100644 --- a/packages/@ionic/cli-framework/src/utils/terminal.ts +++ b/packages/@ionic/cli-framework/src/utils/terminal.ts @@ -14,11 +14,24 @@ if (CI_ENVIRONMENT_VARIABLES_DETECTED.length > 0) { } export interface TerminalInfo { + /** + * Whether the terminal is an interactive TTY or not. + */ readonly tty: boolean; + + /** + * Whether this is in CI or not. + */ readonly ci: boolean; + + /** + * Whether this is a Windows shell or not. + */ + readonly windows: boolean; } export const TERMINAL_INFO: TerminalInfo = Object.freeze({ tty: Boolean(process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY), ci: CI_ENVIRONMENT_VARIABLES_DETECTED.length > 0, + windows: process.platform === 'win32' && (process.env.MSYSTEM && /^MINGW(32|64)$/.test(process.env.MSYSTEM) || process.env.TERM === 'cygwin'), }); diff --git a/packages/@ionic/cli-framework/tsconfig.json b/packages/@ionic/cli-framework/tsconfig.json index 9f121bd21f..a4158e6941 100644 --- a/packages/@ionic/cli-framework/tsconfig.json +++ b/packages/@ionic/cli-framework/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./" + "outDir": "./", + "types": [ + "node" + ] }, "include": [ "../../../types/stream-combiner2.d.ts", diff --git a/packages/@ionic/discover/CHANGELOG.md b/packages/@ionic/discover/CHANGELOG.md index d653932de0..cbebf81ac5 100644 --- a/packages/@ionic/discover/CHANGELOG.md +++ b/packages/@ionic/discover/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.0.7](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.6...@ionic/discover@1.0.7) (2018-10-31) + + + + +**Note:** Version bump only for package @ionic/discover + ## [1.0.6](https://github.com/ionic-team/ionic-cli/compare/@ionic/discover@1.0.5...@ionic/discover@1.0.6) (2018-10-05) diff --git a/packages/@ionic/discover/package.json b/packages/@ionic/discover/package.json index 66e9ee3f9a..00369843a9 100644 --- a/packages/@ionic/discover/package.json +++ b/packages/@ionic/discover/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/discover", - "version": "1.0.6", + "version": "1.0.7", "description": "Simple UDP based protocol for service discovery implemented in pure JS.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -24,9 +24,8 @@ "lint": "tslint --config tslint.js --project tsconfig.json", "build": "npm run clean && tsc", "watch": "tsc -w --preserveWatchOutput", - "test": "jest", - "prepublishOnly": "npm run build", - "cli-scripts:pre-commit": "lint-staged" + "test": "jest --maxWorkers=4", + "prepublishOnly": "npm run build" }, "lint-staged": { "linters": { @@ -44,14 +43,15 @@ }, "devDependencies": { "@types/debug": "0.0.31", + "@types/jest": "^23.3.5", "@types/netmask": "^1.0.30", "@types/node": "^6.0.101", "@types/ws": "^6.0.0", "jest": "^23.0.1", "jest-cli": "^23.0.1", - "lint-staged": "^7.2.0", + "lint-staged": "^8.0.0", "rimraf": "^2.6.2", - "ts-jest": "~23.1.0", + "ts-jest": "^23.10.1", "tslint": "^5.9.1", "typescript": "~3.1.1" } diff --git a/packages/@ionic/discover/src/__tests__/publisher.ts b/packages/@ionic/discover/src/__tests__/publisher.ts index 1e9e6f92dd..f6ab0469c7 100644 --- a/packages/@ionic/discover/src/__tests__/publisher.ts +++ b/packages/@ionic/discover/src/__tests__/publisher.ts @@ -5,10 +5,10 @@ describe('publisher', () => { it('create', () => { const p = new Publisher('devapp', 'conference-app', 8100); p.on('error', () => {}); - expect(p.client).toBeUndefined(); + expect((p as any).client).toBeUndefined(); expect(p.namespace).toEqual('devapp'); expect(p.name).toEqual('conference-app'); - expect(p.interval).toEqual(2000); + expect((p as any).interval).toEqual(2000); expect(p.path).toEqual('/'); expect(p.port).toEqual(8100); expect(p.id.length).toBeGreaterThan(0); @@ -19,9 +19,9 @@ describe('publisher', () => { const p = new Publisher('devapp', 'conference-app', 8100); p.on('error', () => {}); await p.start(); - expect(p.timer).toBeDefined(); + expect((p as any).timer).toBeDefined(); p.stop(); - expect(p.timer).toBeUndefined(); + expect((p as any).timer).toBeUndefined(); }, 1000); it('start/stop', async () => { @@ -29,19 +29,19 @@ describe('publisher', () => { p.on('error', () => {}); await p.start(); expect(p.running).toBeTruthy(); - expect(p.client).toBeDefined(); + expect((p as any).client).toBeDefined(); p.stop(); expect(p.running).toBeFalsy(); - expect(p.client).toBeUndefined(); + expect((p as any).client).toBeUndefined(); }, 1000); it('buildMessage', () => { const p = new Publisher('devapp', 'conference-app', 8100); p.on('error', () => {}); - p.path = '/?devapp=true'; - const message = p.buildMessage('192.168.0.1'); + (p as any).path = '/?devapp=true'; + const message = (p as any).buildMessage('192.168.0.1'); expect(message.t).toEqual(expect.any(Number)); const expected = { t: message.t, diff --git a/packages/@ionic/discover/tsconfig.json b/packages/@ionic/discover/tsconfig.json index 5dbdd53737..46cccd1031 100644 --- a/packages/@ionic/discover/tsconfig.json +++ b/packages/@ionic/discover/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": [ + "node" + ] }, "include": [ "src/**/*.ts" diff --git a/packages/@ionic/lab/CHANGELOG.md b/packages/@ionic/lab/CHANGELOG.md index 8e49d2df8f..dc1fa38b7a 100644 --- a/packages/@ionic/lab/CHANGELOG.md +++ b/packages/@ionic/lab/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.0.12](https://github.com/ionic-team/ionic-cli/compare/@ionic/lab@1.0.11...@ionic/lab@1.0.12) (2018-10-31) + + + + +**Note:** Version bump only for package @ionic/lab + ## [1.0.11](https://github.com/ionic-team/ionic-cli/compare/@ionic/lab@1.0.10...@ionic/lab@1.0.11) (2018-10-05) diff --git a/packages/@ionic/lab/package.json b/packages/@ionic/lab/package.json index ee5fc1642f..df5e97d75c 100644 --- a/packages/@ionic/lab/package.json +++ b/packages/@ionic/lab/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/lab", - "version": "1.0.11", + "version": "1.0.12", "description": "Ionic Lab utility for developing Ionic apps, used by Ionic CLI", "bin": { "ionic-lab": "./bin/ionic-lab" @@ -35,8 +35,7 @@ "watch": "npm run watch.tsc", "watch.stencil": "npm run clean.stencil && concurrently \"stencil build --dev --watch\" \"stencil-dev-server --address localhost --httpPort 8900 --no-open\"", "watch.tsc": "tsc -w --preserveWatchOutput", - "prepublishOnly": "npm run build", - "cli-scripts:pre-commit": "lint-staged" + "prepublishOnly": "npm run build" }, "lint-staged": { "linters": { @@ -47,23 +46,20 @@ ] }, "dependencies": { - "@ionic/cli-framework": "1.1.1", - "@ionic/utils-fs": "0.0.3", + "@ionic/cli-framework": "1.2.0", + "@ionic/utils-fs": "0.0.4", "chalk": "^2.3.0", "express": "^4.16.2", "tslib": "^1.9.0" }, "devDependencies": { - "@stencil/core": "^0.12.4", + "@stencil/core": "^0.15.0", "@stencil/dev-server": "0.0.18", "@types/express": "^4.11.0", "@types/node": "^6.0.101", "concurrently": "^4.0.1", - "jest": "^23.0.1", - "jest-cli": "^23.0.1", - "lint-staged": "^7.2.0", + "lint-staged": "^8.0.0", "rimraf": "^2.6.2", - "ts-jest": "~23.1.0", "tslint": "^5.9.1", "typescript": "~3.1.1" } diff --git a/packages/@ionic/lab/src/stencil/components.d.ts b/packages/@ionic/lab/src/stencil/components.d.ts index c47ef1dedc..9b4bfacb94 100644 --- a/packages/@ionic/lab/src/stencil/components.d.ts +++ b/packages/@ionic/lab/src/stencil/components.d.ts @@ -1,8 +1,9 @@ -/** -* This is an autogenerated file created by the Stencil compiler. -* It contains typing information for all components that exist in this project. -*/ /* tslint:disable */ +/** + * This is an autogenerated file created by the Stencil compiler. + * It contains typing information for all components that exist in this project. + */ + import '@stencil/core'; @@ -37,8 +38,8 @@ export namespace Components { interface IonlabPreview { 'activeDevices': string[]; - 'projectType': string; - 'url': string; + 'projectType'?: string; + 'url'?: string; } interface IonlabPreviewAttributes extends StencilHTMLAttributes { 'activeDevices'?: string[]; diff --git a/packages/@ionic/lab/tsconfig.json b/packages/@ionic/lab/tsconfig.json index 677882b957..e69cb9f958 100644 --- a/packages/@ionic/lab/tsconfig.json +++ b/packages/@ionic/lab/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": [ + "node" + ] }, "include": [ "src/**/*.ts" diff --git a/packages/@ionic/utils-fs/CHANGELOG.md b/packages/@ionic/utils-fs/CHANGELOG.md index 0d01f7a001..527b22d6cb 100644 --- a/packages/@ionic/utils-fs/CHANGELOG.md +++ b/packages/@ionic/utils-fs/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@0.0.3...@ionic/utils-fs@0.0.4) (2018-10-31) + + + + +**Note:** Version bump only for package @ionic/utils-fs + ## [0.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-fs@0.0.2...@ionic/utils-fs@0.0.3) (2018-10-05) diff --git a/packages/@ionic/utils-fs/package.json b/packages/@ionic/utils-fs/package.json index 237eb9c9b1..66584b851f 100644 --- a/packages/@ionic/utils-fs/package.json +++ b/packages/@ionic/utils-fs/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/utils-fs", - "version": "0.0.3", + "version": "0.0.4", "description": "Filesystem utils for Node", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -24,9 +24,8 @@ "lint": "tslint --config tslint.js --project tsconfig.json", "build": "npm run clean && tsc", "watch": "tsc -w --preserveWatchOutput", - "test": "jest", - "prepublishOnly": "npm run build", - "cli-scripts:pre-commit": "lint-staged" + "test": "jest --maxWorkers=4", + "prepublishOnly": "npm run build" }, "lint-staged": { "linters": { @@ -44,14 +43,15 @@ }, "devDependencies": { "@types/debug": "0.0.31", + "@types/jest": "^23.3.5", "@types/ncp": "^2.0.1", "@types/node": "^6.0.101", "@types/through2": "^2.0.33", "jest": "^23.0.1", "jest-cli": "^23.0.1", - "lint-staged": "^7.2.0", + "lint-staged": "^8.0.0", "rimraf": "^2.6.2", - "ts-jest": "~23.1.0", + "ts-jest": "^23.10.1", "tslint": "^5.9.1", "typescript": "~3.1.1" } diff --git a/packages/@ionic/utils-fs/tsconfig.json b/packages/@ionic/utils-fs/tsconfig.json index 5dbdd53737..46cccd1031 100644 --- a/packages/@ionic/utils-fs/tsconfig.json +++ b/packages/@ionic/utils-fs/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": [ + "node" + ] }, "include": [ "src/**/*.ts" diff --git a/packages/@ionic/utils-network/CHANGELOG.md b/packages/@ionic/utils-network/CHANGELOG.md index ebd18eebf4..9f5777a285 100644 --- a/packages/@ionic/utils-network/CHANGELOG.md +++ b/packages/@ionic/utils-network/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.4](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@0.0.3...@ionic/utils-network@0.0.4) (2018-10-31) + + + + +**Note:** Version bump only for package @ionic/utils-network + ## [0.0.3](https://github.com/ionic-team/ionic-cli/compare/@ionic/utils-network@0.0.2...@ionic/utils-network@0.0.3) (2018-10-05) diff --git a/packages/@ionic/utils-network/package.json b/packages/@ionic/utils-network/package.json index 3b9b835d47..b42fc68ebc 100644 --- a/packages/@ionic/utils-network/package.json +++ b/packages/@ionic/utils-network/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/utils-network", - "version": "0.0.3", + "version": "0.0.4", "description": "Network utils for Node", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -24,9 +24,8 @@ "lint": "tslint --config tslint.js --project tsconfig.json", "build": "npm run clean && tsc", "watch": "tsc -w --preserveWatchOutput", - "test": "jest", - "prepublishOnly": "npm run build", - "cli-scripts:pre-commit": "lint-staged" + "test": "jest --maxWorkers=4", + "prepublishOnly": "npm run build" }, "lint-staged": { "linters": { @@ -42,12 +41,13 @@ }, "devDependencies": { "@types/debug": "0.0.31", + "@types/jest": "^23.3.5", "@types/node": "^6.0.101", "jest": "^23.0.1", "jest-cli": "^23.0.1", - "lint-staged": "^7.2.0", + "lint-staged": "^8.0.0", "rimraf": "^2.6.2", - "ts-jest": "~23.1.0", + "ts-jest": "^23.10.1", "tslint": "^5.9.1", "typescript": "~3.1.1" } diff --git a/packages/@ionic/utils-network/tsconfig.json b/packages/@ionic/utils-network/tsconfig.json index 5dbdd53737..46cccd1031 100644 --- a/packages/@ionic/utils-network/tsconfig.json +++ b/packages/@ionic/utils-network/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": [ + "node" + ] }, "include": [ "src/**/*.ts" diff --git a/packages/@ionic/v1-toolkit/CHANGELOG.md b/packages/@ionic/v1-toolkit/CHANGELOG.md index 025b8176f1..bd8b6a03a8 100644 --- a/packages/@ionic/v1-toolkit/CHANGELOG.md +++ b/packages/@ionic/v1-toolkit/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.0.11](https://github.com/ionic-team/ionic-cli/compare/@ionic/v1-toolkit@1.0.10...@ionic/v1-toolkit@1.0.11) (2018-10-31) + + + + +**Note:** Version bump only for package @ionic/v1-toolkit + ## [1.0.10](https://github.com/ionic-team/ionic-cli/compare/@ionic/v1-toolkit@1.0.9...@ionic/v1-toolkit@1.0.10) (2018-10-05) diff --git a/packages/@ionic/v1-toolkit/package.json b/packages/@ionic/v1-toolkit/package.json index 224a526600..cfcb2fc338 100644 --- a/packages/@ionic/v1-toolkit/package.json +++ b/packages/@ionic/v1-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/v1-toolkit", - "version": "1.0.10", + "version": "1.0.11", "description": "Utility CLI for Ionic 1.x apps, used by Ionic CLI", "bin": { "ionic-v1": "./bin/ionic-v1" @@ -29,9 +29,8 @@ "lint": "tslint --config tslint.js --project tsconfig.json", "build": "npm run clean && tsc", "watch": "tsc -w --preserveWatchOutput", - "test": "jest", - "prepublishOnly": "npm run build", - "cli-scripts:pre-commit": "lint-staged" + "test": "jest --maxWorkers=4", + "prepublishOnly": "npm run build" }, "lint-staged": { "linters": { @@ -42,8 +41,8 @@ ] }, "dependencies": { - "@ionic/cli-framework": "1.1.1", - "@ionic/utils-fs": "0.0.3", + "@ionic/cli-framework": "1.2.0", + "@ionic/utils-fs": "0.0.4", "chalk": "^2.3.0", "chokidar": "^2.0.0", "debug": "^4.0.0", @@ -60,13 +59,14 @@ "@types/express": "^4.11.0", "@types/gulp": "^3.8.36", "@types/http-proxy-middleware": "^0.17.2", + "@types/jest": "^23.3.5", "@types/node": "^6.0.101", "@types/ws": "^6.0.0", "jest": "^23.0.1", "jest-cli": "^23.0.1", - "lint-staged": "^7.2.0", + "lint-staged": "^8.0.0", "rimraf": "^2.6.2", - "ts-jest": "~23.1.0", + "ts-jest": "^23.10.1", "tslint": "^5.9.1", "typescript": "~3.1.1" } diff --git a/packages/@ionic/v1-toolkit/tsconfig.json b/packages/@ionic/v1-toolkit/tsconfig.json index a3f6b9ff34..0251d0a21b 100644 --- a/packages/@ionic/v1-toolkit/tsconfig.json +++ b/packages/@ionic/v1-toolkit/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": [ + "node" + ] }, "include": [ "../../../types/slice-ansi.d.ts", diff --git a/packages/cli-scripts/CHANGELOG.md b/packages/cli-scripts/CHANGELOG.md index e231ec6da3..0066652002 100644 --- a/packages/cli-scripts/CHANGELOG.md +++ b/packages/cli-scripts/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.21](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.20...cli-scripts@0.0.21) (2018-10-31) + + + + +**Note:** Version bump only for package cli-scripts + ## [0.0.20](https://github.com/ionic-team/ionic-cli/compare/cli-scripts@0.0.19...cli-scripts@0.0.20) (2018-10-05) diff --git a/packages/cli-scripts/package.json b/packages/cli-scripts/package.json index 2919240434..b74c882fab 100644 --- a/packages/cli-scripts/package.json +++ b/packages/cli-scripts/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "cli-scripts", - "version": "0.0.20", + "version": "0.0.21", "bin": { "ionic-cli-scripts": "./bin/ionic-cli-scripts" }, @@ -10,9 +10,8 @@ "lint": "tslint --config tslint.js --project tsconfig.json", "build": "npm run clean && tsc", "watch": "tsc -w --preserveWatchOutput", - "test": "jest", - "prepublishOnly": "npm run build", - "cli-scripts:pre-commit": "lint-staged" + "test": "jest --maxWorkers=4", + "prepublishOnly": "npm run build" }, "lint-staged": { "linters": { @@ -25,25 +24,26 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "dependencies": { - "@ionic/cli-framework": "1.1.1", - "@ionic/utils-fs": "0.0.3", + "@ionic/cli-framework": "1.2.0", + "@ionic/utils-fs": "0.0.4", "ansi-styles": "^3.2.0", "chalk": "^2.3.0", "escape-string-regexp": "^1.0.5", - "ionic": "4.2.1", + "ionic": "4.3.0", "strip-ansi": "^5.0.0", "tslib": "^1.9.0" }, "devDependencies": { "@types/ansi-styles": "^3.2.0", "@types/escape-string-regexp": "1.0.0", + "@types/jest": "^23.3.5", "@types/node": "^6.0.101", "@types/strip-ansi": "^3.0.0", "jest": "^23.0.1", "jest-cli": "^23.0.1", - "lint-staged": "^7.2.0", + "lint-staged": "^8.0.0", "rimraf": "^2.6.2", - "ts-jest": "~23.1.0", + "ts-jest": "^23.10.1", "tslint": "^5.9.1", "typescript": "~3.1.1" } diff --git a/packages/cli-scripts/src/docs/index.ts b/packages/cli-scripts/src/docs/index.ts index 14b16b0c61..13bd405af9 100644 --- a/packages/cli-scripts/src/docs/index.ts +++ b/packages/cli-scripts/src/docs/index.ts @@ -33,7 +33,7 @@ export class DocsCommand extends Command { for (const projectType of projectTypes) { // TODO: possible to do this without a physical directory? const ctx = { ...baseCtx, execPath: path.resolve(PROJECTS_DIRECTORY, projectType) }; - const executor = await loadExecutor(ctx, [], {}); + const executor = await loadExecutor(ctx, []); const location = await executor.namespace.locate([]); const formatter = new NamespaceSchemaHelpFormatter({ location, namespace: executor.namespace }); diff --git a/packages/cli-scripts/src/index.ts b/packages/cli-scripts/src/index.ts index b01f906fdb..ec95c8e705 100644 --- a/packages/cli-scripts/src/index.ts +++ b/packages/cli-scripts/src/index.ts @@ -1,7 +1,6 @@ import { CommandMap, Namespace, execute } from '@ionic/cli-framework'; import { DocsCommand } from './docs'; -import { PrecommitCommand } from './pre-commit'; class CLIScriptsNamespace extends Namespace { async getMetadata() { @@ -14,7 +13,6 @@ class CLIScriptsNamespace extends Namespace { async getCommands(): Promise { return new CommandMap([ ['docs', async () => new DocsCommand(this)], - ['pre-commit', async () => new PrecommitCommand(this)], ]); } } diff --git a/packages/cli-scripts/src/pre-commit/index.ts b/packages/cli-scripts/src/pre-commit/index.ts deleted file mode 100644 index f17bbe47c0..0000000000 --- a/packages/cli-scripts/src/pre-commit/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Command, CommandLineInputs, CommandLineOptions, CommandMetadata } from '@ionic/cli-framework'; -import { onBeforeExit } from '@ionic/cli-framework/utils/process'; -import { ShellCommand } from '@ionic/cli-framework/utils/shell'; - -export class PrecommitCommand extends Command { - needsStash = false; - - async getMetadata(): Promise { - return { - name: 'pre-commit', - summary: '', - }; - } - - async run(inputs: CommandLineInputs, options: CommandLineOptions) { - this.needsStash = await this.hasLocalChanges(); - - if (this.needsStash) { - await new ShellCommand('git', ['stash', '-k'], { stdio: 'inherit' }).run(); - } - - onBeforeExit(async () => this.cleanup()); - - try { - await new ShellCommand('lerna', ['run', 'cli-scripts:pre-commit'], { stdio: 'inherit' }).run(); - } catch (e) { - throw e; - } finally { - await this.cleanup(); - } - } - - async cleanup() { - if (this.needsStash) { - await new ShellCommand('git', ['stash', 'pop'], { stdio: 'inherit' }).run(); - } - } - - async hasLocalChanges() { - const cmd = new ShellCommand('git', ['diff', '--quiet', '--exit-code'], { stdio: 'ignore' }); - - try { - await cmd.run(); - } catch (e) { - return true; - } - - return false; - } -} diff --git a/packages/ionic/CHANGELOG.md b/packages/ionic/CHANGELOG.md index b8fb7e36b4..8f8993ffec 100644 --- a/packages/ionic/CHANGELOG.md +++ b/packages/ionic/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.3.0](https://github.com/ionic-team/ionic-cli/compare/ionic@4.2.1...ionic@4.3.0) (2018-10-31) + + +### Bug Fixes + +* **capacitor:** switch off livereload for --no-build ([9960047](https://github.com/ionic-team/ionic-cli/commit/9960047)) +* **help:** filter out unnecessary global options ([7809c99](https://github.com/ionic-team/ionic-cli/commit/7809c99)) +* **ssh:** adjust validator to work for OpenSSH 7.8 ([dcc598a](https://github.com/ionic-team/ionic-cli/commit/dcc598a)) +* **terminal:** adjust some feature detection on windows ([8a2ed99](https://github.com/ionic-team/ionic-cli/commit/8a2ed99)) + + +### Features + +* **capacitor:** add --livereload-url option for custom dev server ([b7738f5](https://github.com/ionic-team/ionic-cli/commit/b7738f5)) +* **cordova:** add --livereload-url option for custom dev server ([ad57e36](https://github.com/ionic-team/ionic-cli/commit/ad57e36)) +* **integrations:** --root option for choosing an alternative location ([7e8f11e](https://github.com/ionic-team/ionic-cli/commit/7e8f11e)) +* **multi-app:** determine active project via cwd path match ([f83dc5b](https://github.com/ionic-team/ionic-cli/commit/f83dc5b)) +* **resources:** generate resources without needing platform installation ([4f20554](https://github.com/ionic-team/ionic-cli/commit/4f20554)) +* **start:** better multi-app support ([3c70e87](https://github.com/ionic-team/ionic-cli/commit/3c70e87)) + + + + ## [4.2.1](https://github.com/ionic-team/ionic-cli/compare/ionic@4.2.0...ionic@4.2.1) (2018-10-05) @@ -168,7 +192,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline This release offers support for :sparkles: **Ionic 4** :sparkles: (beta). -:memo: Use the new [CLI Documentation](https://beta.ionicframework.com/docs/cli/overview) on the beta framework documentation website for CLI 4. +:memo: Use the new [CLI Documentation](https://beta.ionicframework.com/docs/cli) on the beta framework documentation website for CLI 4. #### :lollipop: Upgrading from CLI 3 diff --git a/packages/ionic/bin/ionic b/packages/ionic/bin/ionic index 55be9c1798..095f17cb9a 100755 --- a/packages/ionic/bin/ionic +++ b/packages/ionic/bin/ionic @@ -46,7 +46,7 @@ var bootstrap = require(path.resolve(lib, 'bootstrap')); bootstrap.detectLocalCLI() .then(function(localPath) { cli = require(localPath); - return cli.run(pargv, process.env); + return cli.run(pargv); }, function(err) { cli = require(lib); @@ -56,7 +56,7 @@ bootstrap.detectLocalCLI() process.env.IONIC_CLI_LOCAL_ERROR = err; - return cli.run(pargv, process.env); + return cli.run(pargv); }) .then(function() { evt.emit('ready'); diff --git a/packages/ionic/package.json b/packages/ionic/package.json index 076a8d430c..7ff9b00b99 100644 --- a/packages/ionic/package.json +++ b/packages/ionic/package.json @@ -1,6 +1,6 @@ { "name": "ionic", - "version": "4.2.1", + "version": "4.3.0", "description": "A tool for creating and developing Ionic Framework mobile apps.", "homepage": "https://ionicframework.com", "author": "Ionic Team (https://ionicframework.com) ", @@ -17,9 +17,8 @@ "lint": "tslint --config tslint.js --project tsconfig.json", "build": "npm run clean && tsc", "watch": "tsc -w --preserveWatchOutput", - "test": "jest", - "prepublishOnly": "npm run build", - "cli-scripts:pre-commit": "lint-staged" + "test": "jest --maxWorkers=4", + "prepublishOnly": "npm run build" }, "lint-staged": { "linters": { @@ -49,10 +48,10 @@ }, "license": "MIT", "dependencies": { - "@ionic/cli-framework": "1.1.1", - "@ionic/discover": "1.0.6", - "@ionic/utils-fs": "0.0.3", - "@ionic/utils-network": "0.0.3", + "@ionic/cli-framework": "1.2.0", + "@ionic/discover": "1.0.7", + "@ionic/utils-fs": "0.0.4", + "@ionic/utils-network": "0.0.4", "chalk": "^2.3.0", "debug": "^4.0.0", "diff": "^3.4.0", @@ -77,6 +76,7 @@ "@types/debug": "0.0.31", "@types/diff": "^3.2.2", "@types/elementtree": "^0.1.0", + "@types/jest": "^23.3.5", "@types/lodash": "^4.14.104", "@types/node": "^6.0.101", "@types/opn": "^5.1.0", @@ -89,10 +89,10 @@ "@types/uuid": "^3.4.3", "jest": "^23.0.1", "jest-cli": "^23.0.1", - "lint-staged": "^7.2.0", + "lint-staged": "^8.0.0", "rimraf": "^2.6.2", "source-map": "^0.7.0", - "ts-jest": "~23.1.0", + "ts-jest": "^23.10.1", "tslint": "^5.9.1", "typescript": "~3.1.1" } diff --git a/packages/ionic/src/__tests__/guards.ts b/packages/ionic/src/__tests__/guards.ts index 6947139d60..2e813afb49 100644 --- a/packages/ionic/src/__tests__/guards.ts +++ b/packages/ionic/src/__tests__/guards.ts @@ -1,6 +1,8 @@ import * as guards from '../guards'; -const OBJECT_GUARDS = [ +type Guard = (o: any) => boolean; + +const OBJECT_GUARDS: Guard[] = [ guards.isCommand, guards.isCommandPreRun, guards.isStarterManifest, @@ -42,9 +44,7 @@ const OBJECT_GUARDS = [ guards.isMultiProjectConfig, ]; -const ALL_GUARDS = [...OBJECT_GUARDS]; - -const API_RESPONSE_SUCCESS_GUARDS = [ +const API_RESPONSE_SUCCESS_GUARDS: Guard[] = [ guards.isGithubRepoListResponse, guards.isGithubBranchListResponse, guards.isAppAssociationResponse, diff --git a/packages/ionic/src/commands/capacitor/add.ts b/packages/ionic/src/commands/capacitor/add.ts index 544ca9b630..b425083700 100644 --- a/packages/ionic/src/commands/capacitor/add.ts +++ b/packages/ionic/src/commands/capacitor/add.ts @@ -10,7 +10,7 @@ export class AddCommand extends CapacitorCommand implements CommandPreRun { return { name: 'add', type: 'project', - summary: 'Adds a new Capacitor native platform to your Ionic project', + summary: 'Add a native platform to your Ionic project', description: ` ${chalk.green('ionic capacitor add')} will do the following: - Add a new platform specific folder to your project (ios, android, or electron) diff --git a/packages/ionic/src/commands/capacitor/copy.ts b/packages/ionic/src/commands/capacitor/copy.ts index 43cd423622..ca41338dad 100644 --- a/packages/ionic/src/commands/capacitor/copy.ts +++ b/packages/ionic/src/commands/capacitor/copy.ts @@ -10,7 +10,7 @@ export class CopyCommand extends CapacitorCommand implements CommandPreRun { return { name: 'copy', type: 'project', - summary: 'Copies web assets to Capacitor native platforms', + summary: 'Copy web assets to native platforms', description: ` ${chalk.green('ionic capacitor copy')} will do the following: - Copy the ${chalk.bold('www/')} directory into your native platforms. diff --git a/packages/ionic/src/commands/capacitor/open.ts b/packages/ionic/src/commands/capacitor/open.ts index c665f72700..2a5fa536b0 100644 --- a/packages/ionic/src/commands/capacitor/open.ts +++ b/packages/ionic/src/commands/capacitor/open.ts @@ -10,7 +10,7 @@ export class OpenCommand extends CapacitorCommand implements CommandPreRun { return { name: 'open', type: 'project', - summary: 'Opens the IDE for the specified Capacitor native platform project', + summary: 'Open the IDE for a given native platform project', description: ` ${chalk.green('ionic capacitor open')} will do the following: - Open the IDE for your native project (Xcode for iOS, Android Studio for Android) diff --git a/packages/ionic/src/commands/capacitor/run.ts b/packages/ionic/src/commands/capacitor/run.ts index 22b550ccef..dab43ca5d5 100644 --- a/packages/ionic/src/commands/capacitor/run.ts +++ b/packages/ionic/src/commands/capacitor/run.ts @@ -15,6 +15,13 @@ import { CapacitorCommand } from './base'; export class RunCommand extends CapacitorCommand implements CommandPreRun { async getMetadata(): Promise { let groups: string[] = [CommandGroup.Beta]; + const exampleCommands = [ + '', + 'android', + 'android -l', + 'ios --livereload', + 'ios --livereload-url=http://localhost:8100', + ].sort(); const options: CommandMetadataOption[] = [ // Build Options { @@ -30,6 +37,10 @@ export class RunCommand extends CapacitorCommand implements CommandPreRun { type: Boolean, aliases: ['l'], }, + { + name: 'livereload-url', + summary: 'Provide a custom URL to the dev server', + }, ]; const serveRunner = this.project && await this.project.getServeRunner(); @@ -51,7 +62,7 @@ export class RunCommand extends CapacitorCommand implements CommandPreRun { return { name: 'run', type: 'project', - summary: 'Copies web assets to each Capacitor native platform', + summary: 'Run an Ionic project on a connected device', description: ` ${chalk.green('ionic capacitor run')} will do the following: - Perform ${chalk.green('ionic build')} (or run the dev server from ${chalk.green('ionic serve')} with the ${chalk.green('--livereload')} option) @@ -64,10 +75,7 @@ For Android and iOS, you can setup Remote Debugging on your device with browser ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-resources/developer-tips/')} `, - exampleCommands: [ - '', - '-l', - ], + exampleCommands, inputs: [ { name: 'platform', @@ -94,6 +102,15 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-re inputs[0] = platform.trim(); } + if (options['livereload-url']) { + options['livereload'] = true; + } + + if (!options['build'] && options['livereload']) { + this.env.log.warn(`No livereload with ${chalk.green('--no-build')}.`); + options['livereload'] = false; + } + await this.checkForPlatformInstallation(inputs[0]); } @@ -105,21 +122,28 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-re const [ platform ] = inputs; if (options['livereload']) { - const conf = new CapacitorConfig(path.resolve(this.project.directory, CAPACITOR_CONFIG_FILE)); + let livereloadUrl = options['livereload-url'] ? String(options['livereload-url']) : undefined; + + if (!livereloadUrl) { + // TODO: use runner directly + const details = await serve({ flags: this.env.flags, config: this.env.config, log: this.env.log, prompt: this.env.prompt, shell: this.env.shell, project: this.project }, inputs, generateOptionsForCapacitorBuild(inputs, options)); - // TODO: use runner directly - const details = await serve({ flags: this.env.flags, config: this.env.config, log: this.env.log, prompt: this.env.prompt, shell: this.env.shell, project: this.project }, inputs, generateOptionsForCapacitorBuild(inputs, options)); + if (details.externallyAccessible === false) { + const extra = LOCAL_ADDRESSES.includes(details.externalAddress) ? '\nEnsure you have proper port forwarding setup from your device to your computer.' : ''; + this.env.log.warn(`Your device or emulator may not be able to access ${chalk.bold(details.externalAddress)}.${extra}\n\n`); + } - if (details.externallyAccessible === false) { - const extra = LOCAL_ADDRESSES.includes(details.externalAddress) ? '\nEnsure you have proper port forwarding setup from your device to your computer.' : ''; - this.env.log.warn(`Your device or emulator may not be able to access ${chalk.bold(details.externalAddress)}.${extra}\n\n`); + livereloadUrl = `${details.protocol || 'http'}://${details.externalAddress}:${details.port}`; } - conf.setServerUrl(`${details.protocol || 'http'}://${details.externalAddress}:${details.port}`); + const conf = new CapacitorConfig(path.resolve(this.project.directory, CAPACITOR_CONFIG_FILE)); onBeforeExit(async () => { conf.resetServerUrl(); }); + + conf.setServerUrl(livereloadUrl); + } else { if (options['build']) { // TODO: use runner directly diff --git a/packages/ionic/src/commands/capacitor/sync.ts b/packages/ionic/src/commands/capacitor/sync.ts index 4e00179f19..b5524275e5 100644 --- a/packages/ionic/src/commands/capacitor/sync.ts +++ b/packages/ionic/src/commands/capacitor/sync.ts @@ -10,7 +10,7 @@ export class SyncCommand extends CapacitorCommand implements CommandPreRun { return { name: 'sync', type: 'project', - summary: 'Syncs your Capacitor project, equivalent to performing a "copy followed by an "update"', + summary: 'Sync (copy + update) an Ionic project', description: ` ${chalk.green('ionic capacitor sync')} will do the following: - Copy web assets to all Capacitor native platforms diff --git a/packages/ionic/src/commands/capacitor/update.ts b/packages/ionic/src/commands/capacitor/update.ts index 87cfc6aa4b..b32447ffce 100644 --- a/packages/ionic/src/commands/capacitor/update.ts +++ b/packages/ionic/src/commands/capacitor/update.ts @@ -10,7 +10,7 @@ export class UpdateCommand extends CapacitorCommand implements CommandPreRun { return { name: 'update', type: 'project', - summary: 'Updates each Capacitor native platform, and installs any Capacitor/Cordova plugins found', + summary: 'Update Capacitor native platforms, install Capacitor/Cordova plugins', description: ` ${chalk.green('ionic capacitor update')} will do the following: - Update each Capacitor native project, such as any dependencies that need updating. diff --git a/packages/ionic/src/commands/config/get.ts b/packages/ionic/src/commands/config/get.ts index c5f23622db..980ab41625 100644 --- a/packages/ionic/src/commands/config/get.ts +++ b/packages/ionic/src/commands/config/get.ts @@ -1,5 +1,5 @@ import { OptionGroup } from '@ionic/cli-framework'; -import { columnar } from '@ionic/cli-framework/utils/format'; +import { columnar, prettyPath } from '@ionic/cli-framework/utils/format'; import { strcmp } from '@ionic/cli-framework/utils/string'; import chalk from 'chalk'; import * as lodash from 'lodash'; @@ -12,12 +12,14 @@ import { BaseConfigCommand, ConfigContext, getConfigValue } from './base'; export class ConfigGetCommand extends BaseConfigCommand { async getMetadata(): Promise { + const projectFile = this.project ? prettyPath(this.project.filePath) : PROJECT_FILE; + return { name: 'get', type: 'global', summary: 'Print config values', description: ` -This command reads and prints configuration values from the project's ${chalk.bold(PROJECT_FILE)} file. It can also operate on the global CLI configuration (${chalk.bold('~/.ionic/config.json')}) using the ${chalk.green('--global')} option. +This command reads and prints configuration values from the project's ${chalk.bold(projectFile)} file. It can also operate on the global CLI configuration (${chalk.bold('~/.ionic/config.json')}) using the ${chalk.green('--global')} option. For nested properties, separate nest levels with dots. For example, the property name ${chalk.green('integrations.cordova')} will look in the ${chalk.bold('integrations')} object for the ${chalk.bold('cordova')} property. @@ -50,7 +52,7 @@ This command will sanitize config output for known sensitive fields (disabled wh }, { name: 'root', - summary: `Operate on root of ${chalk.bold(PROJECT_FILE)}`, + summary: `Operate on root of ${chalk.bold(projectFile)}`, type: Boolean, hint: chalk.dim('[multi-app]'), groups: [OptionGroup.Advanced], diff --git a/packages/ionic/src/commands/config/index.ts b/packages/ionic/src/commands/config/index.ts index d173443edd..8648ea3521 100644 --- a/packages/ionic/src/commands/config/index.ts +++ b/packages/ionic/src/commands/config/index.ts @@ -1,3 +1,4 @@ +import { prettyPath } from '@ionic/cli-framework/utils/format'; import chalk from 'chalk'; import { PROJECT_FILE } from '../../constants'; @@ -5,13 +6,15 @@ import { CommandMap, Namespace } from '../../lib/namespace'; export class ConfigNamespace extends Namespace { async getMetadata() { + const projectFile = this.project ? prettyPath(this.project.filePath) : PROJECT_FILE; + return { name: 'config', summary: 'Manage CLI and project config values', description: ` These commands are used to programmatically read, write, and delete CLI and project config values. -By default, these commands use your project's ${chalk.bold(PROJECT_FILE)} file. +By default, these commands use your project's ${chalk.bold(prettyPath(projectFile))} file. To use these commands for the global CLI config file (${chalk.bold('~/.ionic/config.json')}), use the ${chalk.green('--global')} flag. `, diff --git a/packages/ionic/src/commands/config/set.ts b/packages/ionic/src/commands/config/set.ts index a7f5051b4d..d20141a942 100644 --- a/packages/ionic/src/commands/config/set.ts +++ b/packages/ionic/src/commands/config/set.ts @@ -1,4 +1,5 @@ import { OptionGroup, validators } from '@ionic/cli-framework'; +import { prettyPath } from '@ionic/cli-framework/utils/format'; import chalk from 'chalk'; import { PROJECT_FILE } from '../../constants'; @@ -9,12 +10,14 @@ import { BaseConfigCommand, getConfigValue, setConfigValue } from './base'; export class ConfigSetCommand extends BaseConfigCommand { async getMetadata(): Promise { + const projectFile = this.project ? prettyPath(this.project.filePath) : PROJECT_FILE; + return { name: 'set', type: 'global', summary: 'Set config values', description: ` -This command writes configuration values to the project's ${chalk.bold(PROJECT_FILE)} file. It can also operate on the global CLI configuration (${chalk.bold('~/.ionic/config.json')}) using the ${chalk.green('--global')} option. +This command writes configuration values to the project's ${chalk.bold(prettyPath(projectFile))} file. It can also operate on the global CLI configuration (${chalk.bold('~/.ionic/config.json')}) using the ${chalk.green('--global')} option. For nested properties, separate nest levels with dots. For example, the property name ${chalk.green('integrations.cordova')} will look in the ${chalk.bold('integrations')} object for the ${chalk.bold('cordova')} property. @@ -57,7 +60,7 @@ By default, if ${chalk.green('property')} exists and is an object or an array, t }, { name: 'root', - summary: `Operate on root of ${chalk.bold(PROJECT_FILE)}`, + summary: `Operate on root of ${chalk.bold(prettyPath(projectFile))}`, type: Boolean, hint: chalk.dim('[multi-app]'), groups: [OptionGroup.Advanced], diff --git a/packages/ionic/src/commands/config/unset.ts b/packages/ionic/src/commands/config/unset.ts index 36210f0319..24193d3e95 100644 --- a/packages/ionic/src/commands/config/unset.ts +++ b/packages/ionic/src/commands/config/unset.ts @@ -1,4 +1,5 @@ import { OptionGroup, validators } from '@ionic/cli-framework'; +import { prettyPath } from '@ionic/cli-framework/utils/format'; import chalk from 'chalk'; import { PROJECT_FILE } from '../../constants'; @@ -9,12 +10,14 @@ import { BaseConfigCommand, getConfigValue, unsetConfigValue } from './base'; export class ConfigUnsetCommand extends BaseConfigCommand { async getMetadata(): Promise { + const projectFile = this.project ? prettyPath(this.project.filePath) : PROJECT_FILE; + return { name: 'unset', type: 'global', summary: 'Delete config values', description: ` -This command deletes configuration values from the project's ${chalk.bold(PROJECT_FILE)} file. It can also operate on the global CLI configuration (${chalk.bold('~/.ionic/config.json')}) using the ${chalk.green('--global')} option. +This command deletes configuration values from the project's ${chalk.bold(prettyPath(projectFile))} file. It can also operate on the global CLI configuration (${chalk.bold('~/.ionic/config.json')}) using the ${chalk.green('--global')} option. For nested properties, separate nest levels with dots. For example, the property name ${chalk.green('integrations.cordova')} will look in the ${chalk.bold('integrations')} object for the ${chalk.bold('cordova')} property. @@ -36,7 +39,7 @@ For multi-app projects, this command is scoped to the current project by default }, { name: 'root', - summary: `Operate on root of ${chalk.bold(PROJECT_FILE)}`, + summary: `Operate on root of ${chalk.bold(prettyPath(projectFile))}`, type: Boolean, hint: chalk.dim('[multi-app]'), groups: [OptionGroup.Advanced], diff --git a/packages/ionic/src/commands/cordova/build.ts b/packages/ionic/src/commands/cordova/build.ts index aed0237382..a2683909f6 100644 --- a/packages/ionic/src/commands/cordova/build.ts +++ b/packages/ionic/src/commands/cordova/build.ts @@ -10,6 +10,7 @@ import { COMMON_CORDOVA_BUILD_COMMAND_OPTIONS, CORDOVA_BUILD_EXAMPLE_COMMANDS, C export class BuildCommand extends CordovaCommand implements CommandPreRun { async getMetadata(): Promise { + const exampleCommands = CORDOVA_BUILD_EXAMPLE_COMMANDS.sort(); const options: CommandMetadataOption[] = [ // Build Options { @@ -42,7 +43,7 @@ The Cordova CLI requires a separator for platform-specific arguments for Android ${chalk.cyan('[1]')}: ${chalk.bold('https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html#using-flags')} ${chalk.cyan('[2]')}: ${chalk.bold('https://cordova.apache.org/docs/en/latest/guide/platforms/ios/index.html#using-flags')} `, - exampleCommands: CORDOVA_BUILD_EXAMPLE_COMMANDS, + exampleCommands, inputs: [ { name: 'platform', diff --git a/packages/ionic/src/commands/cordova/resources.ts b/packages/ionic/src/commands/cordova/resources.ts index 64ca4965bb..4ef5f603a7 100644 --- a/packages/ionic/src/commands/cordova/resources.ts +++ b/packages/ionic/src/commands/cordova/resources.ts @@ -80,16 +80,30 @@ This command uses Ionic servers, so we require you to be logged into your free I } } + async getBuildPlatforms() { + const { getPlatforms } = await import('../../lib/integrations/cordova/project'); + const { RESOURCES } = await import('../../lib/integrations/cordova/resources'); + + debug(`RESOURCES=${Object.keys(RESOURCES).length}`); + + const installedPlatforms = await getPlatforms(this.integration.root); + debug(`installedPlatforms=${installedPlatforms.map(e => chalk.bold(e)).join(', ')}`); + + const buildPlatforms = Object.keys(RESOURCES).filter(p => installedPlatforms.includes(p)); + debug(`buildPlatforms=${buildPlatforms.map(v => chalk.bold(v)).join(', ')}`); + + return buildPlatforms; + } + async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { const { loadConfigXml } = await import('../../lib/integrations/cordova/config'); - const { getPlatforms } = await import('../../lib/integrations/cordova/project'); - const { RESOURCES, addResourcesToConfigXml, createImgDestinationDirectories, findMostSpecificSourceImage, getImageResources, getSourceImages, transformResourceImage, uploadSourceImage } = await import('../../lib/integrations/cordova/resources'); + const { addResourcesToConfigXml, createImgDestinationDirectories, findMostSpecificSourceImage, getImageResources, getSourceImages, transformResourceImage, uploadSourceImage } = await import('../../lib/integrations/cordova/resources'); if (!this.project) { throw new FatalException(`Cannot run ${chalk.green('ionic cordova resources')} outside a project directory.`); } - const [ platform ] = inputs; + const platform = inputs[0] ? String(inputs[0]) : undefined; const { force } = options; const tasks = this.createTaskChain(); @@ -98,21 +112,14 @@ This command uses Ionic servers, so we require you to be logged into your free I let resourceTypes = AVAILABLE_RESOURCE_TYPES.filter((type, index, array) => options[type]); resourceTypes = resourceTypes.length ? resourceTypes : AVAILABLE_RESOURCE_TYPES; - debug(`resourceJsonStructure=${Object.keys(RESOURCES).length}`); - - await this.checkForPlatformInstallation(platform, { promptToInstall: true }); + // await this.checkForPlatformInstallation(platform, { promptToInstall: true }); const conf = await loadConfigXml(this.integration); - const platforms = await getPlatforms(this.integration.root); - const buildPlatforms = Object.keys(RESOURCES).filter(p => platforms.includes(p)); - - debug(`platforms=${platforms.map(e => chalk.bold(e)).join(', ')}`); - debug(`buildPlatforms=${buildPlatforms.map(v => chalk.bold(v)).join(', ')}`); + const buildPlatforms = platform ? [platform] : await this.getBuildPlatforms(); if (buildPlatforms.length === 0) { throw new FatalException(`No platforms detected. Please run: ${chalk.green('ionic cordova platform add')}`); } - debug(`${chalk.cyan('getProjectPlatforms')} completed: ${buildPlatforms.map(v => chalk.bold(v)).join(', ')}`); tasks.next(`Collecting resource configuration and source images`); @@ -220,7 +227,7 @@ This command uses Ionic servers, so we require you to be logged into your free I return response; })); - debug(`${chalk.cyan('uploadSourceImages')} completed: responses=%o`, imageUploadResponses); + debug(`${chalk.cyan('uploadSourceImages')} completed: responses=%O`, imageUploadResponses); srcImagesAvailable = srcImagesAvailable.map((img, index) => { return { @@ -231,7 +238,7 @@ This command uses Ionic servers, so we require you to be logged into your free I }; }); - debug('srcImagesAvailable=%o', srcImagesAvailable); + debug('srcImagesAvailable=%O', srcImagesAvailable); // If any images are asking to be generated but are not of the correct size // inform the user and continue on. @@ -244,7 +251,7 @@ This command uses Ionic servers, so we require you to be logged into your free I return !resourceSourceImage.vector && (img.width > resourceSourceImage.width || img.height > resourceSourceImage.height); }); - debug('imagesTooLargeForSource=%o', imagesTooLargeForSource); + debug('imagesTooLargeForSource=%O', imagesTooLargeForSource); // Remove all images too large for transformations imgResources = imgResources.filter(img => { diff --git a/packages/ionic/src/commands/cordova/run.ts b/packages/ionic/src/commands/cordova/run.ts index 257827a855..630de17313 100644 --- a/packages/ionic/src/commands/cordova/run.ts +++ b/packages/ionic/src/commands/cordova/run.ts @@ -59,7 +59,12 @@ const CORDOVA_RUN_OPTIONS: ReadonlyArray = [ export class RunCommand extends CordovaCommand implements CommandPreRun { async getMetadata(): Promise { let groups: string[] = []; - const exampleCommands = CORDOVA_BUILD_EXAMPLE_COMMANDS; + const exampleCommands = [ + ...CORDOVA_BUILD_EXAMPLE_COMMANDS, + 'android -l', + 'ios --livereload', + 'ios --livereload-url=http://localhost:8100', + ].sort(); const options: CommandMetadataOption[] = [ { name: 'list', @@ -83,6 +88,10 @@ export class RunCommand extends CordovaCommand implements CommandPreRun { type: Boolean, aliases: ['l'], }, + { + name: 'livereload-url', + summary: 'Provide a custom URL to the dev server', + }, ]; const serveRunner = this.project && await this.project.getServeRunner(); @@ -113,7 +122,7 @@ Like running ${chalk.green('cordova run')} or ${chalk.green('cordova emulate')} For Android and iOS, you can setup Remote Debugging on your device with browser development tools using these docs${chalk.cyan('[1]')}. -Just like with ${chalk.green('ionic cordova build')}, you can pass additional options to the Cordova CLI using the ${chalk.green('--')} separator. +Just like with ${chalk.green('ionic cordova build')}, you can pass additional options to the Cordova CLI using the ${chalk.green('--')} separator. To pass additional options to the dev server, consider using ${chalk.green('ionic serve')} and the ${chalk.green('--livereload-url')} option. ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-resources/developer-tips/')} `, @@ -143,6 +152,10 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-re options['proxy'] = false; } + if (options['livereload-url']) { + options['livereload'] = true; + } + if (!options['build'] && options['livereload']) { this.env.log.warn(`No livereload with ${chalk.green('--no-build')}.`); options['livereload'] = false; @@ -181,6 +194,20 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-re const metadata = await this.getMetadata(); if (options['livereload']) { + let livereloadUrl = options['livereload-url'] ? String(options['livereload-url']) : undefined; + + if (!livereloadUrl) { + // TODO: use runner directly + const details = await serve({ flags: this.env.flags, config: this.env.config, log: this.env.log, prompt: this.env.prompt, shell: this.env.shell, project: this.project }, inputs, generateOptionsForCordovaBuild(metadata, inputs, options)); + + if (details.externallyAccessible === false) { + const extra = LOCAL_ADDRESSES.includes(details.externalAddress) ? '\nEnsure you have proper port forwarding setup from your device to your computer.' : ''; + this.env.log.warn(`Your device or emulator may not be able to access ${chalk.bold(details.externalAddress)}.${extra}\n\n`); + } + + livereloadUrl = `${details.protocol || 'http'}://${details.externalAddress}:${details.port}`; + } + const conf = await loadConfigXml(this.integration); onBeforeExit(async () => { @@ -188,21 +215,13 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-re await conf.save(); }); + conf.writeContentSrc(livereloadUrl); + await conf.save(); + const cordovalog = this.env.log.clone(); cordovalog.handlers = createDefaultLoggerHandlers(createPrefixedFormatter(`${chalk.dim(`[cordova]`)} `)); const cordovalogws = cordovalog.createWriteStream(LOGGER_LEVELS.INFO); - // TODO: use runner directly - const details = await serve({ flags: this.env.flags, config: this.env.config, log: this.env.log, prompt: this.env.prompt, shell: this.env.shell, project: this.project }, inputs, generateOptionsForCordovaBuild(metadata, inputs, options)); - - if (details.externallyAccessible === false) { - const extra = LOCAL_ADDRESSES.includes(details.externalAddress) ? '\nEnsure you have proper port forwarding setup from your device to your computer.' : ''; - this.env.log.warn(`Your device or emulator may not be able to access ${chalk.bold(details.externalAddress)}.${extra}\n\n`); - } - - conf.writeContentSrc(`${details.protocol || 'http'}://${details.externalAddress}:${details.port}`); - await conf.save(); - await this.runCordova(filterArgumentsForCordova(metadata, options), { stream: cordovalogws }); await sleepForever(); } else { diff --git a/packages/ionic/src/commands/integrations/enable.ts b/packages/ionic/src/commands/integrations/enable.ts index 4169837c63..e1e7a9ff6b 100644 --- a/packages/ionic/src/commands/integrations/enable.ts +++ b/packages/ionic/src/commands/integrations/enable.ts @@ -32,6 +32,10 @@ Integrations can be re-added with the ${chalk.green('--add')} option. summary: 'Download and add the integration even if enabled', type: Boolean, }, + { + name: 'root', + summary: 'Specify an alternative destination to download into when adding', + }, { name: 'quiet', summary: 'Do not log file operations', @@ -49,6 +53,8 @@ Integrations can be re-added with the ${chalk.green('--add')} option. throw new FatalException(`Cannot run ${chalk.green('ionic integrations enable')} outside a project directory.`); } + const root = options['root'] ? path.resolve(this.project.directory, String(options['root'])) : this.project.directory; + if (!isIntegrationName(name)) { throw new FatalException(`Don't know about ${chalk.green(name)} integration!`); } @@ -60,7 +66,9 @@ Integrations can be re-added with the ${chalk.green('--add')} option. try { if (!integrationConfig || add) { await integration.add({ + root, enableArgs: options['--'] ? options['--'] : undefined, + }, { conflictHandler: async (f, stats) => { const isDirectory = stats.isDirectory(); const filename = `${path.basename(f)}${isDirectory ? '/' : ''}`; @@ -82,7 +90,10 @@ Integrations can be re-added with the ${chalk.green('--add')} option. }, }); - integrationsConfig[name] = {}; + integrationsConfig[name] = { + root: root !== this.project.directory ? path.relative(this.project.rootDirectory, root) : undefined, + }; + this.env.log.ok(`Integration ${chalk.green(integration.name)} added!`); } else { const wasEnabled = integrationConfig.enabled !== false; diff --git a/packages/ionic/src/commands/link.ts b/packages/ionic/src/commands/link.ts index 0c85a35757..306eb99594 100644 --- a/packages/ionic/src/commands/link.ts +++ b/packages/ionic/src/commands/link.ts @@ -1,4 +1,5 @@ import { OptionGroup, createPromptChoiceSeparator, validators } from '@ionic/cli-framework'; +import { prettyPath } from '@ionic/cli-framework/utils/format'; import chalk from 'chalk'; import * as Debug from 'debug'; @@ -25,6 +26,8 @@ const CHOICE_SPECIFIC_BRANCHES = 'specific'; export class LinkCommand extends Command implements CommandPreRun { async getMetadata(): Promise { + const projectFile = this.project ? prettyPath(this.project.filePath) : PROJECT_FILE; + return { name: 'link', type: 'project', @@ -36,7 +39,7 @@ If the ${chalk.green('pro-id')} argument is excluded, this command will prompt y Ionic Pro uses a git-based workflow to manage app updates. During the linking process, select ${chalk.bold('GitHub')} (recommended) or ${chalk.bold('Ionic Pro')} as a git host. See our documentation${chalk.cyan('[1]')} for more information. -Ultimately, this command sets the ${chalk.bold('pro_id')} property in ${chalk.bold(PROJECT_FILE)}, which marks this app as linked. +Ultimately, this command sets the ${chalk.bold('pro_id')} property in ${chalk.bold(prettyPath(projectFile))}, which marks this app as linked. If you are having issues linking, please get in touch with our Support${chalk.cyan('[2]')}. diff --git a/packages/ionic/src/commands/start.ts b/packages/ionic/src/commands/start.ts index bbe8967d0d..35993c0265 100644 --- a/packages/ionic/src/commands/start.ts +++ b/packages/ionic/src/commands/start.ts @@ -8,11 +8,13 @@ import * as Debug from 'debug'; import * as lodash from 'lodash'; import * as path from 'path'; -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun, ResolvedStarterTemplate, StarterManifest, StarterTemplate } from '../definitions'; +import { PROJECT_FILE } from '../constants'; +import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun, IProject, ProjectType, ResolvedStarterTemplate, StarterManifest, StarterTemplate } from '../definitions'; import { getProject } from '../lib'; import { Command } from '../lib/command'; import { FatalException } from '../lib/errors'; import { runCommand } from '../lib/executor'; +import { createProjectFromType } from '../lib/project'; import { prependNodeModulesBinToPath } from '../lib/shell'; import { emoji } from '../lib/utils/emoji'; @@ -28,7 +30,7 @@ interface CommonAppSchema { interface NewAppSchema extends CommonAppSchema { cloned: false; name: string; - type: string; + type: ProjectType; template: string; } @@ -198,6 +200,20 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters options['git'] = true; } + if (this.project && !this.project.name) { + const confirm = await this.env.prompt({ + type: 'confirm', + name: 'confirm', + message: 'You are already in an Ionic project directory. Do you really want to start another project here?', + default: false, + }); + + if (!confirm) { + this.env.log.info('Not starting project within existing project.'); + throw new FatalException(); + } + } + if (!options['type']) { if (this.env.flags.interactive) { this.env.log.info( @@ -221,23 +237,9 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters } const projectType = options['type'] ? String(options['type']) : 'ionic-angular'; - await this.validateProjectType(projectType); - const proId = options['pro-id'] ? String(options['pro-id']) : undefined; - if (this.project) { - const confirm = await this.env.prompt({ - type: 'confirm', - name: 'confirm', - message: 'You are already in an Ionic project directory. Do you really want to start another project here?', - default: false, - }); - - if (!confirm) { - this.env.log.info('Not starting project within existing project.'); - throw new FatalException(); - } - } + await this.validateProjectType(projectType); if (options['v1'] || options['v2']) { throw new FatalException( @@ -358,7 +360,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters this.schema = { cloned, name: inputs[0], - type: projectType, + type: projectType as ProjectType, template: inputs[1], projectId, projectDir, @@ -428,21 +430,32 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters await this.downloadStarterTemplate(projectDir, starterTemplate); } + let project: IProject | undefined; + + if (this.project && this.project.name && !this.schema.cloned) { + // We're in a multi-app setup, so the new config file isn't wanted. + await unlink(path.resolve(projectDir, 'ionic.config.json')); + + project = await createProjectFromType(path.resolve(this.project.rootDirectory, PROJECT_FILE), projectId, this.env, this.schema.type); + project.config.set('type', this.schema.type); + project.config.set('root', path.relative(this.project.rootDirectory, projectDir)); + } else { + project = await getProject(projectDir, undefined, this.env); + } + // start is weird, once the project directory is created, it becomes a // "project" command and so we replace the `Project` instance that was // autogenerated when the CLI booted up. This has worked thus far? - this.namespace.root.project = await getProject(projectDir, undefined, this.env); + this.namespace.root.project = project; if (!this.project) { - throw new FatalException('Error while loading new project. Please report this error!'); + throw new FatalException('Error while loading project.'); } this.env.shell.alterPath = p => prependNodeModulesBinToPath(projectDir, p); - const shellOptions = { cwd: projectDir, stdio: 'inherit' }; - if (!this.schema.cloned) { - if (!options['cordova'] && !options['capacitor']) { + if (typeof options['cordova'] === 'undefined' && !options['capacitor']) { const confirm = await this.env.prompt({ type: 'confirm', name: 'confirm', @@ -464,9 +477,12 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters } await this.project.personalize({ name: this.schema.name, projectId, packageId }); + this.env.log.nl(); } + const shellOptions = { cwd: projectDir, stdio: 'inherit' }; + if (options['deps']) { this.env.log.msg('Installing dependencies may take several minutes.'); this.env.log.rawmsg(await getIonicDevAppText()); diff --git a/packages/ionic/src/definitions.ts b/packages/ionic/src/definitions.ts index 8fa710528b..fa280ae42e 100644 --- a/packages/ionic/src/definitions.ts +++ b/packages/ionic/src/definitions.ts @@ -232,6 +232,7 @@ export interface IProjectConfig { name: string; type?: ProjectType; pro_id?: string; + root?: string; readonly integrations: ProjectIntegrations; readonly hooks?: Record; @@ -242,16 +243,17 @@ export interface IProjectConfig { }; } -export interface MultiProjectConfig { +export interface IMultiProjectConfig { defaultProject?: string; projects: { [key: string]: IProjectConfig | undefined; }; } -export type ProjectFile = IProjectConfig | MultiProjectConfig; +export type ProjectFile = IProjectConfig | IMultiProjectConfig; export interface IProject { + readonly rootDirectory: string; readonly directory: string; readonly filePath: string; readonly name?: string; @@ -279,10 +281,14 @@ export interface IProject { requireGenerateRunner(): Promise<ζgenerate.GenerateRunner>; } -export interface IIntegrationAddOptions { +export interface IntegrationAddDetails { + root: string; enableArgs?: string[]; - conflictHandler?(f: string, stats: fs.Stats): Promise; - onFileCreate?(f: string): void; +} + +export interface IntegrationAddHandlers { + conflictHandler?: (f: string, stats: fs.Stats) => Promise; + onFileCreate?: (f: string) => void; } export interface IIntegration { @@ -290,7 +296,7 @@ export interface IIntegration { readonly summary: string; readonly archiveUrl?: string; - add(opts?: IIntegrationAddOptions): Promise; + add(details: IntegrationAddDetails, handlers?: IntegrationAddHandlers): Promise; enable(): Promise; disable(): Promise; getInfo(): Promise; diff --git a/packages/ionic/src/guards.ts b/packages/ionic/src/guards.ts index 5b4a33d162..570e01e6b6 100644 --- a/packages/ionic/src/guards.ts +++ b/packages/ionic/src/guards.ts @@ -13,10 +13,10 @@ import { GithubRepo, GithubRepoAssociation, ICommand, + IMultiProjectConfig, IProjectConfig, IntegrationName, Login, - MultiProjectConfig, Org, Response, SSHKey, @@ -284,7 +284,7 @@ export function isProjectConfig(configFile: any): configFile is IProjectConfig { && typeof configFile.projects === 'undefined'; } -export function isMultiProjectConfig(configFile: any): configFile is MultiProjectConfig { +export function isMultiProjectConfig(configFile: any): configFile is IMultiProjectConfig { return configFile && typeof configFile.name === 'undefined' && typeof configFile.projects === 'object'; diff --git a/packages/ionic/src/index.ts b/packages/ionic/src/index.ts index d62aa3b577..578227c07b 100644 --- a/packages/ionic/src/index.ts +++ b/packages/ionic/src/index.ts @@ -53,9 +53,9 @@ export async function generateContext(): Promise { }; } -export async function loadExecutor(ctx: IonicContext, pargv: string[], env: NodeJS.ProcessEnv): Promise { +export async function loadExecutor(ctx: IonicContext, pargv: string[]): Promise { if (!_executor) { - const deps = await generateIonicEnvironment(ctx, pargv, env); + const deps = await generateIonicEnvironment(ctx, pargv); const namespace = new IonicNamespace(deps); _executor = new Executor({ namespace }); } @@ -63,12 +63,12 @@ export async function loadExecutor(ctx: IonicContext, pargv: string[], env: Node return _executor; } -export async function run(pargv: string[], env: NodeJS.ProcessEnv) { +export async function run(pargv: string[]): Promise { let err: any; let executor: Executor; try { - executor = await loadExecutor(await generateContext(), pargv, env); + executor = await loadExecutor(await generateContext(), pargv); } catch (e) { process.stderr.write(`${e.message ? e.message : (e.stack ? e.stack : e)}\n`); process.exitCode = 1; @@ -83,9 +83,9 @@ export async function run(pargv: string[], env: NodeJS.ProcessEnv) { ienv.config.set('version', ienv.ctx.version); - const token = env['IONIC_TOKEN']; - const email = env['IONIC_EMAIL']; - const password = env['IONIC_PASSWORD']; + const token = process.env['IONIC_TOKEN']; + const email = process.env['IONIC_EMAIL']; + const password = process.env['IONIC_PASSWORD']; if (token) { const wasLoggedIn = ienv.session.isLoggedIn(); @@ -125,7 +125,7 @@ export async function run(pargv: string[], env: NodeJS.ProcessEnv) { ` ${chalk.green(`ionic ${foundCommand} --help`)}\n\n` ); } else { - await executor.execute(pargv, env); + await executor.execute(pargv, process.env); } if (ienv.flags.interactive) { diff --git a/packages/ionic/src/lib/__tests__/command.ts b/packages/ionic/src/lib/__tests__/command.ts index 05ed7f038f..ae714ef319 100644 --- a/packages/ionic/src/lib/__tests__/command.ts +++ b/packages/ionic/src/lib/__tests__/command.ts @@ -1,118 +1,146 @@ +import { CommandMetadata, NamespaceMetadata } from '../../definitions'; import { Command } from '../command'; +import { Namespace } from '../namespace'; + +class MyNamespace extends Namespace { + async getMetadata(): Promise { + return { + name: 'my', + summary: '', + }; + } +} + +class FooCommand extends Command { + async getMetadata(): Promise { + return { + name: 'foo', + type: 'global', + summary: '', + }; + } + + async run() {} +} + +class BarCommand extends Command { + async getMetadata(): Promise { + return { + name: 'bar', + type: 'global', + summary: '', + inputs: [ + { + name: 'arg1', + summary: '', + }, + { + name: 'arg2', + summary: '', + }, + { + name: 'arg3', + summary: '', + private: true, + }, + { + name: 'arg4', + summary: '', + }, + ], + options: [ + { + name: 'opt1', + summary: '', + type: Boolean, + }, + { + name: 'opt2', + summary: '', + }, + { + name: 'opt3', + summary: '', + default: 'default', + }, + { + name: 'opt4', + summary: '', + private: true, + }, + { + name: 'opt5', + summary: '', + aliases: ['o'], + }, + ], + }; + } + + async run() {} +} describe('ionic', () => { describe('Command', () => { - class FooCommand extends Command { - async getMetadata() { - return { - name: 'foo', - type: 'global', - }; - } - } - - class BarCommand extends Command { - async getMetadata() { - return { - name: 'bar', - type: 'global', - inputs: [ - { - name: 'arg1', - }, - { - name: 'arg2', - }, - { - name: 'arg3', - private: true, - }, - { - name: 'arg4', - }, - ], - options: [ - { - name: 'opt1', - type: Boolean, - }, - { - name: 'opt2', - }, - { - name: 'opt3', - default: 'default', - }, - { - name: 'opt4', - private: true, - }, - { - name: 'opt5', - aliases: ['o'], - }, - ], - }; - } - } - describe('getCleanInputsForTelemetry', () => { + const myns = new MyNamespace(undefined); + // TODO: aliases can be intelligently removed it('should be empty with no inputs', async () => { - const foo = new FooCommand(); - const inputs = []; + const foo = new FooCommand(myns); + const inputs: string[] = []; const results = await foo.getCleanInputsForTelemetry(inputs, { _: inputs }); expect(results).toEqual([]); }); it('should include additional, unknown arguments', async () => { - const foo = new FooCommand(); + const foo = new FooCommand(myns); const inputs = ['a', 'b', 'c']; const results = await foo.getCleanInputsForTelemetry(inputs, { _: inputs }); expect(results).toEqual(['a', 'b', 'c']); }); it('should include additional, unknown options', async () => { - const foo = new FooCommand(); - const inputs = []; + const foo = new FooCommand(myns); + const inputs: string[] = []; const results = await foo.getCleanInputsForTelemetry(inputs, { _: inputs, opt1: true, opt2: 'cow' }); expect(results).toEqual(['--opt1', '--opt2=cow']); }); it('should include known arguments and options', async () => { - const bar = new BarCommand(); + const bar = new BarCommand(myns); const inputs = ['a', 'b']; const results = await bar.getCleanInputsForTelemetry(inputs, { _: inputs, opt1: true, opt2: 'cow', opt3: 'not default' }); expect(results).toEqual(['a', 'b', '--opt1', '--opt2=cow', '--opt3="not default"']); }); it('should exclude options with default values', async () => { - const bar = new BarCommand(); - const inputs = []; + const bar = new BarCommand(myns); + const inputs: string[] = []; const results = await bar.getCleanInputsForTelemetry(inputs, { _: inputs, opt3: 'default' }); expect(results).toEqual([]); }); it('should exclude private arguments and options', async () => { - const bar = new BarCommand(); + const bar = new BarCommand(myns); const inputs = ['a', 'b', 'c', 'd']; const results = await bar.getCleanInputsForTelemetry(inputs, { _: inputs, opt1: true, opt4: 'private!' }); expect(results).toEqual(['a', 'b', '*****', 'd', '--opt1']); }); it('should exclude aliases', async () => { - const bar = new BarCommand(); - const inputs = []; + const bar = new BarCommand(myns); + const inputs: string[] = []; const results = await bar.getCleanInputsForTelemetry(inputs, { _: inputs, o: 'wow', opt5: 'wow' }); expect(results).toEqual(['--opt5=wow']); }); it('should include separated args', async () => { - const bar = new BarCommand(); + const bar = new BarCommand(myns); const inputs = ['a']; const results = await bar.getCleanInputsForTelemetry(inputs, { _: inputs, '--': ['other', 'args'], opt1: true }); expect(results).toEqual(['a', '--opt1', '--', 'other', 'args']); diff --git a/packages/ionic/src/lib/__tests__/executor.ts b/packages/ionic/src/lib/__tests__/executor.ts index 027a86b1c1..36afce6210 100644 --- a/packages/ionic/src/lib/__tests__/executor.ts +++ b/packages/ionic/src/lib/__tests__/executor.ts @@ -1,3 +1,4 @@ +import { CommandMetadata } from '../../definitions'; import { metadataToCmdOptsEnv } from '../executor'; describe('ionic', () => { @@ -6,20 +7,24 @@ describe('ionic', () => { describe('metadataToCmdOptsEnv', () => { - const metadata = { - fullName: 'foo bar', + const metadata: CommandMetadata = { + name: '', + summary: '', + type: 'global', options: [ { name: 'baz', + summary: '', }, { name: 'opt-with-dashes', + summary: '', }, ], }; it('should return empty array for command with no options', () => { - const result = metadataToCmdOptsEnv({ options: [] }, ['cmd']); + const result = metadataToCmdOptsEnv({ name: '', summary: '', type: 'global', options: [] }, ['cmd']); expect(result.size).toEqual(0); }); diff --git a/packages/ionic/src/lib/__tests__/serve.ts b/packages/ionic/src/lib/__tests__/serve.ts index 3222cbd10a..950c6c07c6 100644 --- a/packages/ionic/src/lib/__tests__/serve.ts +++ b/packages/ionic/src/lib/__tests__/serve.ts @@ -1,5 +1,15 @@ import { ServeRunner } from '../serve'; +class MyServeRunner extends ServeRunner { + constructor(protected readonly e: any) { + super(); + } + + async getCommandMetadata(): Promise { } + modifyOpenURL(): any { } + async serveProject(): Promise { } +} + describe('ionic', () => { describe('lib/serve', () => { @@ -28,38 +38,38 @@ describe('ionic', () => { }; it('should provide defaults with no options', () => { - const runner = new ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], {}); + const runner = new MyServeRunner({}); + const result = runner.createOptionsFromCommandLine([], { _: [] }); expect(result).toEqual(defaults); }); it('should provide options from negations of cli flag defaults', () => { - const runner = new ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); + const runner = new MyServeRunner({}); + const result = runner.createOptionsFromCommandLine([], { _: [], livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); expect(result).toEqual({ ...defaults, livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); }); it('should allow overrides of default values', () => { - const runner = new ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { address: 'localhost', port: '1111', 'livereload-port': '2222', 'dev-logger-port': '3333' }); + const runner = new MyServeRunner({}); + const result = runner.createOptionsFromCommandLine([], { _: [], address: 'localhost', port: '1111', 'livereload-port': '2222', 'dev-logger-port': '3333' }); expect(result).toEqual({ ...defaults, address: 'localhost', port: 1111 }); }); it('should respect --local flag', () => { - const runner = new ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { local: true }); + const runner = new MyServeRunner({}); + const result = runner.createOptionsFromCommandLine([], { _: [], local: true }); expect(result).toEqual({ ...defaults, address: 'localhost', devapp: false }); }); it('should respect --project flag', () => { - const runner = new ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { project: 'app' }); + const runner = new MyServeRunner({}); + const result = runner.createOptionsFromCommandLine([], { _: [], project: 'app' }); expect(result).toEqual({ ...defaults, project: 'app' }); }); it('should pass on separated args', () => { - const runner = new ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { '--': ['foo', '--bar'] }); + const runner = new MyServeRunner({}); + const result = runner.createOptionsFromCommandLine([], { _: [], '--': ['foo', '--bar'] }); expect(result).toEqual({ ...defaults, '--': ['foo', '--bar'] }); }); diff --git a/packages/ionic/src/lib/__tests__/ssh.ts b/packages/ionic/src/lib/__tests__/ssh.ts index 84aece9f23..094baa6b86 100644 --- a/packages/ionic/src/lib/__tests__/ssh.ts +++ b/packages/ionic/src/lib/__tests__/ssh.ts @@ -11,16 +11,12 @@ describe('ionic', () => { 'AAAAB3NzaC1yc2EAAAADAQABAAABAQDqQmHXeWG737iR40rMO5enlhHMTGTmTi4Kehql9TxheN1PPujeBlQk6Kl0ylYDjPqqUBrSxq/tdxHyyF8h5QD+L5uO9PHyEVLoNCE0gYRhvSupf6K4EhZNz91KVYchRNZcldt6esf6KQfZIF20fNKLnIBJ5rkGO3vJJJkDZIBMH3IzMI7DUyZIA3RBE7wzfbSaZC3X87GWxS3IYM31IdFHXlKwBNFQpv4PKZ3RAeLRHpSuiyXlIuBWE7KEJCxcbDSFSTc3oFNUkwQACSfDze9Gt2T3zCowVB+G5k1pTpBunmMb6uXdzcqAzR41M2cpbbqRCpRmqzbW8psUbcsyTMM5', ]; - const invalids = [ - 'asdf', - ]; - const fmt = (bytes: string, annotation?: string) => `ssh-rsa ${bytes}${annotation ? ' ' + annotation : ''}`; it('should parse valid public keys', async () => { for (let valid of valids) { const formatted = fmt(valid, 'super'); - const results = await parsePublicKey(formatted); + const results = parsePublicKey(formatted); expect(results).toEqual([formatted, 'ssh-rsa', valid, 'super']); } }); @@ -28,15 +24,13 @@ describe('ionic', () => { it('should parse valid public keys with no annotation', async () => { for (let valid of valids) { const formatted = fmt(valid); - const results = await parsePublicKey(formatted); + const results = parsePublicKey(formatted); expect(results).toEqual([formatted, 'ssh-rsa', valid, '']); } }); it('should throw error for invalid public keys', async () => { - for (let valid of valids) { - expect(() => parsePublicKey(fmt(valid)).rejects.toBe(ERROR_SSH_INVALID_PUBKEY)); - } + expect(() => parsePublicKey('blah')).toThrow(ERROR_SSH_INVALID_PUBKEY); }); }); diff --git a/packages/ionic/src/lib/help.ts b/packages/ionic/src/lib/help.ts index f80a3994b0..73a073cf61 100644 --- a/packages/ionic/src/lib/help.ts +++ b/packages/ionic/src/lib/help.ts @@ -1,4 +1,5 @@ -import { CommandGroup, CommandHelpSchema as BaseCommandHelpSchema, CommandSchemaHelpFormatter as BaseCommandSchemaHelpFormatter, CommandStringHelpFormatter as BaseCommandStringHelpFormatter, NamespaceGroup, NamespaceHelpFormatterDeps as BaseNamespaceHelpFormatterDeps, NamespaceSchemaHelpFormatter as BaseNamespaceSchemaHelpFormatter, NamespaceStringHelpFormatter as BaseNamespaceStringHelpFormatter, OptionGroup, formatOptionName } from '@ionic/cli-framework'; +import { CommandGroup, CommandHelpSchema as BaseCommandHelpSchema, CommandSchemaHelpFormatter as BaseCommandSchemaHelpFormatter, CommandStringHelpFormatter as BaseCommandStringHelpFormatter, NamespaceGroup, NamespaceHelpFormatterDeps as BaseNamespaceHelpFormatterDeps, NamespaceSchemaHelpFormatter as BaseNamespaceSchemaHelpFormatter, NamespaceStringHelpFormatter as BaseNamespaceStringHelpFormatter, OptionGroup, formatOptionName, isOptionVisible } from '@ionic/cli-framework'; +import { filter } from '@ionic/cli-framework/utils/array'; import chalk from 'chalk'; import { CommandMetadata, CommandMetadataInput, CommandMetadataOption, HydratedCommandMetadata, ICommand, INamespace, NamespaceMetadata } from '../definitions'; @@ -32,15 +33,6 @@ const NAMESPACE_DECORATIONS: Decoration[] = [ [NamespaceGroup.Experimental, chalk.red.bold('(experimental)')], ]; -export async function isCommandVisible(cmd: HydratedCommandMetadata): Promise { - const ns = await cmd.namespace.getMetadata(); - return (!cmd.groups || !cmd.groups.includes(CommandGroup.Hidden)) && (!ns.groups || !ns.groups.includes(NamespaceGroup.Hidden)); -} - -export function isOptionVisible(opt: CommandMetadataOption): boolean { - return !opt.groups || !opt.groups.includes(OptionGroup.Hidden); -} - export interface NamespaceHelpFormatterDeps extends BaseNamespaceHelpFormatterDeps { readonly inProject: boolean; readonly version: string; @@ -77,10 +69,9 @@ export class NamespaceStringHelpFormatter extends BaseNamespaceStringHelpFormatt return formatGroupDecorations(COMMAND_DECORATIONS, cmd.groups); } - async getExtraOptions(): Promise { - return GLOBAL_OPTIONS - .filter(opt => isOptionVisible(opt)) - .map(opt => formatOptionName(opt, { showAliases: false })); + async getGlobalOptions(): Promise { + const visibleOptions = await filter(GLOBAL_OPTIONS, async opt => isOptionVisible(opt)); + return visibleOptions.map(opt => formatOptionName(opt, { showAliases: false })); } async formatCommands() { @@ -94,10 +85,6 @@ export class NamespaceStringHelpFormatter extends BaseNamespaceStringHelpFormatt (this.inProject ? await this.formatCommandGroup('Project Commands', projectCmds) : `\n ${strong('Project Commands')}:\n\n You are not in a project directory.\n`) ); } - - async filterCommandCallback(cmd: HydratedCommandMetadata): Promise { - return isCommandVisible(cmd); - } } export class CommandStringHelpFormatter extends BaseCommandStringHelpFormatter { @@ -114,10 +101,6 @@ export class CommandStringHelpFormatter extends BaseCommandStringHelpFormatter { - return isOptionVisible(opt); - } - async formatBeforeSummary(): Promise { const metadata = await this.getCommandMetadata(); return formatGroupDecorations(COMMAND_DECORATIONS, metadata.groups); @@ -129,10 +112,6 @@ export class CommandStringHelpFormatter extends BaseCommandStringHelpFormatter { - async filterCommandCallback(cmd: HydratedCommandMetadata): Promise { - return isCommandVisible(cmd); - } - async formatCommand(cmd: HydratedCommandMetadata): Promise { const { command } = cmd; @@ -156,10 +135,6 @@ export class CommandSchemaHelpFormatter extends BaseCommandSchemaHelpFormatter { - return isOptionVisible(opt); - } } function formatGroupDecorations(decorations: Decoration[], groups?: string[]): string { diff --git a/packages/ionic/src/lib/hooks.ts b/packages/ionic/src/lib/hooks.ts index b63f424051..18a39c0ab7 100644 --- a/packages/ionic/src/lib/hooks.ts +++ b/packages/ionic/src/lib/hooks.ts @@ -1,10 +1,10 @@ import { conform } from '@ionic/cli-framework/utils/array'; +import { prettyPath } from '@ionic/cli-framework/utils/format'; import chalk from 'chalk'; import * as Debug from 'debug'; import * as lodash from 'lodash'; import * as path from 'path'; -import { PROJECT_FILE } from '../constants'; import { HookFn, HookInput, HookName, IConfig, IProject, IShell } from '../definitions'; import { HookException } from './errors'; @@ -73,7 +73,7 @@ export abstract class Hook { })); } catch (e) { throw new HookException( - `An error occurred while running an Ionic CLI hook defined in ${chalk.bold(PROJECT_FILE)}.\n` + + `An error occurred while running an Ionic CLI hook defined in ${chalk.bold(prettyPath(this.e.project.filePath))}.\n` + `Hook: ${chalk.bold(this.name)}\n` + `File: ${chalk.bold(p)}\n\n` + `${chalk.red(e.stack ? e.stack : e)}` diff --git a/packages/ionic/src/lib/index.ts b/packages/ionic/src/lib/index.ts index 1e8c83c098..bf6a776005 100644 --- a/packages/ionic/src/lib/index.ts +++ b/packages/ionic/src/lib/index.ts @@ -8,12 +8,12 @@ import * as path from 'path'; import { ERROR_VERSION_TOO_OLD } from '../bootstrap'; import { PROJECT_FILE } from '../constants'; -import { IProject, InfoItem, IonicContext, IonicEnvironment, IonicEnvironmentFlags, ProjectType } from '../definitions'; +import { IProject, InfoItem, IonicContext, IonicEnvironment, IonicEnvironmentFlags } from '../definitions'; import { CONFIG_FILE, Config, DEFAULT_CONFIG_DIRECTORY, parseGlobalOptions } from './config'; import { Environment } from './environment'; import { Client } from './http'; -import { ProjectDeps, createProjectFromType, determineProjectType } from './project'; +import { ProjectDeps, ProjectDetails, createProjectFromType, determineProjectDetails } from './project'; import { createOnFallback } from './prompts'; import { ProSession } from './session'; import { Shell, prependNodeModulesBinToPath } from './shell'; @@ -30,7 +30,7 @@ export async function getProject(projectDir: string | undefined, projectName: st const { log } = deps; const projectFilePath = path.resolve(projectDir, PROJECT_FILE); let projectFile: { [key: string]: any; } | undefined; - let type: ProjectType | undefined; + let projectDetails: ProjectDetails | undefined; try { projectFile = await readJsonFile(projectFilePath); @@ -40,28 +40,33 @@ export async function getProject(projectDir: string | undefined, projectName: st `Attempted to load project config ${chalk.bold(prettyPath(projectFilePath))} but got error:\n\n` + chalk.red(e.toString()) ); + log.nl(); } if (projectFile) { - projectName = projectName || projectFile.defaultProject; - type = await determineProjectType(projectDir, projectName, projectFile, deps); - - debug(`Project name: ${chalk.bold(String(projectName))}`); + try { + projectDetails = await determineProjectDetails(projectDir, projectName, projectFile, deps); + } catch (e) { + log.warn(e.toString()); + log.nl(); + } } - if (!type) { + if (!projectDetails) { return; } - return createProjectFromType(projectFilePath, projectName, deps, type); + const { name, type } = projectDetails; + + return createProjectFromType(projectFilePath, name, deps, type); } -export async function generateIonicEnvironment(ctx: IonicContext, pargv: string[], env: NodeJS.ProcessEnv): Promise<{ env: IonicEnvironment; project?: IProject; }> { +export async function generateIonicEnvironment(ctx: IonicContext, pargv: string[]): Promise<{ env: IonicEnvironment; project?: IProject; }> { process.chdir(ctx.execPath); const argv = parseGlobalOptions(pargv); const projectName = argv['project'] ? String(argv['project']) : undefined; - const config = new Config(path.resolve(env['IONIC_CONFIG_DIRECTORY'] || DEFAULT_CONFIG_DIRECTORY, CONFIG_FILE)); + const config = new Config(path.resolve(process.env['IONIC_CONFIG_DIRECTORY'] || DEFAULT_CONFIG_DIRECTORY, CONFIG_FILE)); debug('Terminal info: %o', TERMINAL_INFO); @@ -83,7 +88,7 @@ export async function generateIonicEnvironment(ctx: IonicContext, pargv: string[ }); const projectDir = await findBaseDirectory(ctx.execPath, PROJECT_FILE); - const proxyVars = PROXY_ENVIRONMENT_VARIABLES.map((e): [string, string | undefined] => [e, env[e]]).filter(([, v]) => !!v); + const proxyVars = PROXY_ENVIRONMENT_VARIABLES.map((e): [string, string | undefined] => [e, process.env[e]]).filter(([, v]) => !!v); const getInfo = async () => { const osName = await import('os-name'); @@ -117,10 +122,10 @@ export async function generateIonicEnvironment(ctx: IonicContext, pargv: string[ const client = new Client(config); const session = new ProSession({ config, client }); const deps = { client, config, ctx, flags, log, prompt, session, shell }; - const ienv = new Environment({ getInfo, ...deps }); + const env = new Environment({ getInfo, ...deps }); - if (env['IONIC_CLI_LOCAL_ERROR']) { - if (env['IONIC_CLI_LOCAL_ERROR'] === ERROR_VERSION_TOO_OLD) { + if (process.env['IONIC_CLI_LOCAL_ERROR']) { + if (process.env['IONIC_CLI_LOCAL_ERROR'] === ERROR_VERSION_TOO_OLD) { log.warn(`Detected locally installed Ionic CLI, but it's too old--using global CLI.`); } } @@ -129,14 +134,11 @@ export async function generateIonicEnvironment(ctx: IonicContext, pargv: string[ log.warn(`${chalk.green('--yarn')} / ${chalk.green('--no-yarn')} has been removed. Use ${chalk.green(`ionic config set -g npmClient ${argv['yarn'] ? 'yarn' : 'npm'}`)}.`); } - let project: IProject | undefined; + const project = await getProject(projectDir, projectName, deps); - try { - project = await getProject(projectDir, projectName, deps); - } catch (e) { - log.warn(e.toString()); - log.nl(); + if (project) { + shell.alterPath = p => prependNodeModulesBinToPath(project.directory, p); } - return { env: ienv, project }; + return { env, project }; } diff --git a/packages/ionic/src/lib/integrations/capacitor/index.ts b/packages/ionic/src/lib/integrations/capacitor/index.ts index 5332267cfd..7e0b07c9ba 100644 --- a/packages/ionic/src/lib/integrations/capacitor/index.ts +++ b/packages/ionic/src/lib/integrations/capacitor/index.ts @@ -1,7 +1,8 @@ +import { mkdirp } from '@ionic/utils-fs'; import * as path from 'path'; import { BaseIntegration } from '../'; -import { IIntegrationAddOptions, InfoItem, IntegrationName, ProjectPersonalizationDetails } from '../../../definitions'; +import { InfoItem, IntegrationAddDetails, IntegrationAddHandlers, IntegrationName, ProjectPersonalizationDetails } from '../../../definitions'; import { pkgManagerArgs } from '../../utils/npm'; import { CAPACITOR_CONFIG_FILE, CapacitorConfig } from './config'; @@ -11,26 +12,27 @@ export class Integration extends BaseIntegration { readonly summary = `Target native iOS and Android with Capacitor, Ionic's new native layer`; readonly archiveUrl = undefined; - async add(options?: IIntegrationAddOptions): Promise { + async add(details: IntegrationAddDetails, handlers: IntegrationAddHandlers = {}): Promise { let name = this.e.project.config.get('name'); let packageId = 'io.ionic.starter'; - if (options && options.enableArgs) { - if (options.enableArgs[0]) { - name = options.enableArgs[0]; + if (details.enableArgs) { + if (details.enableArgs[0]) { + name = details.enableArgs[0]; } - if (options.enableArgs[1]) { - packageId = options.enableArgs[1]; + if (details.enableArgs[1]) { + packageId = details.enableArgs[1]; } } await this.installCapacitorCore(); await this.installCapacitorCLI(); - await this.e.shell.run('capacitor', ['init', name, packageId], { cwd: this.e.project.directory }); + await mkdirp(details.root); + await this.e.shell.run('capacitor', ['init', name, packageId], { cwd: details.root }); - await super.add(options); + await super.add(details, handlers); } async installCapacitorCore() { diff --git a/packages/ionic/src/lib/integrations/cordova/__tests__/android.ts b/packages/ionic/src/lib/integrations/cordova/__tests__/android.ts index 9e837e2c17..b896e4cc9b 100644 --- a/packages/ionic/src/lib/integrations/cordova/__tests__/android.ts +++ b/packages/ionic/src/lib/integrations/cordova/__tests__/android.ts @@ -13,7 +13,7 @@ describe('ionic', () => { it('should return undefined for a missing source.properties file', async () => { jest.mock('@ionic/utils-fs', () => ({ readFile: () => { - const err = new Error(); + const err: NodeJS.ErrnoException = new Error(); err.code = 'ENOENT'; return Promise.reject(err); }, diff --git a/packages/ionic/src/lib/integrations/cordova/__tests__/resources.ts b/packages/ionic/src/lib/integrations/cordova/__tests__/resources.ts index 1b0700472e..624215d903 100644 --- a/packages/ionic/src/lib/integrations/cordova/__tests__/resources.ts +++ b/packages/ionic/src/lib/integrations/cordova/__tests__/resources.ts @@ -16,7 +16,7 @@ describe('ionic', () => { it('should take the resources structure and flatten it', async () => { const result = resources.getImageResources('/path/to/proj'); - expect(result).toEqual(jasmine.any(Array)); + expect(result).toEqual(expect.any(Array)); expect(result.length).toEqual(53); expect(result.find(img => img.name === 'Default-568h@2x~iphone.png')).toEqual({ platform: 'ios', @@ -72,25 +72,25 @@ describe('ionic', () => { })); it('should create directories that required for all of the images', async () => { - spyOn(fsSpy, 'mkdirp').and.callFake((dir) => Promise.resolve(dir)); + const spy = spyOn(fsSpy, 'mkdirp').and.callFake((dir) => Promise.resolve(dir)); await resources.createImgDestinationDirectories(imgResources); - expect(fsSpy.mkdirp.calls.count()).toEqual(2); - expect(fsSpy.mkdirp.calls.argsFor(0)).toEqual([path.normalize('/resourcesDir/ios/splash')]); - expect(fsSpy.mkdirp.calls.argsFor(1)).toEqual([path.normalize('/resourcesDir/android/splash')]); + expect(spy.calls.count()).toEqual(2); + expect(spy.calls.argsFor(0)).toEqual([path.normalize('/resourcesDir/ios/splash')]); + expect(spy.calls.argsFor(1)).toEqual([path.normalize('/resourcesDir/android/splash')]); }); }); describe('getSourceImages', () => { it('should look in resources directory and platform directories to find images', async () => { - spyOn(fsSpy, 'readDir').and.returnValue(Promise.resolve([])); + const spy = spyOn(fsSpy, 'readDir').and.returnValue(Promise.resolve([])); await resources.getSourceImages('/path/to/proj', ['ios', 'android'], ['splash', 'icon']); - expect(fsSpy.readDir.calls.count()).toEqual(3); - expect(fsSpy.readDir.calls.argsFor(0)).toEqual([path.resolve('/path/to/proj/resources/ios')]); - expect(fsSpy.readDir.calls.argsFor(1)).toEqual([path.resolve('/path/to/proj/resources/android')]); - expect(fsSpy.readDir.calls.argsFor(2)).toEqual([path.resolve('/path/to/proj/resources')]); + expect(spy.calls.count()).toEqual(3); + expect(spy.calls.argsFor(0)).toEqual([path.resolve('/path/to/proj/resources/ios')]); + expect(spy.calls.argsFor(1)).toEqual([path.resolve('/path/to/proj/resources/android')]); + expect(spy.calls.argsFor(2)).toEqual([path.resolve('/path/to/proj/resources')]); }); it('should find all sourceImages available and prioritize based on specificity', async () => { @@ -308,7 +308,7 @@ describe('ionic', () => { imageId: '60278b0fa1d5abf43d07c5ae0f8a0b41' }; - const response = await resources.uploadSourceImage({ config: { getHTTPConfig: () => ({}) } }, sourceImage); + const response = await resources.uploadSourceImage({ config: { getHTTPConfig: () => ({}) } } as any, sourceImage); expect(response).toEqual({ Error: '', Width: 337, diff --git a/packages/ionic/src/lib/integrations/cordova/__tests__/utils.ts b/packages/ionic/src/lib/integrations/cordova/__tests__/utils.ts index cac151be1d..c43c4ca28f 100644 --- a/packages/ionic/src/lib/integrations/cordova/__tests__/utils.ts +++ b/packages/ionic/src/lib/integrations/cordova/__tests__/utils.ts @@ -1,55 +1,69 @@ import { filterArgumentsForCordova, generateOptionsForCordovaBuild } from '../utils'; +import { CommandMetadata } from '../../../../definitions'; describe('ionic', () => { - const buildMetadata = { + const buildMetadata: CommandMetadata = { name: 'build', + summary: '', + type: 'global', inputs: [ { name: 'platform', + summary: '', } ], options: [ { name: 'boolopt', + summary: '', type: Boolean, default: false, }, { name: 'cdvopt1', + summary: '', groups: ['cordova'], }, { name: 'cdvopt2', + summary: '', type: Boolean, groups: ['cordova'], }, { name: 'prod', + summary: '', type: Boolean, groups: ['app-scripts'], }, { name: 'optimizejs', + summary: '', type: Boolean, groups: ['app-scripts'], }, ] }; - const platformMetadata = { + const platformMetadata: CommandMetadata = { name: 'platform', + summary: '', + type: 'global', inputs: [ { name: 'action', + summary: '', }, { name: 'platform', + summary: '', } ], options: [ { name: 'boolopt', + summary: '', type: Boolean, default: false, }, diff --git a/packages/ionic/src/lib/integrations/cordova/index.ts b/packages/ionic/src/lib/integrations/cordova/index.ts index f05e7fb407..3acd19787a 100644 --- a/packages/ionic/src/lib/integrations/cordova/index.ts +++ b/packages/ionic/src/lib/integrations/cordova/index.ts @@ -72,12 +72,18 @@ export class Integration extends BaseIntegration { } async getCordovaVersion(): Promise { - return this.e.shell.cmdinfo('cordova', ['-v', '--no-telemetry', '--no-update-notifier']); + try { + const integration = this.e.project.requireIntegration('cordova'); + return this.e.shell.cmdinfo('cordova', ['-v', '--no-telemetry', '--no-update-notifier'], { cwd: integration.root }); + } catch (e) { + debug('Error while getting Cordova version: %O', e); + } } async getCordovaPlatformVersions(): Promise { try { - const output = await this.e.shell.output('cordova', ['platform', 'ls', '--no-telemetry', '--no-update-notifier'], { showCommand: false }); + const integration = this.e.project.requireIntegration('cordova'); + const output = await this.e.shell.output('cordova', ['platform', 'ls', '--no-telemetry', '--no-update-notifier'], { cwd: integration.root, showCommand: false }); const platforms = output .replace('Installed platforms:', '') .replace(/Available platforms[\s\S]+/, '') @@ -91,7 +97,7 @@ export class Integration extends BaseIntegration { return platforms.join(', '); } catch (e) { - debug('Error while getting Cordova platforms: %o', e); + debug('Error while getting Cordova platforms: %O', e); return 'not available'; } } @@ -103,7 +109,8 @@ export class Integration extends BaseIntegration { ]; try { - const output = await this.e.shell.output('cordova', ['plugin', 'ls', '--no-telemetry', '--no-update-notifier'], { showCommand: false }); + const integration = this.e.project.requireIntegration('cordova'); + const output = await this.e.shell.output('cordova', ['plugin', 'ls', '--no-telemetry', '--no-update-notifier'], { cwd: integration.root, showCommand: false }); const pluginRe = /^([a-z-]+)\s+(\d\.\d\.\d).+$/; const plugins = output .split('\n') @@ -123,7 +130,7 @@ export class Integration extends BaseIntegration { return `${whitelistedPlugins.join(', ')}${count > 0 ? `, (and ${count} other plugins)` : ''}`; } catch (e) { - debug('Error while getting Cordova plugins: %o', e); + debug('Error while getting Cordova plugins: %O', e); return 'not available'; } } diff --git a/packages/ionic/src/lib/integrations/index.ts b/packages/ionic/src/lib/integrations/index.ts index 38db09a5f7..b4fae64498 100644 --- a/packages/ionic/src/lib/integrations/index.ts +++ b/packages/ionic/src/lib/integrations/index.ts @@ -1,13 +1,11 @@ -import * as os from 'os'; -import * as path from 'path'; - +import { copyDirectory, mkdirp, pathExists, readDirSafe, removeDirectory, stat } from '@ionic/utils-fs'; import chalk from 'chalk'; import * as Debug from 'debug'; import * as lodash from 'lodash'; +import * as os from 'os'; +import * as path from 'path'; -import { copyDirectory, mkdirp, pathExists, readDirSafe, removeDirectory, stat } from '@ionic/utils-fs'; - -import { IConfig, IIntegration, IIntegrationAddOptions, ILogger, IProject, IShell, InfoItem, IntegrationName, ProjectPersonalizationDetails } from '../../definitions'; +import { IConfig, IIntegration, ILogger, IProject, IShell, InfoItem, IntegrationAddDetails, IntegrationAddHandlers, IntegrationName, ProjectPersonalizationDetails } from '../../definitions'; import { IntegrationNotFoundException } from '../errors'; import * as ζcapacitor from './capacitor'; @@ -66,13 +64,13 @@ export abstract class BaseIntegration implements IIntegration { // optionally overwritten by subclasses } - async add(opts?: IIntegrationAddOptions): Promise { + async add(details: IntegrationAddDetails, handlers: IntegrationAddHandlers = {}): Promise { if (!this.archiveUrl) { return; } - const onFileCreate = opts && opts.onFileCreate ? opts.onFileCreate : lodash.noop; - const conflictHandler = opts && opts.conflictHandler ? opts.conflictHandler : async () => false; + const onFileCreate = handlers.onFileCreate ? handlers.onFileCreate : lodash.noop; + const conflictHandler = handlers.conflictHandler ? handlers.conflictHandler : async () => false; const { createRequest, download } = await import('../utils/http'); const { tar } = await import('../utils/archive'); @@ -117,7 +115,9 @@ export abstract class BaseIntegration implements IIntegration { this.e.log.info(`Copying integrations files to project`); debug(`Blacklist: ${blacklist.map(f => chalk.bold(f)).join(', ')}`); - await copyDirectory(tmpdir, this.e.project.directory, { + await mkdirp(details.root); + + await copyDirectory(tmpdir, details.root, { filter: f => { if (f === tmpdir) { return true; diff --git a/packages/ionic/src/lib/project/angular/__tests__/index.ts b/packages/ionic/src/lib/project/angular/__tests__/index.ts index b5285bcec7..5c78a47970 100644 --- a/packages/ionic/src/lib/project/angular/__tests__/index.ts +++ b/packages/ionic/src/lib/project/angular/__tests__/index.ts @@ -8,14 +8,14 @@ describe('ionic', () => { describe('AngularProject', () => { it('should set directory attribute', async () => { - const p = new AngularProject('/path/to/proj/file', undefined, {}); + const p = new AngularProject('/path/to/proj/file', undefined, {} as any); expect(p.directory).toEqual(path.resolve('/path/to/proj')); }); describe('getSourceDir', () => { it('should default to src', async () => { - const p = new AngularProject('/path/to/proj/file', undefined, {}); + const p = new AngularProject('/path/to/proj/file', undefined, {} as any); const result = await p.getSourceDir(); expect(result).toEqual(path.resolve('/path/to/proj/src')); }); diff --git a/packages/ionic/src/lib/project/angular/__tests__/serve.ts b/packages/ionic/src/lib/project/angular/__tests__/serve.ts index 245273b207..2a71fe8a59 100644 --- a/packages/ionic/src/lib/project/angular/__tests__/serve.ts +++ b/packages/ionic/src/lib/project/angular/__tests__/serve.ts @@ -1,8 +1,5 @@ import { AngularServeRunner, AngularServeCLI } from '../serve'; -import { ServeRunnerDeps } from '../../../../../lib/serve'; import { CommandLineOptions } from '../../../../definitions'; -import { Project } from '../../../../../lib/project'; -import { AngularProject } from '../../../../../lib/project/angular'; describe('ionic', () => { @@ -34,44 +31,44 @@ describe('ionic', () => { }; it('should provide defaults with no options', () => { - const runner = new AngularServeRunner({} as ServeRunnerDeps); + const runner = new AngularServeRunner({} as any); const result = runner.createOptionsFromCommandLine([], {} as CommandLineOptions); expect(result).toEqual(defaults); }); it('should provide options from negations of cli flag defaults', () => { - const runner = new AngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); + const runner = new AngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); expect(result).toEqual({ ...defaults, livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); }); it('should turn off devapp for cordova', () => { - const runner = new AngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { engine: 'cordova' }); + const runner = new AngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], engine: 'cordova' }); expect(result).toEqual({ ...defaults, devapp: false, engine: 'cordova' }); }); it('should allow overrides of default values', () => { - const runner = new AngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { address: 'localhost', port: '1111' }); + const runner = new AngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], address: 'localhost', port: '1111' }); expect(result).toEqual({ ...defaults, address: 'localhost', port: 1111 }); }); it('should respect --local flag', () => { - const runner = new AngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { local: true }); + const runner = new AngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], local: true }); expect(result).toEqual({ ...defaults, address: 'localhost', devapp: false }); }); it('should respect --project and --configuration flags', () => { - const runner = new AngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { project: 'app', configuration: 'production' }); + const runner = new AngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], project: 'app', configuration: 'production' }); expect(result).toEqual({ ...defaults, project: 'app', configuration: 'production' }); }); it('should pass on separated args', () => { - const runner = new AngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { '--': ['foo', '--bar'] }); + const runner = new AngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], '--': ['foo', '--bar'] }); expect(result).toEqual({ ...defaults, '--': ['foo', '--bar'] }); }); @@ -92,15 +89,15 @@ describe('ionic', () => { const project = { requireIntegration: jest.fn(() => ({ root })), }; - const runner = new AngularServeCLI({ project }); + const runner = new AngularServeCLI({ project } as any); const options = { ...defaults, engine: 'cordova', platform: 'fakePlatform', }; - const result = await runner.serveOptionsToNgArgs(options); - expect(result).toEqual(jasmine.arrayContaining([ + const result = await (runner as any).serveOptionsToNgArgs(options); + expect(result).toEqual(expect.arrayContaining([ `--platform=${options.platform}`, `--cordova-base-path=${root}`, ])); diff --git a/packages/ionic/src/lib/project/angular/build.ts b/packages/ionic/src/lib/project/angular/build.ts index 364d9a6e50..0a67336c07 100644 --- a/packages/ionic/src/lib/project/angular/build.ts +++ b/packages/ionic/src/lib/project/angular/build.ts @@ -117,7 +117,7 @@ class AngularBuildCLI extends BuildCLI { const integration = this.e.project.requireIntegration('cordova'); args.platform = options.platform; - if (this.e.project.directory !== integration.root) { + if (this.e.project.rootDirectory !== integration.root) { args.cordovaBasePath = integration.root; } diff --git a/packages/ionic/src/lib/project/angular/serve.ts b/packages/ionic/src/lib/project/angular/serve.ts index 3d1122612d..817daad764 100644 --- a/packages/ionic/src/lib/project/angular/serve.ts +++ b/packages/ionic/src/lib/project/angular/serve.ts @@ -148,7 +148,7 @@ export class AngularServeCLI extends ServeCLI { const integration = this.e.project.requireIntegration('cordova'); args.platform = options.platform; - if (this.e.project.directory !== integration.root) { + if (this.e.project.rootDirectory !== integration.root) { args.cordovaBasePath = integration.root; } diff --git a/packages/ionic/src/lib/project/index.ts b/packages/ionic/src/lib/project/index.ts index 4e9663881b..86bc5dd58d 100644 --- a/packages/ionic/src/lib/project/index.ts +++ b/packages/ionic/src/lib/project/index.ts @@ -18,13 +18,43 @@ import * as ζserve from '../serve'; const debug = Debug('ionic:lib:project'); -export async function determineProjectType(projectDir: string, projectName: string | undefined, projectConfig: { [key: string]: any; } | undefined, deps: ProjectDeps): Promise { +export interface ProjectDetails { + name?: string; + type: ProjectType; +} + +export async function determineProjectDetails(rootProjectDir: string, projectName: string | undefined, projectConfig: { [key: string]: any; } | undefined, deps: ProjectDeps): Promise { + let name: string | undefined; let type: ProjectType | undefined; + if (projectName) { + name = projectName; + debug(`Project name from projectName: ${chalk.bold(name)}`); + } + if (isProjectConfig(projectConfig)) { type = projectConfig.type; } else if (isMultiProjectConfig(projectConfig)) { - const name = projectName ? projectName : projectConfig.defaultProject; + if (!name) { + const { ctx } = deps; + + for (const [ key, value ] of lodash.entries(projectConfig.projects)) { + if (value && value.root) { + const projectDir = path.resolve(rootProjectDir, value.root); + + if (ctx.execPath.startsWith(projectDir)) { + name = key; + debug(`Project name from path match: ${chalk.bold(name)}`); + break; + } + } + } + } + + if (!name && projectConfig.defaultProject) { + name = projectConfig.defaultProject; + debug(`Project name from defaultProject: ${chalk.bold(name)}`); + } if (!name) { throw new Error( @@ -37,6 +67,7 @@ export async function determineProjectType(projectDir: string, projectName: stri if (config) { type = config.type; + debug(`Project type from config: ${chalk.bold(prettyProjectName(type))} ${type ? chalk.bold(`(${type})`) : ''}`); } else { throw new Error( `Multi-app workspace detected, but project was not found in configuration.\n` + @@ -52,26 +83,28 @@ export async function determineProjectType(projectDir: string, projectName: stri } } - if (type && PROJECT_TYPES.includes(type)) { - debug(`Project type from config: ${chalk.bold(prettyProjectName(type))} ${type ? chalk.bold(`(${type})`) : ''}`); - return type; - } - - for (const projectType of PROJECT_TYPES) { - const p = await createProjectFromType(path.resolve(projectDir, PROJECT_FILE), projectName, deps, projectType); + if (!type) { + for (const projectType of PROJECT_TYPES) { + const p = await createProjectFromType(path.resolve(rootProjectDir, PROJECT_FILE), projectName, deps, projectType); - if (await p.detected()) { - debug(`Project type detected: ${chalk.bold(prettyProjectName(p.type))} ${p.type ? chalk.bold(`(${p.type})`) : ''}`); - return p.type; + if (await p.detected()) { + debug(`Project type from detection: ${chalk.bold(prettyProjectName(p.type))} ${p.type ? chalk.bold(`(${p.type})`) : ''}`); + type = p.type; + break; + } } } + if (type && PROJECT_TYPES.includes(type)) { + return { name, type }; + } + const listWrapOptions = { width: TTY_WIDTH - 8 - 3, indentation: 1 }; // TODO: move some of this to the CLI docs throw new Error( - `Could not determine project type (project config: ${chalk.bold(prettyPath(path.resolve(projectDir, PROJECT_FILE)))}).\n` + + `Could not determine project type (project config: ${chalk.bold(prettyPath(path.resolve(rootProjectDir, PROJECT_FILE)))}).\n` + `- ${wordWrap(`For ${chalk.bold(prettyProjectName('angular'))} projects, make sure ${chalk.green('@ionic/angular')} is listed as a dependency in ${chalk.bold('package.json')}.`, listWrapOptions)}\n` + `- ${wordWrap(`For ${chalk.bold(prettyProjectName('ionic-angular'))} projects, make sure ${chalk.green('ionic-angular')} is listed as a dependency in ${chalk.bold('package.json')}.`, listWrapOptions)}\n` + `- ${wordWrap(`For ${chalk.bold(prettyProjectName('ionic1'))} projects, make sure ${chalk.green('ionic')} is listed as a dependency in ${chalk.bold('bower.json')}.`, listWrapOptions)}\n\n` + @@ -138,7 +171,7 @@ export interface ProjectDeps { } export abstract class Project implements IProject { - readonly directory: string; + readonly rootDirectory: string; abstract readonly type: ProjectType; protected originalConfigFile?: { [key: string]: any }; @@ -157,7 +190,17 @@ export abstract class Project implements IProject { protected readonly e: ProjectDeps ) { - this.directory = path.dirname(filePath); + this.rootDirectory = path.dirname(filePath); + } + + get directory(): string { + const root = this.config.get('root'); + + if (!root) { + return this.rootDirectory; + } + + return path.resolve(this.rootDirectory, root); } get config(): ProjectConfig { @@ -317,7 +360,7 @@ export abstract class Project implements IProject { if (integration) { return { enabled: integration.enabled !== false, - root: integration.root === undefined ? this.directory : path.resolve(this.directory, integration.root), + root: integration.root === undefined ? this.directory : path.resolve(this.rootDirectory, integration.root), }; } } diff --git a/packages/ionic/src/lib/project/ionic-angular/__tests__/build.ts b/packages/ionic/src/lib/project/ionic-angular/__tests__/build.ts index 34cc4c1b03..ab4e74a4d8 100644 --- a/packages/ionic/src/lib/project/ionic-angular/__tests__/build.ts +++ b/packages/ionic/src/lib/project/ionic-angular/__tests__/build.ts @@ -18,14 +18,14 @@ describe('ionic', () => { }; it('should transform defaults', async () => { - const appscripts = new IonicAngularBuildCLI({}); - const result = await appscripts.buildOptionsToAppScriptsArgs({ '--': [] }); + const appscripts = new IonicAngularBuildCLI({} as any); + const result = await (appscripts as any).buildOptionsToAppScriptsArgs({ _: [], '--': [] }); expect(result).toEqual([]); }); it('should transform options', async () => { - const appscripts = new IonicAngularBuildCLI({}); - const result = await appscripts.buildOptionsToAppScriptsArgs(options); + const appscripts = new IonicAngularBuildCLI({} as any); + const result = await (appscripts as any).buildOptionsToAppScriptsArgs(options); expect(result).toEqual(['--prod', '--aot', '--minifyjs', '--minifycss', '--optimizejs', '--generateSourceMap', 'false']); }); diff --git a/packages/ionic/src/lib/project/ionic-angular/__tests__/index.ts b/packages/ionic/src/lib/project/ionic-angular/__tests__/index.ts index 6fe8318306..26ffbaf5ed 100644 --- a/packages/ionic/src/lib/project/ionic-angular/__tests__/index.ts +++ b/packages/ionic/src/lib/project/ionic-angular/__tests__/index.ts @@ -8,14 +8,14 @@ describe('ionic', () => { describe('IonicAngularProject', () => { it('should set directory attribute', async () => { - const p = new IonicAngularProject('/path/to/proj/file', undefined, {}); + const p = new IonicAngularProject('/path/to/proj/file', undefined, {} as any); expect(p.directory).toEqual(path.resolve('/path/to/proj')); }); describe('getSourceDir', () => { it('should default to src', async () => { - const p = new IonicAngularProject('/path/to/proj/file', undefined, {}); + const p = new IonicAngularProject('/path/to/proj/file', undefined, {} as any); const result = await p.getSourceDir(); expect(result).toEqual(path.resolve('/path/to/proj/src')); }); diff --git a/packages/ionic/src/lib/project/ionic-angular/__tests__/serve.ts b/packages/ionic/src/lib/project/ionic-angular/__tests__/serve.ts index 8471f8964a..c451551398 100644 --- a/packages/ionic/src/lib/project/ionic-angular/__tests__/serve.ts +++ b/packages/ionic/src/lib/project/ionic-angular/__tests__/serve.ts @@ -33,38 +33,38 @@ describe('ionic', () => { }; it('should provide defaults with no options', () => { - const runner = new IonicAngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], {}); + const runner = new IonicAngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [] }); expect(result).toEqual(defaults); }); it('should provide options from negations of cli flag defaults', () => { - const runner = new IonicAngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); + const runner = new IonicAngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); expect(result).toEqual({ ...defaults, livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); }); it('should turn off devapp for cordova', () => { - const runner = new IonicAngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { engine: 'cordova' }); + const runner = new IonicAngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], engine: 'cordova' }); expect(result).toEqual({ ...defaults, devapp: false, engine: 'cordova' }); }); it('should allow overrides of default values', () => { - const runner = new IonicAngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { address: 'localhost', port: '1111', 'livereload-port': '2222', 'dev-logger-port': '3333', env: 'prod' }); + const runner = new IonicAngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], address: 'localhost', port: '1111', 'livereload-port': '2222', 'dev-logger-port': '3333', env: 'prod' }); expect(result).toEqual({ ...defaults, address: 'localhost', port: 1111, livereloadPort: 2222, notificationPort: 3333, env: 'prod' }); }); it('should respect --local flag', () => { - const runner = new IonicAngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { local: true }); + const runner = new IonicAngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], local: true }); expect(result).toEqual({ ...defaults, address: 'localhost', devapp: false }); }); it('should pass on separated args', () => { - const runner = new IonicAngularServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { '--': ['foo', '--bar'] }); + const runner = new IonicAngularServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], '--': ['foo', '--bar'] }); expect(result).toEqual({ ...defaults, '--': ['foo', '--bar'] }); }); diff --git a/packages/ionic/src/lib/project/ionic1/__tests__/index.ts b/packages/ionic/src/lib/project/ionic1/__tests__/index.ts index 8d7aa25233..3c82959e26 100644 --- a/packages/ionic/src/lib/project/ionic1/__tests__/index.ts +++ b/packages/ionic/src/lib/project/ionic1/__tests__/index.ts @@ -8,7 +8,7 @@ describe('ionic', () => { describe('Ionic1Project', () => { it('should set directory attribute', async () => { - const p = new Ionic1Project('/path/to/proj/file', undefined, {}); + const p = new Ionic1Project('/path/to/proj/file', undefined, {} as any); expect(p.directory).toEqual(path.resolve('/path/to/proj')); }); diff --git a/packages/ionic/src/lib/project/ionic1/__tests__/serve.ts b/packages/ionic/src/lib/project/ionic1/__tests__/serve.ts index f37f663a62..fe59b6c512 100644 --- a/packages/ionic/src/lib/project/ionic1/__tests__/serve.ts +++ b/packages/ionic/src/lib/project/ionic1/__tests__/serve.ts @@ -32,38 +32,38 @@ describe('ionic', () => { }; it('should provide defaults with no options', () => { - const runner = new Ionic1ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], {}); + const runner = new Ionic1ServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [] }); expect(result).toEqual(defaults); }); it('should provide options from negations of cli flag defaults', () => { - const runner = new Ionic1ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); + const runner = new Ionic1ServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); expect(result).toEqual({ ...defaults, livereload: false, proxy: false, devapp: false, lab: true, open: true, externalAddressRequired: true }); }); it('should turn off devapp for cordova', () => { - const runner = new Ionic1ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { engine: 'cordova' }); + const runner = new Ionic1ServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], engine: 'cordova' }); expect(result).toEqual({ ...defaults, devapp: false, engine: 'cordova' }); }); it('should allow overrides of default values', () => { - const runner = new Ionic1ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { address: 'localhost', port: '1111', 'livereload-port': '2222', 'dev-logger-port': '3333' }); + const runner = new Ionic1ServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], address: 'localhost', port: '1111', 'livereload-port': '2222', 'dev-logger-port': '3333' }); expect(result).toEqual({ ...defaults, address: 'localhost', port: 1111, livereloadPort: 2222, notificationPort: 3333 }); }); it('should respect --local flag', () => { - const runner = new Ionic1ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { local: true }); + const runner = new Ionic1ServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], local: true }); expect(result).toEqual({ ...defaults, address: 'localhost', devapp: false }); }); it('should pass on separated args', () => { - const runner = new Ionic1ServeRunner({}); - const result = runner.createOptionsFromCommandLine([], { '--': ['foo', '--bar'] }); + const runner = new Ionic1ServeRunner({} as any); + const result = runner.createOptionsFromCommandLine([], { _: [], '--': ['foo', '--bar'] }); expect(result).toEqual({ ...defaults, '--': ['foo', '--bar'] }); }); diff --git a/packages/ionic/src/lib/shell.ts b/packages/ionic/src/lib/shell.ts index d2990fc98b..7990567ccc 100644 --- a/packages/ionic/src/lib/shell.ts +++ b/packages/ionic/src/lib/shell.ts @@ -2,6 +2,7 @@ import { ERROR_SHELL_COMMAND_NOT_FOUND, LOGGER_LEVELS, ShellCommandError } from import { createProcessEnv, killProcessTree, onBeforeExit } from '@ionic/cli-framework/utils/process'; import { ShellCommand, which } from '@ionic/cli-framework/utils/shell'; import { combineStreams } from '@ionic/cli-framework/utils/streams'; +import { TERMINAL_INFO } from '@ionic/cli-framework/utils/terminal'; import chalk from 'chalk'; import { ChildProcess, SpawnOptions } from 'child_process'; import * as Debug from 'debug'; @@ -24,7 +25,7 @@ export interface ShellOptions { } export class Shell implements IShell { - readonly alterPath: (p: string) => string; + alterPath: (p: string) => string; constructor(protected readonly e: ShellDeps, options?: ShellOptions) { this.alterPath = options && options.alterPath ? options.alterPath : (p: string) => p; @@ -171,7 +172,7 @@ export class Shell implements IShell { * @see https://github.com/ionic-team/ionic-cli/issues/3563#issuecomment-425232005 */ async resolveCommandPath(command: string, options: SpawnOptions): Promise { - if (process.platform === 'win32') { + if (TERMINAL_INFO.windows) { try { return await which(command, { PATH: options.env.PATH }); } catch (e) { diff --git a/packages/ionic/src/lib/ssh.ts b/packages/ionic/src/lib/ssh.ts index b89cbe63ef..aa7e25f2ca 100644 --- a/packages/ionic/src/lib/ssh.ts +++ b/packages/ionic/src/lib/ssh.ts @@ -20,9 +20,9 @@ export async function parsePublicKeyFile(pubkeyPath: string): Promise<[string, s } /** - * @return Promise<[full pubkey, algorithm, public numbers, annotation]> + * @return [full pubkey, algorithm, public numbers, annotation] */ -export async function parsePublicKey(pubkey: string): Promise<[string, string, string, string]> { +export function parsePublicKey(pubkey: string): [string, string, string, string] { const r = /^(ssh-[A-z0-9]+)\s([A-z0-9+\/=]+)\s?(.+)?$/.exec(pubkey); if (!r) { @@ -54,7 +54,7 @@ export async function validatePrivateKey(keyPath: string): Promise { const f = await readFile(keyPath, { encoding: 'utf8' }); const lines = f.split('\n'); - if (!lines[0].match(/^\-{5}BEGIN [R|D]SA PRIVATE KEY\-{5}$/)) { + if (!lines[0].match(/^\-{5}BEGIN [A-Z]+ PRIVATE KEY\-{5}$/)) { throw ERROR_SSH_INVALID_PRIVKEY; } } diff --git a/packages/ionic/src/lib/utils/emoji.ts b/packages/ionic/src/lib/utils/emoji.ts index 6508b768ff..ba856d460b 100644 --- a/packages/ionic/src/lib/utils/emoji.ts +++ b/packages/ionic/src/lib/utils/emoji.ts @@ -1,10 +1,9 @@ -// Emoji falback, right now just uses fallback on windows, -// but could expand to be more sophisticated to allow emoji -// on Hyper term on windows, for example. -export const emoji = (x: string, fallback: string) => { - if (process.platform === 'win32') { +import { TERMINAL_INFO } from '@ionic/cli-framework/utils/terminal'; + +export function emoji(x: string, fallback: string): string { + if (TERMINAL_INFO.windows) { return fallback; } return x; -}; +} diff --git a/packages/ionic/tsconfig.json b/packages/ionic/tsconfig.json index 0b61aed895..8cee3c4867 100644 --- a/packages/ionic/tsconfig.json +++ b/packages/ionic/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./" + "outDir": "./", + "types": [ + "node" + ] }, "include": [ "../../types/leek.d.ts", diff --git a/tsconfig.base.json b/tsconfig.base.json index d01d8d7634..887da17042 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,9 +10,7 @@ "pretty": true, "strict": true, "target": "es6", - "typeRoots": [ - "node_modules/@types" - ], + "types": [], "lib": [ "es2016" ]