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

Detect changed workspaces #2015

Merged
merged 11 commits into from
Aug 10, 2022
5 changes: 5 additions & 0 deletions .changeset/seven-llamas-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"modular-scripts": minor
---

Add function to detect changed workspaces
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import { matchWorkspaces } from '../../utils/getChangedWorkspaces';
import type { WorkspaceContent } from '../../utils/getAllWorkspaces';

describe('matchWorkspaces', () => {
it('matches absolute manifests paths with holes and duplication to subset of workspace entries', () => {
const workspacesContent: WorkspaceContent = [
new Map(
Object.entries({
'workspace-1': {
path: 'w1',
location: 'w1',
name: 'workspace-1',
version: '0.0.1',
workspace: false,
modular: {
type: 'package',
},
children: [],
parent: null,
dependencies: undefined,
rawPackageJson: {},
},
'workspace-2': {
path: 'w2',
location: '/w2',
name: 'workspace-3',
version: '0.0.1',
workspace: false,
modular: {
type: 'package',
},
children: [],
parent: null,
dependencies: undefined,
rawPackageJson: {},
},
'workspace-3': {
path: 'w3',
location: '/w3',
name: 'workspace-3',
version: '0.0.1',
workspace: false,
modular: {
type: 'package',
},
children: [],
parent: null,
dependencies: undefined,
rawPackageJson: {},
},
}),
),
{
'workspace-1': {
location: 'packages/workspace-1',
workspaceDependencies: ['workspace-2', 'workspace-3'],
mismatchedWorkspaceDependencies: [],
},
'workspace-2': {
location: 'packages/workspace-2',
workspaceDependencies: ['workspace-3'],
mismatchedWorkspaceDependencies: [],
},
'workspace-3': {
location: 'packages/workspace-3',
workspaceDependencies: [],
mismatchedWorkspaceDependencies: [],
},
},
];

const listOfPackageManifests = [
'/my/path/packages/workspace-1/package.json',
null,
'/my/path/packages/workspace-1/package.json',
'/my/path/packages/workspace-3/package.json',
];

expect(
matchWorkspaces(listOfPackageManifests, '/my/path', workspacesContent),
).toEqual([
new Map(
Object.entries({
'workspace-1': {
path: 'w1',
location: 'w1',
name: 'workspace-1',
version: '0.0.1',
workspace: false,
modular: {
type: 'package',
},
children: [],
parent: null,
dependencies: undefined,
rawPackageJson: {},
},
'workspace-3': {
path: 'w3',
location: '/w3',
name: 'workspace-3',
version: '0.0.1',
workspace: false,
modular: {
type: 'package',
},
children: [],
parent: null,
dependencies: undefined,
rawPackageJson: {},
},
}),
),
{
'workspace-1': {
location: 'packages/workspace-1',
workspaceDependencies: ['workspace-2', 'workspace-3'],
mismatchedWorkspaceDependencies: [],
},
'workspace-3': {
location: 'packages/workspace-3',
workspaceDependencies: [],
mismatchedWorkspaceDependencies: [],
},
},
]);
});

it('does not match paths outside of known workspaces', () => {
const workspacesContent: WorkspaceContent = [
new Map(
Object.entries({
'workspace-1': {
path: 'w1',
location: 'w1',
name: 'workspace-1',
version: '0.0.1',
workspace: false,
modular: {
type: 'package',
},
children: [],
parent: null,
dependencies: undefined,
rawPackageJson: {},
},
'workspace-2': {
path: 'w2',
location: '/w2',
name: 'workspace-3',
version: '0.0.1',
workspace: false,
modular: {
type: 'package',
},
children: [],
parent: null,
dependencies: undefined,
rawPackageJson: {},
},
'workspace-3': {
path: 'w3',
location: '/w3',
name: 'workspace-3',
version: '0.0.1',
workspace: false,
modular: {
type: 'package',
},
children: [],
parent: null,
dependencies: undefined,
rawPackageJson: {},
},
}),
),
{
'workspace-1': {
location: 'packages/workspace-1',
workspaceDependencies: ['workspace-2', 'workspace-3'],
mismatchedWorkspaceDependencies: [],
},
'workspace-2': {
location: 'packages/workspace-2',
workspaceDependencies: ['workspace-3'],
mismatchedWorkspaceDependencies: [],
},
'workspace-3': {
location: 'packages/workspace-3',
workspaceDependencies: [],
mismatchedWorkspaceDependencies: [],
},
},
];

const listOfPackageManifests = [
'/my/path/packages/workspace-nope/package.json',
];

expect(
matchWorkspaces(listOfPackageManifests, '/my/path', workspacesContent),
).toEqual([new Map([]), {}]);
});

it('works with a root workspace', () => {
const workspacesContent: WorkspaceContent = [
new Map(
Object.entries({
'workspace-root': {
path: 'w1',
location: 'w1',
name: 'workspace-1',
version: '0.0.1',
workspace: true,
modular: {
type: 'root',
},
children: [],
parent: null,
dependencies: undefined,
rawPackageJson: {},
},
'workspace-non-root': {
path: 'workspace-non-root',
location: '/workspace-non-root',
name: 'workspace-non-root',
version: '0.0.1',
workspace: false,
modular: {
type: 'package',
},
children: [],
parent: null,
dependencies: undefined,
rawPackageJson: {},
},
}),
),
{
'workspace-root': {
location: '',
workspaceDependencies: [],
mismatchedWorkspaceDependencies: [],
},
'workspace-non-root': {
location: 'packages/workspace-non-root',
workspaceDependencies: [],
mismatchedWorkspaceDependencies: [],
},
},
];

const listOfPackageManifests = [
'/my/path/workspace-root/package.json',
'/my/path/workspace-root/packages/workspace-non-root/package.json',
];

expect(
matchWorkspaces(
listOfPackageManifests,
'/my/path/workspace-root',
workspacesContent,
),
).toEqual(workspacesContent);
});
});
5 changes: 4 additions & 1 deletion packages/modular-scripts/src/utils/getAllWorkspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import type {
ModularWorkspacePackage,
} from '@modular-scripts/modular-types';

