New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

yarn-changed-workspaces

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

yarn-changed-workspaces - npm Package Compare versions

Comparing version 1.0.0 to 2.0.0

50

bin/cli.js
#!/usr/bin/env node
const yargs = require("yargs");
const path = require("path");
const { resolve, relative } = require("path");
const { format } = require("util");
const chalk = require("chalk");
const { getChangedWorkspacesIds } = require("../src/getChangedWorkspacesIds");

@@ -12,2 +13,11 @@

const log = (fmt, ...args) => {
console.log(format(fmt, ...args));
};
const print = (color, fmt, ...args) => {
const msg = format(fmt, ...args);
return console.log(process.env.CI ? msg : chalk.white[color](msg));
};
const options = yargs

@@ -17,3 +27,3 @@ .option("projectRoot", {

required: true,
default: path.resolve(process.cwd()),
default: resolve(process.cwd()),
type: "string",

@@ -26,24 +36,34 @@ normalize: true,

})
.option("naming", {
alias: "c",
.option("keyNaming", {
alias: "k",
choices: ["snakeCase", "camelCase"],
})
.option("pattern")
.option("filter", {
choices: ["exclude", "include"],
default: "include",
})
.option("namespace", {
alias: "n",
type: "string",
coerce: (str) => str + "/",
coerce: (str) => str,
}).argv;
(async () => {
const changedWorkspaces = await getChangedWorkspacesIds(options);
if (changedWorkspaces.length <= 0) {
return console.info("No changed workspases");
const cwd = process.cwd();
const workspaces = Object.entries(await getChangedWorkspacesIds(options));
if (workspaces.length <= 0) {
const msg = "No changes found in workspaces";
if (process.env.CI) return console.info(msg);
return console.info(msg);
}
console.info(format("Changed workspaces: %s", changedWorkspaces.length));
changedWorkspaces.forEach((id) => console.info(id));
console.info(
workspaces
.map(([id, files]) =>
[
id,
...[...files].sort().map((filePath) => {
const path = relative(cwd, filePath);
if (process.env.CI) return " " + path;
return " " + chalk.green(path);
}),
].join("\n")
)
.join("\n")
);
})();
{
"name": "yarn-changed-workspaces",
"version": "1.0.0",
"version": "2.0.0",
"main": "./index.js",

@@ -16,2 +16,3 @@ "bin": "./bin/cli.js",

"dependencies": {
"chalk": "^4.1.0",
"glob": "^7.1.6",

@@ -18,0 +19,0 @@ "lodash": "^4.17.20",

# yarn-changed-workspaces
This small utility helps to identify changed workspaces in `yarn` monorepo pattern.
A small utility tool to be used in CI/CD pipelines along with `git` to trigger dependent libraries' workflows in a monorepo pattern.
## Usage
## Install
```
yarn global add yarn-changed-workspaces
yarn-changed-workspaces
```
## CLI
```
yarn-changed-workspaces --help
```
## Node.js
`./package.json`
```json
{
"workspaces": ["packages/*"]
}
```
```js
const getChangedWorkspaces = require("yarn-changed-workspaces");
(async () => {
const workspaces = await getChangedWorkspaces({
branch: "master",
projectRoot: process.cwd(),
});
console.log("changes", workspaces);
})();
```
## Control scope of change
`./package.json`
```json
{
"workspaces": ["packages/*"]
}
```
`./packages/app/package.json`
```json
{
"workspace": {
"files": ["!**/*.(test|spec).(j|t)s(x)?"]
}
}
```
## Limitation
`git` is the core diffing tool. **This library will not work if you use a differnt distributed version-control system** for tracking changes in source code during software development

@@ -1,11 +0,14 @@

const filterWorkspaces = ({ workspaces, files }) => {
return [
...workspaces.reduce((items, { id, path }) => {
const isChanged = files.some((fp) => fp.startsWith(path));
if (isChanged) items.add(id);
return items;
}, new Set()),
];
const { relative } = require("path");
const { all } = require("micromatch");
const filterWorkspaces = ({ workspace, files }) => {
return files.filter((filePath) => {
const { path, config = {} } = workspace;
if (!filePath.startsWith(path)) return false;
if (!config.files) return true;
const relativePath = relative(path, filePath);
return all(relativePath, config.files);
});
};
exports.filterWorkspaces = filterWorkspaces;
const { filterWorkspaces } = require("./filterWorkspaces");
describe("filterWorkspaces", () => {
test("it returns workspaces with matched files", () => {
test("it selects files which match workspace path", () => {
expect(
filterWorkspaces({
workspaces: [{ id: "app1", path: "/app/packages/app1" }],
files: ["/app/packages/app1/file1"],
workspace: { id: "myapp", path: "/myapp" },
files: ["/myapp/src/myfile.js", "/test/myfile.js"],
})
).toEqual(["app1"]);
).toEqual(["/myapp/src/myfile.js"]);
});
test("it does not return anything if files are not from the package", () => {
test("it filters files based on config.files", () => {
expect(
filterWorkspaces({
workspaces: [{ id: "app1", path: "/app/packages/app1" }],
files: ["/file1"],
workspace: {
id: "myapp",
path: "/myapp",
config: { files: ["!**/*.test.js", "**/*.js"] },
},
files: ["/myapp/src/myfile.js", "/myapp/src/mytest.test.js"],
})
).toEqual([]);
).toEqual(["/myapp/src/myfile.js"]);
});
test("it returns unique ids", () => {
expect(
filterWorkspaces({
workspaces: [{ id: "app1", path: "/app1" }],
files: ["/app1/file1", "/app1/file2"],
})
).toEqual(["app1"]);
});
});

@@ -21,2 +21,3 @@ const { join } = require("path");

path,
config: pkg.workspace,
dependencies: Object.keys({

@@ -23,0 +24,0 @@ ...pkg.dependencies,

@@ -28,3 +28,3 @@ const { promises: fs } = require("fs");

_glob.mockImplementationOnce((_, cb) =>
cb(null, ["/app/packages/app1", "/app/packages/file1"])
cb(null, ["/app/packages/workspace", "/app/packages/file"])
);

@@ -38,4 +38,4 @@ fs.stat.mockImplementationOnce(() => ({

readJSONFile.mockImplementationOnce(() => ({
name: "app1",
path: "/app/packages/app1",
name: "workspace",
path: "/app/packages/workspace",
dependencies: { dependencies: "" },

@@ -51,4 +51,4 @@ devDependencies: { devDependencies: "" },

{
id: "app1",
path: "/app/packages/app1",
id: "workspace",
path: "/app/packages/workspace",
dependencies: [

@@ -64,5 +64,5 @@ "dependencies",

expect(readJSONFile).toHaveBeenCalledWith(
"/app/packages/app1/package.json"
"/app/packages/workspace/package.json"
);
});
});

@@ -10,15 +10,15 @@ const { getTouchedFiles } = require("./getTouchedFiles");

test("it returns unique files paths", async () => {
getTouchedFiles.mockImplementationOnce(() => ["/app/file1"]);
getTrackedFiles.mockImplementationOnce(() => ["/app/file1"]);
getTouchedFiles.mockImplementationOnce(() => ["/myapp/myfile"]);
getTrackedFiles.mockImplementationOnce(() => ["/myapp/myfile"]);
await expect(
getChangedFiles({ branch: "master", cwd: "/app" })
).resolves.toEqual(["/app/file1"]);
getChangedFiles({ branch: "master", cwd: "/myapp" })
).resolves.toEqual(["/myapp/myfile"]);
expect(getTouchedFiles).toHaveBeenCalledWith({
cwd: "/app",
cwd: "/myapp",
branch: "master",
});
expect(getTrackedFiles).toHaveBeenCalledWith({
cwd: "/app",
cwd: "/myapp",
});
});
});

@@ -7,8 +7,3 @@ const { resolve } = require("path");

const getChangedWorkspaces = async ({
branch,
projectRoot,
pattern,
filter,
}) => {
const getChangedWorkspaces = async ({ branch, projectRoot }) => {
const path = resolve(projectRoot);

@@ -19,10 +14,5 @@ const [workspaces, files] = await Promise.all([

]);
return getTouchedDependencies({
files,
filter,
pattern,
workspaces,
});
return getTouchedDependencies({ files, workspaces });
};
exports.getChangedWorkspaces = getChangedWorkspaces;

@@ -12,17 +12,11 @@ const { getChangedFiles } = require("./getChangedFiles");

test("it calls getWorkspaces", async () => {
getTouchedDependencies.mockImplementationOnce(() => ["app1"]);
await expect(
getChangedWorkspaces({ branch: "master", projectRoot: "/app" })
).resolves.toEqual(["app1"]);
expect(getWorkspaces).toHaveBeenCalledWith("/app");
await getChangedWorkspaces({ branch: "mybranch", projectRoot: "/" });
expect(getWorkspaces).toHaveBeenCalledWith("/");
});
test("it calls getChangedFiles", async () => {
getTouchedDependencies.mockImplementationOnce(() => ["app1"]);
await expect(
getChangedWorkspaces({ branch: "master", projectRoot: "/app" })
).resolves.toEqual(["app1"]);
await getChangedWorkspaces({ branch: "mybranch", projectRoot: "/" });
expect(getChangedFiles).toHaveBeenCalledWith({
cwd: "/app",
branch: "master",
cwd: "/",
branch: "mybranch",
});

@@ -33,27 +27,10 @@ });

getWorkspaces.mockImplementationOnce(() => ({
app1: {
id: "app1",
path: "/app/packages/app1",
dependencies: [],
},
myapp: { id: "myapp", path: "/packages/myapp", dependencies: [] },
}));
getChangedFiles.mockImplementationOnce(() => ["/app/packages/app1/file1"]);
getTouchedDependencies.mockImplementationOnce(() => ["app1"]);
await expect(
getChangedWorkspaces({
pattern: "*.js",
filter: "include",
projectRoot: "/app",
})
).resolves.toEqual(["app1"]);
getChangedFiles.mockImplementationOnce(() => ["/packages/myapp/myfile"]);
await getChangedWorkspaces({ branch: "mybranch", projectRoot: "/" });
expect(getTouchedDependencies).toHaveBeenCalledWith({
files: ["/app/packages/app1/file1"],
filter: "include",
pattern: "*.js",
files: ["/packages/myapp/myfile"],
workspaces: {
app1: {
id: "app1",
path: "/app/packages/app1",
dependencies: [],
},
myapp: { id: "myapp", path: "/packages/myapp", dependencies: [] },
},

@@ -60,0 +37,0 @@ });

@@ -5,10 +5,17 @@ const _ = require("lodash");

const getChangedWorkspacesIds = async (options) => {
const { naming, namespace } = options;
const formatName = (str) => (naming ? _[naming](str) : str);
const getChangedWorkspacesIds = async ({
namespace,
keyNaming,
projectRoot,
branch,
}) => {
const formatName = (str) => (keyNaming ? _[keyNaming](str) : str);
const normalizeName = (str) => (namespace ? str.replace(namespace, "") : str);
const workspaces = await getChangedWorkspaces(options);
return [...workspaces].map((id) => formatName(normalizeName(id))).sort();
const workspaces = await getChangedWorkspaces({ branch, projectRoot });
return Object.entries(workspaces).reduce((obj, [id, files]) => {
if (files.length <= 0) return obj;
return { ...obj, [formatName(normalizeName(id))]: files };
}, {});
};
module.exports.getChangedWorkspacesIds = getChangedWorkspacesIds;

@@ -8,19 +8,27 @@ const { getChangedWorkspaces } = require("./getChangedWorkspaces");

test("it removes namespaces", async () => {
getChangedWorkspaces.mockImplementationOnce(() => ["nsp/app-1"]);
getChangedWorkspaces.mockImplementationOnce(() => ({
"packages/app": ["packages/app/file"],
}));
await expect(
getChangedWorkspacesIds({ namespace: "nsp/" })
).resolves.toEqual(["app-1"]);
getChangedWorkspacesIds({ namespace: "packages/" })
).resolves.toEqual({ app: ["packages/app/file"] });
});
test("it applies a naming convention", async () => {
getChangedWorkspaces.mockImplementationOnce(() => ["nsp/app-1"]);
getChangedWorkspaces.mockImplementationOnce(() => ({
"packages/app": ["packages/app/file"],
}));
await expect(
getChangedWorkspacesIds({ naming: "snakeCase" })
).resolves.toEqual(["nsp_app_1"]);
getChangedWorkspacesIds({ keyNaming: "snakeCase" })
).resolves.toEqual({ packages_app: ["packages/app/file"] });
});
test("it sorts ASC", async () => {
getChangedWorkspaces.mockImplementationOnce(() => ["foo", "bar"]);
await expect(getChangedWorkspacesIds({})).resolves.toEqual(["bar", "foo"]);
test("it returns empty list if no files changed", async () => {
getChangedWorkspaces.mockImplementationOnce(() => ({
"packages/app": [],
}));
await expect(
getChangedWorkspacesIds({ namespace: "packages/" })
).resolves.toEqual({});
});
});

@@ -1,22 +0,19 @@

const { filterFiles } = require("./filterFiles");
const { filterWorkspaces } = require("./filterWorkspaces");
const getTouchedDependencies = ({ workspaces, files, pattern, filter }) => {
const visited = new Set();
const queue = filterWorkspaces({
files: filterFiles({ files, pattern, filter }),
workspaces: Object.values(workspaces),
});
while (queue.length > 0) {
const id = queue.pop();
if (visited.has(id)) continue;
const workspaceA = workspaces[id];
Object.values(workspaces)
.filter((workspaceB) => workspaceB.dependencies.includes(workspaceA.id))
.forEach((workspaceB) => queue.push(workspaceB.id));
visited.add(id);
}
return [...visited].sort();
const getTouchedDependencies = ({ workspaces, files }) => {
return Object.values(workspaces).reduce((changed, wa) => {
const _files = filterWorkspaces({ workspace: wa, files });
changed[wa.id] = changed[wa.id] || [];
changed[wa.id] = changed[wa.id].concat(_files);
if (_files.length <= 0) return changed;
Object.values(workspaces).forEach((wb) => {
if (wa === wb) return;
if (!wb.dependencies.includes(wa.id)) return;
changed[wb.id] = changed[wb.id] || [];
changed[wb.id] = changed[wb.id].concat(wa.path);
});
return changed;
}, {});
};
exports.getTouchedDependencies = getTouchedDependencies;

@@ -1,15 +0,5 @@

const { filterWorkspaces } = require("./filterWorkspaces");
const { getTouchedDependencies } = require("./getTouchedDependencies");
jest.mock("./filterFiles");
jest.mock("./filterWorkspaces");
describe("getTouchedDependencies", () => {
test("it returns empty list if no change", () => {
filterWorkspaces.mockImplementationOnce(() => []);
expect(getTouchedDependencies({ workspaces: {} })).toEqual([]);
});
test("it returns changed workspaces", () => {
filterWorkspaces.mockImplementationOnce(() => ["app1"]);
test("it returns empty lists if no change", () => {
expect(

@@ -29,8 +19,11 @@ getTouchedDependencies({

},
files: [],
})
).toEqual(["app1"]);
).toEqual({
app1: [],
app2: [],
});
});
test("it returns deep dependants", () => {
filterWorkspaces.mockImplementationOnce(() => ["app1"]);
test("it returns workspaces with changed files", () => {
expect(

@@ -47,16 +40,14 @@ getTouchedDependencies({

path: "/app/packages/app2",
dependencies: ["app1"],
dependencies: [],
},
app3: {
id: "app3",
path: "/app/packages/app3",
dependencies: ["app2"],
},
},
files: ["/app/packages/app1/file1"],
})
).toEqual(["app1", "app2", "app3"]);
).toEqual({
app1: ["/app/packages/app1/file1"],
app2: [],
});
});
test("it prevents infinite resolution", () => {
filterWorkspaces.mockImplementationOnce(() => ["app1"]);
test("it returns deep dependants", () => {
expect(

@@ -68,8 +59,23 @@ getTouchedDependencies({

path: "/app/packages/app1",
dependencies: [],
},
app2: {
id: "app2",
path: "/app/packages/app2",
dependencies: ["app1"],
},
app3: {
id: "app3",
path: "/app/packages/app3",
dependencies: ["app2"],
},
},
files: ["/app/packages/app1/file1", "/app/packages/app2/file1"],
})
).toEqual(["app1"]);
).toEqual({
app1: ["/app/packages/app1/file1"],
app2: ["/app/packages/app1", "/app/packages/app2/file1"],
app3: ["/app/packages/app2"],
});
});
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc