patternplate-server
Version:
Programmatically serve atomic patterns via a REST API
382 lines (308 loc) • 13.9 kB
JavaScript
import assert from 'assert';
import {debuglog} from 'util';
import denodeify from 'denodeify';
import {resolve, join, dirname, extname, relative} from 'path';
import {readFile as readFileNodeback} from 'fs';
import boxen from 'boxen';
import {find, flatten, isEqual, omit, uniq} from 'lodash';
import {padEnd} from 'lodash/fp';
import throat from 'throat';
import chalk from 'chalk';
import exists from 'path-exists';
import coreModuleNames from 'node-core-module-names';
import {resolvePathFormatString} from 'patternplate-transforms-core';
import ora from 'ora';
import {ok, wait, ready} from '../../../library/log/decorations';
import {loadTransforms} from '../../../library/transforms';
import {normalizeFormats} from '../../../library/pattern';
import copyStatic from '../common/copy-static';
import getArtifactMtimes from '../../../library/utilities/get-artifact-mtimes';
import getArtifactsToPrune from '../../../library/utilities/get-artifacts-to-prune';
import getPackageString from './get-package-string';
import getPatternMtimes from '../../../library/utilities/get-pattern-mtimes';
import getPatterns from '../../../library/utilities/get-patterns';
import getPatternsToBuild from '../../../library/utilities/get-patterns-to-build';
import removeFile from '../../../library/filesystem/remove-file';
import writeSafe from '../../../library/filesystem/write-safe';
const pkg = require(resolve(process.cwd(), 'package.json'));
const readFile = denodeify(readFileNodeback);
const pathFormatString = '%(outputName)s/%(patternId)s/index.%(extension)s';
const where = `Configure it at configuration/patternplate-server/tasks.js.`;
const fallbackManifest = {
dependencies: {},
devDependencies: {},
ppcommonjs: {},
ppDependencies: {},
ppDevdependencies: {}
};
async function exportAsCommonjs(application, settings) {
assert(typeof settings.patterns === 'object', `build-commonjs needs a valid patterns configuration. ${where} build-commonjs.patterns`);
assert(typeof settings.patterns.formats === 'object', `build-commonjs needs a valid patterns.formats configuration. ${where} build-commonjs.patterns.formats`);
assert(typeof settings.transforms === 'object', `build-commonjs needs a valid transforms configuration. ${where} build-commonjs.transforms`);
let spinner = ora().start();
const debug = debuglog('commonjs');
debug('calling commonjs with');
debug(settings);
const cwd = application.runtime.patterncwd || application.runtime.cwd;
const patternRoot = resolve(cwd, 'patterns');
const commonjsRoot = resolve(cwd, settings.out || join('build', 'build-commonjs'));
const manifestPath = resolve(commonjsRoot, 'package.json');
const filters = {...settings.filters || {}, baseNames: ['index']};
const warnings = [];
const warn = application.log.warn;
application.log.warn = (...args) => {
if (args.some(arg => arg.includes('Deprecation'))) {
warnings.push(args);
return;
}
warn(...args);
};
// Override pattern config
settings.patterns.formats = normalizeFormats(settings.patterns.formats);
application.configuration.patterns = settings.patterns;
// Reinitialize transforms
application.configuration.transforms = settings.transforms || {};
application.transforms = (await loadTransforms(settings.transforms || {}))(application);
// start reading pattern mtimes, ignore dependencies
const mtimesStart = new Date();
application.log.debug(wait`Obtaining pattern modification times`);
const readingPatternMtimes = getPatternMtimes('./patterns', {
resolveDependencies: false,
filters
});
// start reading artifact mtimes
const artifactMtimesStart = new Date();
const readingArtifactMtimes = getArtifactMtimes(
commonjsRoot,
application.configuration.patterns,
application.configuration.transforms
);
// wait for all mtimes to trickle in
const patternMtimes = await readingPatternMtimes;
application.log.debug(ok`Read pattern modification times ${mtimesStart}`);
// wait for all artifact mtimes
const artifactMtimes = await readingArtifactMtimes;
application.log.debug(ok`Read artifact modification times ${artifactMtimesStart}`);
// check if package.json is in distribution
const hasManifest = await exists(resolve(commonjsRoot, 'package.json'));
const previousPkgString = hasManifest ? (await readFile(manifestPath)).toString('utf-8') : null;
const pkgConfig = settings.pkg || {};
const previousPkg = hasManifest ? parseJSON(previousPkgString) : fallbackManifest;
const previousPkgConfig = previousPkg.ppcommonjs || {};
const previousDependencies = previousPkg.ppDependencies || {};
const previousDevdependencies = previousPkg.ppDevdependencies || {};
const pkgConfigChanged = !isEqual(previousPkgConfig, pkgConfig) ||
!isEqual(previousDependencies, pkg.dependencies || {}) ||
!isEqual(previousDevdependencies, pkg.devDependencies || {});
// obtain patterns we have to build
application.log.debug(wait`Calculating pattern collection to build`);
let buildCount = 1;
const patternFilter = pkgConfigChanged ? i => i :
getPatternsToBuild(artifactMtimes, application.configuration.patterns);
const patternsToBuild = patternMtimes
.filter(patternFilter)
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
const padMaxBuild = padEnd(patternsToBuild.map(pattern => pattern.id.length)
.reduce((a, b) => a > b ? a : b, 0) + 1);
if (pkgConfigChanged) {
application.log.debug(ok`Manifest or pkg config change, building all ${patternMtimes.length} patterns`);
}
const pruneDetectionStart = new Date();
application.log.debug(wait`Searching for artifacts to prune`);
let pruneCount = 1;
const artifactsToPrune = getArtifactsToPrune(commonjsRoot, patternMtimes, artifactMtimes, {
resolve: pathFormatString,
formats: settings.patterns.formats,
transforms: settings.transforms
});
const padMaxPrune = padEnd(artifactsToPrune.map(artifact => artifact.length)
.reduce((a, b) => a > b ? a : b, 0) + 1);
application.log.debug(ok`Detected ${artifactsToPrune.length} artifacts to prune ${pruneDetectionStart}`);
const pruneStart = new Date();
application.log.debug(wait`Pruning ${artifactsToPrune.length} artifacts`);
const pruning = Promise.all(artifactsToPrune.map(throat(1, async path => {
if (settings['dry-run']) {
return Promise.resolve();
}
spinner.text = `prune ${padMaxPrune(path)} ${pruneCount}/${artifactsToPrune.length}`;
await removeFile(dirname(path));
pruneCount += 1;
})));
const pruned = await pruning;
application.log.debug(ready`Pruned ${pruned.length} artifact files ${pruneStart}`);
spinner.text = `${pruned.length}/${artifactsToPrune.length} pruned`;
spinner.succeed();
spinner.stop();
spinner = ora().start();
// build patterns in parallel
const buildStart = new Date();
const building = Promise.all(patternsToBuild.map(throat(1, async pattern => {
const filterStart = new Date();
application.log.debug(wait`Checking for files of ${pattern.id} to exclude from transform.`);
let changedFiles = [];
// enhance filters config to build only files that are modified
const artifact = find(artifactMtimes, {id: pattern.id});
if (artifact) {
// build up mtime registry for pattern files
const filesMtimes = pattern.files.reduce((results, file, index) => {
return {...results, [file]: pattern.mtimes[index]};
}, {});
// build up registry for artifact files
const artifactFilesMtimes = artifact.files.reduce((results, file, index) => {
const path = relative(commonjsRoot, file);
return {...results, [path]: artifact.mtimes[index]};
}, {});
// find pattern files with newer mtime than
// - their artifact
// - their folder
// - their pattern.json
changedFiles = pattern.files.filter(file => {
const formatKey = extname(file).slice(1);
const format = application.configuration.patterns.formats[formatKey];
if (!format) {
return false;
}
const transformNames = format.transforms || [];
const lastTransformName = transformNames[transformNames.length - 1];
const lastTransform = application.configuration.transforms[lastTransformName] || {};
const targetExtension = lastTransform.outFormat || formatKey;
const targetFile = resolvePathFormatString(
pathFormatString,
pattern.id,
format.name.toLowerCase(),
targetExtension
);
const targetFileMtime = artifactFilesMtimes[targetFile] || 0;
const fileMtime = filesMtimes[file];
const dirMtime = filesMtimes[dirname(file)];
const metaMtime = filesMtimes[join(dirname(file), 'pattern.json')];
return fileMtime > targetFileMtime ||
dirMtime > targetFileMtime ||
metaMtime > targetFileMtime;
})
.filter(Boolean);
}
if (artifact) {
filters.in3 = changedFiles.map(file => extname(file).slice(1));
const formats = chalk.grey(`[${filters.inFormats.join(', ')}]`);
application.log.debug(
ok`Building ${filters.inFormats.length} files for ${pattern.id} ${formats} ${filterStart}`);
} else {
application.log.debug(ok`Building all files for ${pattern.id} ${filterStart}`);
}
if (settings['dry-run']) {
return Promise.resolve({});
}
const transformStart = new Date();
application.log.debug(wait`Transforming pattern ${pattern.id}`);
spinner.text = `build ${padMaxBuild(pattern.id)} ${buildCount}/${patternsToBuild.length}`;
buildCount += 1;
// obtain transformed pattern by id
const patternList = await getPatterns({
id: pattern.id,
base: patternRoot,
config: application.configuration,
factory: application.pattern.factory,
transforms: application.transforms,
log: application.log,
filters
}, application.cache);
application.log.debug(ok`Transformed pattern ${pattern.id} ${transformStart}`);
const writeStart = new Date();
application.log.debug(ok`Writing artifacts of ${pattern.id}`);
// Write results to disk
const writingArtifacts = Promise.all(patternList.map(async patternItem => {
const writingPatternItems = Promise.all(
Object.entries(patternItem.results)
.map(async resultsEntry => {
const [resultName, result] = resultsEntry;
const relativePath = resolvePathFormatString(pathFormatString, patternItem.id, resultName.toLowerCase(), result.out);
const resultPath = join(commonjsRoot, relativePath);
return writeSafe(resultPath, result.buffer);
}));
return await writingPatternItems;
}));
const written = await writingArtifacts;
application.log.debug(ok`Wrote ${written.length} artifacts for ${pattern.id} ${writeStart}`);
return patternList;
})));
const built = await building;
application.log.debug(ready`Built ${built.length} from ${patternsToBuild.length} planned and ${patternMtimes.length} artifacts overall ${buildStart}`);
spinner.text = `${built.length}/${patternsToBuild.length} built`;
spinner.succeed();
if (settings['dry-run']) {
await building;
spinner.text = `Dry-run executed successfully ${buildStart}`;
spinner.succeed();
return;
}
if (application.resources) {
const resources = application.resources
.filter(r => Boolean(r.pattern))
.filter(r => Boolean(r.file));
await Promise.all(resources.map(async resource => {
const format = resource.file.format;
const formatConfig = application.configuration.patterns.formats[format];
const resourcePath = resolvePathFormatString(pathFormatString, resource.pattern, formatConfig.name, resource.type);
const artifactPath = join(commonjsRoot, resourcePath);
return writeSafe(artifactPath, await resource.content);
}));
}
const copyStart = new Date();
application.log.debug(wait`Copying static files`);
await copyStatic(cwd, commonjsRoot);
application.log.debug(ready`Copied static files. ${copyStart}`);
spinner.text = `static files copied`;
spinner.succeed();
// Extract dependency information
const dependencyLists = flatten(built).reduce((registry, patternItem) => {
const {meta: {dependencies, devDependencies}} = patternItem;
return {
dependencies: [...registry.dependencies, ...(dependencies || [])],
devDependencies: [...registry.devDependencies, ...(devDependencies || [])]
};
}, {
dependencies: [],
devDependencies: []
});
const deps = pkg.dependencies || {};
const devDeps = pkg.devDependencies || {};
const dependencies = dependencyLists.dependencies
.reduce((results, dependencyName) => {
return {...results,
[dependencyName]: deps[dependencyName] || devDeps[dependencyName] || '*'};
}, previousPkg.dependencies);
const devDependencies = dependencyLists.devDependencies
.reduce((results, dependencyName) => {
return {...results,
[dependencyName]: deps[dependencyName] || devDeps[dependencyName] || '*'};
}, previousPkg.devDependencies);
const prunedDependencies = omit(dependencies, [...(settings.ignoredDependencies || []), coreModuleNames]);
const prunedDevDependencies = omit(devDependencies, [...(settings.ignoredDevDependencies || []), coreModuleNames, ...Object.keys(dependencies)]);
const updatedPackageString = getPackageString(prunedDependencies, previousPkg,
{
devDependencies: prunedDevDependencies, ppcommonjs: pkgConfig,
ppDependencies: pkg.dependencies, ppDevdependencies: pkg.devDependencies
},
omit(pkg, ['dependencies', 'devDependencies', 'scripts', 'config', 'main']),
settings.pkg);
if (updatedPackageString !== previousPkgString) {
const pkgStart = new Date();
application.log.debug(wait`Writing package.json`);
await writeSafe(manifestPath, updatedPackageString);
application.log.debug(ready`Wrote package.json ${pkgStart}`);
}
const messages = uniq(warnings)
.map(warning => warning.join(' '));
messages.forEach(message => {
console.log(boxen(message, {borderColor: 'yellow', padding: 1}));
});
}
export default exportAsCommonjs;
function parseJSON(jsonString) {
try {
return JSON.parse(jsonString);
} catch (error) {
return fallbackManifest;
}
}