Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move snap/hash generation to component-version #8716

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
7 changes: 7 additions & 0 deletions .bitmap
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,13 @@
"mainFile": "index.ts",
"rootDir": "scopes/component/component-tree"
},
"component-version": {
"name": "component-version",
"scope": "teambit.component",
"version": "1.0.3",
"mainFile": "index.ts",
"rootDir": "components/component-version"
},
"component-writer": {
"name": "component-writer",
"scope": "teambit.component",
Expand Down
3 changes: 3 additions & 0 deletions components/component-version/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import InvalidVersion from './invalid-version';

export { InvalidVersion };
7 changes: 7 additions & 0 deletions components/component-version/exceptions/invalid-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BitError } from '@teambit/bit-error';

export default class InvalidVersion extends BitError {
constructor(version?: string | null) {
super(`error: version ${version || '(empty)'} is not a valid semantic version. learn more: https://semver.org`);
}
}
23 changes: 23 additions & 0 deletions components/component-version/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Version, LATEST_VERSION } from './version';
import versionParser, {
isHash,
isSnap,
isTag,
HASH_SIZE,
SHORT_HASH_MINIMUM_SIZE,
generateSnap,
} from './version-parser';
import { InvalidVersion } from './exceptions';

export {
Version,
LATEST_VERSION,
versionParser,
isHash,
isSnap,
isTag,
generateSnap,
InvalidVersion,
HASH_SIZE,
SHORT_HASH_MINIMUM_SIZE,
};
29 changes: 29 additions & 0 deletions components/component-version/version-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect } from 'chai';

import versionParser from './version-parser';
import { InvalidVersion } from './exceptions';

describe('versionParser()', () => {
it('should return latest version representation', () => {
const version = versionParser('latest');
expect(version.latest).to.equal(true);
expect(version.versionNum).to.equal(null);
});

it('should throw invalid version', () => {
const version = () => versionParser('$1');
expect(version).to.throw(InvalidVersion);
});

it('should return a concrete version', () => {
const version = versionParser('0.0.1');
expect(version.latest).to.equal(false);
expect(version.versionNum).to.equal('0.0.1');
});

it('should parse given version as latest', () => {
const version = versionParser('latest');
expect(version.versionNum).to.equal(null);
expect(version.latest).to.equal(true);
});
});
79 changes: 79 additions & 0 deletions components/component-version/version-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import semver from 'semver';
import crypto from 'crypto';
import { InvalidVersion } from './exceptions';
import { Version, LATEST_VERSION } from './version';

export const HASH_SIZE = 40;

/**
* because the directory structure is `XX/YY....`, it needs to have at least three characters.
*/
export const SHORT_HASH_MINIMUM_SIZE = 3;

function isLatest(versionStr: string): boolean {
return versionStr === LATEST_VERSION;
}

function isSemverValid(versionStr: string) {
return Boolean(semver.valid(versionStr));
}

function returnSemver(versionStr: string): Version {
return new Version(versionStr, false);
}

function returnLatest(): Version {
return new Version(null, true);
}

function returnSnap(hash: string): Version {
return new Version(hash, false);
}

/**
* a snap is a 40 characters hash encoded in HEX. so it can be a-f and 0-9.
* also, for convenience, a short-hash can be used, which is a minimum of 3 characters.
*/
export function isHash(str: string | null | undefined): boolean {
return typeof str === 'string' && isHex(str) && str.length >= SHORT_HASH_MINIMUM_SIZE && str.length <= HASH_SIZE;
}

/**
* a component version can be a tag (semver) or a snap (hash)
*/
export function isTag(str?: string): boolean {
return typeof str === 'string' && isSemverValid(str);
}

/**
* a component version can be a tag (semver) or a snap (hash).
* a snap must be 40 characters long and consist of a-f and 0-9. (hex).
*/
export function isSnap(str: string | null | undefined): boolean {
return isHash(str) && typeof str === 'string' && str.length === HASH_SIZE;
}

/**
* generate a random valid snap hash (which is a 40 characters long hash encoded in HEX)
*/
export function generateSnap(): string {
// @ts-ignore until @types/node is updated to v18 or above
return crypto.createHash('sha1').update(crypto.randomUUID()).digest('hex');
}

export default function versionParser(versionStr: string | null | undefined): Version {
if (!versionStr) return returnLatest();
if (isLatest(versionStr)) return returnLatest();
if (isSemverValid(versionStr)) return returnSemver(versionStr);
if (isHash(versionStr)) return returnSnap(versionStr);

throw new InvalidVersion(versionStr.toString());
}

