diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d1bdf98567..d45811f0b1de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] +- support configuring the logger level by running `bit config set log_level `. +- [#2268](https://github.com/teambit/bit/issues/2268) prevent logger from holding the terminal once a command is completed + +## [[14.7.5-dev.1] - 2020-02-06] + +- [#2211](https://github.com/teambit/bit/issues/2211) fix bit export to not export non-staged dependencies +- [#2308](https://github.com/teambit/bit/issues/2308) fix "Cannot read property 'scope' of undefined" error on bit export + ## [[14.7.4] - 2020-02-06](https://github.com/teambit/bit/releases/tag/v14.7.4) - [#2300](https://github.com/teambit/bit/issues/2300) improve `bit export` performance by pushing new tags only diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c2731ed8182..ba9cd93e35a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,6 +79,8 @@ In some cases, you might get very helpful info by prefixing Bit command with `BI To print the log messages on the console, prefix your command with `BIT_LOG=`, e.g. `BIT_LOG=error`. +The log level written to the log file is by default "debug". To change it, run `bit config set log_level `, e.g. `bit config set log_level silly`. + ### Lint Run eslint and tsc (for type checking) diff --git a/e2e/commands/export.e2e.1.ts b/e2e/commands/export.e2e.1.ts index 81644b180224..c724b970f747 100644 --- a/e2e/commands/export.e2e.1.ts +++ b/e2e/commands/export.e2e.1.ts @@ -641,6 +641,39 @@ describe('bit export command', function() { expect(output).to.have.string(`${anotherRemote}/foo2`); }); }); + // fixes https://github.com/teambit/bit/issues/2308 + // here, the component foo1 has a new dependency "bar", this dependency has been exported + // already, so we expect "bit export" to not attempt to export it. + describe('export with no ids, no remote and no flags when a dependency is from another collection', () => { + let output; + before(() => { + helper.scopeHelper.getClonedLocalScope(localScopeBefore); + helper.scopeHelper.getClonedRemoteScope(remoteScopeBefore); + helper.scopeHelper.getClonedScope(anotherRemoteScopeBefore, anotherRemotePath); + const { scopeName, scopePath } = helper.scopeHelper.getNewBareScope(); + helper.scopeHelper.addRemoteScope(scopePath); + helper.scopeHelper.addRemoteScope(scopePath, helper.scopes.remotePath); + helper.fs.outputFile('bar.js', ''); + helper.command.addComponent('bar.js'); + helper.fs.outputFile('foo1.js', 'require("./bar");'); + helper.command.tagAllComponents(); + helper.command.exportComponent('bar', scopeName); + output = helper.command.runCmd('bit export'); + }); + it('should export successfully all ids, each to its own remote', () => { + const remoteList = helper.command.listRemoteScopeParsed(); + expect(remoteList).to.have.lengthOf(1); + expect(remoteList[0].id).to.have.string('foo1'); + + const anotherRemoteListJson = helper.command.runCmd(`bit list ${anotherRemote} --json`); + const anotherRemoteList = JSON.parse(anotherRemoteListJson); + expect(anotherRemoteList).to.have.lengthOf(1); + expect(anotherRemoteList[0].id).to.have.string('foo2'); + }); + it('should not export the dependency that was not intended to be exported', () => { + expect(output).to.not.have.string('bar'); + }); + }); describe('export with ids, no remote and the flag --last-scope', () => { let output; before(() => { diff --git a/e2e/commands/link.e2e.1.ts b/e2e/commands/link.e2e.1.ts index 1d3b5fa6f2dd..ee19f3254bcc 100644 --- a/e2e/commands/link.e2e.1.ts +++ b/e2e/commands/link.e2e.1.ts @@ -50,7 +50,7 @@ describe('bit link', function() { ).to.be.a.directory(); }); }); - describe('when scopeDefault is overridden for this component', () => { + describe('when defaultScope is overridden for this component', () => { let linkOutput; before(() => { helper.scopeHelper.getClonedLocalScope(beforeLink); diff --git a/e2e/functionalities/default-scope.e2e.2.ts b/e2e/functionalities/default-scope.e2e.2.ts new file mode 100644 index 000000000000..2e91e6170a55 --- /dev/null +++ b/e2e/functionalities/default-scope.e2e.2.ts @@ -0,0 +1,51 @@ +import chai, { expect } from 'chai'; +import Helper from '../../src/e2e-helper/e2e-helper'; + +chai.use(require('chai-fs')); + +describe('default scope functionality', function() { + this.timeout(0); + const helper = new Helper(); + after(() => { + helper.scopeHelper.destroy(); + }); + describe('basic flow', () => { + helper.scopeHelper.setNewLocalAndRemoteScopes(); + helper.fixtures.populateWorkspaceWithThreeComponentsAndModulePath(); + helper.bitJson.addDefaultScope(); + helper.command.runCmd('bit link'); + }); + it('bit status should not break', () => { + const status = helper.command.statusJson(); + expect(status.newComponents).have.lengthOf(3); + expect(status.invalidComponents).have.lengthOf(0); + }); + describe('tagging the components', () => { + let tagOutput; + before(() => { + tagOutput = helper.command.tagAllComponents(); + }); + it('should be able to to tag them successfully', () => { + expect(tagOutput).to.have.string('tagged'); + }); + it('bit status should not show any issue', () => { + const status = helper.command.statusJson(); + expect(status.stagedComponents).have.lengthOf(3); + expect(status.newComponents).have.lengthOf(0); + expect(status.modifiedComponent).have.lengthOf(0); + expect(status.invalidComponents).have.lengthOf(0); + }); + describe('exporting the components', () => { + before(() => { + helper.command.exportAllComponents(); + }); + it('should be able to export them all successfully', () => { + const status = helper.command.statusJson(); + expect(status.stagedComponents).have.lengthOf(0); + expect(status.newComponents).have.lengthOf(0); + expect(status.modifiedComponent).have.lengthOf(0); + expect(status.invalidComponents).have.lengthOf(0); + }); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 0cbe425bbd23..45405702cf53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "bit-bin", - "version": "14.7.3", + "version": "14.7.5-dev.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1988,6 +1988,12 @@ "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-5.0.1.tgz", "integrity": "sha512-wYxU3kp5zItbxKmeRYCEplS2MW7DzyBnxPGj+GJVHZEUZiK/nn5Ei1sUFgURDh+X051+zsGe28iud3oHjrYWQQ==" }, + "@types/bluebird": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.29.tgz", + "integrity": "sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==", + "dev": true + }, "@types/chai": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz", diff --git a/package.json b/package.json index bc51f7a2131b..9b0a55d9ef16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bit-bin", - "version": "14.7.4", + "version": "14.7.5-dev.1", "license": "Apache-2.0", "main": "./dist/api.js", "preferGlobal": true, @@ -203,6 +203,7 @@ "@babel/preset-env": "^7.7.7", "@babel/preset-typescript": "^7.7.7", "@babel/register": "^7.4.4", + "@types/bluebird": "^3.5.29", "@types/chai": "^4.2.5", "@types/chai-arrays": "^1.0.3", "@types/chai-fs": "^2.0.2", diff --git a/src/api/consumer/lib/migrate.ts b/src/api/consumer/lib/migrate.ts index ad55b07b6b3f..883cc64cab07 100644 --- a/src/api/consumer/lib/migrate.ts +++ b/src/api/consumer/lib/migrate.ts @@ -15,12 +15,12 @@ export default (async function migrate( scopePath: string, verbose: boolean ): Promise { - logger.debug('migrate.migrate, starting migration process'); + logger.silly('migrate.migrate, starting migration process'); if (verbose) console.log('starting migration process'); // eslint-disable-line no-console let scope; // If a scope path provided we will run the migrate only for the scope if (scopePath) { - logger.debug(`migrate.migrate, running migration process for scope in path ${scopePath}`); + logger.silly(`migrate.migrate, running migration process for scope in path ${scopePath}`); if (verbose) console.log(`running migration process for scope in path ${scopePath}`); // eslint-disable-line no-console scope = await loadScope(scopePath); return scope.migrate(verbose); @@ -34,7 +34,7 @@ export default (async function migrate( await consumer.migrate(verbose); // const consumerMigrationResult = await consumer.migrate(verbose); // if (!consumerMigrationResult) - logger.debug('migrate.migrate, running migration process for scope in consumer'); + logger.silly('migrate.migrate, running migration process for scope in consumer'); if (verbose) console.log('running migration process for scope in consumer'); // eslint-disable-line no-console return scope.migrate(verbose); }); diff --git a/src/app.ts b/src/app.ts index 32db4cc86f14..1e997d25fd5d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,5 @@ import 'reflect-metadata'; -import * as BPromise from 'bluebird'; +import Bluebird from 'bluebird'; import { Harmony } from './harmony'; import HooksManager from './hooks'; import { BitCliExt } from './extensions/cli'; @@ -12,7 +12,7 @@ process.env.MEMFS_DONT_WARN = 'true'; // suppress fs experimental warnings from // removing this, default to longStackTraces also when env is `development`, which impacts the // performance dramatically. (see http://bluebirdjs.com/docs/api/promise.longstacktraces.html) -BPromise.config({ +Bluebird.config({ longStackTraces: true }); diff --git a/src/bit-id/README.md b/src/bit-id/README.md index a5a90d59b694..9339aff92557 100644 --- a/src/bit-id/README.md +++ b/src/bit-id/README.md @@ -9,7 +9,8 @@ That's the preferable representation of BitId. Wherever possible, use this forma A string representation of BitId. `BitId.toString()` generates this string. For example: `my-scope/my-name@0.0.1`. When an ID is entered by the end user it's always a string. -The problem with the string representation is that since the dynamic-namespace introduced, it's not clear from the string whether an ID has a scope or not. In the previous example, `my-scope/my-name` could be interpreted as a scopereadonly name or only a name. +The problem with the string representation is that since the dynamic-namespace introduced, it's not clear from the string whether an ID has a scope or not. +In the previous example, `my-scope/my-name` could be interpreted as an id with scope (`{ scope: 'my-scope', name: 'my-name' }`) or an id without a scope (`{ scope: null, name: 'my-scope/my-name' }`). See the next section how to safely parse the string. ## How to transform a Bit ID string to a BitId instance diff --git a/src/constants.ts b/src/constants.ts index 17d96cb75562..f60c4d6e1c5f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -208,6 +208,8 @@ export const CFG_GIT_EXECUTABLE_PATH = 'git_path'; export const CFG_LOG_JSON_FORMAT = 'log_json_format'; +export const CFG_LOG_LEVEL = 'log_level'; + export const CFG_NO_WARNINGS = 'no_warnings'; export const CFG_INTERACTIVE = 'interactive'; @@ -403,3 +405,5 @@ export const DEPENDENCIES_FIELDS = ['dependencies', 'devDependencies', 'peerDepe const MISSING_DEPS_SPACE_COUNT = 10; export const MISSING_DEPS_SPACE = ' '.repeat(MISSING_DEPS_SPACE_COUNT); export const MISSING_NESTED_DEPS_SPACE = ' '.repeat(MISSING_DEPS_SPACE_COUNT + 2); + +export const CONCURRENT_IO_LIMIT = 100; // limit number of files to read/write/delete/symlink at the same time diff --git a/src/consumer/component/package-json-vinyl.ts b/src/consumer/component/package-json-vinyl.ts index f4f8319a6ac7..e8764b309056 100644 --- a/src/consumer/component/package-json-vinyl.ts +++ b/src/consumer/component/package-json-vinyl.ts @@ -30,7 +30,6 @@ export default class PackageJsonVinyl extends AbstractVinyl { // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! logger.debug(`package-json-vinyl.write, path ${this.path}`); // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! - // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! await fs.outputFile(this.path, this.contents); // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! return this.path; diff --git a/src/consumer/component/sources/data-to-persist.ts b/src/consumer/component/sources/data-to-persist.ts index c329351cf47d..688bc5d41975 100644 --- a/src/consumer/component/sources/data-to-persist.ts +++ b/src/consumer/component/sources/data-to-persist.ts @@ -1,4 +1,5 @@ import * as path from 'path'; +import Bluebird from 'bluebird'; import fs from 'fs-extra'; import { ComponentCapsule } from '../../../extensions/capsule-ext'; import AbstractVinyl from './abstract-vinyl'; @@ -6,6 +7,7 @@ import Symlink from '../../../links/symlink'; import logger from '../../../logger/logger'; import RemovePath from './remove-path'; import removeFilesAndEmptyDirsRecursively from '../../../utils/fs/remove-files-and-empty-dirs-recursively'; +import { CONCURRENT_IO_LIMIT as concurrency } from '../../../constants'; export default class DataToPersist { files: AbstractVinyl[]; @@ -161,10 +163,10 @@ export default class DataToPersist { return dataToPersist; } async _persistFilesToFS() { - return Promise.all(this.files.map(file => file.write())); + return Bluebird.map(this.files, file => file.write(), { concurrency }); } async _persistSymlinksToFS() { - return Promise.all(this.symlinks.map(symlink => symlink.write())); + return Bluebird.map(this.symlinks, symlink => symlink.write(), { concurrency }); } async _deletePathsFromFS() { const pathWithRemoveItsDirIfEmptyEnabled = this.remove.filter(p => p.removeItsDirIfEmpty).map(p => p.path); @@ -172,7 +174,7 @@ export default class DataToPersist { if (pathWithRemoveItsDirIfEmptyEnabled.length) { await removeFilesAndEmptyDirsRecursively(pathWithRemoveItsDirIfEmptyEnabled); } - return Promise.all(restPaths.map(removePath => removePath.persistToFS())); + return Bluebird.map(restPaths, removePath => removePath.persistToFS(), { concurrency }); } _validateAbsolute() { // it's important to make sure that all paths are absolute before writing them to the diff --git a/src/consumer/consumer.ts b/src/consumer/consumer.ts index 4a6e9f0e6d6d..07634f0ca431 100644 --- a/src/consumer/consumer.ts +++ b/src/consumer/consumer.ts @@ -72,6 +72,7 @@ import { AutoTagResult } from '../scope/component-ops/auto-tag'; import ShowDoctorError from '../error/show-doctor-error'; import { EnvType } from '../legacy-extensions/env-extension-types'; import loadFlattenedDependenciesForCapsule from './component-ops/load-flattened-dependencies'; +import { packageNameToComponentId } from '../utils/bit/package-name-to-component-id'; type ConsumerProps = { projectPath: string; @@ -240,7 +241,7 @@ export default class Consumer { const bitmapVersion = this.bitMap.version || '0.10.9'; if (semver.gte(bitmapVersion, BIT_VERSION)) { - logger.debug('bit.map version is up to date'); + logger.silly('bit.map version is up to date'); return { run: false }; @@ -711,34 +712,19 @@ export default class Consumer { getComponentIdFromNodeModulesPath(requirePath: string, bindingPrefix: string): BitId { requirePath = pathNormalizeToLinux(requirePath); - // Temp fix to support old components before the migration has been running - bindingPrefix = bindingPrefix === 'bit' ? '@bit' : bindingPrefix; - const prefix = requirePath.includes('node_modules') ? `node_modules/${bindingPrefix}/` : `${bindingPrefix}/`; - const withoutPrefix = requirePath.substr(requirePath.indexOf(prefix) + prefix.length); - const componentName = withoutPrefix.includes('/') - ? withoutPrefix.substr(0, withoutPrefix.indexOf('/')) // the part after the first slash is the path inside the package - : withoutPrefix; - const pathSplit = componentName.split(NODE_PATH_COMPONENT_SEPARATOR); - if (pathSplit.length < 2) throw new GeneralError(`component has an invalid require statement: ${requirePath}`); - // since the dynamic namespaces feature introduced, the require statement doesn't have a fixed - // number of separators. - // also, a scope name may or may not include a dot. depends whether it's on bitHub or self hosted. - // we must check against BitMap to get the correct scope and name of the id. - if (pathSplit.length === 2) { - return new BitId({ scope: pathSplit[0], name: pathSplit[1] }); + const prefix = requirePath.includes('node_modules') ? 'node_modules/' : ''; + const withoutPrefix = prefix ? requirePath.substr(requirePath.indexOf(prefix) + prefix.length) : requirePath; + + if (!withoutPrefix.includes('/')) { + throw new GeneralError( + 'getComponentIdFromNodeModulesPath expects the path to have at least one slash for the scoped package, such as @bit/' + ); } - const mightBeScope = R.head(pathSplit); - const mightBeName = R.tail(pathSplit).join('/'); - const mightBeId = new BitId({ scope: mightBeScope, name: mightBeName }); - const allBitIds = this.bitMap.getAllBitIds(); - if (allBitIds.searchWithoutVersion(mightBeId)) return mightBeId; - // only bit hub has the concept of having the username in the scope name. - if (bindingPrefix !== 'bit' && bindingPrefix !== '@bit') return mightBeId; - // pathSplit has 3 or more items. the first two are the scope, the rest is the name. - // for example "user.scopeName.utils.is-string" => scope: user.scopeName, name: utils/is-string - const scope = pathSplit.splice(0, 2).join('.'); - const name = pathSplit.join('/'); - return new BitId({ scope, name }); + const packageSplitBySlash = withoutPrefix.split('/'); + // the part after the second slash is the path inside the package, just ignore it. + // (e.g. @bit/my-scope.my-name/internal-path.js). + const packageName = `${packageSplitBySlash[0]}/${packageSplitBySlash[1]}`; + return packageNameToComponentId(this, packageName, bindingPrefix); } composeRelativeComponentPath(bitId: BitId): string { diff --git a/src/e2e-helper/e2e-bit-json-helper.ts b/src/e2e-helper/e2e-bit-json-helper.ts index f554c7596021..2957e018fc98 100644 --- a/src/e2e-helper/e2e-bit-json-helper.ts +++ b/src/e2e-helper/e2e-bit-json-helper.ts @@ -27,6 +27,9 @@ export default class BitJsonHelper { bitJson.overrides = overrides; this.write(bitJson); } + addDefaultScope(scope = this.scopes.remote) { + this.addKeyVal(undefined, 'defaultScope', scope); + } getEnvByType(bitJson: Record, envType: 'compiler' | 'tester') { const basePath = ['env', envType]; const env = R.path(basePath, bitJson); diff --git a/src/e2e-helper/e2e-fixtures-helper.ts b/src/e2e-helper/e2e-fixtures-helper.ts index b33558684092..403acc6e6d9e 100644 --- a/src/e2e-helper/e2e-fixtures-helper.ts +++ b/src/e2e-helper/e2e-fixtures-helper.ts @@ -89,6 +89,15 @@ export default class FixtureHelper { this.addComponentBarFoo(); } + populateWorkspaceWithThreeComponentsAndModulePath() { + this.fs.createFile('utils', 'is-type.js', fixtures.isType); + this.addComponentUtilsIsType(); + this.fs.createFile('utils', 'is-string.js', fixtures.isStringModulePath(this.scopes.remote)); + this.addComponentUtilsIsString(); + this.createComponentBarFoo(fixtures.barFooModulePath(this.scopes.remote)); + this.addComponentBarFoo(); + } + /** * @deprecated use populateWorkspaceWithThreeComponents() */ diff --git a/src/jsdoc/jsdoc/jsdoc-parser.ts b/src/jsdoc/jsdoc/jsdoc-parser.ts index 60ebe8c5a163..1cc21ce1cd8c 100644 --- a/src/jsdoc/jsdoc/jsdoc-parser.ts +++ b/src/jsdoc/jsdoc/jsdoc-parser.ts @@ -25,7 +25,7 @@ export default async function parse(data: string, filePath?: PathOsBased): Promi docs.forEach(doc => extractDataRegex(doc, doclets, filePath)); } catch (e) { // never mind, ignore the doc of this source - logger.debug(`failed parsing docs using on path ${filePath} with error`, e); + logger.silly(`failed parsing docs using on path ${filePath} with error`, e); } // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! return doclets.filter(doclet => doclet.access === 'public'); diff --git a/src/jsdoc/react/react-parser.ts b/src/jsdoc/react/react-parser.ts index 176dbc103ec4..64187d1647bb 100644 --- a/src/jsdoc/react/react-parser.ts +++ b/src/jsdoc/react/react-parser.ts @@ -110,7 +110,7 @@ export default async function parse(data: string, filePath?: PathOsBased): Promi return formatted; } } catch (err) { - logger.debug(`failed parsing docs using docgen on path ${filePath} with error`, err); + logger.silly(`failed parsing docs using docgen on path ${filePath} with error`, err); } return undefined; } diff --git a/src/legacy-extensions/env-extension.ts b/src/legacy-extensions/env-extension.ts index d0685c9e1078..f8b3cd40bee2 100644 --- a/src/legacy-extensions/env-extension.ts +++ b/src/legacy-extensions/env-extension.ts @@ -98,8 +98,7 @@ export default class EnvExtension extends BaseExtension { opts: { verbose: boolean; dontPrintEnvMsg?: boolean }, context?: Record ): Promise { - Analytics.addBreadCrumb('env-extension', 'install env extension'); - logger.debug('env-extension - install env extension'); + logger.debugAndAddBreadCrumb('env-extension', 'install env extension'); // Skip the installation in case of using specific file // options.file usually used for develop your extension @@ -143,7 +142,7 @@ export default class EnvExtension extends BaseExtension { */ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! toBitJsonObject(ejectedEnvDirectory: string): { [key: string]: EnvExtensionObject } { - logger.debug('env-extension, toBitJsonObject'); + logger.silly('env-extension, toBitJsonObject'); const files = {}; this.files.forEach(file => { // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! @@ -227,7 +226,7 @@ export default class EnvExtension extends BaseExtension { } async reload(scopePath: string, context?: Record): Promise { - logger.debug('env-extension, reload'); + logger.silly('env-extension, reload'); if (context) { this.context = context; } @@ -331,7 +330,7 @@ export default class EnvExtension extends BaseExtension { static async loadFromSerializedModelObject( modelObject: EnvExtensionSerializedModel & { envType: EnvType } ): Promise { - logger.debug('env-extension, loadFromModelObject'); + logger.silly('env-extension, loadFromModelObject'); const baseExtensionProps: BaseExtensionProps = super.loadFromModelObject(modelObject); let files = []; if (modelObject.files && !R.isEmpty(modelObject.files)) { @@ -373,7 +372,7 @@ export default class EnvExtension extends BaseExtension { envType: EnvType; context?: Record; }): Promise { - logger.debug(`env-extension (${envType}) loadFromCorrectSource`); + logger.silly(`env-extension (${envType}) loadFromCorrectSource`); const isAuthor = componentOrigin === COMPONENT_ORIGINS.AUTHORED; const componentHasWrittenConfig = componentConfig && componentConfig.componentHasWrittenConfig; // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! @@ -390,12 +389,12 @@ export default class EnvExtension extends BaseExtension { // $FlowFixMe we made sure before that componentConfig is defined // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! const configPath = path.dirname(componentConfig.path); - logger.debug(`env-extension loading ${envType} from component config`); + logger.silly(`env-extension loading ${envType} from component config`); return loadFromConfig({ envConfig, envType, consumerPath, scopePath, configPath, context }); } if (!componentHasWrittenConfig && !isAuthor && componentFromModel && componentFromModel[envType]) { // config was not written into component dir, load the config from the model - logger.debug(`env-extension, loading ${envType} from the model`); + logger.silly(`env-extension, loading ${envType} from the model`); return componentFromModel[envType]; } if (overridesFromConsumer && overridesFromConsumer.env && overridesFromConsumer.env[envType]) { @@ -403,12 +402,12 @@ export default class EnvExtension extends BaseExtension { logger.debug(`env-extension, ${envType} was manually removed from the consumer config overrides`); return null; } - logger.debug(`env-extension, loading ${envType} from the consumer config overrides`); + logger.silly(`env-extension, loading ${envType} from the consumer config overrides`); const envConfig = { [envType]: AbstractConfig.transformEnvToObject(overridesFromConsumer.env[envType]) }; return loadFromConfig({ envConfig, envType, consumerPath, scopePath, configPath: consumerPath, context }); } if (isAuthor && workspaceConfig[envType]) { - logger.debug(`env-extension, loading ${envType} from the consumer config`); + logger.silly(`env-extension, loading ${envType} from the consumer config`); const envConfig = { [envType]: workspaceConfig[envType] }; return loadFromConfig({ envConfig, envType, consumerPath, scopePath, configPath: consumerPath, context }); } diff --git a/src/legacy-extensions/env-factory.ts b/src/legacy-extensions/env-factory.ts index 41dfdf8423b0..801fc8e63f8d 100644 --- a/src/legacy-extensions/env-factory.ts +++ b/src/legacy-extensions/env-factory.ts @@ -9,7 +9,7 @@ import { BaseExtensionModel } from './base-extension'; import Repository from '../scope/objects/repository'; export default (async function makeEnv(envType: EnvType, props: EnvLoadArgsProps): Promise { - logger.debug(`env-factory, create ${envType}`); + logger.silly(`env-factory, create ${envType}`); props.envType = envType; props.throws = true; const envExtensionProps: EnvExtensionProps = await EnvExtension.load(props); @@ -26,7 +26,7 @@ export async function makeEnvFromModel( modelObject: string | BaseExtensionModel, repository: Repository ): Promise { - logger.debug(`env-factory, create ${envType} from model`); + logger.silly(`env-factory, create ${envType} from model`); if (!modelObject) return undefined; const actualObject = typeof modelObject === 'string' diff --git a/src/logger/logger.ts b/src/logger/logger.ts index 4a4cac6b1b34..82be2164c81f 100644 --- a/src/logger/logger.ts +++ b/src/logger/logger.ts @@ -5,7 +5,7 @@ import format from 'string-format'; // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! import winston, { Logger } from 'winston'; import * as path from 'path'; -import { GLOBAL_LOGS, DEBUG_LOG, CFG_LOG_JSON_FORMAT, CFG_NO_WARNINGS } from '../constants'; +import { GLOBAL_LOGS, DEBUG_LOG, CFG_LOG_JSON_FORMAT, CFG_NO_WARNINGS, CFG_LOG_LEVEL } from '../constants'; import { Analytics } from '../analytics/analytics'; import { getSync } from '../api/consumer/lib/global-config'; @@ -15,10 +15,12 @@ const extensionsLoggers = new Map(); const jsonFormat = yn(getSync(CFG_LOG_JSON_FORMAT), { default: false }); +const logLevel = getSync(CFG_LOG_LEVEL) || 'debug'; + export const baseFileTransportOpts = { filename: DEBUG_LOG, format: jsonFormat ? winston.format.combine(winston.format.timestamp(), winston.format.json()) : getFormat(), - level: 'debug', + level: logLevel, maxsize: 10 * 1024 * 1024, // 10MB maxFiles: 10, // If true, log files will be rolled based on maxsize and maxfiles, but in ascending order. @@ -60,6 +62,15 @@ class BitLogger { constructor(logger: Logger) { this.logger = logger; + logger.on('error', err => { + // eslint-disable-next-line no-console + console.log('got an error from the logger', err); + }); + } + + silly(...args: any[]) { + // @ts-ignore + this.logger.silly(...args); } debug(...args: any[]) { diff --git a/src/scope/component-ops/export-scope-components.ts b/src/scope/component-ops/export-scope-components.ts index 3b0f2cdc1821..86349b2b7c2c 100644 --- a/src/scope/component-ops/export-scope-components.ts +++ b/src/scope/component-ops/export-scope-components.ts @@ -81,9 +81,15 @@ export async function exportMany({ } const remotes: Remotes = await getScopeRemotes(scope); if (remoteName) { + logger.debugAndAddBreadCrumb('export-scope-components', 'export all ids to one remote'); return exportIntoRemote(remoteName, ids); } + logger.debugAndAddBreadCrumb('export-scope-components', 'export ids to multiple remotes'); const groupedByScope = await sortAndGroupByScope(); + const groupedByScopeString = groupedByScope + .map(item => `scope "${item.scopeName}": ${item.ids.toString()}`) + .join(', '); + logger.debug(`export-scope-components, export to the following scopes ${groupedByScopeString}`); const results = await pMapSeries(groupedByScope, result => exportIntoRemote(result.scopeName, result.ids)); return { exported: BitIds.uniqFromArray(R.flatten(results.map(r => r.exported))), @@ -189,6 +195,15 @@ export async function exportMany({ * 3) it's possible to have circle dependencies inside the same scope, and non-circle * dependencies between the different scopes. in this case, the toposort should be done after * removing the ids participated in the circular. + * + * once the sort is done, it returns an array of { scopeName: string; ids: BitIds }. + * keep in mind that this array might have multiple items with the same scopeName, that's totally + * valid and it will cause multiple round-trip to the same scope. there is no other way around + * it. + * the sort is done after eliminating circles, so it's possible to execute topsort. once the + * components are topological sorted, they are added one by one to the results array. If the last + * item in the array has the same scope as the currently inserted component, it can be added to + * the same scope group. otherwise, a new item needs to be added to the array with the new scope. */ async function sortAndGroupByScope(): Promise<{ scopeName: string; ids: BitIds }[]> { const grouped = ids.toGroupByScopeName(idsWithFutureScope); @@ -209,8 +224,13 @@ export async function exportMany({ return; } } - const idWithFutureScope = idsWithFutureScope.searchWithoutScopeAndVersion(id) as BitId; - groupedArraySorted.push({ scopeName: idWithFutureScope.scope as string, ids: new BitIds(id) }); + const idWithFutureScope = idsWithFutureScope.searchWithoutScopeAndVersion(id); + if (idWithFutureScope) { + groupedArraySorted.push({ scopeName: idWithFutureScope.scope as string, ids: new BitIds(id) }); + } + // otherwise, it's in the graph, but not in the idWithFutureScope array. this is probably just a + // dependency of one of the pending-export ids, and that dependency is not supposed to be + // export, so just ignore it. }; if (cycles.length) { const cyclesWithMultipleScopes = cycles.filter(cycle => { diff --git a/src/scope/models/model-component.ts b/src/scope/models/model-component.ts index 3f08ba865352..d49dc51d2e35 100644 --- a/src/scope/models/model-component.ts +++ b/src/scope/models/model-component.ts @@ -457,7 +457,7 @@ export default class Component extends BitObject { } validateBeforePersisting(componentStr: string): void { - logger.debug(`validating component object: ${this.hash().hash} ${this.id()}`); + logger.silly(`validating component object: ${this.hash().hash} ${this.id()}`); const component = Component.parse(componentStr); component.validate(); } diff --git a/src/scope/models/version.ts b/src/scope/models/version.ts index 34fd95fbec68..80f9f13e4dd5 100644 --- a/src/scope/models/version.ts +++ b/src/scope/models/version.ts @@ -367,7 +367,7 @@ export default class Version extends BitObject { } validateBeforePersisting(versionStr: string): void { - logger.debug(`validating version object, hash: ${this.hash().hash}`); + logger.silly(`validating version object, hash: ${this.hash().hash}`); const version = Version.parse(versionStr); version.validate(); } diff --git a/src/scope/objects/repository.ts b/src/scope/objects/repository.ts index bb4a688a7f92..0edf4e172cdd 100644 --- a/src/scope/objects/repository.ts +++ b/src/scope/objects/repository.ts @@ -103,7 +103,7 @@ export default class Repository { }) .catch(err => { if (err.code === 'ENOENT') { - logger.debug(`Failed finding a ref file ${this.objectPath(ref)}.`); + logger.silly(`Failed finding a ref file ${this.objectPath(ref)}.`); } else { logger.error(`Failed reading a ref file ${this.objectPath(ref)}. Error: ${err.message}`); } @@ -329,7 +329,7 @@ export default class Repository { _deleteOne(ref: Ref): Promise { this.removeFromCache(ref); const pathToDelete = this.objectPath(ref); - logger.debug(`repository._deleteOne: deleting ${pathToDelete}`); + logger.silly(`repository._deleteOne: deleting ${pathToDelete}`); return removeFile(pathToDelete, true); } @@ -344,7 +344,7 @@ export default class Repository { // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! if (this.scopeJson.groupName) options.gid = await resolveGroupId(this.scopeJson.groupName); const objectPath = this.objectPath(object.hash()); - logger.debug(`repository._writeOne: ${objectPath}`); + logger.silly(`repository._writeOne: ${objectPath}`); // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX! return writeFile(objectPath, contents, options); } diff --git a/src/scope/scope.ts b/src/scope/scope.ts index 33d82cbc4385..bdbafa9f7c2d 100644 --- a/src/scope/scope.ts +++ b/src/scope/scope.ts @@ -181,14 +181,14 @@ export default class Scope { * @memberof Consumer */ async migrate(verbose: boolean): Promise { - logger.debug('scope.migrate, running migration process for scope'); + logger.silly('scope.migrate, running migration process for scope'); if (verbose) console.log('running migration process for scope'); // eslint-disable-line // We start to use this process after version 0.10.9, so we assume the scope is in the last production version const scopeVersion = this.scopeJson.get('version') || '0.10.9'; if (semver.gte(scopeVersion, BIT_VERSION)) { const upToDateMsg = 'scope version is up to date'; if (verbose) console.log(upToDateMsg); // eslint-disable-line - logger.debug(`scope.migrate, ${upToDateMsg}`); + logger.silly(`scope.migrate, ${upToDateMsg}`); return { run: false }; diff --git a/src/utils/bit/package-name-to-component-id.spec.ts b/src/utils/bit/package-name-to-component-id.spec.ts new file mode 100644 index 000000000000..934e2c2d7c6c --- /dev/null +++ b/src/utils/bit/package-name-to-component-id.spec.ts @@ -0,0 +1,83 @@ +import { expect } from 'chai'; +import { packageNameToComponentId } from './package-name-to-component-id'; +import { Consumer } from '../../consumer'; +import { BitIds, BitId } from '../../bit-id'; + +describe('packageNameToComponentId', function() { + this.timeout(0); + let consumer: Consumer; + before(() => { + // @ts-ignore + consumer = new Consumer({ projectPath: '', config: {} }); + }); + it('should parse the path correctly when a component is not in bitMap and has one dot', () => { + const result = packageNameToComponentId(consumer, '@bit/remote.comp', '@bit'); + expect(result.scope).to.equal('remote'); + expect(result.name).to.equal('comp'); + }); + it('should parse the path correctly when a component is not in bitMap and has two dots', () => { + const result = packageNameToComponentId(consumer, '@bit/remote.comp.comp2', '@bit'); + expect(result.scope).to.equal('remote.comp'); + expect(result.name).to.equal('comp2'); + }); + it('should parse the path correctly when a component is not in bitMap and has three dots', () => { + const result = packageNameToComponentId(consumer, '@bit/remote.comp.comp2.comp3', '@bit'); + expect(result.scope).to.equal('remote.comp'); + expect(result.name).to.equal('comp2/comp3'); + }); + describe('with defaultScope', () => { + describe('when the defaultScope has dot', () => { + it('should return bitId without scope when the component is in .bitmap without scope', () => { + // @ts-ignore + consumer.bitMap = { getAllBitIds: () => new BitIds(new BitId({ name: 'bar/foo' })) }; + consumer.config.defaultScope = 'bit.utils'; + const result = packageNameToComponentId(consumer, '@bit/bit.utils.bar.foo', '@bit'); + expect(result.scope).to.be.null; + expect(result.name).to.equal('bar/foo'); + }); + it('should return bitId with scope when the component is in .bitmap with scope', () => { + // @ts-ignore + consumer.bitMap = { getAllBitIds: () => new BitIds(new BitId({ scope: 'bit.utils', name: 'bar/foo' })) }; + consumer.config.defaultScope = 'bit.utils'; + const result = packageNameToComponentId(consumer, '@bit/bit.utils.bar.foo', '@bit'); + expect(result.scope).to.equal('bit.utils'); + expect(result.name).to.equal('bar/foo'); + }); + it('should return bitId with scope when the component is not .bitmap at all', () => { + // @ts-ignore + consumer.bitMap = { getAllBitIds: () => new BitIds() }; + consumer.config.defaultScope = 'bit.utils'; + const result = packageNameToComponentId(consumer, '@bit/bit.utils.bar.foo', '@bit'); + expect(result.scope).to.equal('bit.utils'); + expect(result.name).to.equal('bar/foo'); + }); + }); + describe('when the defaultScope does not have dot', () => { + before(() => { + consumer.config.defaultScope = 'utils'; + }); + it('should return bitId without scope when the component is in .bitmap without scope', () => { + // @ts-ignore + consumer.bitMap = { getAllBitIds: () => new BitIds(new BitId({ name: 'bar/foo' })) }; + const result = packageNameToComponentId(consumer, '@bit/utils.bar.foo', '@bit'); + expect(result.scope).to.be.null; + expect(result.name).to.equal('bar/foo'); + }); + it('should return bitId with scope when the component is in .bitmap with scope', () => { + // @ts-ignore + consumer.bitMap = { getAllBitIds: () => new BitIds(new BitId({ scope: 'utils', name: 'bar/foo' })) }; + const result = packageNameToComponentId(consumer, '@bit/utils.bar.foo', '@bit'); + expect(result.scope).to.equal('utils'); + expect(result.name).to.equal('bar/foo'); + }); + it('should return bitId with scope when the component is not .bitmap at all', () => { + // @ts-ignore + consumer.bitMap = { getAllBitIds: () => new BitIds() }; + const result = packageNameToComponentId(consumer, '@bit/utils.bar.foo', '@bit'); + // looks weird, but the default is a dot in the scope. + expect(result.scope).to.equal('utils.bar'); + expect(result.name).to.equal('foo'); + }); + }); + }); +}); diff --git a/src/utils/bit/package-name-to-component-id.ts b/src/utils/bit/package-name-to-component-id.ts new file mode 100644 index 000000000000..9d01378cd74a --- /dev/null +++ b/src/utils/bit/package-name-to-component-id.ts @@ -0,0 +1,68 @@ +import R from 'ramda'; +import { Consumer } from '../../consumer'; +import { BitId } from '../../bit-id'; +import { NODE_PATH_COMPONENT_SEPARATOR } from '../../constants'; +import GeneralError from '../../error/general-error'; + +/** + * convert a component package-name to BitId. + * e.g. `@bit/bit.utils/is-string` => { scope: bit.utils, name: is-string } + */ +// eslint-disable-next-line import/prefer-default-export +export function packageNameToComponentId(consumer: Consumer, packageName: string, bindingPrefix: string): BitId { + // Temp fix to support old components before the migration has been running + const prefix = bindingPrefix === 'bit' ? '@bit/' : `${bindingPrefix}/`; + const componentName = packageName.substr(packageName.indexOf(prefix) + prefix.length); + + const nameSplit = componentName.split(NODE_PATH_COMPONENT_SEPARATOR); + if (nameSplit.length < 2) + throw new GeneralError( + `package-name is an invalid BitId: ${componentName}, it is missing the scope-name, please set your workspace with a defaultScope` + ); + // since the dynamic namespaces feature introduced, the require statement doesn't have a fixed + // number of separators. + // also, a scope name may or may not include a dot. depends whether it's on bitHub or self hosted. + // we must check against BitMap to get the correct scope and name of the id. + if (nameSplit.length === 2) { + return new BitId({ scope: nameSplit[0], name: nameSplit[1] }); + } + const defaultScope = consumer.config.defaultScope; + const allBitIds = consumer.bitMap.getAllBitIds(); + + if (defaultScope && componentName.startsWith(`${defaultScope}.`)) { + const idWithDefaultScope = byDefaultScope(defaultScope, nameSplit); + const bitmapHasExact = allBitIds.hasWithoutVersion(idWithDefaultScope); + if (bitmapHasExact) return idWithDefaultScope; + const idWithoutScope = allBitIds.searchWithoutScopeAndVersion(idWithDefaultScope.changeScope(null)); + if (idWithoutScope) return idWithoutScope; + // otherwise, the component is not in .bitmap, continue with other strategies. + } + const mightBeId = createBitIdAssumeScopeDoesNotHaveDot(nameSplit); + if (allBitIds.searchWithoutVersion(mightBeId)) return mightBeId; + // only bit hub has the concept of having the username in the scope name. + if (bindingPrefix !== 'bit' && bindingPrefix !== '@bit') return mightBeId; + // pathSplit has 3 or more items. the first two are the scope, the rest is the name. + // for example "user.scopeName.utils.is-string" => scope: user.scopeName, name: utils/is-string + return createBitIdAssumeScopeHasDot(nameSplit); +} + +// scopes on bit.dev always have dot in the name +function createBitIdAssumeScopeHasDot(nameSplit: string[]): BitId { + const nameSplitClone = [...nameSplit]; + const scope = nameSplitClone.splice(0, 2).join('.'); + const name = nameSplitClone.join('/'); + return new BitId({ scope, name }); +} + +// local scopes (self-hosted) can not have any dot in the name +function createBitIdAssumeScopeDoesNotHaveDot(nameSplit: string[]): BitId { + const scope = R.head(nameSplit); + const name = R.tail(nameSplit).join('/'); + return new BitId({ scope, name }); +} + +function byDefaultScope(defaultScope: string, nameSplit: string[]): BitId { + return defaultScope.includes('.') + ? createBitIdAssumeScopeHasDot(nameSplit) + : createBitIdAssumeScopeDoesNotHaveDot(nameSplit); +}