feat(providence-analytics): support "exportDefaultFrom" in find-exports analyzer
This commit is contained in:
parent
5925364ffe
commit
78697d3b00
6 changed files with 85 additions and 11 deletions
|
|
@ -32,6 +32,7 @@
|
|||
"@babel/core": "^7.10.1",
|
||||
"@babel/parser": "^7.5.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-syntax-export-default-from": "^7.18.6",
|
||||
"@babel/register": "^7.5.5",
|
||||
"@babel/traverse": "^7.5.5",
|
||||
"@babel/types": "^7.9.0",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ const { default: traverse } = require('@babel/traverse');
|
|||
const { Analyzer } = require('./helpers/Analyzer.js');
|
||||
const { trackDownIdentifier } = require('./helpers/track-down-identifier.js');
|
||||
const { normalizeSourcePaths } = require('./helpers/normalize-source-paths.js');
|
||||
const { getReferencedDeclaration } = require('../utils/get-source-code-fragment-of-declaration.js');
|
||||
|
||||
const { LogService } = require('../services/LogService.js');
|
||||
|
||||
/**
|
||||
|
|
@ -135,6 +137,9 @@ function getLocalNameSpecifiers(node) {
|
|||
.filter(s => s);
|
||||
}
|
||||
|
||||
const isImportingSpecifier = pathOrNode =>
|
||||
pathOrNode.type === 'ImportDefaultSpecifier' || pathOrNode.type === 'ImportSpecifier';
|
||||
|
||||
/**
|
||||
* @desc Finds import specifiers and sources for a given ast result
|
||||
* @param {BabelAst} ast
|
||||
|
|
@ -150,17 +155,38 @@ function findExportsPerAstEntry(ast, { skipFileImports }) {
|
|||
// Unfortunately, we cannot have async functions in babel traverse.
|
||||
// Therefore, we store a temp reference to path that we use later for
|
||||
// async post processing (tracking down original export Identifier)
|
||||
let globalScopeBindings;
|
||||
|
||||
traverse(ast, {
|
||||
Program: {
|
||||
enter(babelPath) {
|
||||
const body = babelPath.get('body');
|
||||
if (body.length) {
|
||||
globalScopeBindings = body[0].scope.bindings;
|
||||
}
|
||||
},
|
||||
},
|
||||
ExportNamedDeclaration(path) {
|
||||
const exportSpecifiers = getExportSpecifiers(path.node);
|
||||
const localMap = getLocalNameSpecifiers(path.node);
|
||||
const source = path.node.source?.value;
|
||||
transformedEntry.push({ exportSpecifiers, localMap, source, __tmp: { path } });
|
||||
},
|
||||
ExportDefaultDeclaration(path) {
|
||||
ExportDefaultDeclaration(defaultExportPath) {
|
||||
const exportSpecifiers = ['[default]'];
|
||||
const source = path.node.declaration.name;
|
||||
transformedEntry.push({ exportSpecifiers, source, __tmp: { path } });
|
||||
let source;
|
||||
if (defaultExportPath.node.declaration?.type !== 'Identifier') {
|
||||
source = defaultExportPath.node.declaration.name;
|
||||
} else {
|
||||
const importOrDeclPath = getReferencedDeclaration({
|
||||
referencedIdentifierName: defaultExportPath.node.declaration.name,
|
||||
globalScopeBindings,
|
||||
});
|
||||
if (isImportingSpecifier(importOrDeclPath)) {
|
||||
source = importOrDeclPath.parentPath.node.source.value;
|
||||
}
|
||||
}
|
||||
transformedEntry.push({ exportSpecifiers, source, __tmp: { path: defaultExportPath } });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
|
|||
if (reexportMatch || shouldLookForDefaultExport) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Are we dealing with a re-export ?
|
||||
if (path.node.specifiers && path.node.specifiers.length) {
|
||||
exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName);
|
||||
|
|
@ -202,6 +203,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
|
|||
}
|
||||
}
|
||||
reexportMatch = true;
|
||||
|
||||
pendingTrackDownPromise = trackDownIdentifier(
|
||||
newSource,
|
||||
localName,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class AstService {
|
|||
static _getBabelAst(code) {
|
||||
const ast = babelParser.parse(code, {
|
||||
sourceType: 'module',
|
||||
plugins: ['importMeta', 'dynamicImport', 'classProperties'],
|
||||
plugins: ['importMeta', 'dynamicImport', 'classProperties', 'exportDefaultFrom'],
|
||||
});
|
||||
return ast;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function getFilePathOrExternalSource({ rootPath, localPath }) {
|
|||
* const y = x;
|
||||
* export const myIdentifier = y;
|
||||
* ```
|
||||
* - We started in getSourceCodeFragmentOfDeclaration (looing for 'myIdentifier'), which found VariableDeclarator of export myIdentifier
|
||||
* - We started in getSourceCodeFragmentOfDeclaration (looking for 'myIdentifier'), which found VariableDeclarator of export myIdentifier
|
||||
* - getReferencedDeclaration is called with { referencedIdentifierName: 'y', ... }
|
||||
* - now we will look in globalScopeBindings, till we find declaration of 'y'
|
||||
* - Is it a ref? Call ourselves with referencedIdentifierName ('x' in example above)
|
||||
|
|
@ -34,7 +34,10 @@ function getReferencedDeclaration({ referencedIdentifierName, globalScopeBinding
|
|||
([key]) => key === referencedIdentifierName,
|
||||
);
|
||||
|
||||
if (refDeclaratorBinding.path.type === 'ImportSpecifier') {
|
||||
if (
|
||||
refDeclaratorBinding.path.type === 'ImportSpecifier' ||
|
||||
refDeclaratorBinding.path.type === 'ImportDefaultSpecifier'
|
||||
) {
|
||||
return refDeclaratorBinding.path;
|
||||
}
|
||||
|
||||
|
|
@ -159,4 +162,5 @@ async function getSourceCodeFragmentOfDeclaration({
|
|||
module.exports = {
|
||||
getSourceCodeFragmentOfDeclaration,
|
||||
getFilePathOrExternalSource,
|
||||
getReferencedDeclaration,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -66,6 +66,16 @@ describe('Analyzer "find-exports"', () => {
|
|||
expect(firstEntry.result[0].source).to.equal(undefined);
|
||||
});
|
||||
|
||||
it(`supports [export default fn(){}] (default export)`, async () => {
|
||||
mockProject([`export default x => x * 3`]);
|
||||
await providence(findExportsQueryConfig, _providenceCfg);
|
||||
const queryResult = queryResults[0];
|
||||
const firstEntry = getEntry(queryResult);
|
||||
expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
|
||||
expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('[default]');
|
||||
expect(firstEntry.result[0].source).to.equal(undefined);
|
||||
});
|
||||
|
||||
it(`supports [export {default as x} from 'y'] (default re-export)`, async () => {
|
||||
mockProject({
|
||||
'./file-with-default-export.js': 'export default 1;',
|
||||
|
|
@ -105,6 +115,26 @@ describe('Analyzer "find-exports"', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`supports [import {x} from 'y'; export default x] (named re-export as default)`, async () => {
|
||||
mockProject([`import {x} from 'y'; export default x;`]);
|
||||
await providence(findExportsQueryConfig, _providenceCfg);
|
||||
const queryResult = queryResults[0];
|
||||
const firstEntry = getEntry(queryResult);
|
||||
expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
|
||||
expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('[default]');
|
||||
expect(firstEntry.result[0].source).to.equal('y');
|
||||
});
|
||||
|
||||
it(`supports [import x from 'y'; export default x] (default re-export as default)`, async () => {
|
||||
mockProject([`import x from 'y'; export default x;`]);
|
||||
await providence(findExportsQueryConfig, _providenceCfg);
|
||||
const queryResult = queryResults[0];
|
||||
const firstEntry = getEntry(queryResult);
|
||||
expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
|
||||
expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('[default]');
|
||||
expect(firstEntry.result[0].source).to.equal('y');
|
||||
});
|
||||
|
||||
it(`supports [export { x } from 'my/source'] (re-export named specifier)`, async () => {
|
||||
mockProject([`export { x } from 'my/source'`]);
|
||||
await providence(findExportsQueryConfig, _providenceCfg);
|
||||
|
|
@ -191,17 +221,18 @@ describe('Analyzer "find-exports"', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
// TODO: myabe in the future: This experimental syntax requires enabling the parser plugin: 'exportDefaultFrom'
|
||||
it.skip(`stores rootFileMap of an exported Identifier`, async () => {
|
||||
it(`stores rootFileMap of an exported Identifier`, async () => {
|
||||
mockProject({
|
||||
'./src/reexport.js': `
|
||||
// a direct default import
|
||||
import RefDefault from 'exporting-ref-project';
|
||||
|
||||
export RefDefault;
|
||||
export default RefDefault;
|
||||
`,
|
||||
'./index.js': `
|
||||
export { ExtendRefDefault } from './src/reexport.js';
|
||||
import ExtendRefDefault from './src/reexport.js';
|
||||
|
||||
export default ExtendRefDefault;
|
||||
`,
|
||||
});
|
||||
await providence(findExportsQueryConfig, _providenceCfg);
|
||||
|
|
@ -210,7 +241,7 @@ describe('Analyzer "find-exports"', () => {
|
|||
|
||||
expect(firstEntry.result[0].rootFileMap).to.eql([
|
||||
{
|
||||
currentFileSpecifier: 'ExtendRefDefault',
|
||||
currentFileSpecifier: '[default]',
|
||||
rootFile: {
|
||||
file: 'exporting-ref-project',
|
||||
specifier: '[default]',
|
||||
|
|
@ -218,6 +249,16 @@ describe('Analyzer "find-exports"', () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`correctly handles empty files`, async () => {
|
||||
// These can be encountered while scanning repos.. They should not break the code...
|
||||
mockProject([`// some comment here...`]);
|
||||
await providence(findExportsQueryConfig, _providenceCfg);
|
||||
const queryResult = queryResults[0];
|
||||
const firstEntry = getEntry(queryResult);
|
||||
expect(firstEntry.result[0].exportSpecifiers).to.eql(['[file]']);
|
||||
expect(firstEntry.result[0].source).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Export variable types', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue