diff --git a/e2e/commands/link.e2e.1.ts b/e2e/commands/link.e2e.1.ts index 1d3b5fa6f2d..ee19f3254bc 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 00000000000..2e91e6170a5 --- /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/src/consumer/consumer.ts b/src/consumer/consumer.ts index 7e7fd688304..488720e9e67 100644 --- a/src/consumer/consumer.ts +++ b/src/consumer/consumer.ts @@ -71,6 +71,7 @@ import ComponentsPendingImport from './component-ops/exceptions/components-pendi import { AutoTagResult } from '../scope/component-ops/auto-tag'; import ShowDoctorError from '../error/show-doctor-error'; import { EnvType } from '../extensions/env-extension-types'; +import { packageNameToComponentId } from '../utils/bit/package-name-to-component-id'; type ConsumerProps = { projectPath: string; @@ -699,34 +700,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 f554c759602..2957e018fc9 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 b3355868409..403acc6e6d9 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/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 00000000000..934e2c2d7c6 --- /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 00000000000..9d01378cd74 --- /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); +}