/**
* check if the string consists of valid hexadecimal characters
*/
function isHex(str: string) {
const hexRegex = /^[0-9a-fA-F]+$/;
return hexRegex.test(str);
}
25 changes: 25 additions & 0 deletions components/component-version/version.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect } from 'chai';

import { Version } from './version';

describe('Version', () => {
describe('toString()', () => {
it('should return latest', () => {
const version = new Version(null, true);
expect(version.toString()).to.equal('latest');
});

it('should return concrete version number', () => {
// @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
const version = new Version(12, false);
expect(version.toString()).to.equal('12');
});

it('should throw an invalid version exception', () => {
const version = new Version(null, false);
expect(() => {
version.toString();
}).to.throw();
});
});
});
33 changes: 33 additions & 0 deletions components/component-version/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import semver from 'semver';
import { InvalidVersion } from './exceptions';

export const LATEST_VERSION = 'latest';

export class Version {
versionNum: string | null | undefined;
latest: boolean;

constructor(versionNum: string | null | undefined, latest: boolean) {
this.versionNum = versionNum;
this.latest = latest;
if (versionNum && latest) {
throw new Error(`a component version cannot have both: version and "latest"`);
}
}

toString() {
if (!this.versionNum && this.latest) return 'latest';
if (this.versionNum && !this.latest) return this.versionNum.toString();
throw new InvalidVersion(this.versionNum);
}

isLaterThan(otherVersion: Version): boolean {
if (!this.versionNum || this.versionNum === LATEST_VERSION) {
return true;
}
if (!otherVersion.versionNum || otherVersion.versionNum === LATEST_VERSION) {
return false;
}
return semver.gt(this.versionNum, otherVersion.versionNum);
}
}
4 changes: 1 addition & 3 deletions scopes/component/snapping/tag-model-component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import mapSeries from 'p-map-series';
import { isEmpty } from 'lodash';
import { ReleaseType } from 'semver';
import { v4 } from 'uuid';
import { BitError } from '@teambit/bit-error';
import { Scope } from '@teambit/legacy/dist/scope';
import { ComponentID, ComponentIdList } from '@teambit/component-id';
Expand All @@ -15,7 +14,6 @@ import { getBasicLog } from '@teambit/legacy/dist/utils/bit/basic-log';
import { Component } from '@teambit/component';
import deleteComponentsFiles from '@teambit/legacy/dist/consumer/component-ops/delete-component-files';
import logger from '@teambit/legacy/dist/logger/logger';
import { sha1 } from '@teambit/legacy/dist/utils';
import { AutoTagResult, getAutoTagInfo } from '@teambit/legacy/dist/scope/component-ops/auto-tag';
import { getValidVersionOrReleaseType } from '@teambit/legacy/dist/utils/semver-helper';
import { BuilderMain, OnTagOpts } from '@teambit/builder';
Expand Down Expand Up @@ -82,7 +80,7 @@ function updateDependenciesVersions(

function setHashes(componentsToTag: ConsumerComponent[]): void {
componentsToTag.forEach((componentToTag) => {
componentToTag.setNewVersion(sha1(v4()));
componentToTag.setNewVersion();
});
}

Expand Down
6 changes: 3 additions & 3 deletions src/consumer/component/consumer-component.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { ComponentID, ComponentIdList } from '@teambit/component-id';
import fs from 'fs-extra';
import { v4 } from 'uuid';
import * as path from 'path';
import R from 'ramda';
import { IssuesList } from '@teambit/component-issues';
import { BitId } from '@teambit/legacy-bit-id';
import { generateSnap } from '@teambit/component-version';
import { BitError } from '@teambit/bit-error';
import { getCloudDomain, BIT_WORKSPACE_TMP_DIRNAME, BuildStatus, DEFAULT_LANGUAGE, Extensions } from '../../constants';
import docsParser from '../../jsdoc/parser';
import { Doclet } from '../../jsdoc/types';
import logger from '../../logger/logger';
import { ScopeListItem } from '../../scope/models/model-component';
import Version, { DepEdge, Log } from '../../scope/models/version';
import { pathNormalizeToLinux, sha1 } from '../../utils';
import { pathNormalizeToLinux } from '../../utils';
import { PathLinux, PathOsBased, PathOsBasedRelative } from '../../utils/path';
import ComponentMap from '../bit-map/component-map';
import { IgnoredDirectory } from '../component-ops/add-components/exceptions/ignored-directory';
Expand Down Expand Up @@ -269,7 +269,7 @@ export default class Component {
this.peerDependencies = new Dependencies(peerDependencies);
}

setNewVersion(version = sha1(v4())) {
setNewVersion(version = generateSnap()) {
this.previouslyUsedVersion = this.id.hasVersion() ? this.version : undefined;
this.version = version;
}
Expand Down