d29b8e65ec
refs bd6a295674
- earlier this week I refactored this block of code to get rid of
explicit `stat` calls, to make the fs operations a little more
lightweight
- I inadvertantly forgot that readdir doesn't follow symlinks, and we
were previously use stat that does, so it was ignoring themes that were
symlinked into `content/themes`
- instead of rolling back my change, I've added an if-statement to call
`fs.stat` and check the origin of the symlink to see if it's a
directory
- also added a test that fails without this change
159 lines
5.0 KiB
JavaScript
159 lines
5.0 KiB
JavaScript
/*
|
|
* # Package Utils
|
|
*
|
|
* Ghost has support for several different types of sub-packages:
|
|
* - Themes: have always been packages, but we're going to lean more heavily on npm & package.json in future
|
|
* - Adapters: replace fundamental pieces like storage, will become npm modules
|
|
*
|
|
* These utils facilitate loading, reading, managing etc, packages from the file system.
|
|
*
|
|
*/
|
|
const _ = require('lodash');
|
|
const fs = require('fs-extra');
|
|
const join = require('path').join;
|
|
const errors = require('@tryghost/errors');
|
|
const parse = require('./parse');
|
|
|
|
// Require bluebird with its own namespace and use it explicitly where we need additional features
|
|
const Bluebird = require('bluebird');
|
|
|
|
const notAPackageRegex = /^\.|_messages|README.md|node_modules|bower_components/i;
|
|
const packageJSONPath = 'package.json';
|
|
|
|
/**
|
|
* @typedef {object} PackageList
|
|
* @typedef {object} Package
|
|
*/
|
|
|
|
/**
|
|
* Recursively read directory and find the packages in it
|
|
*
|
|
* @param {string} absolutePath
|
|
* @param {string} packageName
|
|
* @returns {Promise<Package>}
|
|
*/
|
|
async function processPackage(absolutePath, packageName) {
|
|
const pkg = {
|
|
name: packageName,
|
|
path: absolutePath
|
|
};
|
|
|
|
try {
|
|
const packageJSON = await parse(join(absolutePath, packageJSONPath));
|
|
pkg['package.json'] = packageJSON;
|
|
} catch (err) {
|
|
// ignore invalid package.json for now,
|
|
// because Ghost does not rely/use them at the moment
|
|
// in the future, this .catch() will need to be removed,
|
|
// so that error is thrown on invalid json syntax
|
|
pkg['package.json'] = null;
|
|
}
|
|
|
|
return pkg;
|
|
}
|
|
|
|
/**
|
|
* ### Filter Packages
|
|
* Normalizes packages read by readPackages so that the themes module can use them.
|
|
* Iterates over each package and return an array of objects which are simplified representations of the package
|
|
* with 3 properties:
|
|
*
|
|
* @typedef {object} SimplePackage
|
|
* @prop {string} name - the package name
|
|
* @prop {object|boolean} package - contents of the package.json or false if there isn't one
|
|
* @prop {boolean} active - set to true if this package is active
|
|
*
|
|
* This data structure is used for listings of packages provided over the API and as such
|
|
* deliberately combines multiple sources of information in order to be efficient.
|
|
*
|
|
* TODO: simplify the package.json representation to contain only fields we use
|
|
*
|
|
* @param {PackageList} packages object made up of packages keyed by name as returned by readPackages
|
|
* @param {array|string} [active] optional set of names of packages that are active
|
|
* @returns {Array<SimplePackage>} array of objects with useful info about themes
|
|
*/
|
|
function filter(packages, active) {
|
|
// turn active into an array if it isn't one, so this function can deal with lists and one-offs
|
|
if (!Array.isArray(active)) {
|
|
active = [active];
|
|
}
|
|
|
|
return _.reduce(packages, function (result, pkg, key) {
|
|
let item = {};
|
|
if (!key.match(notAPackageRegex)) {
|
|
item = {
|
|
name: key,
|
|
package: pkg['package.json'] || false,
|
|
active: _.indexOf(active, key) !== -1
|
|
};
|
|
|
|
result.push(item);
|
|
}
|
|
|
|
return result;
|
|
}, []);
|
|
}
|
|
|
|
/**
|
|
* @param {string} packagePath
|
|
* @param {string} packageName
|
|
* @returns {Promise<Package>}
|
|
*/
|
|
async function readPackage(packagePath, packageName) {
|
|
const absolutePath = join(packagePath, packageName);
|
|
|
|
try {
|
|
const stat = await fs.stat(absolutePath);
|
|
if (!stat.isDirectory()) {
|
|
return {};
|
|
}
|
|
|
|
const pkg = await processPackage(absolutePath, packageName);
|
|
const res = {};
|
|
res[packageName] = pkg;
|
|
return res;
|
|
} catch (err) {
|
|
return Promise.reject(new errors.NotFoundError({
|
|
message: 'Package not found',
|
|
err: err,
|
|
help: 'path: ' + packagePath,
|
|
context: 'name: ' + packageName
|
|
}));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} packagePath
|
|
* @returns {Promise<PackageList>}
|
|
*/
|
|
async function readPackages(packagePath) {
|
|
return Bluebird.resolve(fs.readdir(packagePath, {withFileTypes: true}))
|
|
.filter(async function (packageFile) {
|
|
// Filter out things which are not packages by regex
|
|
if (packageFile.name.match(notAPackageRegex)) {
|
|
return;
|
|
}
|
|
|
|
if (packageFile.isSymbolicLink()) {
|
|
const packageFileOrig = await fs.stat(join(packagePath, packageFile.name));
|
|
return packageFileOrig.isDirectory();
|
|
}
|
|
|
|
// Check the remaining items to ensure they are a directory
|
|
return packageFile.isDirectory();
|
|
})
|
|
.map(function readPackageJson(packageFile) {
|
|
const absolutePath = join(packagePath, packageFile.name);
|
|
return processPackage(absolutePath, packageFile.name);
|
|
})
|
|
.then(function (packages) {
|
|
return _.keyBy(packages, 'name');
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
filter,
|
|
readPackage,
|
|
readPackages
|
|
};
|