Last active
May 28, 2021 05:01
-
-
Save miyaokamarina/6596410110cc49e403826a1071f70132 to your computer and use it in GitHub Desktop.
Scan JS/TS files for exported symbols
This file contains hidden or 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
// WTFPL or MIT | |
// | |
// Extracts all the exported symbols from files. | |
// For example, if you have a prelude and want to feed all its symbols | |
// to the `webpack.ProvidePlugin` or your fav Babel auto-import plugin. | |
// | |
// Written to be used with webpack (depends on its resolver), | |
// but it’s pretty easy to get rid of this dependency. | |
// | |
// Only supports ES modules. Doesn’t support Flow. | |
// | |
// When scanning TS files even with `types=false`, it’s still not guaranteed, | |
// the result will not include any type-only symbols. The scanner will do its | |
// best, but in some cases it currently can’t distinct value symbols from type | |
// symbols. | |
// | |
// Example usage with a Babel plugin: | |
// | |
// const scan = require('./scan-exports'); | |
// | |
// const symbols = scan({ config: require('./webpack.config'), entry: '~/prelude' }); | |
// const prelude = Object.fromEntries([...symbols].map(symbol => [symbol, { from: '~/prelude' }])); | |
// | |
// module.exports = { | |
// plugins: [ | |
// ['@xymopen/babel-plugin-auto-import', prelude], | |
// ], | |
// }; | |
// | |
// Depends on the `fs-extra-sync` (`./fses`): | |
// https://gist.github.com/miyaokamarina/f7c7cd03cf41b396386966ada5a6efa6 | |
const up = require('upath'); | |
const wp = require('webpack'); | |
const bp = require('@babel/parser'); | |
const fs = require('./fses'); | |
/** | |
* @param {string} name | |
*/ | |
const inferBabelOptions = name => { | |
/** @type {(string | [string, object])[]} */ let plugins = ['decorators-legacy', 'exportDefaultFrom', 'topLevelAwait']; | |
if (/\.(?:jsx?|mjs)$/.test(name)) plugins.push('throwExpressions'); | |
if (/\.[jt]sx$/.test(name)) plugins.push('jsx'); | |
if (/\.tsx?$/.test(name)) { | |
if (/\.d\.ts$/.test(name)) plugins.push(['typescript', { dts: true }]); | |
else plugins.push('typescript'); | |
} | |
return { | |
sourceFilename: name, | |
sourceType: 'module', | |
errorRecovery: true, | |
allowUndeclaredExports: true, | |
plugins, | |
}; | |
}; | |
/** | |
* @returns {boolean} | |
*/ | |
const selectExport = node => { | |
switch (node.type) { | |
case 'ExportNamedDeclaration': | |
case 'ExportDefaultDeclaration': | |
case 'ExportAllDeclaration': | |
return true; | |
default: | |
return false; | |
} | |
}; | |
/** | |
* @param {object} options.config webpack configuration object | |
* @param {string} options.entry A file to start scan from. | |
* @param {boolean} [options.recursive=true] Include re-exported symbols. | |
* @param {boolean} [options.types=true] Include type-only exports. | |
* @return {Set<string>} | |
*/ | |
const scanExports = options => { | |
let { config, entry, recursive = true, types = true } = options; | |
let webpack = wp(config); | |
let root = webpack.context; // Project root. | |
webpack.inputFileSystem = fs; // Use the sync filesystem to allow the `resolver.resolveSync` method. | |
let resolver = webpack.resolverFactory.get('normal', webpack.options.resolve); | |
/** | |
* Returns a project-relative path. | |
* | |
* @param {string} name | |
* @returns {string} | |
*/ | |
const relative = name => up.relative(root, name); | |
/** | |
* Resolves a file by module identifier. | |
* | |
* Tweak this function if you don’t need webpack. | |
* | |
* @param {string} base Directory to resolve a module identifier in. | |
* @param {string} name The module identifier to resolve. | |
* @returns {string | undefined} | |
*/ | |
const resolve = (base, name) => { | |
try { | |
return relative(resolver.resolveSync({}, base, name) || '') || undefined; | |
} catch (err) { | |
return undefined; | |
} | |
}; | |
/** | |
* @param {string} [name] | |
*/ | |
const scanFile = name => { | |
const visitNode = node => { | |
switch (node.type) { | |
case 'ExportDefaultDeclaration': | |
return; | |
case 'ExportNamedDeclaration': | |
if (node.exportKind === 'type' && !types) return; // export type {} from, export type, export interface | |
if (node.declaration) return visitNode(node.declaration); // export const/let/var/class/function | |
if (node.specifiers) return node.specifiers.forEach(visitNode); // export {} | |
return; | |
case 'Identifier': | |
symbols.add(node.name); | |
return; | |
case 'ObjectPattern': | |
return node.properties.forEach(visitNode); | |
case 'ArrayPattern': | |
return node.elements.forEach(visitNode); | |
case 'VariableDeclaration': | |
return node.declarations.forEach(visitNode); | |
case 'ObjectProperty': | |
return visitNode(node.value); | |
case 'RestElement': | |
return visitNode(node.argument); | |
case 'ExportSpecifier': | |
case 'ExportDefaultSpecifier': | |
case 'ExportNamespaceSpecifier': | |
return visitNode(node.exported); | |
case 'FunctionDeclaration': | |
case 'ClassDeclaration': | |
case 'VariableDeclarator': | |
case 'TSModuleDeclaration': | |
case 'TSInterfaceDeclaration': | |
case 'TSTypeAliasDeclaration': | |
return visitNode(node.id); | |
case 'TSEnumDeclaration': | |
if (node.const && !types) return; | |
return visitNode(node.id); | |
case 'ExportAllDeclaration': | |
if (recursive) queue.push(resolve(base, node.source.value)); | |
return; | |
default: | |
console.log(node); | |
return; | |
} | |
}; | |
if (!name || visited.has(name)) return; | |
else visited.add(name); | |
let full = up.join(root, name); | |
let base = up.dirname(name); | |
let text = fs.utf8Sync(full); | |
let file = bp.parse(text, inferBabelOptions(name)); | |
file.program.body.filter(selectExport).forEach(visitNode); | |
}; | |
/** @type {Set<string>} */ let visited = new Set(); | |
/** @type {Set<string>} */ let symbols = new Set(); | |
/** @type {string[]} */ let queue = [resolve(root, entry)]; | |
while (queue.length) { | |
scanFile(queue.shift()); | |
} | |
return symbols; | |
}; | |
module.exports = scanExports; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment