Skip to content

Commit

Permalink
feat: basic support for workspaces
Browse files Browse the repository at this point in the history
  • Loading branch information
gabidobo committed Aug 23, 2023
1 parent 072355c commit 2e0868e
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 51 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@yarnpkg/lockfile": "1.1.0",
"@yarnpkg/parsers": "3.0.0-rc.39",
"d3-node": "3.0.0",
"fast-glob": "^3.3.1",
"ini": "^4.0.0",
"node-fetch": "^3.3.1",
"ora": "6.1.2",
Expand Down
9 changes: 0 additions & 9 deletions src/files/lockfiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const {exec} = require('child_process');
const {existsWantedLockfile, readWantedLockfile} = require('@pnpm/lockfile-file');
const yarnLockfileParser = require('@yarnpkg/lockfile');
const {parseSyml} = require('@yarnpkg/parsers');
const {UsageError} = require('../errors');

const getCommandVersion = (command) =>
new Promise((resolve) => {
Expand All @@ -19,14 +18,12 @@ const getCommandVersion = (command) =>

const loadLockfiles = async (appPath) => {
const lockfiles = {};
let lockfileFound = false;

// NPM
try {
const lockfileContent = await fs.promises.readFile(path.join(appPath, 'package-lock.json'), {
encoding: 'utf-8',
});
lockfileFound = true;
try {
const lockfileData = JSON.parse(lockfileContent);
lockfiles.npm = {
Expand All @@ -46,7 +43,6 @@ const loadLockfiles = async (appPath) => {
const lockfileContent = await fs.promises.readFile(path.join(appPath, 'yarn.lock'), {
encoding: 'utf-8',
});
lockfileFound = true;
const versionMatch = lockfileContent.match(/yarn lockfile v(\d+)/);
if (versionMatch) {
try {
Expand Down Expand Up @@ -89,7 +85,6 @@ const loadLockfiles = async (appPath) => {

// PNPM
if (await existsWantedLockfile(appPath)) {
lockfileFound = true;
try {
const lockfileData = await readWantedLockfile(appPath, {});
lockfiles.pnpm = {
Expand All @@ -103,10 +98,6 @@ const loadLockfiles = async (appPath) => {
}
}

if (!lockfileFound) {
throw new UsageError('No lockfile found');
}

return lockfiles;
};

Expand Down
25 changes: 15 additions & 10 deletions src/files/packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ const getFolderSize = (folderPath) =>
}
});

const getPackageSize = async (packagePath) => {
try {
let totalSize = await getFolderSize(packagePath);
const modulesPath = path.join(packagePath, 'node_modules');
if (fs.existsSync(modulesPath)) {
totalSize -= await getFolderSize(modulesPath);
}
return totalSize;
} catch (error) {
return undefined;
}
};

const loadInstalledPackages = async (rootPath, subPath = '') => {
let packageAtRootData;
const currentPath = path.join(rootPath, subPath);
Expand All @@ -40,15 +53,7 @@ const loadInstalledPackages = async (rootPath, subPath = '') => {
} catch (error) {}

if (packageAtRootData) {
try {
let totalSize = await getFolderSize(currentPath);
const modulesPath = path.join(currentPath, 'node_modules');
if (fs.existsSync(modulesPath)) {
totalSize -= await getFolderSize(modulesPath);
}
packageAtRootData.size = totalSize;
// eslint-disable-next-line no-empty
} catch (error) {}
packageAtRootData.size = await getPackageSize(currentPath);
}

const subdirectories = (await fs.promises.readdir(currentPath, {withFileTypes: true}))
Expand All @@ -65,4 +70,4 @@ const loadInstalledPackages = async (rootPath, subPath = '') => {
return packageAtRootData ? [packageAtRootData, ...allChildren] : allChildren;
};

module.exports = {loadInstalledPackages};
module.exports = {loadInstalledPackages, getPackageSize};
46 changes: 46 additions & 0 deletions src/files/workspace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const fs = require('fs');
const path = require('path');
const fg = require('fast-glob');

const {loadManifest, getPackageSize} = require('.');

const loadWorkspace = async (startPath) => {
const resolvedAppPath = path.resolve(startPath);
const manifestPath = path.join(resolvedAppPath, 'package.json');
if (fs.existsSync(manifestPath)) {
const manifest = await loadManifest(resolvedAppPath);

if (Array.isArray(manifest.workspaces)) {
const entries = await fg(manifest.workspaces, {
onlyDirectories: true,
unique: true,
cwd: resolvedAppPath,
});

const workspaceProjects = await entries.reduce(async (aggPromise, relativePath) => {
const agg = await aggPromise;
const projectPath = path.join(resolvedAppPath, relativePath);
const projectManifest = await loadManifest(projectPath);
agg.push({
...projectManifest,
relativePath,
size: await getPackageSize(projectPath),
});
return agg;
}, Promise.resolve([]));

return {
path: resolvedAppPath,
workspaceProjects,
};
}
}

if (resolvedAppPath !== '/') {
return loadWorkspace(path.join(resolvedAppPath, '..'));
}

return null;
};

module.exports = {loadWorkspace};
22 changes: 15 additions & 7 deletions src/graph/generateNpmGraph.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
const {processDependenciesForPackage, processPlaceholders, makeNode} = require('./utils');
const {
processDependenciesForPackage,
processPlaceholders,
makeNode,
seedNodes,
} = require('./utils');

const packageNameFromPath = (path) => {
// TODO: For locally linked packages this might not include a `node_modules` string
const parts = path.split('node_modules');
return parts[parts.length - 1].slice(1);
};

const generateNpmGraph = ({packages}) => {
const generateNpmGraph = ({packages, manifest, workspace}) => {
const allPackages = [];
const placeholders = [];
let root = null;

seedNodes({
initialNodes: [manifest, ...(workspace?.workspaceProjects || [])],
allPackages,
placeholders,
});

const root = allPackages[0];

Object.entries(packages).forEach(([packageLocation, packageData]) => {
const {name: originalName, version, resolved, integrity} = packageData;
Expand All @@ -22,10 +34,6 @@ const generateNpmGraph = ({packages}) => {
...(integrity && {integrity}),
});

if (packageLocation === '') {
root = newPackage;
}

processDependenciesForPackage({
dependencies: packageData,
newPackage,
Expand Down
15 changes: 5 additions & 10 deletions src/graph/generatePnpmGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const {
processPlaceholders,
makeNode,
SEMVER_REGEXP,
seedNodes,
} = require('./utils');

const parsePath = (path) => {
Expand All @@ -22,23 +23,17 @@ const parsePath = (path) => {
return {name, version};
};

const generatePnpmGraph = ({data, manifest}) => {
const generatePnpmGraph = ({data, manifest, workspace}) => {
const allPackages = [];
const placeholders = [];
const root = makeNode({
name: manifest.name,
version: manifest.version,
engines: manifest.engines,
});

processDependenciesForPackage({
dependencies: {dependencies: manifest.dependencies, devDependencies: manifest.devDependencies},
newPackage: root,
seedNodes({
initialNodes: [manifest, ...(workspace?.workspaceProjects || [])],
allPackages,
placeholders,
});

allPackages.push(root);
const root = allPackages[0];

Object.entries(data).forEach(([id, packageData]) => {
const {
Expand Down
15 changes: 5 additions & 10 deletions src/graph/generateYarnGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,20 @@ const {
processDependenciesForPackage,
processPlaceholders,
makeNode,
seedNodes,
} = require('./utils');

const generateYarnGraph = ({data, manifest}) => {
const generateYarnGraph = ({data, manifest, workspace}) => {
const allPackages = [];
const placeholders = [];
const root = makeNode({
name: manifest.name,
version: manifest.version,
engines: manifest.engines,
});

processDependenciesForPackage({
dependencies: {dependencies: manifest.dependencies, devDependencies: manifest.devDependencies},
newPackage: root,
seedNodes({
initialNodes: [manifest, ...(workspace?.workspaceProjects || [])],
allPackages,
placeholders,
});

allPackages.push(root);
const root = allPackages[0];

Object.entries(data).forEach(([id, packageData]) => {
const {version, resolved, integrity, resolution, checksum} = packageData;
Expand Down
21 changes: 19 additions & 2 deletions src/graph/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {UsageError} = require('../errors');
const {loadLockfile, loadManifest, loadInstalledPackages} = require('../files');
const {loadWorkspace} = require('../files/workspace');
const {getRegistryDataMultiple} = require('../registry');
const generateNpmGraph = require('./generateNpmGraph');
const generatePnpmGraph = require('./generatePnpmGraph');
Expand All @@ -10,7 +11,16 @@ const generateGraphPromise = async (
appPath,
{packageData, loadDataFrom = false, rootIsShell = false, includeDev = false, onProgress} = {},
) => {
const lockfile = await loadLockfile(appPath);
const workspace = await loadWorkspace(appPath);
let lockfile = await loadLockfile(appPath);

if (!lockfile && workspace) {
lockfile = await loadLockfile(workspace.path);
}

if (!lockfile) {
throw new UsageError('No lockfile found');
}

if (lockfile.error) {
throw new Error(lockfile.error);
Expand All @@ -31,21 +41,28 @@ const generateGraphPromise = async (
'Npm v1 lockfiles are not supported. Please upgrade your lockfile to v2.',
);
}
graph = await generateNpmGraph(lockfile.data);
graph = await generateNpmGraph({
packages: lockfile.data.packages,
manifest,
workspace,
});
} else if (lockfile.manager === 'yarn-classic') {
graph = await generateYarnGraph({
data: lockfile.data,
manifest,
workspace,
});
} else if (lockfile.manager === 'yarn') {
graph = await generateYarnGraph({
data: lockfile.data,
manifest,
workspace,
});
} else if (lockfile.manager === 'pnpm') {
graph = await generatePnpmGraph({
data: lockfile.data?.packages || {},
manifest,
workspace,
});
}

Expand Down
22 changes: 22 additions & 0 deletions src/graph/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,27 @@ const addDependencyGraphData = ({root, processedNodes = [], packageData = []}) =
return root;
};

const seedNodes = ({initialNodes, allPackages, placeholders}) => {
initialNodes.forEach((nodeManifest) => {
const node = makeNode({
name: nodeManifest.name,
version: nodeManifest.version,
engines: nodeManifest.engines,
});

processDependenciesForPackage({
dependencies: nodeManifest,
newPackage: node,
allPackages,
placeholders,
});

processPlaceholders({newPackage: node, placeholders});

allPackages.push(node);
});
};

module.exports = {
SEMVER_REGEXP,
makeNode,
Expand All @@ -308,4 +329,5 @@ module.exports = {
postProcessGraph,
addDependencyGraphData,
normalizeLicense,
seedNodes,
};
Loading

0 comments on commit 2e0868e

Please sign in to comment.