-
-
Save zodern/e0cddbdf5ec5c71fb270d8d9ed11adad to your computer and use it in GitHub Desktop.
Meteor file walker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fs = require('fs'); | |
const path = require('path'); | |
const readAndParse = require('./parse'); | |
// TODO: the order is important | |
// extensions of files that are compiled into js | |
// and can import other js files. | |
const parseableExt = ['.js', '.jsx', '.svelte', '.ts', '.tsx']; | |
// These folders are not eagerly loaded by Meteor | |
// TODO: check if we should only exclude some of these when | |
// they are at the top level | |
const notEagerlyLoadedDirs = [ | |
'imports', | |
'node_modules', | |
'public', | |
// TODO: have an option to include tests | |
'tests', | |
'test', | |
'packages', | |
'private', | |
]; | |
// The path will start with one of these if | |
// it imports an app file | |
const appFileImport = ['.', path.posix.sep, path.win32.sep]; | |
function shouldWalk(folderPath, archList) { | |
const basename = path.basename(folderPath); | |
if (basename[0] === '.' || notEagerlyLoadedDirs.includes(basename)) { | |
return false; | |
} | |
const parts = folderPath.split(path.sep); | |
if (!archList.includes('server') && parts.includes('server')) { | |
return false; | |
} | |
if (!archList.includes('client') && parts.includes('client')) { | |
return false; | |
} | |
return true; | |
} | |
function findExt(filePath) { | |
const ext = parseableExt.find(possibleExt => { | |
const exists = fs.existsSync(filePath + possibleExt); | |
return exists; | |
}); | |
if (ext) { | |
return filePath + ext; | |
} | |
// Maybe it is the index file in a folder | |
// TODO: check if this should be before or after checking extensions | |
try { | |
const stat = fs.statSync(filePath); | |
if (stat.isDirectory()) { | |
return findExt(`${filePath}${path.sep}index`); | |
} | |
} catch (e) { | |
// TODO: only ignore certain errors | |
} | |
} | |
function shouldParse(filePath) { | |
const ext = path.extname(filePath); | |
const basename = path.basename(filePath); | |
// TODO: have an option to parse test files | |
if ( | |
basename.endsWith(`.app-tests${ext}`) || | |
basename.endsWith(`.spec${ext}`) || | |
basename.endsWith(`.test${ext}`) | |
) { | |
return false; | |
} | |
return basename[0] !== '.' && parseableExt.includes(ext); | |
} | |
function isMeteorPackage(importPath) { | |
return importPath.startsWith('meteor/'); | |
} | |
function isNpmDependency(importPath) { | |
return !appFileImport.includes(importPath[0]); | |
} | |
const handledFiles = new Set(); | |
const cachedParsedFile = new Map(); | |
function getAbsFilePath(filePath) { | |
// some files have no ext or are only the ext (.gitignore, .editorconfig, etc.) | |
const existingExt = | |
path.extname(filePath) || path.basename(filePath).startsWith('.'); | |
if (!existingExt) { | |
// TODO: should maybe only do this if a file doesn't exists with the given path | |
// since we might be importing a file with no extension. | |
const pathWithExt = findExt(filePath); | |
if (!pathWithExt) { | |
console.log('unable to find ext', filePath); | |
return pathWithExt; | |
} | |
return pathWithExt; | |
} | |
// TODO: if the file doesn't exist, we must try other extensions | |
return filePath; | |
} | |
function handleFile(_filePath, appPath, onFile, from) { | |
const filePath = getAbsFilePath(_filePath); | |
if (!shouldParse(filePath) || handledFiles.has(filePath)) { | |
return; | |
} | |
handledFiles.add(filePath); | |
const realPath = fs.realpathSync.native(filePath); | |
const ast = cachedParsedFile.get(realPath) || readAndParse(filePath); | |
cachedParsedFile.set(realPath, ast); | |
const imports = readAndParse.findImports(filePath, ast, appPath); | |
onFile({ path: filePath, ast, imports }); | |
imports | |
.filter( | |
({ source }) => !isMeteorPackage(source) && !isNpmDependency(source), | |
) | |
.map(({ source }) => { | |
if (source[0] === '/') { | |
source = appPath + source; | |
} | |
return path.resolve(path.dirname(filePath), source); | |
}) | |
.forEach(importPath => { | |
handleFile(importPath, appPath, onFile, filePath); | |
}); | |
} | |
function handleFolder(folderPath, appPath, archList, onFile) { | |
const dirents = fs.readdirSync(folderPath, { withFileTypes: true }); | |
for (let i = 0; i < dirents.length; i += 1) { | |
if (dirents[i].isDirectory()) { | |
if (shouldWalk(path.resolve(folderPath, dirents[i].name), archList)) { | |
handleFolder( | |
path.resolve(folderPath, dirents[i].name), | |
appPath, | |
archList, | |
onFile, | |
); | |
} | |
} else if (dirents[i].isFile()) { | |
const filePath = path.resolve(folderPath, dirents[i].name); | |
handleFile(filePath, appPath, onFile); | |
} | |
} | |
} | |
module.exports = function walkApp(appPath, archList, onFile) { | |
console.log('walking', appPath); | |
handleFolder(appPath, appPath, archList, onFile); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { parse: meteorBabelParser } = require('meteor-babel/parser.js'); | |
const fs = require('fs'); | |
const path = require('path'); | |
const { parse } = require('recast'); | |
const defaultParserOptions = require('reify/lib/parsers/babel.js').options; | |
// TODO: it would be better to have Babel compile the file first | |
// instead of handling some plugins specially, but that would | |
// require copying a large amount of code from Meteor's babel compiler | |
const { resolvePath } = require('babel-plugin-module-resolver'); | |
const { visit } = require('ast-types'); | |
function findBabelConfig(startDir, appDir) { | |
const babelRcPath = path.join(startDir, '.babelrc'); | |
const packageJsonPath = path.join(startDir, 'package.json'); | |
if (fs.existsSync(babelRcPath)) { | |
return babelRcPath; | |
} | |
if (fs.existsSync(packageJsonPath)) { | |
return packageJsonPath; | |
} | |
const parentDir = path.resolve(startDir, '..'); | |
if (!parentDir.includes(appDir)) { | |
return false; | |
} | |
return findBabelConfig(parentDir, appDir); | |
} | |
function findModuleResolveConfig(filePath, appDir) { | |
const fileDir = path.dirname(filePath); | |
const babelConfigPath = findBabelConfig(fileDir, appDir); | |
if (babelConfigPath) { | |
const babelConfigContent = fs.readFileSync(babelConfigPath, 'utf-8'); | |
// TODO: error handling | |
const babelConfig = JSON.parse(babelConfigContent); | |
const moduleResolvePluginConfig = | |
babelConfig.plugins.find(plugin => plugin[0] === 'module-resolver') || []; | |
return moduleResolvePluginConfig[1]; | |
} | |
} | |
module.exports = function readAndParse(filePath) { | |
const content = fs.readFileSync(filePath, 'utf-8'); | |
const ast = parse(content, { | |
parser: { | |
parse: source => | |
meteorBabelParser(source, { | |
...defaultParserOptions, | |
tokens: true, | |
}), | |
}, | |
}); | |
return ast; | |
}; | |
module.exports.findImports = function findImports(filePath, ast, appDir) { | |
const moduleResolveConfig = findModuleResolveConfig(filePath, appDir); | |
const result = []; | |
// TODO: handle require | |
visit(ast, { | |
visitImportDeclaration(nodePath) { | |
let importPath = nodePath.value.source.value; | |
if (moduleResolveConfig) { | |
const origPath = importPath; | |
importPath = | |
resolvePath(importPath, filePath, { | |
...moduleResolveConfig, | |
cwd: appDir, | |
}) || origPath; | |
} | |
result.push({ | |
source: importPath, | |
specifiers: nodePath.value.specifiers, | |
}); | |
return false; | |
}, | |
visitExportNamedDeclaration(nodePath) { | |
if (!nodePath.node.source) { | |
return false; | |
} | |
let importPath = nodePath.node.source.value; | |
if (moduleResolveConfig) { | |
const origPath = importPath; | |
importPath = | |
resolvePath(importPath, filePath, { | |
...moduleResolveConfig, | |
cwd: appDir, | |
}) || origPath; | |
} | |
result.push({ | |
source: importPath, | |
}); | |
return false; | |
}, | |
visitCallExpression(nodePath) { | |
if (nodePath.node.callee.type !== 'Import') { | |
return this.traverse(nodePath); | |
} | |
if (nodePath.node.arguments[0].type !== 'StringLiteral') { | |
throw new Error('Unable to handle non-string dynamic imports'); | |
} | |
let importPath = nodePath.node.arguments[0].value; | |
if (moduleResolveConfig) { | |
const origPath = importPath; | |
importPath = | |
resolvePath(importPath, filePath, { | |
...moduleResolveConfig, | |
cwd: appDir, | |
}) || origPath; | |
} | |
result.push({ | |
source: importPath, | |
}); | |
this.traverse(nodePath); | |
}, | |
}); | |
return result; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment