-
-
Notifications
You must be signed in to change notification settings - Fork 93
Description
Context
The current version of semver (2.x) has some known limitations mainly because we designed it as an Nx executor.
Here are some examples:
- performance issues as we have to run the executor on each project when running in independent mode
- executor is hard to parallelize due to concurrent access to git
- grouping commits is harder
The other major issue is that development workflows and project structures can vary a lot between workspaces. Thus, grouping and versioning strategies can vary a lot. Providing multiple options to cover all different use cases increases the surface of semver and can even make it confusing or false feature-rich.
Goals
In order to fix the issues above, semver 3 will be designed with the following goals:
- semver should run once on the whole workspace as a standalone script instead of a project executor:
yarn semver
oryarn nx semver
. - semver should allow extension using custom strategy implementations (e.g.
semver.config.ts
) instead of options.
sequenceDiagram
Note over Core: 1. resolve strategy
Core->>Core: resolveStrategy(): Strategy
Note over Core: 2. build versionable tree based on nx dep graph
Core->>Core: getNxProjects()
Core->>Strategy: resolveVersionables(projects: NxProject[]): VersionableInfo[]
Note over Core,Strategy: resolve tag prefix for each versionable
Core->>Strategy: resolveTagPrefix(versionable: VersionableInfo): string
Note over Core: 3. resolve last version for each versionable
Core->>Core: resolveLastVersion(tagPrefix: string): Version
Note over Core: 4. resolve changes (commits + deps commits)
Core->>Core: resolveChanges(paths: string[], since: string): Changes
Note over Core: 5. Build versionable tree based on dep graph
Core->>Core: resolveDependencies(versionableInfos: VersionableInfo[]): VersionableInfo & {deps: VersionableInfo[]}
Note over Core: Group everything in Versionable object
Note over Core,Strategy: bump
Core->>Strategy: bump(...)
Note over Core,Strategy: update files
Core->>Strategy: updateFiles(...)
Note over Core: commit
Core->>Strategy: commit(...)
Note over Core: finalize
Core->>Strategy: finalize(...)
classDiagram
VersionableNode o-- VersionableNode
VersionableInfo <|-- Versionable
Versionable <|-- VersionableNode
note for NxProject "all these properties are used by the strategy\n to group projects into versionables"
class NxProject {
type: 'app' | 'lib';
name: string;
path: string;
tags: string[];
}
class VersionableInfo {
name: string;
paths: string[];
}
class Versionable {
changes: Change[];
dependencies: Versionable[];
tagPrefix: string;
version: Version;
}
Raw draft notes
strategy = resolveConfig();
semver.getProjects();
// [{name: 'a', path: 'apps/a'}, {name: 'a-ui', path: 'libs/a/ui', tags: ...}, {name: 'x', path: 'libs/x'}]
||
\/
strategy.resolveVersionables();
||
\/
class Versionable {
name: string;
paths: string[];
}
// [{name: 'a', paths: ['apps/a', 'libs/a/ui']}, {name: 'x', paths: ['libs/x']}]
||
\/
semver.buildGraph();
||
\/
class VersionableWithDeps {
name: string;
paths: string[];
deps: VersionableWithDeps[];
}
||
\/
strategy.resolveTagPrefix()
||
\/
class VersionableWithDeps+TagPrefix {
name: string;
paths: string[];
deps: VersionableWithDeps[];
tagPrefix: string;
}
||
\/
semver.resolveLastVersion()
||
\/
class {
name: string;
paths: string[];
deps: VersionableWithDeps[];
tagPrefix: string;
version: string;
}
||
\/
semver.computeChanges()
||
\/
class {
name: string;
paths: string[];
deps: Versionable...[];
tagPrefix: string;
version: string;
commits: Commit[];
}
Resolve groups
interface Versionable {
name: string;
paths: string[];
publishable: boolean; // ignore this for now
changes: Changes[];
deps: Versionable[];
}
type GroupResoverStrategy = (workspace: Workspace) => Versionable[];
// ex. independent
const independentStrategy: GroupResoverStrategy = (workspace) => {
return workspace.getProjects();
}
// ex. sync
const syncStrategy: GroupResoverStrategy () => { return {name: 'my-workspace', path: '/'} }
// ex. group by nx tag
// workspace:
// - apps/a (nx tag: scope:a)
// - apps/b (nx tag: scope:b)
// - libs/a/ui (nx tag: scope:a)
// - libs/a/core ...
groupByNxTag('scope')(workspace); // => [{name: 'a', paths: ['apps/a', 'libs/a/ui']}, {name: 'b', paths: ['apps/b']}]
Resolve tag prefix
type TagPrefixResolver = (versionable: Versionable) => string;
Resolve last version
git tag -l 'semver-*' --sort=-v:refname | head -1
Build graph
TODO
Filter deps
The default implementation of this step is filtering all publishable deps.
Given: A => B publishable & C
Then graph would be: A => C
Compute changes
Given the following versionables:
- a:
apps/a
,libs/a/ui
- x:
libs/x
- y:
libs/y
& nx dep graph is apps/a
=> libs/a/ui
=> libs/x
=> libs/y
When a breaking change happens on y
Then
getChanges(a); //
getChanges(x); // 1.0.0 => 1.0.1
getChanges(y); // {commits: [{type: 'breaking change', message: 'xxx'}]}
function bump(versionable) {
const commits = getCommits(versionable.name);
const depsChanges = getDeps(versionable.name).reduce((dep, acc) => ({...acc, [dep.name]: computeBumpVersion(...)}), {});
const newVersion = computeBump(versionable, commits, depsChanges);
return {
version: newVersion
}
}