-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
135 lines (120 loc) · 4.23 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
const path = require('path')
const fs = require('fs')
const flatMap = require('lodash.flatmap')
const acorn = require('acorn-loose')
const find = require('find')
const flatten = require('lodash.flatten')
/**
* @typedef {Object} Options
* @property {String[]} extensions Array of extensions to try to resolve automatically
*/
/**
* Find correct entry point based on access path
*
* - If path points to a folder, resolve to the `index.js` file contained in this folder
* - If path exists and points to a file, return this path
* - If path doesn't exist, try to add extensions `.js` or `.jsx`
*
* @param {String} relPath Relative path pointing to a JS *module*
* @param {Options} opts Options
* @return {String} Relative path pointing to the corresponding file
*/
const findCorrectPath = (relPath, opts) => {
if (fs.existsSync(relPath)) {
if (fs.lstatSync(relPath).isDirectory()) {
return `${relPath}/index.js`
}
return relPath
}
const fileByExtension = opts.extensions
.map(ext => `${relPath}.${ext}`)
.find(filePath => fs.existsSync(filePath))
if (fileByExtension) {
return fileByExtension
}
console.warn(`
Can't find any file at ${relPath} within the extensions ${opts.extensions}.
If this file exists and can be defined as an entrypoint, add its extension in the option extensions.
`)
}
/**
* Find matching `export { default as` declarations for the given file content.
*
* @param {String} filePath Path of the file
* @param {String} fileContent Content of the file (should be JS code)
* @param {Options} opts Options
* @return {Object}
* @property {String} componentName Name of the exported component
* @property {String} componentPath Relative path of the found entry point
*/
const findExportDeclarationsForContent = (filePath, fileContent, opts) => {
const parsed = acorn.parse(fileContent, {
sourceType: 'module',
ecmaVersion: opts.ecmaVersion || 2020,
})
return flatMap(
parsed.body.filter(expr => expr.type === 'ExportNamedDeclaration'),
exportDeclaration => {
const exportedNames = exportDeclaration.specifiers
.filter(specifier => specifier.type === 'ExportSpecifier')
.filter(specifier => specifier.local.name === 'default')
.map(specifier => specifier.exported.name)
const componentPath = exportDeclaration.source.value
const relativePath = findCorrectPath(path.join(filePath, '..', componentPath), opts)
if (!relativePath) {
return []
}
return exportedNames.map(componentName => ({ componentName, componentPath: relativePath }))
},
)
}
/**
* Read the at the given path to parse it and find matching `export { default as` declarations
*
* @param {String} filePath Path of the file
* @param {Options} opts Options
* @return {Object}
* @property {String} componentName Name of the exported component
* @property {String} componentPath Relative path of the found entry point
*/
const findExportDeclarationsForFile = (filePath, opts) => {
return new Promise(resolve => {
fs.readFile(filePath, { encoding: 'utf-8' }, (err, data) => {
try {
resolve(findExportDeclarationsForContent(filePath, data, opts))
} catch (error) {
console.warn(`Could not parse ${filePath}. No entrypoint will be generated for this file.`)
}
})
})
}
/**
* Find all files that can contain `export { default as` declarations.
* Only file paths returned by this method should be parsed.
*
* @param {String} sourcePath Base folder of sources
* @return {String[]} Array of file paths
*/
const findEligibleFiles = sourcePath => {
return new Promise(resolve => {
find.file(/index\.js$/, sourcePath, files => resolve(files))
})
}
const generateEntryPoints = async (sourcePath, opts = { extensions: ['js', 'jsx', 'json'] }) => {
try {
const files = await findEligibleFiles(sourcePath)
const entryPoints = flatten(
await Promise.all(files.map(filePath => findExportDeclarationsForFile(filePath, opts))),
)
return entryPoints.reduce(
(prev, { componentName, componentPath }) => ({
...prev,
[componentName]: `./${componentPath.replace(/\\/g, '/')}`,
}),
{},
)
} catch (e) {
console.error(e)
}
}
module.exports = { generateEntryPoints }