codekingpro/portable-devtools
115k
1/* globals config, dirname, package, basename, yes, prompt */
2
3const fs = require('fs/promises')
4const path = require('path')
5const validateLicense = require('@npmcli/package-json/lib/license')
6const validateName = require('validate-npm-package-name')
7const npa = require('npm-package-arg')
8const semver = require('semver')
9
10// more popular packages should go here, maybe?
11const testPkgs = [
12 'coco',
13 'coffee-script',
14 'expresso',
15 'jasmine',
16 'jest',
17 'mocha',
18 'streamline',
19 'tap',
20]
21const isTestPkg = p => testPkgs.includes(p)
22
23const invalid = (msg) => Object.assign(new Error(msg), { notValid: true })
24
25const readDeps = (test, excluded) => async () => {
26 const dirs = await fs.readdir('node_modules').catch(() => null)
27
28 if (!dirs) {
29 return
30 }
31
32 const deps = {}
33 for (const dir of dirs) {
34 if (dir.match(/^\./) || test !== isTestPkg(dir) || excluded[dir]) {
35 continue
36 }
37
38 const dp = path.join(dirname, 'node_modules', dir, 'package.json')
39 const p = await fs.readFile(dp, 'utf8').then((d) => JSON.parse(d)).catch(() => null)
40
41 if (!p || !p.version || p?._requiredBy?.some((r) => r === '#USER')) {
42 continue
43 }
44
45 deps[dir] = config.get('save-exact') ? p.version : config.get('save-prefix') + p.version
46 }
47
48 return deps
49}
50
51const getConfig = (key) => {
52 // dots take precedence over dashes
53 const def = config?.defaults?.[`init.${key}`]
54 const val = config.get(`init.${key}`)
55 return (val !== def && val) ? val : config.get(`init-${key.replace(/\./g, '-')}`)
56}
57
58const getName = () => {
59 const rawName = package.name || basename
60 let name = rawName
61 .replace(/^node-|[.-]js$/g, '')
62 .replace(/\s+/g, ' ')
63 .replace(/ /g, '-')
64 .toLowerCase()
65
66 let spec
67 try {
68 spec = npa(name)
69 } catch {
70 spec = {}
71 }
72
73 let scope = config.get('scope')
74
75 if (scope) {
76 if (scope.charAt(0) !== '@') {
77 scope = '@' + scope
78 }
79 if (spec.scope) {
80 name = scope + '/' + spec.name.split('/')[1]
81 } else {
82 name = scope + '/' + name
83 }
84 }
85
86 return name
87}
88
89const name = getName()
90exports.name = yes ? name : prompt('package name', name, (data) => {
91 const its = validateName(data)
92 if (its.validForNewPackages) {
93 return data
94 }
95 const errors = (its.errors || []).concat(its.warnings || [])
96 return invalid(`Sorry, ${errors.join(' and ')}.`)
97})
98
99const version = package.version || getConfig('version') || '1.0.0'
100exports.version = yes ? version : prompt('version', version, (v) => {
101 if (semver.valid(v)) {
102 return v
103 }
104 return invalid(`Invalid version: "${v}"`)
105})
106
107if (!package.description) {
108 exports.description = yes ? '' : prompt('description')
109}
110
111if (!package.main) {
112 exports.main = async () => {
113 const files = await fs.readdir(dirname)
114 .then(list => list.filter((f) => f.match(/\.js$/)))
115 .catch(() => [])
116
117 let index
118 if (files.includes('index.js')) {
119 index = 'index.js'
120 } else if (files.includes('main.js')) {
121 index = 'main.js'
122 } else if (files.includes(basename + '.js')) {
123 index = basename + '.js'
124 } else {
125 index = files[0] || 'index.js'
126 }
127
128 return yes ? index : prompt('entry point', index)
129 }
130}
131
132if (!package.bin) {
133 exports.bin = async () => {
134 try {
135 const d = await fs.readdir(path.resolve(dirname, 'bin'))
136 // just take the first js file we find there, or nada
137 let r = d.find(f => f.match(/\.js$/))
138 if (r) {
139 r = `bin/${r}`
140 }
141 return r
142 } catch {
143 // no bins
144 }
145 }
146}
147
148exports.directories = async () => {
149 const dirs = await fs.readdir(dirname)
150
151 const res = dirs.reduce((acc, d) => {
152 if (/^examples?$/.test(d)) {
153 acc.example = d
154 } else if (/^tests?$/.test(d)) {
155 acc.test = d
156 } else if (/^docs?$/.test(d)) {
157 acc.doc = d
158 } else if (d === 'man') {
159 acc.man = d
160 } else if (d === 'lib') {
161 acc.lib = d
162 }
163
164 return acc
165 }, {})
166
167 return Object.keys(res).length === 0 ? undefined : res
168}
169
170if (!package.dependencies) {
171 exports.dependencies = readDeps(false, package.devDependencies || {})
172}
173
174if (!package.devDependencies) {
175 exports.devDependencies = readDeps(true, package.dependencies || {})
176}
177
178// MUST have a test script!
179if (!package.scripts) {
180 const scripts = package.scripts || {}
181 const notest = 'echo "Error: no test specified" && exit 1'
182 exports.scripts = async () => {
183 const d = await fs.readdir(path.join(dirname, 'node_modules')).catch(() => [])
184
185 // check to see what framework is in use, if any
186 let command
187 if (!scripts.test || scripts.test === notest) {
188 const commands = {
189 tap: 'tap test/*.js',
190 expresso: 'expresso test',
191 mocha: 'mocha',
192 }
193 for (const [k, v] of Object.entries(commands)) {
194 if (d.includes(k)) {
195 command = v
196 }
197 }
198 }
199
200 const promptArgs = ['test command', (t) => t || notest]
201 if (command) {
202 promptArgs.splice(1, 0, command)
203 }
204 scripts.test = yes ? command || notest : prompt(...promptArgs)
205
206 return scripts
207 }
208}
209
210if (!package.repository) {
211 exports.repository = async () => {
212 const gitConfigPath = path.resolve(dirname, '.git', 'config')
213 const gconf = await fs.readFile(gitConfigPath, 'utf8').catch(() => '')
214 const lines = gconf.split(/\r?\n/)
215
216 let url
217 const i = lines.indexOf('[remote "origin"]')
218
219 if (i !== -1) {
220 url = lines[i + 1]
221 if (!url.match(/^\s*url =/)) {
222 url = lines[i + 2]
223 }
224 if (!url.match(/^\s*url =/)) {
225 url = null
226 } else {
227 url = url.replace(/^\s*url = /, '')
228 }
229 }
230
231 if (url && url.match(/^git@github.com:/)) {
232 url = url.replace(/^git@github.com:/, 'https://github.com/')
233 }
234
235 return yes ? url || '' : prompt('git repository', url || undefined)
236 }
237}
238
239if (!package.keywords) {
240 exports.keywords = yes ? '' : prompt('keywords', (data) => {
241 if (!data) {
242 return
243 }
244 if (Array.isArray(data)) {
245 data = data.join(' ')
246 }
247 if (typeof data !== 'string') {
248 return data
249 }
250 return data.split(/[\s,]+/)
251 })
252}
253
254if (!package.author) {
255 const authorName = getConfig('author.name')
256 exports.author = authorName
257 ? {
258 name: authorName,
259 email: getConfig('author.email'),
260 url: getConfig('author.url'),
261 }
262 : yes ? '' : prompt('author')
263}
264
265const license = package.license || getConfig('license') || 'ISC'
266exports.license = yes ? license : prompt('license', license, (data) => {
267 if (validateLicense(data)) {
268 return data
269 }
270 return invalid('Sorry, license should be a valid SPDX license expression')
271})
272
273const type = package.type || getConfig('type') || 'commonjs'
274exports.type = yes ? type : prompt('type', type, (data) => {
275 return data
276})
277
278// Only include private field if it already exists or if explicitly set in config
279const configPrivate = getConfig('private')
280if (package.private !== undefined || configPrivate !== undefined) {
281 if (package.private !== undefined) {
282 exports.private = package.private
283 } else if (!config.isDefault || !config.isDefault('init-private')) {
284 exports.private = configPrivate
285 }
286}
287 