fsUtil = require('safefs')
pathUtil = require('path')
First we need to import the libraries we require.
Load in the file system libraries.
SafeFS is aliased to fsUtil
as it provides protection against a lot of the common gotchas
fsUtil = require('safefs')
pathUtil = require('path')
CSON = require('cson')
TypeChecker is used for checking data types
typeChecker = require('typechecker')
{TaskGroup} = require('taskgroup')
eachr = require('eachr')
extendr = require('extendr')
Load in our other project files
backerUtil = require('./backer-util')
badgeUtil = require('./badge-util')
historyUtil = require('./history-util')
installUtil = require('./install-util')
licenseUtil = require('./license-util')
projectzUtil = require('./projectz-util')
class Projectz
cwd: null
The absolute paths for all the package files
pathsForPackages: null
The absolute paths for all the readme files
pathsForReadmes: null
The data for each of our packages
dataForPackages: null
The merged data for each of our packages
dataForPackagesMerged: null
The enhanced data for each of our packages
dataForPackagesEnhanced: null
The data for each of our readme files
dataForReadmes: null
The enhanced data for each of our readme files
dataForReadmesEnhanced: null
Our log function to use
log: null
Options:
cwd
the directory that we wish to do our work on, defaults to process.cwd()
log
the log function to use, first argument being the log levelUsage:
project = require('projectz').create(opts)
project = new (require('projectz').Projectz)(opts)
constructor: (opts={}) ->
Apply our current working directory
@cwd = pathUtil.resolve(opts.cwd) if opts.cwd
@cwd ?= process.cwd()
Apply our logging function
@log = opts.log or null
Apply our determined paths for packages
@pathsForPackages =
projectz: pathUtil.join(@cwd, 'projectz.cson')
package: pathUtil.join(@cwd, 'package.json')
bower: pathUtil.join(@cwd, 'bower.json')
component: pathUtil.join(@cwd, 'component.json')
jquery: pathUtil.join(@cwd, 'jquery.json')
Apply our determined paths for readmes
@pathsForReadmes =
readme: pathUtil.join(@cwd, 'README.md')
history: pathUtil.join(@cwd, 'HISTORY.md')
contributing: pathUtil.join(@cwd, 'CONTRIBUTING.md')
backers: pathUtil.join(@cwd, 'BACKERS.md')
license: pathUtil.join(@cwd, 'LICENSE.md')
log: (args...) =>
@config.log?(args...)
@
load: (next) ->
Reset/apply our data for the different properties
@dataForPackages = {}
@dataForPackagesMerged = {}
@dataForPackagesEnhanced = {}
@dataForReadmes = {}
@dataForReadmesEnhanced = {}
Create our serial task group to allot our tasks into and once it completes continue to the next handler
tasks = new TaskGroup().once('complete', next)
Load readme and package data
tasks.addTask @loadPaths.bind(@)
Merge our package data
tasks.addTask @mergePackages.bind(@)
Fetch the latest contributors. This is after the merging as we access merged properties to be able to do this.
tasks.addTask @loadContributors.bind(@)
Enhance our package data
tasks.addTask @enhancePackages.bind(@)
Enhance our readme data
tasks.addTask @enhanceReadmes.bind(@)
Finish up
return tasks.run(); @
Fetch the contributors for the repo if we have it
Usage: loadContributors (err) ->
loadContributors: (next) ->
log = @log
Check if we have the repo data, if we don't then we should exit and chain right away
repo = @dataForPackagesMerged.repo
unless repo
log('info', 'Skipping loading contributors as project repo is not defined')
return next(); @
If we do have a repo, then fetch the contributor data for it
fetchContributors = require('getcontributors').create(log: @log)
fetchContributors.fetchContributorsFromRepos [repo], (err,result) =>
return next(err) if err
@dataForPackagesMerged.contributors = contributors = fetchContributors.getContributors()
log('info', "Loaded #{contributors.length} contributors from #{repo} repository")
return next()
Finish up
return @
loadPaths: (next) ->
Create the parallel task group and once they've all completed fire our completion callback
tasks = new TaskGroup().setConfig(concurrency:0).once('complete', next)
First load in the packages
tasks.addTask (complete) =>
@loadPackages @pathsForPackages, (err,dataForPackages) =>
return complete(err) if err
@dataForPackages = dataForPackages
return complete()
Then load in our readmes
tasks.addTask (complete) =>
@loadReadmes @pathsForReadmes, (err,dataForReadmes) =>
return complete(err) if err
@dataForReadmes = dataForReadmes
return complete()
Finish up
return tasks.run(); @
Load in the packages we have specified
Usage: loadPackages paths, (err, dataForPackages) ->
loadPackages: (pathsForPackages, next) ->
dataForPackages = {}
tasks = new TaskGroup().setConfig(concurrency:0).once 'complete', (err) ->
return next(err) if err
return next(null, dataForPackages)
eachr pathsForPackages, (value,key) ->
tasks.addTask (complete) ->
dataForPackages[key] = null
fsUtil.exists value, (exists) ->
return complete() if exists is false
CSON.parseFile value, (err,data) ->
return complete(err) if err
dataForPackages[key] = data
return complete()
return true
Finish up
return tasks.run(); @
Load in the readmes we have specified
Usage: loadPackages paths, (err, dataForReadmes) ->
loadReadmes: (pathsForReadmes, next) ->
dataForReadmes = {}
tasks = new TaskGroup().setConfig(concurrency:0).once 'complete', (err) ->
return next(err) if err
return next(null, dataForReadmes)
eachr pathsForReadmes, (value,key) ->
tasks.addTask (complete) ->
dataForReadmes[key] = null
fsUtil.exists value, (exists) ->
return complete() if exists is false
fsUtil.readFile value, (err,data) ->
return complete(err) if err
dataForReadmes[key] = data.toString()
return complete()
Finish up
return tasks.run(); @
mergePackages: (next) ->
By first merging in all the package data together into the enhanced data
extendr.extend(
@dataForPackagesMerged
@dataForPackages.component
@dataForPackages.bower
@dataForPackages.jquery
@dataForPackages.package
@dataForPackages.projectz
)
@dataForPackagesMerged.badges ?= {}
@dataForPackagesMerged.readmes ?= {}
@dataForPackagesMerged.packages ?= {}
eachr @dataForReadmes, (value, name) =>
@dataForPackagesMerged.readmes[name] = @dataForPackages.readmes?[name] ? value?
return true
eachr @dataForPackages, (value, name) =>
@dataForPackagesMerged.packages[name] = @dataForPackages.packages?[name] ? value?
return true
Fallback demo field, by scanning homepage
@dataForPackagesMerged.homepage ?= @dataForPackagesMerged.homepage
@dataForPackagesMerged.demo ?= @dataForPackagesMerged.homepage
Fallback repo, by scanning repository and homepage
unless @dataForPackagesMerged.repo
if @dataForPackagesMerged.repository?.url
@dataForPackagesMerged.repo = @dataForPackagesMerged.repository?.url
else if (@dataForPackagesMerged.homepage or '').indexOf('github.com') isnt -1
@dataForPackagesMerged.repo = @dataForPackagesMerged.homepage
Extract out repo full name from urls
if @dataForPackagesMerged.repo
@dataForPackagesMerged.repo = @dataForPackagesMerged.repo.replace(/^.+?github.com\//, '').replace(/(\.git|\/)+$/, '') or null
Fallback title from name
if @dataForPackagesMerged.name
@dataForPackagesMerged.title ?= @dataForPackagesMerged.name
Fallback username out of repo
if @dataForPackagesMerged.repo
@dataForPackagesMerged.username ?= @dataForPackagesMerged.repo.replace(/\/.*$/, '')
Fallback repository field, by scanning repo
if @dataForPackagesMerged.repo
@dataForPackagesMerged.repository ?= {
type: 'git'
url: "https://github.com/#{@dataForPackagesMerged.repo}.git"
}
@dataForPackagesMerged.bugs ?= {
url: "https://github.com/#{@dataForPackagesMerged.repo}/issues"
}
Enhance keywords, with CSV format
@dataForPackagesMerged.keywords = @dataForPackagesMerged.keywords.split(/[,\n]+/) if typeChecker.isString(@dataForPackagesMerged.keywords)
Fallback contributors
@dataForPackagesMerged.contributors ?= []
Fallback sponsors
@dataForPackagesMerged.sponsors ?= []
@dataForPackagesMerged.sponsors = @dataForPackagesMerged.sponsors.split(/[,\n]/).map((i)->i.trim()) if typeChecker.isString(@dataForPackagesMerged.sponsors)
Fallback maintainers
@dataForPackagesMerged.maintainers ?= @dataForPackagesMerged.maintainer or ''
@dataForPackagesMerged.maintainers = @dataForPackagesMerged.maintainers.split(/[,\n]/).map((i)->i.trim()) if typeChecker.isString(@dataForPackagesMerged.maintainers)
@dataForPackagesMerged.maintainers = [@dataForPackagesMerged.maintainers] unless typeChecker.isArray(@dataForPackagesMerged.maintainers)
@dataForPackagesMerged.maintainer = @dataForPackagesMerged.maintainers.join(', ')
Fallback authors
@dataForPackagesMerged.authors ?= @dataForPackagesMerged.author or ''
@dataForPackagesMerged.authors = @dataForPackagesMerged.authors.split(/[,\n]/).map((i)->i.trim()) if typeChecker.isString(@dataForPackagesMerged.authors)
@dataForPackagesMerged.authors = [@dataForPackagesMerged.authors] unless typeChecker.isArray(@dataForPackagesMerged.authors)
@dataForPackagesMerged.author = @dataForPackagesMerged.authors.join(', ')
Fallback licenses
@dataForPackagesMerged.licenses ?= @dataForPackagesMerged.license or ''
@dataForPackagesMerged.licenses = @dataForPackagesMerged.licenses.split(/[,\n]/).map((i)->i.trim()) if typeChecker.isString(@dataForPackagesMerged.licenses)
@dataForPackagesMerged.licenses = [@dataForPackagesMerged.licenses] unless typeChecker.isArray(@dataForPackagesMerged.licenses)
@dataForPackagesMerged.licenses = @dataForPackagesMerged.licenses.map (value) =>
value = {type:value} if typeChecker.isString(value)
return value
delete @dataForPackagesMerged.license
Finish up
return next(); @
enhancePackages: (next) ->
Create the data for the package.json
format
@dataForPackagesEnhanced.package = extendr.extend(
New Object
{}
Old Data
@dataForPackages.package
Enhanced Data
{
name: @dataForPackagesMerged.name
version: @dataForPackagesMerged.version
licenses: @dataForPackagesMerged.licenses
description: @dataForPackagesMerged.description
keywords: @dataForPackagesMerged.keywords
author: @dataForPackagesMerged.author
maintainers: @dataForPackagesMerged.maintainers
contributors: @dataForPackagesMerged.contributors.map (contributor) -> contributor.text
bugs: @dataForPackagesMerged.bugs
engines: @dataForPackagesMerged.engines
dependencies: @dataForPackagesMerged.dependencies
devDependencies: @dataForPackagesMerged.devDependencies
main: @dataForPackagesMerged.main
}
Explicit Data
@dataForPackagesMerged.packages.package
)
Clean up licenses
switch @dataForPackagesEnhanced.package.licenses.length
when 0
delete @dataForPackagesEnhanced.package.licenses
delete @dataForPackagesEnhanced.package.license
when 1
@dataForPackagesEnhanced.package.license = @dataForPackagesEnhanced.package.licenses[0]
delete @dataForPackagesEnhanced.package.licenses
when 2
delete @dataForPackagesEnhanced.package.license
Create the data for the jquery.json
format, which is essentially exactly the same as the package.json
format so just extend that
@dataForPackagesEnhanced.jquery = extendr.extend(
New Object
{}
Old Data
@dataForPackages.jquery
Enhanced Data
@dataForPackagesEnhanced.package
Explicit Data
@dataForPackagesMerged.jquery
)
Create the data for the component.json
format
@dataForPackagesEnhanced.component = extendr.extend(
New Object
{}
Old Data
@dataForPackages.component
Enhanced Data
{
name: @dataForPackagesMerged.name
version: @dataForPackagesMerged.version
license: @dataForPackagesMerged.licenses?[0].type
description: @dataForPackagesMerged.description
keywords: @dataForPackagesMerged.keywords
demo: @dataForPackagesMerged.demo
main: @dataForPackagesMerged.main
scripts: [@dataForPackagesMerged.main]
}
Explicit Data
@dataForPackagesMerged.packages.component
)
Create the data for the bower.json
format
@dataForPackagesEnhanced.bower = extendr.extend(
New Object
{}
Old Data
@dataForPackages.bower
Enhanced Data
{
name: @dataForPackagesMerged.name
version: @dataForPackagesMerged.version
dependencies: @dataForPackagesMerged.dependencies
devDependencies: @dataForPackagesMerged.devDependencies
main: @dataForPackagesMerged.main
}
Explicit Data
@dataForPackagesMerged.packages.bower
)
Finish up
return next(); @
enhanceReadmes: (next) ->
opts = @dataForPackagesMerged
eachr @dataForReadmes, (data, name) =>
return unless data
data = projectzUtil.replaceSection(['TITLE', 'NAME'], data, "# #{opts.title}")
data = projectzUtil.replaceSection(['BADGES', 'BADGE'], data, badgeUtil.getBadgesSection(opts))
data = projectzUtil.replaceSection(['DESCRIPTION'], data, "#{opts.description}")
data = projectzUtil.replaceSection(['INSTALL'], data, installUtil.getInstallInstructions(opts))
data = projectzUtil.replaceSection(['CONTRIBUTE', 'CONTRIBUTING'], data, backerUtil.getContributeSection(opts))
data = projectzUtil.replaceSection(['BACKERS', 'BACKER'], data, backerUtil.getBackerSection(opts))
data = projectzUtil.replaceSection(['BACKERSFILE', 'BACKERFILE'], data, backerUtil.getBackerFile(opts))
data = projectzUtil.replaceSection(['HISTORY', 'CHANGES', 'CHANGELOG'], data, historyUtil.getHistorySection(opts))
data = projectzUtil.replaceSection(['LICENSE', 'LICENSES'], data, licenseUtil.getLicenseSection(opts))
data = projectzUtil.replaceSection(['LICENSEFILE'], data, licenseUtil.getLicenseFile(opts))
@dataForReadmesEnhanced[name] = data
return true
Finish up
return next(); @
save: (next) ->
log = @log
log('info', "Writing changes...")
tasks = new TaskGroup().setConfig(concurrency:0).once 'complete', (err) ->
return next(err) if err
log('info', "Wrote changes")
return next()
Save package files
eachr @dataForPackagesMerged.packages, (enabled, name) =>
return if name is 'projectz'
unless enabled
log('info', "Skipping package file: #{name}")
return
path = @pathsForPackages[name]
log('info', "Writing package file: #{path}")
tasks.addTask (complete) =>
data = JSON.stringify(@dataForPackagesEnhanced[name], null, ' ')
fsUtil.writeFile(path, data, complete)
return true
Safe readme files
eachr @dataForPackagesMerged.readmes, (enabled, name) =>
return if name is 'projectz'
unless enabled
log('info', "Skipping readme file: #{name}")
return
path = @pathsForReadmes[name]
log('info', "Writing readme file: #{name}")
tasks.addTask (complete) =>
data = @dataForReadmesEnhanced[name]
fsUtil.writeFile(path, data, complete)
return true
Finish up
return tasks.run(); @
module.exports =
Projectz: Projectz
create: (opts) -> new Projectz(opts)