UNPKG

4.3 kBJavaScriptView Raw
1const fs = require('fs')
2const path = require('path')
3
4/**
5 * Read and return file data
6 * @param {string} fp file path
7 * @return {Promise<string>} file data
8 */
9const readFile = fp =>
10 new Promise((resolve, reject) => {
11 fs.readFile(fp, 'utf8', (err, data) => {
12 if (err) return reject(err)
13 resolve(data)
14 })
15 })
16
17/**
18 * Check if a file exists
19 * @param {string} fp file path
20 * @return {Promise<boolean>} whether it exists
21 */
22const pathExists = fp =>
23 new Promise(resolve => {
24 fs.access(fp, err => {
25 resolve(!err)
26 })
27 })
28
29/**
30 * @typedef {(filepath: string) => any} Load
31 * @typedef {{test: RegExp, load: Load}} Loader
32 */
33
34module.exports = class JoyCon {
35 constructor(
36 /** @type {{files?: string[], cwd?: string}} */
37 { files, cwd = process.cwd() } = {}
38 ) {
39 this.options = {
40 files,
41 cwd
42 }
43 /** @type {Map<string, boolean>} */
44 this.existsCache = new Map()
45 /** @type {Set<Loader>} */
46 this.loaders = new Set()
47 }
48
49 /**
50 * Add a loader
51 * @public
52 * @param {Loader} loader
53 */
54 addLoader(loader) {
55 this.loaders.add(loader)
56
57 return this
58 }
59
60 /**
61 * Resolve the files in working directory or parent directory
62 * @private
63 * @param {string[]} files Files to search
64 * @param {string} cwd Working directory
65 * @param {string} stopDir The directory to stop searching
66 * @return {Promise<string|null>}
67 */
68 async recusivelyResolve(files, cwd, stopDir) {
69 // Don't traverse above the module root
70 if (cwd === stopDir || path.basename(cwd) === 'node_modules') {
71 return null
72 }
73
74 for (const filename of files) {
75 const file = path.join(cwd, filename)
76 const exists = this.existsCache.has(file) ?
77 this.existsCache.get(file) :
78 await pathExists(file) // eslint-disable-line no-await-in-loop
79 if (exists) {
80 this.existsCache.set(file, true)
81 return file
82 }
83 }
84
85 // Continue in the parent directory
86 return this.recusivelyResolve(files, path.dirname(cwd), stopDir)
87 }
88
89 /**
90 * Search files and resolve the path
91 * @param {string[]=} files Files to search
92 * @param {string=} cwd Working directory
93 * @param {string=} stopDir The directory to stop searching
94 */
95 resolve(files, cwd, stopDir) {
96 files = files || this.options.files
97 cwd = cwd || this.options.cwd
98 stopDir = stopDir || path.parse(cwd).root
99
100 if (!files || files.length === 0) {
101 return Promise.reject(new Error('files must be an non-empty array!'))
102 }
103
104 return this.recusivelyResolve(files, cwd, stopDir)
105 }
106
107 /**
108 * Search files and resolve the path and data
109 * @param {string[]=} files Files to search
110 * @param {string=} cwd Working directory
111 * @param {string=} stopDir The directory to stop searching
112 */
113 async load(files, cwd, stopDir) {
114 const filepath = await this.resolve(files, cwd, stopDir)
115 if (filepath) {
116 try {
117 const load = this.findLoader(filepath)
118 if (load) {
119 return {
120 path: filepath,
121 data: await load(filepath)
122 }
123 }
124
125 const extname = path.extname(filepath).slice(1)
126 if (extname === 'js') {
127 return {
128 path: filepath,
129 data: require(filepath)
130 }
131 }
132
133 const data = await readFile(filepath)
134
135 if (extname === 'json') {
136 return {
137 path: filepath,
138 data: require('json5').parse(data)
139 }
140 }
141
142 // Don't parse data
143 // If it's neither .js nor .json
144 // Leave this to user-land
145 return {
146 path: filepath,
147 data
148 }
149 } catch (err) {
150 if (err.code === 'MODULE_NOT_FOUND' || err.code === 'ENOENT') {
151 this.existsCache.delete(filepath)
152 return {}
153 }
154
155 throw err
156 }
157 }
158
159 return {}
160 }
161
162 /**
163 * Find a loader for given path
164 * @param {string} filepath file path
165 * @return {Load|null}
166 */
167 findLoader(filepath) {
168 for (const loader of this.loaders) {
169 if (loader.test && loader.test.test(filepath)) {
170 return loader.load
171 }
172 }
173
174 return null
175 }
176
177 /** Clear cache used by this instance */
178 clearCache() {
179 this.existsCache.clear()
180
181 return this
182 }
183}