diff --git a/README.md b/README.md index 2998eb9c5b..35f1b4342e 100644 --- a/README.md +++ b/README.md @@ -673,7 +673,6 @@ Running `lerna` without arguments will show all commands/options. - `commands.bootstrap.scope`: an array of globs that restricts which packages will be bootstrapped when running the `lerna bootstrap` command. - `packages`: Array of globs to use as package locations. - ### Common `devDependencies` Most `devDependencies` can be pulled up to the root of a Lerna repo. @@ -896,9 +895,9 @@ May also be configured in `lerna.json`: #### --use-workspaces -Enables integration with [Yarn Workspaces](https://github.com/yarnpkg/rfcs/blob/master/implemented/0000-workspaces-install-phase-1.md) (available since yarn@0.27+). -The values in the array are the commands in which Lerna will delegate operation to Yarn (currently only bootstrapping). -If `--use-workspaces` is true then `packages` will be overridden by the value from `package.json/workspaces.` +Enables integration with [Yarn Workspaces](https://github.com/yarnpkg/rfcs/blob/master/implemented/0000-workspaces-install-phase-1.md) (available since yarn@0.27+). +The values in the array are the commands in which Lerna will delegate operation to Yarn (currently only bootstrapping). +If `--use-workspaces` is true then `packages` will be overridden by the value from `package.json/workspaces.` May also be configured in `lerna.json`: ```js @@ -924,6 +923,63 @@ The root-level package.json must also include a `workspaces` array: This list is broadly similar to lerna's `packages` config (a list of globs matching directories with a package.json), except it does not support recursive globs (`"**"`, a.k.a. "globstars"). +#### --use-git-version + +Allow target versions of dependent packages to be written as [git hosted urls](https://github.com/npm/hosted-git-info) instead of a plain version number. +If enabled, Lerna will attempt to extract and save the interpackage dependency versions from `package.json` files using git url-aware parser. + +Eg. assuming monorepo with 2 packages where `my-package-1` depends on `my-package-2`, `package.json` of `my-package-1` could be: +``` +// packages/my-package-1/package.json +{ + name: "my-package-1", + version: "1.0.0", + bin: "bin.js", + dependencies: { + "my-package-2": "github:example-user/my-package-2#v1.0.0" + }, + devDependencies: { + "my-dev-dependency": "^1.0.0" + }, + peerDependencies: { + "my-peer-dependency": "^1.0.0" + } +} +``` +For the case above Lerna will read the version of `my-package-2` dependency as `1.0.0`. + +This allows packages to be distributed via git repos if eg. packages are private and [private npm repo is not an option](https://www.dotconferences.com/2016/05/fabien-potencier-monolithic-repositories-vs-many-repositories). + +Please note that using `--use-git-version` +- is limited to urls with [`committish`](https://docs.npmjs.com/files/package.json#git-urls-as-dependencies) part present (ie. `github:example-user/my-package-2` is invalid) +- requires `publish` command to be used with `--exact` + +May also be configured in `lerna.json`: +```js +{ + ... + "useGitVersion": true +} +``` + +#### --git-version-prefix + +Defines version prefix string (defaults to 'v') ignored when extracting version number from a commitish part of git url. +Everything after the prefix will be considered a version. + + +Eg. given `github:example-user/my-package-2#v1.0.0` and `gitVersionPrefix: 'v'` version will be read as `1.0.0`. + +Only used if `--use-git-version` is set to `true`. + +May also be configured in `lerna.json`: +```js +{ + ... + "gitVersionPrefix": "v" +} +``` + #### --stream Stream output from child processes immediately, prefixed with the originating diff --git a/package.json b/package.json index 344d2a6d9e..dbf34a42ff 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "glob-parent": "^3.1.0", "globby": "^6.1.0", "graceful-fs": "^4.1.11", + "hosted-git-info": "^2.5.0", "inquirer": "^3.2.2", "is-ci": "^1.0.10", "load-json-file": "^3.0.0", diff --git a/src/Command.js b/src/Command.js index 2e810d027d..23f4fb53d2 100644 --- a/src/Command.js +++ b/src/Command.js @@ -5,11 +5,13 @@ import log from "npmlog"; import ChildProcessUtilities from "./ChildProcessUtilities"; import FileSystemUtilities from "./FileSystemUtilities"; import GitUtilities from "./GitUtilities"; +import GitVersionParser from "./GitVersionParser"; import PackageUtilities from "./PackageUtilities"; import Repository from "./Repository"; import filterFlags from "./utils/filterFlags"; import writeLogFile from "./utils/writeLogFile"; import UpdatedPackagesCollector from "./UpdatedPackagesCollector"; +import VersionSerializer from "./VersionSerializer"; // handle log.success() log.addLevel("success", 3001, { fg: "green", bold: true }); @@ -294,7 +296,8 @@ export default class Command { } runPreparations() { - const { scope, ignore, registry, since } = this.options; + const { rootPath, packageConfigs } = this.repository; + const { scope, ignore, registry, since, useGitVersion, gitVersionPrefix } = this.options; if (scope) { log.info("scope", scope); @@ -309,10 +312,22 @@ export default class Command { } try { - this.repository.buildPackageGraph(); - this.packages = this.repository.packages; - this.packageGraph = this.repository.packageGraph; - this.filteredPackages = PackageUtilities.filterPackages(this.packages, { scope, ignore }); + const versionParser = useGitVersion && new GitVersionParser(gitVersionPrefix); + const packages = PackageUtilities.getPackages({ rootPath, packageConfigs }); + const packageGraph = PackageUtilities.getPackageGraph(packages, false, versionParser); + + if (useGitVersion) { + packages.forEach((pkg) => { + pkg.versionSerializer = new VersionSerializer({ + graphDependencies: packageGraph.get(pkg.name).dependencies, + versionParser + }); + }); + } + + this.packages = packages; + this.packageGraph = packageGraph; + this.filteredPackages = PackageUtilities.filterPackages(packages, { scope, ignore }); // The UpdatedPackagesCollector requires that filteredPackages be present prior to checking for // updates. That's okay because it further filters based on what's already been filtered. diff --git a/src/GitVersionParser.js b/src/GitVersionParser.js new file mode 100644 index 0000000000..8b6fdf4950 --- /dev/null +++ b/src/GitVersionParser.js @@ -0,0 +1,22 @@ +import { escapeRegExp } from "lodash"; +import hostedGitInfo from "hosted-git-info"; + +export default class GitVersionParser { + constructor(versionPrefix = "v") { + this._gitUrlPattern = new RegExp(`(.+?#${escapeRegExp(versionPrefix)})(.+)$`); + } + + parseVersion(version) { + const gitInfo = hostedGitInfo.fromUrl(version); + let targetMatches; + + if (gitInfo && gitInfo.committish) { + targetMatches = this._gitUrlPattern.exec(version); + } + + return { + prefix: targetMatches ? targetMatches[1] : null, + version: targetMatches ? targetMatches[2] : version + }; + } +} diff --git a/src/Package.js b/src/Package.js index 390fbb3745..872a2293e8 100644 --- a/src/Package.js +++ b/src/Package.js @@ -2,6 +2,7 @@ import dedent from "dedent"; import log from "npmlog"; import path from "path"; import semver from "semver"; +import _ from "lodash"; import dependencyIsSatisfied from "./utils/dependencyIsSatisfied"; import NpmUtilities from "./NpmUtilities"; @@ -60,12 +61,21 @@ export default class Package { return this._package.scripts || {}; } + set versionSerializer(versionSerializer) { + this._versionSerializer = versionSerializer; + + if (versionSerializer) { + this._package = versionSerializer.deserialize(this._package); + } + } + isPrivate() { return !!this._package.private; } toJSON() { - return this._package; + const pkg = _.cloneDeep(this._package); + return this._versionSerializer ? this._versionSerializer.serialize(pkg) : pkg; } /** diff --git a/src/PackageGraph.js b/src/PackageGraph.js index c595f26a34..2d830df19e 100644 --- a/src/PackageGraph.js +++ b/src/PackageGraph.js @@ -20,7 +20,7 @@ export class PackageGraphNode { * devDependencies that would normally be included. */ export default class PackageGraph { - constructor(packages, depsOnly = false) { + constructor(packages, depsOnly = false, versionParser) { this.nodes = []; this.nodesByName = {}; @@ -38,11 +38,17 @@ export default class PackageGraph { for (let d = 0; d < depNames.length; d++) { const depName = depNames[d]; - const depVersion = dependencies[depName]; const packageNode = this.nodesByName[depName]; - if (packageNode && semver.satisfies(packageNode.package.version, depVersion)) { - node.dependencies.push(depName); + if (packageNode) { + const depVersion = (versionParser + ? versionParser.parseVersion(dependencies[depName]).version + : dependencies[depName] + ); + + if (semver.satisfies(packageNode.package.version, depVersion)) { + node.dependencies.push(depName); + } } } } diff --git a/src/PackageUtilities.js b/src/PackageUtilities.js index 29fccfc058..9b85689a94 100644 --- a/src/PackageUtilities.js +++ b/src/PackageUtilities.js @@ -87,8 +87,8 @@ export default class PackageUtilities { return packages; } - static getPackageGraph(packages, depsOnly) { - return new PackageGraph(packages, depsOnly); + static getPackageGraph(packages, depsOnly, versionParser) { + return new PackageGraph(packages, depsOnly, versionParser); } /** diff --git a/src/Repository.js b/src/Repository.js index 66903ce4dd..8be3c4b87c 100644 --- a/src/Repository.js +++ b/src/Repository.js @@ -8,7 +8,6 @@ import semver from "semver"; import dependencyIsSatisfied from "./utils/dependencyIsSatisfied"; import Package from "./Package"; -import PackageUtilities from "./PackageUtilities"; const DEFAULT_PACKAGE_GLOB = "packages/*"; @@ -67,22 +66,6 @@ export default class Repository { .map(parentDir => path.resolve(this.rootPath, parentDir)); } - get packages() { - if (!this._packages) { - this.buildPackageGraph(); - } - - return this._packages; - } - - get packageGraph() { - if (!this._packageGraph) { - this.buildPackageGraph(); - } - - return this._packageGraph; - } - get packageJson() { if (!this._packageJson) { try { @@ -117,11 +100,6 @@ export default class Repository { return this.version === "independent"; } - buildPackageGraph() { - this._packages = PackageUtilities.getPackages(this); - this._packageGraph = PackageUtilities.getPackageGraph(this._packages); - } - hasDependencyInstalled(depName, version) { log.silly("hasDependencyInstalled", "ROOT", depName, version); diff --git a/src/UpdatedPackagesCollector.js b/src/UpdatedPackagesCollector.js index 53d8f02e8f..08c2c1f2d4 100644 --- a/src/UpdatedPackagesCollector.js +++ b/src/UpdatedPackagesCollector.js @@ -37,7 +37,7 @@ export default class UpdatedPackagesCollector { this.logger = command.logger; this.repository = command.repository; this.packages = command.filteredPackages; - this.packageGraph = command.repository.packageGraph; + this.packageGraph = command.packageGraph; this.options = command.options; } diff --git a/src/VersionSerializer.js b/src/VersionSerializer.js new file mode 100644 index 0000000000..c4465a0eed --- /dev/null +++ b/src/VersionSerializer.js @@ -0,0 +1,46 @@ +export default class VersionSerializer { + constructor({ graphDependencies, versionParser }) { + this._graphDependencies = graphDependencies; + this._versionParser = versionParser; + this._dependenciesKeys = ["dependencies", "devDependencies", "peerDependencies"]; + this._strippedPrefixes = new Map(); + } + + serialize(pkg) { + this._dependenciesKeys.forEach((key) => { + this._prependPrefix(pkg[key] || {}); + }); + + return pkg; + } + + deserialize(pkg) { + this._dependenciesKeys.forEach((key) => { + this._stripPrefix(pkg[key] || {}); + }); + + return pkg; + } + + _prependPrefix(dependencies) { + this._strippedPrefixes.forEach((prefix, name) => { + const version = dependencies[name]; + if (version) { + dependencies[name] = `${prefix}${version}`; + } + }); + } + + _stripPrefix(dependencies) { + Object.keys(dependencies).forEach((name) => { + if (this._graphDependencies.includes(name)) { + const result = this._versionParser.parseVersion(dependencies[name]); + + if (result.prefix) { + dependencies[name] = result.version; + this._strippedPrefixes.set(name, result.prefix); + } + } + }); + } +} diff --git a/src/commands/PublishCommand.js b/src/commands/PublishCommand.js index ec9f28b5d2..caedecfda7 100644 --- a/src/commands/PublishCommand.js +++ b/src/commands/PublishCommand.js @@ -156,6 +156,13 @@ export default class PublishCommand extends Command { this.gitRemote = this.options.gitRemote || "origin"; this.gitEnabled = !(this.options.canary || this.options.skipGit); + if (this.options.useGitVersion && !this.options.exact) { + throw new Error(dedent` + Using git version without 'exact' option is not recommended. + Please make sure you publish with --exact. + `); + } + if (this.options.canary) { this.logger.info("canary", "enabled"); } diff --git a/test/Command.js b/test/Command.js index 1e612f8e61..28f09a5a91 100644 --- a/test/Command.js +++ b/test/Command.js @@ -236,22 +236,43 @@ describe("Command", () => { }); describe(".runPreparations()", () => { + let testDir; + + function cli(cmd, ...args) { + return execa(cmd, args, { cwd: testDir }); + } + + function run(opts) { + const cmd = new OkCommand([], opts, testDir); + return cmd.run().then(() => cmd); + } + + describe("get .packages", () => { + + it("returns the list of packages", async () => { + testDir = await initFixture("Command/basic") + const { packages } = await run(); + expect(packages).toEqual([]); + }); + }); + + describe("get .packageGraph", () => { + + it("returns the graph of packages", async () => { + testDir = await initFixture("Command/basic") + const { packageGraph } = await run(); + expect(packageGraph).toBeDefined(); + expect(packageGraph).toHaveProperty("nodes", []); + expect(packageGraph).toHaveProperty("nodesByName", {}); + }); + }); + describe(".filteredPackages", () => { - let testDir; beforeEach(() => initFixture("UpdatedCommand/basic").then((dir) => { testDir = dir; })); - function cli(cmd, ...args) { - return execa(cmd, args, { cwd: testDir }); - } - - function run(opts) { - const cmd = new OkCommand([], opts, testDir); - return cmd.run().then(() => cmd); - } - it("--scope should filter packages", async () => { const { filteredPackages } = await run({ scope: ["package-2", "package-4"] }); expect(filteredPackages.length).toEqual(2); diff --git a/test/GitVersionParser.js b/test/GitVersionParser.js new file mode 100644 index 0000000000..4fa1d1a988 --- /dev/null +++ b/test/GitVersionParser.js @@ -0,0 +1,71 @@ +import log from "npmlog"; + +// file under test +import GitVersionParser from "../src/GitVersionParser"; + +// silence logs +log.level = "silent"; + +describe("GitVersionParser", () => { + + describe("parseVersion - without prefix", () => { + const parser = new GitVersionParser(""); + + it("should work for semver version", () => { + expect(parser.parseVersion("0.0.2")).toEqual({ + prefix: null, + version: "0.0.2" + }); + + expect(parser.parseVersion("~0.0.2")).toEqual({ + prefix: null, + version: "~0.0.2" + }); + }); + + it("should work for git url", () => { + expect(parser.parseVersion("github:user-foo/project-foo#v0.0.1")).toEqual({ + prefix: "github:user-foo/project-foo#", + version: "v0.0.1" + }); + + expect(parser.parseVersion("git@github.com:user-foo/project-foo#0.0.5")).toEqual({ + prefix: "git@github.com:user-foo/project-foo#", + version: "0.0.5" + }); + }); + }); + + describe("parseVersion - with version prefix", () => { + const parser = new GitVersionParser("v"); + + it("should work for semver version", () => { + expect(parser.parseVersion("0.0.2")).toEqual({ + prefix: null, + version: "0.0.2" + }); + + expect(parser.parseVersion("~0.0.2")).toEqual({ + prefix: null, + version: "~0.0.2" + }); + }); + + it("should work for git url", () => { + expect(parser.parseVersion("github:user-foo/project-foo#v0.0.1")).toEqual({ + prefix: "github:user-foo/project-foo#v", + version: "0.0.1" + }); + + expect(parser.parseVersion("git@github.com:user-foo/project-foo#0.0.5")).toEqual({ + prefix: null, + version: "git@github.com:user-foo/project-foo#0.0.5" + }); + + expect(parser.parseVersion("git@github.com:user-foo/project-foo#v0.0.5")).toEqual({ + prefix: "git@github.com:user-foo/project-foo#v", + version: "0.0.5" + }); + }); + }); +}); diff --git a/test/Package.js b/test/Package.js index d04f7264a5..ed7b12ca30 100644 --- a/test/Package.js +++ b/test/Package.js @@ -102,15 +102,76 @@ describe("Package", () => { }); }); + describe(".set versionSerializer", () => { + it("should call 'deserialize' method of serializer'", () => { + + const mockSerializer = { + serialize: jest.fn((pkg) => pkg), + deserialize: jest.fn((pkg) => pkg) + }; + + pkg.versionSerializer = mockSerializer; + + expect(mockSerializer.deserialize).toBeCalled(); + expect(mockSerializer.deserialize).toBeCalledWith(pkg._package); + expect(mockSerializer.serialize).not.toBeCalled(); + }); + }); + + describe(".toJSON()", () => { - it("should return internal package for serialization", () => { - expect(pkg.toJSON()).toBe(pkg._package); + it("should return clone of internal package for serialization", () => { + expect(pkg.toJSON()).not.toBe(pkg._package); + expect(pkg.toJSON()).toEqual(pkg._package); const implicit = JSON.stringify(pkg, null, 2); const explicit = JSON.stringify(pkg._package, null, 2); expect(implicit).toBe(explicit); }); + + it("should not change internal package with versionSerializer", () => { + pkg._package.state = "serialized" + + const mockSerializer = { + serialize: jest.fn((pkg) => { + pkg.state = "serialized" + return pkg; + }), + deserialize: jest.fn((pkg) => { + pkg.state = "deserialized" + return pkg + }) + }; + + const serializedPkg = Object.assign({}, pkg._package, { state: "serialized" }) + const deserializedPkg = Object.assign({}, pkg._package, { state: "deserialized" }) + + pkg.versionSerializer = mockSerializer; + expect(mockSerializer.deserialize).toBeCalled(); + expect(pkg._package).toEqual(deserializedPkg); + + const serializedResult = pkg.toJSON(); + expect(pkg._package).toEqual(deserializedPkg); + expect(serializedResult).toEqual(serializedPkg); + + expect(mockSerializer.serialize).toBeCalled(); + }); + + it("should use versionSerializer.serialize on internal package before return", () => { + const mockSerializer = { + serialize: jest.fn((pkg) => pkg), + deserialize: jest.fn((pkg) => pkg) + }; + + pkg.versionSerializer = mockSerializer; + + expect(pkg.toJSON()).toEqual(pkg._package); + + expect(mockSerializer.deserialize).toBeCalled(); + expect(mockSerializer.serialize).toBeCalled(); + expect(mockSerializer.serialize).toBeCalledWith(pkg._package); + }); }); describe(".runScript()", () => { diff --git a/test/PackageGraph.js b/test/PackageGraph.js new file mode 100644 index 0000000000..b7246191e7 --- /dev/null +++ b/test/PackageGraph.js @@ -0,0 +1,76 @@ +import log from "npmlog"; + +// file under test +import Package from "../src/Package"; +import PackageGraph from "../src/PackageGraph"; + +// silence logs +log.level = "silent"; + +describe("PackageGraph", () => { + + function createPackages(version, dependencyVersion = version) { + dependencyVersion = dependencyVersion || version; + + return [ + new Package( + { + name: "my-package-1", + version: version, + bin: "bin.js", + scripts: { "my-script": "echo 'hello world'" }, + dependencies: { "my-dependency": "^1.0.0" }, + devDependencies: { "my-dev-dependency": "^1.0.0" }, + peerDependencies: { "my-peer-dependency": "^1.0.0" } + }, + "/path/to/package1" + ), + new Package( + { + name: "my-package-2", + version: version, + bin: "bin.js", + scripts: { "my-script": "echo 'hello world'" }, + dependencies: { "my-dependency": "^1.0.0" }, + devDependencies: { "my-package-1": dependencyVersion }, + peerDependencies: { "my-peer-dependency": "^1.0.0" } + }, + "/path/to/package2" + ) + ]; + } + + describe("constructor", () => { + it(".get should return dependencies", () => { + const [ pkg1, pkg2 ] = createPackages("0.0.1"); + const graph = new PackageGraph([ pkg1, pkg2 ]); + + expect(graph.get(pkg1.name).dependencies).toEqual([]); + expect(graph.get(pkg2.name).dependencies).toEqual([pkg1.name]); + }); + + it(".get should not return the dependencies for unrecognized versions", () => { + const [ pkg1, pkg2 ] = createPackages("0.0.1", "github:user-foo/project-foo#v0.0.1"); + const graph = new PackageGraph([ pkg1, pkg2 ]); + + expect(graph.get(pkg1.name).dependencies).toEqual([]); + expect(graph.get(pkg2.name).dependencies).toEqual([]); + }); + + it(".get should not return the dependencies for unrecognized versions", () => { + const [ pkg1, pkg2 ] = createPackages("0.0.1", "github:user-foo/project-foo#v0.0.1"); + + const mockParser = { + parseVersion: jest.fn().mockReturnValue({ + prefix: "github:user-foo/project-foo#v", + version: "0.0.1" + }) + }; + + const graph = new PackageGraph([pkg1, pkg2], false, mockParser); + + expect(graph.get(pkg1.name).dependencies).toEqual([]); + expect(graph.get(pkg2.name).dependencies).toEqual([ pkg1.name ]); + }); + }); +}); diff --git a/test/Repository.js b/test/Repository.js index 65e7a3ea1b..20dd653bd0 100644 --- a/test/Repository.js +++ b/test/Repository.js @@ -138,33 +138,7 @@ describe("Repository", () => { path.join(testDir, "packages"), path.join(testDir, "dir/nested"), path.join(testDir, "globstar"), - ]) - }); - }); - - describe("get .packages", () => { - it("returns the list of packages", () => { - const repo = new Repository(testDir); - expect(repo.packages).toEqual([]); - }); - - it("caches the initial value", () => { - const repo = new Repository(testDir); - expect(repo.packages).toBe(repo.packages); - }); - }); - - describe("get .packageGraph", () => { - it("returns the graph of packages", () => { - const repo = new Repository(testDir); - expect(repo.packageGraph).toBeDefined(); - expect(repo.packageGraph).toHaveProperty("nodes", []); - expect(repo.packageGraph).toHaveProperty("nodesByName", {}); - }); - - it("caches the initial value", () => { - const repo = new Repository(testDir); - expect(repo.packageGraph).toBe(repo.packageGraph); + ]); }); }); diff --git a/test/VersionSerializer.js b/test/VersionSerializer.js new file mode 100644 index 0000000000..32f1db321c --- /dev/null +++ b/test/VersionSerializer.js @@ -0,0 +1,146 @@ +import log from "npmlog"; + +// file under test +import VersionSerializer from "../src/VersionSerializer"; + +// silence logs +log.level = "silent"; + +describe("VersionSerializer", () => { + + let serializer; + + beforeEach(() => { + const parser = { + parseVersion(version) { + const chunks = version.split("#"); + return { + prefix: chunks.length > 1 ? chunks[0] + "#" : null, + version: chunks.length > 1 ? chunks[1] : version, + }; + } + }; + serializer = new VersionSerializer({ + graphDependencies: [ + "my-package-1", "my-package-2", "my-package-3" + ], + versionParser: parser + }); + }); + + describe("deserialize", () => { + + it("should use version parser for inter-package dependencies only", () => { + const mockParser = { + parseVersion: jest.fn().mockReturnValue({ + prefix: null, + version: "0.0.1" + }) + }; + + serializer = new VersionSerializer({ + graphDependencies: [ + "my-package-1", "my-package-2", "my-package-3" + ], + versionParser: mockParser + }); + + const pkg = { + name: "my-package-1", + version: "1.0.0", + bin: "bin.js", + scripts: { "my-script": "echo 'hello world'" }, + dependencies: { "my-dependency": "^1.0.0" }, + devDependencies: { "my-package-2": "^1.0.0" }, + peerDependencies: { "my-package-3": "^1.0.0" } + }; + + serializer.deserialize(pkg); + expect(mockParser.parseVersion).toHaveBeenCalledTimes(2); + }); + + it("should not touch versions parser does not recognize", () => { + const pkg = { + name: "my-package-1", + version: "1.0.0", + bin: "bin.js", + scripts: { "my-script": "echo 'hello world'" }, + dependencies: { "my-dependency": "^1.0.0" }, + devDependencies: { "my-package-2": "^1.0.0" }, + peerDependencies: { "my-package-3": "^1.0.0" } + }; + + expect(serializer.deserialize(pkg)).toEqual(pkg); + }); + + it("should extract versions recognized by parser", () => { + expect(serializer.deserialize({ + name: "my-package-1", + version: "1.0.0", + bin: "bin.js", + scripts: { "my-script": "echo 'hello world'" }, + dependencies: { "my-dependency": "dont-touch-this#1.0.0" }, + devDependencies: { "my-package-2": "bbb#1.0.0" }, + peerDependencies: { "my-package-3": "ccc#1.0.0" } + })).toEqual({ + name: "my-package-1", + version: "1.0.0", + bin: "bin.js", + scripts: { "my-script": "echo 'hello world'" }, + dependencies: { "my-dependency": "dont-touch-this#1.0.0" }, + devDependencies: { "my-package-2": "1.0.0" }, + peerDependencies: { "my-package-3": "1.0.0" } + }); + }); + }); + + describe("serialize", () => { + + it("should not touch versions parser does not recognize", () => { + const pkg = { + name: "my-package-1", + version: "1.0.0", + bin: "bin.js", + scripts: { "my-script": "echo 'hello world'" }, + dependencies: { "my-dependency": "^1.0.0" }, + devDependencies: { "my-package-2": "^1.0.0" }, + peerDependencies: { "my-package-3": "^1.0.0" } + }; + + expect(serializer.serialize(pkg)).toEqual(pkg); + }); + + it("should write back version strings transformed by deserialize", () => { + + // since serializer is stateful, version prefixes will be stored in its state + serializer.deserialize({ + name: "my-package-1", + version: "1.0.0", + bin: "bin.js", + scripts: { "my-script": "echo 'hello world'" }, + dependencies: { "my-dependency": "dont-touch-this#1.0.0" }, + devDependencies: { "my-package-2": "bbb#1.0.0" }, + peerDependencies: { "my-package-3": "ccc#1.0.0" } + }) + + // the preserved prefixes should be written back + expect(serializer.serialize({ + name: "my-package-1", + version: "1.0.0", + bin: "bin.js", + scripts: { "my-script": "echo 'hello world'" }, + dependencies: { "my-dependency": "dont-touch-this#1.0.0" }, + devDependencies: { "my-package-2": "1.0.0" }, + peerDependencies: { "my-package-3": "1.0.0" } + })).toEqual({ + name: "my-package-1", + version: "1.0.0", + bin: "bin.js", + scripts: { "my-script": "echo 'hello world'" }, + dependencies: { "my-dependency": "dont-touch-this#1.0.0" }, + devDependencies: { "my-package-2": "bbb#1.0.0" }, + peerDependencies: { "my-package-3": "ccc#1.0.0" } + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 8b41e83c7c..255c316bbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1926,6 +1926,10 @@ hosted-git-info@^2.1.4: version "2.2.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.2.0.tgz#7a0d097863d886c0fabbdcd37bf1758d8becf8a5" +hosted-git-info@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + html-encoding-sniffer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz#79bf7a785ea495fe66165e734153f363ff5437da"