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/core": "^7.10.1",
|
||||||
"@babel/parser": "^7.5.5",
|
"@babel/parser": "^7.5.5",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
|
"@babel/plugin-syntax-export-default-from": "^7.18.6",
|
||||||
"@babel/register": "^7.5.5",
|
"@babel/register": "^7.5.5",
|
||||||
"@babel/traverse": "^7.5.5",
|
"@babel/traverse": "^7.5.5",
|
||||||
"@babel/types": "^7.9.0",
|
"@babel/types": "^7.9.0",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ const { default: traverse } = require('@babel/traverse');
|
||||||
const { Analyzer } = require('./helpers/Analyzer.js');
|
const { Analyzer } = require('./helpers/Analyzer.js');
|
||||||
const { trackDownIdentifier } = require('./helpers/track-down-identifier.js');
|
const { trackDownIdentifier } = require('./helpers/track-down-identifier.js');
|
||||||
const { normalizeSourcePaths } = require('./helpers/normalize-source-paths.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');
|
const { LogService } = require('../services/LogService.js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -135,6 +137,9 @@ function getLocalNameSpecifiers(node) {
|
||||||
.filter(s => s);
|
.filter(s => s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isImportingSpecifier = pathOrNode =>
|
||||||
|
pathOrNode.type === 'ImportDefaultSpecifier' || pathOrNode.type === 'ImportSpecifier';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc Finds import specifiers and sources for a given ast result
|
* @desc Finds import specifiers and sources for a given ast result
|
||||||
* @param {BabelAst} ast
|
* @param {BabelAst} ast
|
||||||
|
|
@ -150,17 +155,38 @@ function findExportsPerAstEntry(ast, { skipFileImports }) {
|
||||||
// Unfortunately, we cannot have async functions in babel traverse.
|
// Unfortunately, we cannot have async functions in babel traverse.
|
||||||
// Therefore, we store a temp reference to path that we use later for
|
// Therefore, we store a temp reference to path that we use later for
|
||||||
// async post processing (tracking down original export Identifier)
|
// async post processing (tracking down original export Identifier)
|
||||||
|
let globalScopeBindings;
|
||||||
|
|
||||||
traverse(ast, {
|
traverse(ast, {
|
||||||
|
Program: {
|
||||||
|
enter(babelPath) {
|
||||||
|
const body = babelPath.get('body');
|
||||||
|
if (body.length) {
|
||||||
|
globalScopeBindings = body[0].scope.bindings;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
ExportNamedDeclaration(path) {
|
ExportNamedDeclaration(path) {
|
||||||
const exportSpecifiers = getExportSpecifiers(path.node);
|
const exportSpecifiers = getExportSpecifiers(path.node);
|
||||||
const localMap = getLocalNameSpecifiers(path.node);
|
const localMap = getLocalNameSpecifiers(path.node);
|
||||||
const source = path.node.source?.value;
|
const source = path.node.source?.value;
|
||||||
transformedEntry.push({ exportSpecifiers, localMap, source, __tmp: { path } });
|
transformedEntry.push({ exportSpecifiers, localMap, source, __tmp: { path } });
|
||||||
},
|
},
|
||||||
ExportDefaultDeclaration(path) {
|
ExportDefaultDeclaration(defaultExportPath) {
|
||||||
const exportSpecifiers = ['[default]'];
|
const exportSpecifiers = ['[default]'];
|
||||||
const source = path.node.declaration.name;
|
let source;
|
||||||
transformedEntry.push({ exportSpecifiers, source, __tmp: { path } });
|
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) {
|
if (reexportMatch || shouldLookForDefaultExport) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Are we dealing with a re-export ?
|
// Are we dealing with a re-export ?
|
||||||
if (path.node.specifiers && path.node.specifiers.length) {
|
if (path.node.specifiers && path.node.specifiers.length) {
|
||||||
exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName);
|
exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName);
|
||||||
|
|
@ -202,6 +203,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reexportMatch = true;
|
reexportMatch = true;
|
||||||
|
|
||||||
pendingTrackDownPromise = trackDownIdentifier(
|
pendingTrackDownPromise = trackDownIdentifier(
|
||||||
newSource,
|
newSource,
|
||||||
localName,
|
localName,
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class AstService {
|
||||||
static _getBabelAst(code) {
|
static _getBabelAst(code) {
|
||||||
const ast = babelParser.parse(code, {
|
const ast = babelParser.parse(code, {
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
plugins: ['importMeta', 'dynamicImport', 'classProperties'],
|
plugins: ['importMeta', 'dynamicImport', 'classProperties', 'exportDefaultFrom'],
|
||||||
});
|
});
|
||||||
return ast;
|
return ast;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function getFilePathOrExternalSource({ rootPath, localPath }) {
|
||||||
* const y = x;
|
* const y = x;
|
||||||
* export const myIdentifier = y;
|
* 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', ... }
|
* - getReferencedDeclaration is called with { referencedIdentifierName: 'y', ... }
|
||||||
* - now we will look in globalScopeBindings, till we find declaration of '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)
|
* - Is it a ref? Call ourselves with referencedIdentifierName ('x' in example above)
|
||||||
|
|
@ -34,7 +34,10 @@ function getReferencedDeclaration({ referencedIdentifierName, globalScopeBinding
|
||||||
([key]) => key === referencedIdentifierName,
|
([key]) => key === referencedIdentifierName,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (refDeclaratorBinding.path.type === 'ImportSpecifier') {
|
if (
|
||||||
|
refDeclaratorBinding.path.type === 'ImportSpecifier' ||
|
||||||
|
refDeclaratorBinding.path.type === 'ImportDefaultSpecifier'
|
||||||
|
) {
|
||||||
return refDeclaratorBinding.path;
|
return refDeclaratorBinding.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,4 +162,5 @@ async function getSourceCodeFragmentOfDeclaration({
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getSourceCodeFragmentOfDeclaration,
|
getSourceCodeFragmentOfDeclaration,
|
||||||
getFilePathOrExternalSource,
|
getFilePathOrExternalSource,
|
||||||
|
getReferencedDeclaration,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,16 @@ describe('Analyzer "find-exports"', () => {
|
||||||
expect(firstEntry.result[0].source).to.equal(undefined);
|
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 () => {
|
it(`supports [export {default as x} from 'y'] (default re-export)`, async () => {
|
||||||
mockProject({
|
mockProject({
|
||||||
'./file-with-default-export.js': 'export default 1;',
|
'./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 () => {
|
it(`supports [export { x } from 'my/source'] (re-export named specifier)`, async () => {
|
||||||
mockProject([`export { x } from 'my/source'`]);
|
mockProject([`export { x } from 'my/source'`]);
|
||||||
await providence(findExportsQueryConfig, _providenceCfg);
|
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(`stores rootFileMap of an exported Identifier`, async () => {
|
||||||
it.skip(`stores rootFileMap of an exported Identifier`, async () => {
|
|
||||||
mockProject({
|
mockProject({
|
||||||
'./src/reexport.js': `
|
'./src/reexport.js': `
|
||||||
// a direct default import
|
// a direct default import
|
||||||
import RefDefault from 'exporting-ref-project';
|
import RefDefault from 'exporting-ref-project';
|
||||||
|
|
||||||
export RefDefault;
|
export default RefDefault;
|
||||||
`,
|
`,
|
||||||
'./index.js': `
|
'./index.js': `
|
||||||
export { ExtendRefDefault } from './src/reexport.js';
|
import ExtendRefDefault from './src/reexport.js';
|
||||||
|
|
||||||
|
export default ExtendRefDefault;
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
await providence(findExportsQueryConfig, _providenceCfg);
|
await providence(findExportsQueryConfig, _providenceCfg);
|
||||||
|
|
@ -210,7 +241,7 @@ describe('Analyzer "find-exports"', () => {
|
||||||
|
|
||||||
expect(firstEntry.result[0].rootFileMap).to.eql([
|
expect(firstEntry.result[0].rootFileMap).to.eql([
|
||||||
{
|
{
|
||||||
currentFileSpecifier: 'ExtendRefDefault',
|
currentFileSpecifier: '[default]',
|
||||||
rootFile: {
|
rootFile: {
|
||||||
file: 'exporting-ref-project',
|
file: 'exporting-ref-project',
|
||||||
specifier: '[default]',
|
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', () => {
|
describe('Export variable types', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue