1daa6eda61
- fixed type warnings - introduced explicit use of bluebird to help with type warnings and also make it clear when we're actually using Bluebird features
156 lines
4.9 KiB
JavaScript
156 lines
4.9 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))
|
|
.filter(function (packageName) {
|
|
// Filter out things which are not packages by regex
|
|
if (packageName.match(notAPackageRegex)) {
|
|
return;
|
|
}
|
|
// Check the remaining items to ensure they are a directory
|
|
return fs.stat(join(packagePath, packageName))
|
|
.then(function (stat) {
|
|
return stat.isDirectory();
|
|
});
|
|
})
|
|
.map(function readPackageJson(packageName) {
|
|
const absolutePath = join(packagePath, packageName);
|
|
return processPackage(absolutePath, packageName);
|
|
})
|
|
.then(function (packages) {
|
|
return _.keyBy(packages, 'name');
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
filter,
|
|
readPackage,
|
|
readPackages
|
|
};
|