UNPKG

16.5 kBJavaScriptView Raw
1'use strict'
2/* eslint key-spacing:0 */
3var __importDefault =
4 (this && this.__importDefault) ||
5 function (mod) {
6 return mod && mod.__esModule ? mod : { default: mod }
7 }
8Object.defineProperty(exports, '__esModule', { value: true })
9exports.Projectz = void 0
10// Imports
11// First we need to import the libraries we require.
12// Load in the file system libraries
13const fs_1 = require('fs')
14const { readdir, readFile, writeFile } = fs_1.promises
15const path_1 = require('path')
16// Errlop used for wrapping errors
17const errlop_1 = __importDefault(require('errlop'))
18// CSON is used for loading in our configuration files
19const cson_parser_1 = require('cson-parser')
20// [TypeChecker](https://github.com/bevry/typechecker) is used for checking data types
21const typechecker_1 = require('typechecker')
22// Fellow handling, and contributor fetching
23const fellow_1 = __importDefault(require('fellow'))
24// Load in our other project files
25const backer_js_1 = require('./backer.js')
26const badge_js_1 = require('./badge.js')
27const history_js_1 = require('./history.js')
28const install_js_1 = require('./install.js')
29const license_js_1 = require('./license.js')
30const util_js_1 = require('./util.js')
31async function parseFile(path) {
32 try {
33 const str = await readFile(path, 'utf-8')
34 const data = cson_parser_1.parse(str)
35 return data
36 } catch (err) {
37 return Promise.reject(
38 new errlop_1.default(`failed parsing the file: ${path}`, err)
39 )
40 }
41}
42// Definition
43// Projects is defined as a class to ensure we can run multiple instances of it
44class Projectz {
45 // Apply options
46 constructor(opts) {
47 /** our log function to use (logLevel, ...messages) */
48 this.log = function () {}
49 /**
50 * The absolute paths for all the package files.
51 * Should be arranged in the order of merging preference.
52 */
53 this.filenamesForPackageFiles = {
54 component: null,
55 bower: null,
56 jquery: null,
57 package: null,
58 projectz: null,
59 }
60 /** the data for each of our package files */
61 this.dataForPackageFiles = {}
62 /** the absolute paths for all the meta files */
63 this.filenamesForReadmeFiles = {
64 // gets filled in with relative paths
65 readme: null,
66 history: null,
67 contributing: null,
68 backers: null,
69 license: null,
70 }
71 /** the data for each of our readme files */
72 this.dataForReadmeFiles = {}
73 this.cwd = opts.cwd ? path_1.resolve(opts.cwd) : process.cwd()
74 if (opts.log) this.log = opts.log
75 }
76 /** Compile the project */
77 async compile() {
78 // Load readme and package data
79 await this.loadPaths()
80 // Enhance our package data
81 const enhancedPackagesData = await this.enhancePackagesData()
82 // Enhance our readme data
83 const enhancedReadmesData = await this.enhanceReadmesData(
84 enhancedPackagesData
85 )
86 // Save
87 await this.save(enhancedPackagesData, enhancedReadmesData)
88 }
89 /** Load in the paths we have specified */
90 async loadPaths() {
91 // Apply our determined paths for packages
92 const packages = Object.keys(this.filenamesForPackageFiles)
93 const ReadmeFiles = Object.keys(this.filenamesForReadmeFiles)
94 // Load
95 const files = await readdir(this.cwd)
96 for (const file of files) {
97 const filePath = path_1.join(this.cwd, file)
98 for (const key of packages) {
99 const basename = file.toLowerCase().split('.').slice(0, -1).join('.')
100 if (basename === key) {
101 this.log('info', `Reading package file: ${filePath}`)
102 const data = await parseFile(filePath)
103 this.filenamesForPackageFiles[key] = file
104 this.dataForPackageFiles[key] = data
105 }
106 }
107 for (const key of ReadmeFiles) {
108 if (file.toLowerCase().startsWith(key)) {
109 this.log('info', `Reading meta file: ${filePath}`)
110 const data = await readFile(filePath, 'utf-8')
111 this.filenamesForReadmeFiles[key] = file
112 this.dataForReadmeFiles[key] = data.toString()
113 }
114 }
115 }
116 }
117 /** Merge and enhance the packages data */
118 async enhancePackagesData() {
119 // ----------------------------------
120 // Combine
121 // Combine the package data
122 const mergedPackagesData = {
123 keywords: [],
124 editions: [],
125 badges: {
126 list: [],
127 config: {},
128 },
129 bugs: {},
130 readmes: {},
131 packages: {},
132 repository: {},
133 github: {},
134 dependencies: {},
135 devDependencies: {},
136 }
137 for (const key of Object.keys(this.filenamesForPackageFiles)) {
138 Object.assign(mergedPackagesData, this.dataForPackageFiles[key])
139 }
140 // ----------------------------------
141 // Validation
142 // Validate keywords field
143 if (typechecker_1.isString(mergedPackagesData.keywords)) {
144 return Promise.reject(
145 new Error('projectz: keywords field must be array instead of CSV')
146 )
147 }
148 // Validate sponsors array
149 if (mergedPackagesData.sponsor) {
150 return Promise.reject(
151 new Error('projectz: sponsor field is deprecated, use sponsors field')
152 )
153 }
154 if (typechecker_1.isString(mergedPackagesData.sponsors)) {
155 return Promise.reject(
156 new Error('projectz: sponsors field must be array instead of CSV')
157 )
158 }
159 // Validate maintainers array
160 if (mergedPackagesData.maintainer) {
161 return Promise.reject(
162 new Error(
163 'projectz: maintainer field is deprecated, use maintainers field'
164 )
165 )
166 }
167 if (typechecker_1.isString(mergedPackagesData.maintainers)) {
168 return Promise.reject(
169 new Error('projectz: maintainers field must be array instead of CSV')
170 )
171 }
172 // Validate license SPDX string
173 if (typechecker_1.isPlainObject(mergedPackagesData.license)) {
174 return Promise.reject(
175 new Error(
176 'projectz: license field must now be a valid SPDX string: https://docs.npmjs.com/files/package.json#license'
177 )
178 )
179 }
180 if (typechecker_1.isPlainObject(mergedPackagesData.licenses)) {
181 return Promise.reject(
182 new Error(
183 'projectz: licenses field is deprecated, you must now use the license field as a valid SPDX string: https://docs.npmjs.com/files/package.json#license'
184 )
185 )
186 }
187 // Validate enhanced fields
188 const objs = ['badges', 'readmes', 'packages', 'github', 'bugs']
189 for (const key of objs) {
190 if (!typechecker_1.isPlainObject(mergedPackagesData[key])) {
191 return Promise.reject(
192 new Error(`projectz: ${key} property must be an object`)
193 )
194 }
195 }
196 // Validate package values
197 for (const [key, value] of Object.entries(mergedPackagesData.packages)) {
198 if (!typechecker_1.isPlainObject(value)) {
199 return Promise.reject(
200 new Error(
201 `projectz: custom package data for package ${key} must be an object`
202 )
203 )
204 }
205 }
206 // Validate badges field
207 if (
208 !Array.isArray(mergedPackagesData.badges.list) ||
209 (mergedPackagesData.badges.config &&
210 !typechecker_1.isPlainObject(mergedPackagesData.badges.config))
211 ) {
212 return Promise.reject(
213 new Error(
214 'projectz: badges field must be in the format of: {list: [], config: {}}\nSee https://github.com/bevry/badges for details.'
215 )
216 )
217 }
218 // ----------------------------------
219 // Ensure
220 // Ensure repository is an object
221 if (typeof mergedPackagesData.repository === 'string') {
222 const githubSlug = util_js_1.getGithubSlug(mergedPackagesData)
223 if (githubSlug) {
224 mergedPackagesData.repository = {
225 type: 'git',
226 url: `https://github.com/${githubSlug}.git`,
227 }
228 }
229 }
230 // Fallback name
231 if (!mergedPackagesData.name) {
232 mergedPackagesData.name = path_1.dirname(this.cwd)
233 }
234 // Fallback version
235 if (!mergedPackagesData.version) {
236 mergedPackagesData.version = '0.1.0'
237 }
238 // Fallback demo field, by scanning homepage
239 if (!mergedPackagesData.demo && mergedPackagesData.homepage) {
240 mergedPackagesData.demo = mergedPackagesData.homepage
241 }
242 // Fallback title from name
243 if (!mergedPackagesData.title) {
244 mergedPackagesData.title = mergedPackagesData.name
245 }
246 // Fallback description
247 if (!mergedPackagesData.description) {
248 mergedPackagesData.description = 'no description was provided'
249 }
250 // Fallback browsers field, by checking if `component` or `bower` package files exists, or if the `browser` or `jspm` fields are defined
251 if (mergedPackagesData.browsers == null) {
252 mergedPackagesData.browsers = Boolean(
253 this.filenamesForPackageFiles.bower ||
254 this.filenamesForPackageFiles.component ||
255 mergedPackagesData.browser ||
256 mergedPackagesData.jspm
257 )
258 }
259 // ----------------------------------
260 // Enhance Respository
261 // Converge and extract repository information
262 let github
263 if (mergedPackagesData.repository) {
264 const githubSlug = util_js_1.getGithubSlug(mergedPackagesData)
265 if (githubSlug) {
266 // Extract parts
267 const [githubUsername, githubRepository] = githubSlug.split('/')
268 const githubUrl = 'https://github.com/' + githubSlug
269 const githubRepositoryUrl = githubUrl + '.git'
270 // Github data
271 github = {
272 username: githubUsername,
273 repository: githubRepository,
274 slug: githubSlug,
275 url: githubUrl,
276 repositoryUrl: githubRepositoryUrl,
277 }
278 // Badges
279 Object.assign(mergedPackagesData.badges.config, {
280 githubUsername,
281 githubRepository,
282 githubSlug,
283 })
284 // Fallback bugs field by use of repo
285 if (!mergedPackagesData.bugs) {
286 mergedPackagesData.bugs = github && {
287 url: `https://github.com/${github.slug}/issues`,
288 }
289 }
290 // Fetch contributors
291 // await getContributorsFromRepo(githubSlug)
292 }
293 }
294 // ----------------------------------
295 // Enhance People
296 // Fellows
297 const authors = fellow_1.default.add(
298 mergedPackagesData.authors || mergedPackagesData.author
299 )
300 const contributors = fellow_1.default
301 .add(mergedPackagesData.contributors)
302 .filter((fellow) => fellow.name.includes('[bot]') === false)
303 const maintainers = fellow_1.default
304 .add(mergedPackagesData.maintainers)
305 .filter((fellow) => fellow.name.includes('[bot]') === false)
306 const sponsors = fellow_1.default.add(mergedPackagesData.sponsors)
307 // ----------------------------------
308 // Enhance Packages
309 // Create the data for the `package.json` format
310 const pkg = Object.assign(
311 // New Object
312 {},
313 // Old Data
314 this.dataForPackageFiles.package || {},
315 // Enhanced Data
316 {
317 name: mergedPackagesData.name,
318 version: mergedPackagesData.version,
319 license: mergedPackagesData.license,
320 description: mergedPackagesData.description,
321 keywords: mergedPackagesData.keywords,
322 author: util_js_1
323 .getPeopleTextArray(authors, {
324 displayEmail: true,
325 displayYears: true,
326 })
327 .join(', '),
328 maintainers: util_js_1.getPeopleTextArray(maintainers, {
329 displayEmail: true,
330 urlFields: ['githubUrl', 'url'],
331 }),
332 contributors: util_js_1.getPeopleTextArray(contributors, {
333 displayEmail: true,
334 urlFields: ['githubUrl', 'url'],
335 }),
336 repository: mergedPackagesData.repository,
337 bugs: mergedPackagesData.bugs,
338 engines: mergedPackagesData.engines,
339 dependencies: mergedPackagesData.dependencies,
340 devDependencies: mergedPackagesData.devDependencies,
341 main: mergedPackagesData.main,
342 },
343 // Explicit data
344 mergedPackagesData.packages.package || {}
345 )
346 // Trim
347 // @ts-ignore
348 if (typechecker_1.isEmptyPlainObject(pkg.dependencies))
349 delete pkg.dependencies
350 // @ts-ignore
351 if (typechecker_1.isEmptyPlainObject(pkg.devDependencies))
352 delete pkg.devDependencies
353 // Badges
354 if (!mergedPackagesData.badges.config.npmPackageName && pkg.name) {
355 mergedPackagesData.badges.config.npmPackageName = pkg.name
356 }
357 // Create the data for the `jquery.json` format, which is essentially the same as the `package.json` format so just extend that
358 const jquery = Object.assign(
359 // New Object
360 {},
361 // Old Data
362 this.dataForPackageFiles.jquery || {},
363 // Enhanced Data
364 pkg,
365 // Explicit data
366 mergedPackagesData.packages.jquery || {}
367 )
368 // Create the data for the `component.json` format
369 const component = Object.assign(
370 // New Object
371 {},
372 // Old Data
373 this.dataForPackageFiles.component || {},
374 // Enhanced Data
375 {
376 name: mergedPackagesData.name,
377 version: mergedPackagesData.version,
378 license: mergedPackagesData.license,
379 description: mergedPackagesData.description,
380 keywords: mergedPackagesData.keywords,
381 demo: mergedPackagesData.demo,
382 main: mergedPackagesData.main,
383 scripts: [mergedPackagesData.main],
384 },
385 // Explicit data
386 mergedPackagesData.packages.component || {}
387 )
388 // Create the data for the `bower.json` format
389 const bower = Object.assign(
390 // New Object
391 {},
392 // Old Data
393 this.dataForPackageFiles.bower || {},
394 // Enhanced Data
395 {
396 name: mergedPackagesData.name,
397 version: mergedPackagesData.version,
398 license: mergedPackagesData.license,
399 description: mergedPackagesData.description,
400 keywords: mergedPackagesData.keywords,
401 authors: util_js_1.getPeopleTextArray(authors, {
402 displayYears: true,
403 displayEmail: true,
404 }),
405 main: mergedPackagesData.main,
406 },
407 // Explicit data
408 mergedPackagesData.packages.bower || {}
409 )
410 // ----------------------------------
411 // Enhance Combination
412 // Merge together
413 const enhancedPackagesData = Object.assign({}, mergedPackagesData, {
414 // Add paths so that our helpers have access to them
415 filenamesForPackageFiles: this.filenamesForPackageFiles,
416 filenamesForReadmeFiles: this.filenamesForReadmeFiles,
417 // Other
418 github,
419 // Fellows
420 authors,
421 contributors,
422 maintainers,
423 sponsors,
424 // Create the data for the `package.json` format
425 package: pkg,
426 jquery,
427 component,
428 bower,
429 })
430 // Return
431 return enhancedPackagesData
432 }
433 /** Merge and enhance the readmes data */
434 async enhanceReadmesData(data) {
435 const enhancedReadmesData = {}
436 /* eslint prefer-const: 0 */
437 for (let [key, value] of Object.entries(this.dataForReadmeFiles)) {
438 if (!value) {
439 this.log('debug', `Enhancing readme value: ${key} — skipped`)
440 continue
441 }
442 value = util_js_1.replaceSection(
443 ['TITLE', 'NAME'],
444 value,
445 `<h1>${data.title}</h1>`
446 )
447 value = util_js_1.replaceSection(
448 ['BADGES', 'BADGE'],
449 value,
450 badge_js_1.getBadgesSection.bind(null, data)
451 )
452 value = util_js_1.replaceSection(['DESCRIPTION'], value, data.description)
453 value = util_js_1.replaceSection(
454 ['INSTALL'],
455 value,
456 install_js_1.getInstallInstructions.bind(null, data)
457 )
458 value = util_js_1.replaceSection(
459 ['CONTRIBUTE', 'CONTRIBUTING'],
460 value,
461 data.github
462 ? backer_js_1.getContributeSection.bind(null, data)
463 : '<!-- github projects only -->'
464 )
465 value = util_js_1.replaceSection(
466 ['BACKERS', 'BACKER'],
467 value,
468 data.github
469 ? backer_js_1.getBackerSection.bind(null, data)
470 : '<!-- github projects only -->'
471 )
472 value = util_js_1.replaceSection(
473 ['BACKERSFILE', 'BACKERFILE'],
474 value,
475 data.github
476 ? backer_js_1.getBackerFile.bind(null, data)
477 : '<!-- github projects only -->'
478 )
479 value = util_js_1.replaceSection(
480 ['HISTORY', 'CHANGES', 'CHANGELOG'],
481 value,
482 data.github
483 ? history_js_1.getHistorySection.bind(null, data)
484 : '<!-- github projects only -->'
485 )
486 value = util_js_1.replaceSection(
487 ['LICENSE', 'LICENSES'],
488 value,
489 data.github
490 ? license_js_1.getLicenseSection.bind(null, data)
491 : '<!-- github projects only -->'
492 )
493 value = util_js_1.replaceSection(
494 ['LICENSEFILE'],
495 value,
496 license_js_1.getLicenseFile.bind(null, data)
497 )
498 enhancedReadmesData[key] = util_js_1.trim(value) + '\n'
499 this.log('info', `Enhanced readme value: ${key}`)
500 }
501 return enhancedReadmesData
502 }
503 /** Save the data we've loaded into the files */
504 async save(enhancedPackagesData, enhancedReadmesData) {
505 // Prepare
506 this.log('info', 'Writing changes...')
507 await Promise.all([
508 // save package files
509 ...Object.entries(this.filenamesForPackageFiles).map(
510 async ([key, filename]) => {
511 if (!filename || key === 'projectz') return
512 const filepath = path_1.join(this.cwd, filename)
513 this.log('info', `Saving package file: ${filepath}`)
514 const data =
515 JSON.stringify(enhancedPackagesData[key], null, ' ') + '\n'
516 return writeFile(filepath, data)
517 }
518 ),
519 // save readme files
520 ...Object.entries(this.filenamesForReadmeFiles).map(
521 async ([key, filename]) => {
522 if (!filename) return
523 const filepath = path_1.join(this.cwd, filename)
524 this.log('info', `Saving readme file: ${filepath}`)
525 const data = enhancedReadmesData[key]
526 return writeFile(filepath, data)
527 }
528 ),
529 ])
530 // log
531 this.log('info', 'Wrote changes')
532 }
533}
534exports.Projectz = Projectz