chore: clean and lint providence

This commit is contained in:
Thijs Louisse 2023-11-08 18:41:12 +01:00
parent f601d59d77
commit e7a65a1e51
12 changed files with 76 additions and 846 deletions

View file

@ -1,272 +0,0 @@
/* eslint-disable no-shadow, no-param-reassign */
import pathLib from 'path';
import babelTraverse from '@babel/traverse';
import { Analyzer } from '../core/Analyzer.js';
import { trackDownIdentifier } from './helpers/track-down-identifier--legacy.js';
import { normalizeSourcePaths } from './helpers/normalize-source-paths.js';
import { getReferencedDeclaration } from '../utils/get-source-code-fragment-of-declaration.js';
import { LogService } from '../core/LogService.js';
/**
* @typedef {import('@babel/types').File} File
* @typedef {import('@babel/types').Node} Node
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
* @typedef {import('../../../types/index.js').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @typedef {import('../../../types/index.js').FindExportsAnalyzerEntry} FindExportsAnalyzerEntry
* @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('./helpers/track-down-identifier.js').RootFile} RootFile
* @typedef {object} RootFileMapEntry
* @typedef {string} currentFileSpecifier this is the local name in the file we track from
* @typedef {RootFile} rootFile contains file(filePath) and specifier
* @typedef {RootFileMapEntry[]} RootFileMap
* @typedef {{ exportSpecifiers:string[]; localMap: object; source:string, __tmp: { path:string } }} FindExportsSpecifierObj
*/
/**
* @param {FindExportsSpecifierObj[]} transformedFile
*/
async function trackdownRoot(transformedFile, relativePath, projectPath) {
const fullCurrentFilePath = pathLib.resolve(projectPath, relativePath);
for (const specObj of transformedFile) {
/** @type {RootFileMap} */
const rootFileMap = [];
if (specObj.exportSpecifiers[0] === '[file]') {
rootFileMap.push(undefined);
} else {
/**
* './src/origin.js': `export class MyComp {}`
* './index.js:' `export { MyComp as RenamedMyComp } from './src/origin'`
*
* Goes from specifier like 'RenamedMyComp' to object for rootFileMap like:
* {
* currentFileSpecifier: 'RenamedMyComp',
* rootFile: {
* file: './src/origin.js',
* specifier: 'MyCompDefinition',
* }
* }
*/
for (const specifier of specObj.exportSpecifiers) {
let rootFile;
let localMapMatch;
if (specObj.localMap) {
localMapMatch = specObj.localMap.find(m => m.exported === specifier);
}
// TODO: find out if possible to use trackDownIdentifierFromScope
if (specObj.source) {
// TODO: see if still needed: && (localMapMatch || specifier === '[default]')
const importedIdentifier = localMapMatch?.local || specifier;
rootFile = await trackDownIdentifier(
specObj.source,
importedIdentifier,
fullCurrentFilePath,
projectPath,
);
/** @type {RootFileMapEntry} */
const entry = {
currentFileSpecifier: specifier,
rootFile,
};
rootFileMap.push(entry);
} else {
/** @type {RootFileMapEntry} */
const entry = {
currentFileSpecifier: specifier,
rootFile: { file: '[current]', specifier },
};
rootFileMap.push(entry);
}
}
}
specObj.rootFileMap = rootFileMap;
}
return transformedFile;
}
function cleanup(transformedFile) {
transformedFile.forEach(specObj => {
if (specObj.__tmp) {
delete specObj.__tmp;
}
});
return transformedFile;
}
/**
* @returns {string[]}
*/
function getExportSpecifiers(node) {
// handles default [export const g = 4];
if (node.declaration) {
if (node.declaration.declarations) {
return [node.declaration.declarations[0].id.name];
}
if (node.declaration.id) {
return [node.declaration.id.name];
}
}
// handles (re)named specifiers [export { x (as y)} from 'y'];
return node.specifiers.map(s => {
let specifier;
if (s.exported) {
// { x as y }
specifier = s.exported.name === 'default' ? '[default]' : s.exported.name;
} else {
// { x }
specifier = s.local.name;
}
return specifier;
});
}
/**
* @returns {object[]}
*/
function getLocalNameSpecifiers(node) {
return node.specifiers
.map(s => {
if (s.exported && s.local && s.exported.name !== s.local.name) {
return {
// if reserved keyword 'default' is used, translate it into 'providence keyword'
local: s.local.name === 'default' ? '[default]' : s.local.name,
exported: s.exported.name,
};
}
return undefined;
})
.filter(s => s);
}
const isImportingSpecifier = pathOrNode =>
pathOrNode.type === 'ImportDefaultSpecifier' || pathOrNode.type === 'ImportSpecifier';
/**
* Finds import specifiers and sources for a given ast result
* @param {File} babelAst
* @param {FindExportsConfig} config
*/
function findExportsPerAstFile(babelAst, { skipFileImports }) {
LogService.debug(`Analyzer "find-exports": started findExportsPerAstFile method`);
// Visit AST...
/** @type {FindExportsSpecifierObj[]} */
const transformedFile = [];
// 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;
babelTraverse.default(babelAst, {
Program(babelPath) {
// enter(babelPath) {
const body = babelPath.get('body');
if (body.length) {
globalScopeBindings = body[0].scope.bindings;
}
// },
},
ExportNamedDeclaration(astPath) {
const exportSpecifiers = getExportSpecifiers(astPath.node);
const localMap = getLocalNameSpecifiers(astPath.node);
const source = astPath.node.source?.value;
const entry = { exportSpecifiers, localMap, source, __tmp: { astPath } };
if (astPath.node.assertions?.length) {
entry.assertionType = astPath.node.assertions[0].value?.value;
}
transformedFile.push(entry);
},
ExportDefaultDeclaration(defaultExportPath) {
const exportSpecifiers = ['[default]'];
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;
}
}
transformedFile.push({ exportSpecifiers, source, __tmp: { astPath: defaultExportPath } });
},
});
if (!skipFileImports) {
// Always add an entry for just the file 'relativePath'
// (since this also can be imported directly from a search target project)
transformedFile.push({
exportSpecifiers: ['[file]'],
// source: relativePath,
});
}
return transformedFile;
}
export default class FindExportsAnalyzer extends Analyzer {
/** @type {AnalyzerName} */
static analyzerName = 'find-exports';
/** @type {'babel'|'swc-to-babel'} */
static requiredAst = 'swc-to-babel';
/**
* Finds export specifiers and sources
* @param {FindExportsConfig} customConfig
*/
async execute(customConfig = {}) {
/**
* @typedef FindExportsConfig
* @property {boolean} [onlyInternalSources=false]
* @property {boolean} [skipFileImports=false] Instead of both focusing on specifiers like
* [import {specifier} 'lion-based-ui/foo.js'], and [import 'lion-based-ui/foo.js'] as a result,
* not list file exports
*/
const cfg = {
targetProjectPath: null,
skipFileImports: false,
...customConfig,
};
/**
* Prepare
*/
const cachedAnalyzerResult = this._prepare(cfg);
if (cachedAnalyzerResult) {
return cachedAnalyzerResult;
}
/**
* Traverse
*/
const projectPath = cfg.targetProjectPath;
const traverseEntryFn = async (ast, { relativePath }) => {
let transformedFile = findExportsPerAstFile(ast, cfg);
transformedFile = await normalizeSourcePaths(transformedFile, relativePath, projectPath);
transformedFile = await trackdownRoot(transformedFile, relativePath, projectPath);
transformedFile = cleanup(transformedFile);
return { result: transformedFile };
};
const queryOutput = await this._traverse({
traverseEntryFn,
filePaths: cfg.targetFilePaths,
projectPath: cfg.targetProjectPath,
});
/**
* Finalize
*/
return this._finalize(queryOutput, cfg);
}
}

View file

@ -1,189 +0,0 @@
/* eslint-disable no-shadow, no-param-reassign */
import babelTraverse from '@babel/traverse';
import { isRelativeSourcePath } from '../utils/relative-source-path.js';
import { normalizeSourcePaths } from './helpers/normalize-source-paths.js';
import { Analyzer } from '../core/Analyzer.js';
import { LogService } from '../core/LogService.js';
/**
* @typedef {import('@babel/types').File} File
* @typedef {import('@babel/types').Node} Node
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
* @typedef {import('../../../types/index.js').AnalyzerConfig} AnalyzerConfig
* @typedef {import('../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../../../types/index.js').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry
* @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
*/
/**
* Options that allow to filter 'on a file basis'.
* We can also filter on the total result
*/
const /** @type {AnalyzerConfig} */ options = {
/**
* Only leaves entries with external sources:
* - keeps: '@open-wc/testing'
* - drops: '../testing'
* @param {FindImportsAnalyzerQueryOutput} result
* @param {string} targetSpecifier for instance 'LitElement'
*/
onlyExternalSources(result) {
return result.filter(entry => !isRelativeSourcePath(entry.source));
},
};
/**
* @param {Node} node
*/
function getImportOrReexportsSpecifiers(node) {
return node.specifiers.map(s => {
if (
s.type === 'ImportDefaultSpecifier' ||
s.type === 'ExportDefaultSpecifier' ||
(s.type === 'ExportSpecifier' && s.exported?.name === 'default')
) {
return '[default]';
}
if (s.type === 'ImportNamespaceSpecifier' || s.type === 'ExportNamespaceSpecifier') {
return '[*]';
}
if ((s.imported && s.type === 'ImportNamespaceSpecifier') || s.type === 'ImportSpecifier') {
return s.imported.name;
}
if (s.exported && s.type === 'ExportNamespaceSpecifier') {
return s.exported.name;
}
return s.local.name;
});
}
/**
* Finds import specifiers and sources
* @param {File} babelAst
*/
function findImportsPerAstFile(babelAst, context) {
LogService.debug(`Analyzer "find-imports": started findImportsPerAstFile method`);
// https://github.com/babel/babel/blob/672a58660f0b15691c44582f1f3fdcdac0fa0d2f/packages/babel-core/src/transformation/index.ts#L110
// Visit AST...
/** @type {Partial<FindImportsAnalyzerEntry>[]} */
const transformedFile = [];
babelTraverse.default(babelAst, {
ImportDeclaration(path) {
const importSpecifiers = getImportOrReexportsSpecifiers(path.node);
if (!importSpecifiers.length) {
importSpecifiers.push('[file]'); // apparently, there was just a file import
}
const source = path.node.source.value;
const entry = /** @type {Partial<FindImportsAnalyzerEntry>} */ ({ importSpecifiers, source });
if (path.node.assertions?.length) {
entry.assertionType = path.node.assertions[0].value?.value;
}
transformedFile.push(entry);
},
// Dynamic imports
CallExpression(path) {
if (path.node.callee?.type !== 'Import') {
return;
}
// TODO: check for specifiers catched via obj destructuring?
// TODO: also check for ['file']
const importSpecifiers = ['[default]'];
let source = path.node.arguments[0].value;
if (!source) {
// TODO: with advanced retrieval, we could possibly get the value
source = '[variable]';
}
transformedFile.push({ importSpecifiers, source });
},
ExportNamedDeclaration(path) {
if (!path.node.source) {
return; // we are dealing with a regular export, not a reexport
}
const importSpecifiers = getImportOrReexportsSpecifiers(path.node);
const source = path.node.source.value;
const entry = /** @type {Partial<FindImportsAnalyzerEntry>} */ ({ importSpecifiers, source });
if (path.node.assertions?.length) {
entry.assertionType = path.node.assertions[0].value?.value;
}
transformedFile.push(entry);
},
// ExportAllDeclaration(path) {
// if (!path.node.source) {
// return; // we are dealing with a regular export, not a reexport
// }
// const importSpecifiers = ['[*]'];
// const source = path.node.source.value;
// transformedFile.push({ importSpecifiers, source });
// },
});
return transformedFile;
}
export default class FindImportsAnalyzer extends Analyzer {
/** @type {AnalyzerName} */
static analyzerName = 'find-imports';
/** @type {'babel'|'swc-to-babel'} */
requiredAst = 'swc-to-babel';
/**
* Finds import specifiers and sources
* @param {FindImportsConfig} customConfig
*/
async execute(customConfig = {}) {
/**
* @typedef FindImportsConfig
* @property {boolean} [keepInternalSources=false] by default, relative paths like '../x.js' are
* filtered out. This option keeps them.
* means that 'external-dep/file' will be resolved to 'external-dep/file.js' will both be stored
* as the latter
*/
const cfg = {
targetProjectPath: null,
// post process file
keepInternalSources: false,
...customConfig,
};
/**
* Prepare
*/
const cachedAnalyzerResult = this._prepare(cfg);
if (cachedAnalyzerResult) {
return cachedAnalyzerResult;
}
/**
* Traverse
*/
const queryOutput = await this._traverse(async (ast, context) => {
let transformedFile = findImportsPerAstFile(ast, context);
// Post processing based on configuration...
transformedFile = await normalizeSourcePaths(
transformedFile,
context.relativePath,
cfg.targetProjectPath,
);
if (!cfg.keepInternalSources) {
transformedFile = options.onlyExternalSources(transformedFile);
}
return { result: transformedFile };
});
// if (cfg.sortBySpecifier) {
// queryOutput = sortBySpecifier.execute(queryOutput, {
// ...cfg,
// specifiersKey: 'importSpecifiers',
// });
// }
/**
* Finalize
*/
return this._finalize(queryOutput, cfg);
}
}

