Skip to content

Commit

Permalink
feat: experimental Composer support
Browse files Browse the repository at this point in the history
  • Loading branch information
gabidobo committed Sep 8, 2023
1 parent 5ec513b commit 93c9e4b
Show file tree
Hide file tree
Showing 22 changed files with 710 additions and 284 deletions.
2 changes: 1 addition & 1 deletion src/cli/checkUpdates.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const {getRegistryData} = require('../registry');
module.exports = async () => {
try {
const {version: currentVersion} = await loadManifest(path.join(__dirname, '../..'));
const data = await getRegistryData('@sandworm/audit');
const data = await getRegistryData('npm', '@sandworm/audit');
const latestVersion = data['dist-tags']?.latest;

return semver.lt(currentVersion, latestVersion);
Expand Down
30 changes: 30 additions & 0 deletions src/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// eslint-disable-next-line no-promise-executor-return
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const fetch = async (url, opts) => {
let retryCount = 3;
const nodeFetch = (await import('node-fetch')).default;

while (retryCount > 0) {
try {
// eslint-disable-next-line no-await-in-loop
const responseRaw = await nodeFetch(url, opts);
if (!responseRaw.ok) {
throw new Error(`Error ${responseRaw.status} from registry: ${responseRaw.statusText}`);
} else {
return responseRaw;
}
} catch (e) {
retryCount -= 1;
if (retryCount === 0) {
throw e;
}
// eslint-disable-next-line no-await-in-loop
await sleep(1000);
}
}

throw new Error();
};

module.exports = fetch;
24 changes: 23 additions & 1 deletion src/files/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,33 @@ const outputFilenames = (name, version) => {
};
};

const loadManifest = (appPath) => {
const javascriptManifestPath = path.join(appPath, 'package.json');
const phpManifestPath = path.join(appPath, 'composer.json');

if (fs.existsSync(javascriptManifestPath)) {
const manifest = loadJsonFile(javascriptManifestPath);
return {
...manifest,
language: 'javascript',
};
}
if (fs.existsSync(phpManifestPath)) {
const manifest = loadJsonFile(phpManifestPath);
return {
...manifest,
language: 'php',
};
}

return null;
};

module.exports = {
RESOLVED_ISSUES_FILENAME,
...lockfiles,
...packages,
loadManifest: (appPath) => loadJsonFile(path.join(appPath, 'package.json')),
loadManifest,
loadNpmConfigs,
loadResolvedIssues: (appPath) => loadJsonFile(path.join(appPath, RESOLVED_ISSUES_FILENAME)) || [],
saveResolvedIssues: (appPath, content) =>
Expand Down
28 changes: 25 additions & 3 deletions src/files/lockfiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ const {parseSyml} = require('@yarnpkg/parsers');

const getCommandVersion = (command) =>
new Promise((resolve) => {
exec(`${command} -v`, (err, stdout, stderr) => {
exec(`${command} --version`, (err, stdout, stderr) => {
if (stderr || err) {
resolve(null);
} else {
resolve(stdout?.replace?.('\n', ''));
resolve(stdout?.replace?.('\n', '').match?.(/\d+(\.\d+)+/)?.[0]);
}
});
});
Expand Down Expand Up @@ -98,13 +98,35 @@ const loadLockfiles = async (appPath) => {
}
}

// COMPOSER
try {
const lockfileContent = await fs.promises.readFile(path.join(appPath, 'composer.lock'), {
encoding: 'utf-8',
});
try {
const lockfileData = JSON.parse(lockfileContent);
lockfiles.composer = {
manager: 'composer',
managerVersion: await getCommandVersion('composer'),
data: lockfileData,
lockfileVersion: 1,
};
} catch (err) {
lockfiles.composer = {
manager: 'composer',
error: `Could not parse composer.lock: ${err.message}`,
};
}
// eslint-disable-next-line no-empty
} catch {}

return lockfiles;
};

const loadLockfile = async (appPath) => {
const lockfiles = await loadLockfiles(appPath);

return lockfiles.npm || lockfiles.yarn || lockfiles.pnpm;
return lockfiles.npm || lockfiles.yarn || lockfiles.pnpm || lockfiles.composer;
};

module.exports = {
Expand Down
62 changes: 50 additions & 12 deletions src/files/packages.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const {exec} = require('child_process');
const fs = require('fs');
const path = require('path');
const {normalizeComposerManifest} = require('../registry/utils');

const packageSizeCache = {};

Expand Down Expand Up @@ -41,19 +42,56 @@ const getPackageSize = async (packagePath) => {
};

const loadInstalledPackages = async (rootPath, subPath = '') => {
let packageAtRootData;
const currentPath = path.join(rootPath, subPath);
try {
const manifestContent = await fs.promises.readFile(path.join(currentPath, 'package.json'), {
encoding: 'utf-8',
});
packageAtRootData = JSON.parse(manifestContent);
packageAtRootData.relativePath = subPath;
// eslint-disable-next-line no-empty
} catch (error) {}
const currentDirname = currentPath.split(path.sep).pop();
const manifestFilenames = {
npm: 'package.json',
composer: 'composer.json',
};

let packagesAtRoot = (
await Promise.all(
Object.entries(manifestFilenames).map(async ([manager, manifestFilename]) => {
try {
const manifestContent = await fs.promises.readFile(
path.join(currentPath, manifestFilename),
{
encoding: 'utf-8',
},
);
const packageAtRootData = JSON.parse(manifestContent);
packageAtRootData.relativePath = subPath;
packageAtRootData.packageType = manager;
packageAtRootData.size = await getPackageSize(currentPath);

return packageAtRootData;
// eslint-disable-next-line no-empty
} catch (error) {
return null;
}
}),
)
).filter((m) => m && m.name && m.version);

if (packageAtRootData) {
packageAtRootData.size = await getPackageSize(currentPath);
if (currentDirname === 'composer' && fs.existsSync(path.join(currentPath, 'installed.json'))) {
try {
const composerInstalledData = await fs.promises.readFile(
path.join(currentPath, 'installed.json'),
{
encoding: 'utf-8',
},
);
const composerInstalled = JSON.parse(composerInstalledData);
packagesAtRoot = await Promise.all(
(composerInstalled.packages || []).map(async (p) => ({
...normalizeComposerManifest(p),
relativePath: path.join(subPath, p['install-path']),
packageType: 'composer',
size: await getPackageSize(path.join(currentPath, p['install-path'])),
})),
);
// eslint-disable-next-line no-empty
} catch (error) {}
}

const subdirectories = (await fs.promises.readdir(currentPath, {withFileTypes: true}))
Expand All @@ -67,7 +105,7 @@ const loadInstalledPackages = async (rootPath, subPath = '') => {
return [...children, ...subDirChildren];
}, Promise.resolve([]));

return packageAtRootData ? [packageAtRootData, ...allChildren] : allChildren;
return [...packagesAtRoot, ...allChildren];
};

module.exports = {loadInstalledPackages, getPackageSize};
54 changes: 54 additions & 0 deletions src/graph/generateComposerGraph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const {
processDependenciesForPackage,
processPlaceholders,
makeNode,
seedNodes,
} = require('./utils');

const generateComposerGraph = ({data, manifest}) => {
const allPackages = [];
const placeholders = [];

seedNodes({
initialNodes: [
{
...manifest,
dependencies: manifest.require,
devDependencies: manifest['require-dev'],
},
],
allPackages,
placeholders,
altTilde: true,
});

const root = allPackages[0];

(data.packages || []).forEach((packageData) => {
const {name, version} = packageData;

const newPackage = makeNode({
name,
version,
});

processDependenciesForPackage({
dependencies: {
dependencies: packageData.require,
devDependencies: packageData['require-dev'],
},
newPackage,
allPackages,
placeholders,
altTilde: true,
});

processPlaceholders({newPackage, placeholders});

allPackages.push(newPackage);
});

return {root, allPackages};
};

module.exports = generateComposerGraph;
31 changes: 21 additions & 10 deletions src/graph/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
const {UsageError} = require('../errors');
const {loadLockfile, loadManifest, loadInstalledPackages} = require('../files');
const {loadWorkspace} = require('../files/workspace');
const {postProcessGraph, addDependencyGraphData} = require('./utils');
const {getRegistryData} = require('../registry');
const generateNpmGraph = require('./generateNpmGraph');
const generatePnpmGraph = require('./generatePnpmGraph');
const generateYarnGraph = require('./generateYarnGraph');
const {postProcessGraph, addDependencyGraphData} = require('./utils');
const {getRegistryData} = require('../registry');
const generateComposerGraph = require('./generateComposerGraph');

const generateGraphPromise = async (
appPath,
Expand Down Expand Up @@ -42,13 +43,7 @@ const generateGraphPromise = async (
manifest,
workspace,
});
} else if (lockfile.manager === 'yarn-classic') {
graph = await generateYarnGraph({
data: lockfile.data,
manifest,
workspace,
});
} else if (lockfile.manager === 'yarn') {
} else if (lockfile.manager === 'yarn' || lockfile.manager === 'yarn-classic') {
graph = await generateYarnGraph({
data: lockfile.data,
manifest,
Expand All @@ -60,6 +55,11 @@ const generateGraphPromise = async (
manifest,
workspace,
});
} else if (lockfile.manager === 'composer') {
graph = await generateComposerGraph({
data: lockfile.data,
manifest,
});
}

const {root, allPackages} = graph;
Expand Down Expand Up @@ -89,8 +89,18 @@ const generateGraphPromise = async (
}

if (loadDataFrom === 'disk') {
const managersToPackageType = {
npm: 'npm',
yarn: 'npm',
'yarn-classic': 'npm',
pnpm: 'npm',
composer: 'composer',
};
const installedPackages = await loadInstalledPackages(workspace?.path || appPath);
additionalPackageData = additionalPackageData.concat(
await loadInstalledPackages(workspace?.path || appPath),
installedPackages.filter(
({packageType}) => packageType === managersToPackageType[lockfile.manager],
),
);
}

Expand All @@ -99,6 +109,7 @@ const generateGraphPromise = async (
const registryErrors = await addDependencyGraphData({
root: processedRoot,
packageData: additionalPackageData,
packageManager: lockfile.manager,
loadDataFrom,
includeDev,
getRegistryData,
Expand Down
Loading

0 comments on commit 93c9e4b

Please sign in to comment.