type WorkspaceContent = [Map<string, ModularWorkspacePackage>, WorkspaceMap];
export type WorkspaceContent = [
Map<string, ModularWorkspacePackage>,
WorkspaceMap,
];
export interface PackageManagerInfo {
getWorkspaceCommand: string;
formatWorkspaceCommandOutput: (stdout: string) => WorkspaceMap;
Expand Down
65 changes: 65 additions & 0 deletions packages/modular-scripts/src/utils/getChangedWorkspaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import path from 'path';
import pkgUp from 'pkg-up';
import { getDiffedFiles } from './gitActions';
import getModularRoot from './getModularRoot';
import type { WorkspaceContent } from './getAllWorkspaces';

// Gets a list of changed files, then maps them to their workspace and returns a subset of WorkspaceContent
export async function getChangedWorkspaces(
workspaceContent: WorkspaceContent,
targetBranch: string,
): Promise<WorkspaceContent> {
const diffedFiles = getDiffedFiles(targetBranch);
const modularRoot = getModularRoot();

// Resolve each of the changed files to their nearest package.json. The resulting list can contain duplicates and null holes
const packageManifestPaths = await Promise.all(
diffedFiles.map((changedFile) => pkgUp({ cwd: path.dirname(changedFile) })),
);

return matchWorkspaces(packageManifestPaths, modularRoot, workspaceContent);
}

// Match workspace manifest paths to a subset of WorkspaceContent. This function works completely in memory and is test-friendly
export function matchWorkspaces(
packagePaths: (string | null)[],
root: string,
workspaceContent: WorkspaceContent,
): WorkspaceContent {
const [packages, workspaces] = workspaceContent;
const workspaceEntries = Object.entries(workspaces);
const result: WorkspaceContent = [new Map([]), {}];

for (const packagePath of packagePaths) {
const [resultPackages, resultWorkspaces] = result;
// Ignore holes
if (!packagePath) continue;
// Get the package directory from the package.json path and make it relative to the root, for comparison with the original WorkspaceContent
const packageDir = path.relative(root, path.dirname(packagePath));
steveukx marked this conversation as resolved.
Show resolved Hide resolved
// Match the package directory to its entry WorkspaceContent, using pathEquality
const foundEntry = workspaceEntries.find(([_, { location }]) =>
pathEquality(location, packageDir),
);
// If found, insert the entries into the WorkspaceContent that we are building
if (foundEntry) {
const [foundWorkspaceName, foundWorkspace] = foundEntry;
resultWorkspaces[foundWorkspaceName] = foundWorkspace;
const foundPackage = packages.get(foundWorkspaceName);
if (foundPackage) {
resultPackages.set(foundWorkspaceName, foundPackage);
}
}
}
return result;
}

// Path equality !== string equality
function pathEquality(path1: string | null, path2: string | null) {
if (path1 === null || path2 === null) return false;
path1 = path.resolve(path1);
path2 = path.resolve(path2);
// Win32 is case insensitive
return process.platform === 'win32'
? path1.toLowerCase() === path2.toLowerCase()
: path1 === path2;
}
9 changes: 4 additions & 5 deletions packages/modular-scripts/src/utils/gitActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,9 @@ function getModifiedAndUntrackedFileChanges(): string[] {
}

// Get all diffed files from git remote origin HEAD
function getGitDiff(): string[] {
const defaultBranch = getGitDefaultBranch();
function getGitDiff(targetBranch: string): string[] {
sgb-io marked this conversation as resolved.
Show resolved Hide resolved
// get a commit sha between the HEAD of the current branch and git remote origin HEAD
const sha = execa.sync('git', ['merge-base', 'HEAD', defaultBranch], {
const sha = execa.sync('git', ['merge-base', 'HEAD', targetBranch], {
cwd: getModularRoot(),
});

Expand All @@ -85,10 +84,10 @@ function getGitDiff(): string[] {
return [];
}

export function getDiffedFiles(): string[] {
export function getDiffedFiles(targetBranch = getGitDefaultBranch()): string[] {
return Array.from(
new Set([
...getGitDiff(),
...getGitDiff(targetBranch),
...getModifiedAndUntrackedFileChanges(),
]).values(),
);
Expand Down