View file

@ -20,6 +20,7 @@ import { LogService } from '../core/LogService.js';
* @param {SwcNode} node * @param {SwcNode} node
*/ */
function getImportOrReexportsSpecifiers(node) { function getImportOrReexportsSpecifiers(node) {
// @ts-expect-error
return node.specifiers.map(s => { return node.specifiers.map(s => {
if ( if (
s.type === 'ImportDefaultSpecifier' || s.type === 'ImportDefaultSpecifier' ||
@ -40,7 +41,7 @@ function getImportOrReexportsSpecifiers(node) {
* Finds import specifiers and sources * Finds import specifiers and sources
* @param {SwcAstModule} swcAst * @param {SwcAstModule} swcAst
*/ */
function findImportsPerAstFile(swcAst, context) { function findImportsPerAstFile(swcAst) {
LogService.debug(`Analyzer "find-imports": started findImportsPerAstFile method`); LogService.debug(`Analyzer "find-imports": started findImportsPerAstFile method`);
// https://github.com/babel/babel/blob/672a58660f0b15691c44582f1f3fdcdac0fa0d2f/packages/babel-core/src/transformation/index.ts#L110 // https://github.com/babel/babel/blob/672a58660f0b15691c44582f1f3fdcdac0fa0d2f/packages/babel-core/src/transformation/index.ts#L110
@ -129,15 +130,18 @@ export default class FindImportsSwcAnalyzer extends Analyzer {
* Traverse * Traverse
*/ */
const queryOutput = await this._traverse(async (swcAst, context) => { const queryOutput = await this._traverse(async (swcAst, context) => {
let transformedFile = findImportsPerAstFile(swcAst, context); // @ts-expect-error
let transformedFile = findImportsPerAstFile(swcAst);
// Post processing based on configuration... // Post processing based on configuration...
transformedFile = await normalizeSourcePaths( transformedFile = await normalizeSourcePaths(
transformedFile, transformedFile,
context.relativePath, context.relativePath,
// @ts-expect-error
cfg.targetProjectPath, cfg.targetProjectPath,
); );
if (!cfg.keepInternalSources) { if (!cfg.keepInternalSources) {
// @ts-expect-error
transformedFile = transformedFile.filter(entry => !isRelativeSourcePath(entry.source)); transformedFile = transformedFile.filter(entry => !isRelativeSourcePath(entry.source));
} }

View file

@ -1,3 +1,5 @@
/* eslint-disable no-shadow */
// @ts-nocheck
import fs from 'fs'; import fs from 'fs';
import pathLib from 'path'; import pathLib from 'path';
import babelTraverse from '@babel/traverse'; import babelTraverse from '@babel/traverse';

View file

@ -39,16 +39,16 @@ function isExternalProject(source, projectName) {
* Other than with import, no binding is created for MyClass by Babel(?) * Other than with import, no binding is created for MyClass by Babel(?)
* This means 'path.scope.getBinding('MyClass')' returns undefined * This means 'path.scope.getBinding('MyClass')' returns undefined
* and we have to find a different way to retrieve this value. * and we have to find a different way to retrieve this value.
* @param {SwcPath} astPath Babel ast traversal path * @param {SwcPath} swcPath Babel ast traversal path
* @param {IdentifierName} identifierName the name that should be tracked (and that exists inside scope of astPath) * @param {IdentifierName} identifierName the name that should be tracked (and that exists inside scope of astPath)
*/ */
function getBindingAndSourceReexports(astPath, identifierName) { function getBindingAndSourceReexports(swcPath, identifierName) {
// Get to root node of file and look for exports like `export { identifierName } from 'src';` // Get to root node of file and look for exports like `export { identifierName } from 'src';`
let source; let source;
let bindingType; let bindingType;
let bindingPath; let bindingPath;
let curPath = astPath; let curPath = swcPath;
while (curPath.parentPath) { while (curPath.parentPath) {
curPath = curPath.parentPath; curPath = curPath.parentPath;
} }

View file

@ -6,8 +6,8 @@ import { trackDownIdentifier } from '../analyzers/helpers/track-down-identifier.
import { toPosixPath } from './to-posix-path.js'; import { toPosixPath } from './to-posix-path.js';
/** /**
* @typedef {import('@babel/types').Node} Node * @typedef {import('@swc/core').Node} SwcNode
* @typedef {import('@babel/traverse').NodePath} NodePath * @typedef {import('../../../types/index.js').SwcPath} SwcPath
* @typedef {import('../../../types/index.js').SwcBinding} SwcBinding * @typedef {import('../../../types/index.js').SwcBinding} SwcBinding
* @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot * @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
@ -39,13 +39,10 @@ export function getFilePathOrExternalSource({ rootPath, localPath }) {
* - Is it a ref? Call ourselves with referencedIdentifierName ('x' in example above) * - Is it a ref? Call ourselves with referencedIdentifierName ('x' in example above)
* - is it a non ref declaration? Return the path of the node * - is it a non ref declaration? Return the path of the node
* @param {{ referencedIdentifierName:string, globalScopeBindings:{[key:string]:SwcBinding}; }} opts * @param {{ referencedIdentifierName:string, globalScopeBindings:{[key:string]:SwcBinding}; }} opts
* @returns {NodePath} * @returns {SwcPath|null}
*/ */
export function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) { export function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) {
// We go from referencedIdentifierName 'y' to binding (VariableDeclarator path) 'y'; // We go from referencedIdentifierName 'y' to binding (VariableDeclarator path) 'y';
// const [, refDeclaratorBinding] =
// Object.entries(globalScopeBindings).find(([key]) => key === referencedIdentifierName) || [];
const refDeclaratorBinding = globalScopeBindings[referencedIdentifierName]; const refDeclaratorBinding = globalScopeBindings[referencedIdentifierName];
// We provided a referencedIdentifierName that is not in the globalScopeBindings // We provided a referencedIdentifierName that is not in the globalScopeBindings
@ -80,7 +77,7 @@ export function getReferencedDeclaration({ referencedIdentifierName, globalScope
* ``` * ```
* *
* @param {{ filePath: PathFromSystemRoot; exportedIdentifier: string; projectRootPath: PathFromSystemRoot }} opts * @param {{ filePath: PathFromSystemRoot; exportedIdentifier: string; projectRootPath: PathFromSystemRoot }} opts
* @returns {Promise<{ sourceNodePath: string; sourceFragment: string|null; externalImportSource: string; }>} * @returns {Promise<{ sourceNodePath: SwcPath; sourceFragment: string|null; externalImportSource: string|null; }>}
*/ */
export async function getSourceCodeFragmentOfDeclaration({ export async function getSourceCodeFragmentOfDeclaration({
filePath, filePath,
@ -94,7 +91,7 @@ export async function getSourceCodeFragmentOfDeclaration({
// TODO: fix swc-to-babel lib to make this compatible with 'swc-to-babel' mode of getAst // TODO: fix swc-to-babel lib to make this compatible with 'swc-to-babel' mode of getAst
const swcAst = AstService._getSwcAst(code); const swcAst = AstService._getSwcAst(code);
/** @type {NodePath} */ /** @type {SwcPath} */
let finalNodePath; let finalNodePath;
swcTraverse( swcTraverse(
@ -114,20 +111,25 @@ export async function getSourceCodeFragmentOfDeclaration({
const globalScopeBindings = getPathFromNode(astPath.node.body?.[0])?.scope.bindings; const globalScopeBindings = getPathFromNode(astPath.node.body?.[0])?.scope.bindings;
if (exportedIdentifier === '[default]') { if (exportedIdentifier === '[default]') {
const defaultExportPath = getPathFromNode( const defaultExportPath = /** @type {SwcPath} */ (
astPath.node.body.find(child => getPathFromNode(
astPath.node.body.find((/** @type {{ type: string; }} */ child) =>
['ExportDefaultDeclaration', 'ExportDefaultExpression'].includes(child.type), ['ExportDefaultDeclaration', 'ExportDefaultExpression'].includes(child.type),
), ),
)
); );
const isReferenced = defaultExportPath?.node.expression?.type === 'Identifier'; const isReferenced = defaultExportPath?.node.expression?.type === 'Identifier';
if (!isReferenced) { if (!isReferenced) {
finalNodePath = defaultExportPath.get('decl') || defaultExportPath.get('expression'); finalNodePath = defaultExportPath.get('decl') || defaultExportPath.get('expression');
} else { } else {
finalNodePath = getReferencedDeclaration({ finalNodePath = /** @type {SwcPath} */ (
getReferencedDeclaration({
referencedIdentifierName: defaultExportPath.node.expression.value, referencedIdentifierName: defaultExportPath.node.expression.value,
// @ts-expect-error
globalScopeBindings, globalScopeBindings,
}); })
);
} }
} else { } else {
const variableDeclaratorPath = astPath.scope.bindings[exportedIdentifier].path; const variableDeclaratorPath = astPath.scope.bindings[exportedIdentifier].path;
@ -145,10 +147,13 @@ export async function getSourceCodeFragmentOfDeclaration({
// it must be an exported declaration // it must be an exported declaration
finalNodePath = contentPath; finalNodePath = contentPath;
} else { } else {
finalNodePath = getReferencedDeclaration({ finalNodePath = /** @type {SwcPath} */ (
getReferencedDeclaration({
referencedIdentifierName: name, referencedIdentifierName: name,
// @ts-expect-error
globalScopeBindings, globalScopeBindings,
}); })
);
} }
} }
}, },
@ -156,9 +161,12 @@ export async function getSourceCodeFragmentOfDeclaration({
{ needsAdvancedPaths: true }, { needsAdvancedPaths: true },
); );
// @ts-expect-error
if (finalNodePath.type === 'ImportSpecifier') { if (finalNodePath.type === 'ImportSpecifier') {
// @ts-expect-error
const importDeclNode = finalNodePath.parentPath.node; const importDeclNode = finalNodePath.parentPath.node;
const source = importDeclNode.source.value; const source = importDeclNode.source.value;
// @ts-expect-error
const identifierName = finalNodePath.node.imported?.value || finalNodePath.node.local?.value; const identifierName = finalNodePath.node.imported?.value || finalNodePath.node.local?.value;
const currentFilePath = filePath; const currentFilePath = filePath;
@ -170,13 +178,14 @@ export async function getSourceCodeFragmentOfDeclaration({
); );
const filePathOrSrc = getFilePathOrExternalSource({ const filePathOrSrc = getFilePathOrExternalSource({
rootPath: projectRootPath, rootPath: projectRootPath,
localPath: rootFile.file, localPath: /** @type {PathRelativeFromProjectRoot} */ (rootFile.file),
}); });
// TODO: allow resolving external project file paths // TODO: allow resolving external project file paths
if (!filePathOrSrc.startsWith('/')) { if (!filePathOrSrc.startsWith('/')) {
// So we have external project; smth like '@lion/input/x.js' // So we have external project; smth like '@lion/input/x.js'
return { return {
// @ts-expect-error
sourceNodePath: finalNodePath, sourceNodePath: finalNodePath,
sourceFragment: null, sourceFragment: null,
externalImportSource: filePathOrSrc, externalImportSource: filePathOrSrc,
@ -184,17 +193,20 @@ export async function getSourceCodeFragmentOfDeclaration({
} }
return getSourceCodeFragmentOfDeclaration({ return getSourceCodeFragmentOfDeclaration({
filePath: filePathOrSrc, filePath: /** @type {PathFromSystemRoot} */ (filePathOrSrc),
exportedIdentifier: rootFile.specifier, exportedIdentifier: rootFile.specifier,
projectRootPath, projectRootPath,
}); });
} }
return { return {
// @ts-expect-error
sourceNodePath: finalNodePath, sourceNodePath: finalNodePath,
sourceFragment: code.slice( sourceFragment: code.slice(
finalNodePath.node?.span?.start - 1 - offset, // @ts-expect-error
finalNodePath.node?.span?.end - 1 - offset, finalNodePath.node.span.start - 1 - offset,
// @ts-expect-error
finalNodePath.node.span.end - 1 - offset,
), ),
// sourceFragment: finalNodePath.node?.raw || finalNodePath.node?.value, // sourceFragment: finalNodePath.node?.raw || finalNodePath.node?.value,
externalImportSource: null, externalImportSource: null,

View file

@ -1,4 +1,4 @@
import { isBuiltin } from 'node:module'; import { isBuiltin } from 'module';
import path from 'path'; import path from 'path';
import { nodeResolve } from '@rollup/plugin-node-resolve'; import { nodeResolve } from '@rollup/plugin-node-resolve';
import { LogService } from '../core/LogService.js'; import { LogService } from '../core/LogService.js';

View file

@ -71,6 +71,7 @@ export function getPathFromNode(node) {
* @returns {SwcPath} * @returns {SwcPath}
*/ */
function createSwcPath(node, parent, stop, scope) { function createSwcPath(node, parent, stop, scope) {
/** @type {SwcPath} */
const swcPath = { const swcPath = {
node, node,
parent, parent,
@ -78,9 +79,9 @@ function createSwcPath(node, parent, stop, scope) {
// TODO: "pre-traverse" the missing scope parts instead via getter that adds refs and bindings for current scope // TODO: "pre-traverse" the missing scope parts instead via getter that adds refs and bindings for current scope
scope, scope,
parentPath: parent ? getPathFromNode(parent) : null, parentPath: parent ? getPathFromNode(parent) : null,
get(/** @type {string} */ name) { get(/** @type {string} */ id) {
const swcPathForNode = getPathFromNode(node[name]); const swcPathForNode = getPathFromNode(node[id]);
if (node[name] && !swcPathForNode) { if (node[id] && !swcPathForNode) {
// throw new Error( // throw new Error(
// `[swcTraverse]: Use {needsAdvancedPaths: true} to find path for node: ${node[name]}`, // `[swcTraverse]: Use {needsAdvancedPaths: true} to find path for node: ${node[name]}`,
// ); // );

View file

@ -1,330 +0,0 @@
import { expect } from 'chai';
import { it } from 'mocha';
import { providence } from '../../../src/program/providence.js';
import { QueryService } from '../../../src/program/core/QueryService.js';
import { setupAnalyzerTest } from '../../../test-helpers/setup-analyzer-test.js';
import { mockProject, getEntry, getEntries } from '../../../test-helpers/mock-project-helpers.js';
import FindExportsAnalyzer from '../../../src/program/analyzers/find-exports.js';
/**
* @typedef {import('../../../types/index.js').ProvidenceConfig} ProvidenceConfig
*/
setupAnalyzerTest();
describe('Analyzer "find-exports"', async () => {
const findExportsQueryConfig = await QueryService.getQueryConfigFromAnalyzer(FindExportsAnalyzer);
/** @type {Partial<ProvidenceConfig>} */
const _providenceCfg = {
targetProjectPaths: ['/fictional/project'], // defined in mockProject
};
describe('Export notations', () => {
it(`supports [export const x = 0] (named specifier)`, async () => {
mockProject([`export const x = 0`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstResult = getEntry(queryResults[0]).result[0];
expect(firstResult.exportSpecifiers).to.eql(['x']);
expect(firstResult.source).to.be.undefined;
});
it(`supports [export default class X {}] (default export)`, async () => {
mockProject([`export default class X {}`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstResult = getEntry(queryResults[0]).result[0];
expect(firstResult.exportSpecifiers).to.eql(['[default]']);
expect(firstResult.source).to.be.undefined;
});
it(`supports [export default fn(){}] (default export)`, async () => {
mockProject([`export default x => x * 3`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstResult = getEntry(queryResults[0]).result[0];
expect(firstResult.exportSpecifiers).to.eql(['[default]']);
expect(firstResult.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;',
'./file-with-default-re-export.js':
"export { default as namedExport } from './file-with-default-export.js';",
});
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstResult = getEntry(queryResults[0]).result[0];
expect(firstResult).to.eql({
exportSpecifiers: ['[default]'],
source: undefined,
rootFileMap: [
{
currentFileSpecifier: '[default]',
rootFile: { file: '[current]', specifier: '[default]' },
},
],
});
const secondEntry = getEntry(queryResults[0], 1);
expect(secondEntry.result[0]).to.eql({
exportSpecifiers: ['namedExport'],
source: './file-with-default-export.js',
localMap: [{ exported: 'namedExport', local: '[default]' }],
normalizedSource: './file-with-default-export.js',
rootFileMap: [
{
currentFileSpecifier: 'namedExport',
rootFile: { file: './file-with-default-export.js', specifier: '[default]' },
},
],
});
});
it(`supports [import {x} from 'y'; export default x] (named re-export as default)`, async () => {
mockProject([`import {x} from 'y'; export default x;`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
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;`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
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'`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('x');
expect(firstEntry.result[0].source).to.equal('my/source');
});
it(`supports [export { x as y } from 'my/source'] (re-export renamed specifier)`, async () => {
mockProject([`export { x as y } from 'my/source'`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('y');
expect(firstEntry.result[0].source).to.equal('my/source');
});
it(`supports [export styles from './styles.css' assert { type: "css" }] (import assertions)`, async () => {
mockProject({
'./styles.css': '.block { display:block; };',
'./x.js': `export { styles as default } from './styles.css' assert { type: "css" };`,
});
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
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('./styles.css');
expect(firstEntry.result[0].rootFileMap[0]).to.eql({
currentFileSpecifier: '[default]',
rootFile: {
file: './styles.css',
specifier: '[default]',
},
});
});
it(`supports [import styles from './styles.css' assert { type: "css" }; export default styles;] (import assertions)`, async () => {
mockProject({
'./styles.css': '.block { display:block; };',
'./x.js': `import styles from './styles.css' assert { type: "css" }; export default styles;`,
});
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
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('./styles.css');
expect(firstEntry.result[0].rootFileMap[0]).to.eql({
currentFileSpecifier: '[default]',
rootFile: {
file: './styles.css',
specifier: '[default]',
},
});
});
it(`stores meta info(local name) of renamed specifiers`, async () => {
mockProject([`export { x as y } from 'my/source'`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
// This info will be relevant later to identify 'transitive' relations
expect(firstEntry.result[0].localMap).to.eql([
{
local: 'x',
exported: 'y',
},
]);
});
it(`supports [export { x, y } from 'my/source'] (multiple re-exported named specifiers)`, async () => {
mockProject([`export { x, y } from 'my/source'`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
expect(firstEntry.result[0].exportSpecifiers.length).to.equal(2);
expect(firstEntry.result[0].exportSpecifiers).to.eql(['x', 'y']);
expect(firstEntry.result[0].source).to.equal('my/source');
});
it(`stores rootFileMap of an exported Identifier`, async () => {
mockProject({
'./src/OriginalComp.js': `export class OriginalComp {}`,
'./src/inbetween.js': `export { OriginalComp as InBetweenComp } from './OriginalComp.js'`,
'./index.js': `export { InBetweenComp as MyComp } from './src/inbetween.js'`,
});
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
const secondEntry = getEntry(queryResults[0], 1);
const thirdEntry = getEntry(queryResults[0], 2);
expect(firstEntry.result[0].rootFileMap).to.eql([
{
currentFileSpecifier: 'MyComp', // this is the local name in the file we track from
rootFile: {
file: './src/OriginalComp.js', // the file containing declaration
specifier: 'OriginalComp', // the specifier that was exported in file
},
},
]);
expect(secondEntry.result[0].rootFileMap).to.eql([
{
currentFileSpecifier: 'InBetweenComp',
rootFile: {
file: './src/OriginalComp.js',
specifier: 'OriginalComp',
},
},
]);
expect(thirdEntry.result[0].rootFileMap).to.eql([
{
currentFileSpecifier: 'OriginalComp',
rootFile: {
file: '[current]',
specifier: 'OriginalComp',
},
},
]);
});
it(`stores rootFileMap of an exported Identifier`, async () => {
mockProject({
'./src/reexport.js': `
// a direct default import
import RefDefault from 'exporting-ref-project';
export default RefDefault;
`,
'./index.js': `
import ExtendRefDefault from './src/reexport.js';
export default ExtendRefDefault;
`,
});
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
expect(firstEntry.result[0].rootFileMap).to.eql([
{
currentFileSpecifier: '[default]',
rootFile: {
file: 'exporting-ref-project',
specifier: '[default]',
},
},
]);
});
it(`correctly handles empty files`, async () => {
// These can be encountered while scanning repos.. They should not break the code...
mockProject([`// some comment here...`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
expect(firstEntry.result[0].exportSpecifiers).to.eql(['[file]']);
expect(firstEntry.result[0].source).to.equal(undefined);
});
});
describe('Export variable types', () => {
it(`classes`, async () => {
mockProject([`export class X {}`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('X');
expect(firstEntry.result[0].source).to.be.undefined;
});
it(`functions`, async () => {
mockProject([`export function y() {}`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]);
expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('y');
expect(firstEntry.result[0].source).to.be.undefined;
});
// ...etc?
// ...TODO: create custom hooks to store meta info about types etc.
});
describe('Default post processing', () => {
// onlyInternalSources: false,
// keepOriginalSourcePaths: false,
// filterSpecifier: null,
});
describe('Options', () => {
// TODO: Move to dashboard
it.skip(`"metaConfig.categoryConfig"`, async () => {
mockProject(
[
`export const foo = null`, // firstEntry
`export const bar = null`, // secondEntry
`export const baz = null`, // thirdEntry
],
{
projectName: 'my-project',
filePaths: ['./foo.js', './packages/bar/test/bar.test.js', './temp/baz.js'],
},
);
const findExportsCategoryQueryObj = await QueryService.getQueryConfigFromAnalyzer(
'find-exports',
{
metaConfig: {
categoryConfig: [
{
project: 'my-project',
categories: {
fooCategory: localFilePath => localFilePath.startsWith('./foo'),
barCategory: localFilePath => localFilePath.startsWith('./packages/bar'),
testCategory: localFilePath => localFilePath.includes('/test/'),
},
},
],
},
},
);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
const [firstEntry, secondEntry, thirdEntry] = getEntries(queryResult);
expect(firstEntry.meta.categories).to.eql(['fooCategory']);
// not mutually exclusive...
expect(secondEntry.meta.categories).to.eql(['barCategory', 'testCategory']);
expect(thirdEntry.meta.categories).to.eql([]);
});
});
});

View file

@ -301,23 +301,23 @@ describe('Analyzer "find-exports"', async () => {
}, },
); );
const findExportsCategoryQueryObj = await QueryService.getQueryConfigFromAnalyzer( // const findExportsCategoryQueryObj = await QueryService.getQueryConfigFromAnalyzer(
'find-exports', // 'find-exports',
{ // {
metaConfig: { // metaConfig: {
categoryConfig: [ // categoryConfig: [
{ // {
project: 'my-project', // project: 'my-project',
categories: { // categories: {
fooCategory: localFilePath => localFilePath.startsWith('./foo'), // fooCategory: localFilePath => localFilePath.startsWith('./foo'),
barCategory: localFilePath => localFilePath.startsWith('./packages/bar'), // barCategory: localFilePath => localFilePath.startsWith('./packages/bar'),
testCategory: localFilePath => localFilePath.includes('/test/'), // testCategory: localFilePath => localFilePath.includes('/test/'),
}, // },
}, // },
], // ],
}, // },
}, // },
); // );
const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0]; const queryResult = queryResults[0];

View file

@ -4,6 +4,6 @@
"outDir": "./dist-types", "outDir": "./dist-types",
"rootDir": "." "rootDir": "."
}, },
"include": ["src", "types"], "include": ["types"],
"exclude": ["dist-types"] "exclude": ["dist-types"]
} }

View file

@ -1,7 +1,7 @@
export type SwcScope = { export type SwcScope = {
id: number; id: number;
parentScope?: Scope; parentScope?: Scope;
bindings: { [key: string]: Binding }; bindings: { [key: string]: SwcBinding };
path: SwcPath | null; path: SwcPath | null;
_pendingRefsWithoutBinding: SwcNode[]; _pendingRefsWithoutBinding: SwcNode[];
_isIsolatedBlockStatement: boolean; _isIsolatedBlockStatement: boolean;
@ -12,15 +12,17 @@ export type SwcBinding = {
identifier: SwcNode; identifier: SwcNode;
// kind: string; // kind: string;
refs: SwcNode[]; refs: SwcNode[];
path: SwcPath; path: SwcPath | null | undefined;
}; };
export type SwcPath = { export type SwcPath = {
node: SwcNode; node: SwcNode;
parent: SwcNode; parent: SwcNode;
stop: function; stop: function;
scope: SwcScope; scope: SwcScope | undefined;
parentPath: SwcPath; parentPath: SwcPath | null | undefined;
get: (id: string) => SwcPath | undefined;
type: string;
}; };
type SwcVisitorFn = (swcPath: SwcPath) => void; type SwcVisitorFn = (swcPath: SwcPath) => void;