UNPKG

3.62 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 {string[]} */
37 files,
38 /** @type {{cwd?: string, stopDir?: string}} */
39 { cwd = process.cwd(), stopDir = path.parse(cwd).root } = {}
40 ) {
41 if (!files || files.length === 0) {
42 throw new Error('files must be an non-empty array!')
43 }
44
45 this.files = files
46
47 this.options = {
48 cwd,
49 stopDir
50 }
51 /** @type {Map<string, boolean>} */
52 this.existsCache = new Map()
53 /** @type {Set<Loader>} */
54 this.loaders = new Set()
55 }
56
57 /**
58 * Add a loader
59 * @public
60 * @param {Loader} loader
61 */
62 addLoader(loader) {
63 this.loaders.add(loader)
64
65 return this
66 }
67
68 /**
69 * Resolve the files in working directory or parent directory
70 * @private
71 * @param {string} cwd Working directory
72 * @return {Promise<string|null>}
73 */
74 async recusivelyResolve(cwd = this.options.cwd) {
75 // Don't traverse above the module root
76 if (cwd === this.options.stopDir || path.basename(cwd) === 'node_modules') {
77 return null
78 }
79
80 for (const filename of this.files) {
81 const file = path.join(cwd, filename)
82 const exists = this.existsCache.has(file) ?
83 this.existsCache.get(file) :
84 await pathExists(file) // eslint-disable-line no-await-in-loop
85 if (exists) {
86 this.existsCache.set(file, true)
87 return file
88 }
89 }
90
91 // Continue in the parent directory
92 return this.recusivelyResolve(path.dirname(cwd))
93 }
94
95 resolve() {
96 return this.recusivelyResolve(this.options.cwd)
97 }
98
99 /**
100 * Load the file in working directory
101 */
102 async load() {
103 const filepath = await this.resolve()
104 if (filepath) {
105 try {
106 const load = this.findLoader(filepath)
107 if (load) {
108 return {
109 path: filepath,
110 data: await load(filepath)
111 }
112 }
113
114 const extname = path.extname(filepath).slice(1)
115 if (extname === 'js') {
116 return {
117 path: filepath,
118 data: require(filepath)
119 }
120 }
121
122 const data = await readFile(filepath)
123
124 if (extname === 'json') {
125 return {
126 path: filepath,
127 data: require('json5').parse(data)
128 }
129 }
130
131 // Don't parse data
132 // If it's neither .js nor .json
133 // Leave this to user-land
134 return {
135 path: filepath,
136 data
137 }
138 } catch (err) {
139 if (err.code === 'MODULE_NOT_FOUND' || err.code === 'ENOENT') {
140 this.existsCache.delete(filepath)
141 return {}
142 }
143
144 throw err
145 }
146 }
147
148 return {}
149 }
150
151 /**
152 * Find a loader for given path
153 * @param {string} filepath file path
154 * @return {Load|null}
155 */
156 findLoader(filepath) {
157 for (const loader of this.loaders) {
158 if (loader.test && loader.test.test(filepath)) {
159 return loader.load
160 }
161 }
162
163 return null
164 }
165}