feat(providence): full swc support, including performant traverse tool

This commit is contained in:
Thijs Louisse 2023-05-09 16:15:21 +02:00
parent e063229891
commit ea803894ae
52 changed files with 3584 additions and 459 deletions

View file

@ -23,11 +23,10 @@
"providence": "./src/cli/index.js" "providence": "./src/cli/index.js"
}, },
"files": [ "files": [
"dashboard/src",
"src" "src"
], ],
"scripts": { "scripts": {
"dashboard": "node ./dashboard/server.js --run-server --serve-from-package-root", "dashboard": "node ./src/dashboard/server.js --run-server --serve-from-package-root",
"postinstall": "npx patch-package", "postinstall": "npx patch-package",
"match-lion-imports": "npm run providence -- analyze match-imports --search-target-collection @lion-targets --reference-collection @lion-references --measure-perf --skip-check-match-compatibility", "match-lion-imports": "npm run providence -- analyze match-imports --search-target-collection @lion-targets --reference-collection @lion-references --measure-perf --skip-check-match-compatibility",
"providence": "node --max-old-space-size=8192 ./src/cli/index.js", "providence": "node --max-old-space-size=8192 ./src/cli/index.js",

View file

@ -7,7 +7,7 @@ import { QueryService } from '../program/core/QueryService.js';
import { InputDataService } from '../program/core/InputDataService.js'; import { InputDataService } from '../program/core/InputDataService.js';
import { toPosixPath } from '../program/utils/to-posix-path.js'; import { toPosixPath } from '../program/utils/to-posix-path.js';
import { getCurrentDir } from '../program/utils/get-current-dir.js'; import { getCurrentDir } from '../program/utils/get-current-dir.js';
import { dashboardServer } from '../../dashboard/server.js'; import { dashboardServer } from '../dashboard/server.js';
import { _providenceModule } from '../program/providence.js'; import { _providenceModule } from '../program/providence.js';
import { _cliHelpersModule } from './cli-helpers.js'; import { _cliHelpersModule } from './cli-helpers.js';
import { _extendDocsModule } from './launch-providence-with-extend-docs.js'; import { _extendDocsModule } from './launch-providence-with-extend-docs.js';

View file

@ -1,14 +1,14 @@
import fs from 'fs'; import fs from 'fs';
import pathLib from 'path'; import pathLib from 'path';
import { startDevServer } from '@web/dev-server'; import { startDevServer } from '@web/dev-server';
import { ReportService } from '../src/program/core/ReportService.js'; import { ReportService } from '../program/core/ReportService.js';
import { providenceConfUtil } from '../src/program/utils/providence-conf-util.js'; import { providenceConfUtil } from '../program/utils/providence-conf-util.js';
import { getCurrentDir } from '../src/program/utils/get-current-dir.js'; import { getCurrentDir } from '../program/utils/get-current-dir.js';
/** /**
* @typedef {import('../types/index.js').PathFromSystemRoot} PathFromSystemRoot * @typedef {import('../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../types/index.js').GatherFilesConfig} GatherFilesConfig * @typedef {import('../../types/index.js').GatherFilesConfig} GatherFilesConfig
* @typedef {import('../types/index.js').AnalyzerName} AnalyzerName * @typedef {import('../../types/index.js').AnalyzerName} AnalyzerName
*/ */
/** /**

View file

@ -1,9 +1,10 @@
/* eslint-disable no-shadow, no-param-reassign */ /* eslint-disable no-shadow, no-param-reassign */
import path from 'path'; import path from 'path';
import t from '@babel/types'; import t from '@babel/types';
// @ts-ignore
import babelTraverse from '@babel/traverse'; import babelTraverse from '@babel/traverse';
import { Analyzer } from '../core/Analyzer.js'; import { Analyzer } from '../core/Analyzer.js';
import { trackDownIdentifierFromScope } from './helpers/track-down-identifier.js'; import { trackDownIdentifierFromScope } from './helpers/track-down-identifier--legacy.js';
/** /**
* @typedef {import('@babel/types').File} File * @typedef {import('@babel/types').File} File
@ -227,7 +228,7 @@ export default class FindClassesAnalyzer extends Analyzer {
static analyzerName = 'find-classes'; static analyzerName = 'find-classes';
/** @type {'babel'|'swc-to-babel'} */ /** @type {'babel'|'swc-to-babel'} */
requiredAst = 'babel'; static requiredAst = 'babel';
/** /**
* Will find all public members (properties (incl. getter/setters)/functions) of a class and * Will find all public members (properties (incl. getter/setters)/functions) of a class and

View file

@ -2,7 +2,7 @@ import path from 'path';
import t from '@babel/types'; import t from '@babel/types';
import babelTraverse from '@babel/traverse'; import babelTraverse from '@babel/traverse';
import { Analyzer } from '../core/Analyzer.js'; import { Analyzer } from '../core/Analyzer.js';
import { trackDownIdentifierFromScope } from './helpers/track-down-identifier.js'; import { trackDownIdentifierFromScope } from './helpers/track-down-identifier--legacy.js';
/** /**
* @typedef {import('@babel/types').File} File * @typedef {import('@babel/types').File} File
@ -94,7 +94,7 @@ export default class FindCustomelementsAnalyzer extends Analyzer {
static analyzerName = 'find-customelements'; static analyzerName = 'find-customelements';
/** @type {'babel'|'swc-to-babel'} */ /** @type {'babel'|'swc-to-babel'} */
requiredAst = 'swc-to-babel'; static requiredAst = 'swc-to-babel';
/** /**
* Finds export specifiers and sources * Finds export specifiers and sources

View file

@ -0,0 +1,272 @@
/* 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,6 +1,6 @@
/* eslint-disable no-shadow, no-param-reassign */ /* eslint-disable no-shadow, no-param-reassign */
import pathLib from 'path'; import path from 'path';
import babelTraverse from '@babel/traverse'; import { swcTraverse } from '../utils/swc-traverse.js';
import { Analyzer } from '../core/Analyzer.js'; import { Analyzer } from '../core/Analyzer.js';
import { trackDownIdentifier } from './helpers/track-down-identifier.js'; import { trackDownIdentifier } from './helpers/track-down-identifier.js';
import { normalizeSourcePaths } from './helpers/normalize-source-paths.js'; import { normalizeSourcePaths } from './helpers/normalize-source-paths.js';
@ -8,12 +8,18 @@ import { getReferencedDeclaration } from '../utils/get-source-code-fragment-of-d
import { LogService } from '../core/LogService.js'; import { LogService } from '../core/LogService.js';
/** /**
* @typedef {import('@babel/types').File} File * @typedef {import("@swc/core").Module} SwcAstModule
* @typedef {import('@babel/types').Node} Node * @typedef {import("@swc/core").Node} SwcNode
* @typedef {import("@swc/core").VariableDeclaration} SwcVariableDeclaration
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
* @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst
* @typedef {import('../../../types/index.js').FindExportsAnalyzerResult} FindExportsAnalyzerResult * @typedef {import('../../../types/index.js').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @typedef {import('../../../types/index.js').FindExportsAnalyzerEntry} FindExportsAnalyzerEntry * @typedef {import('../../../types/index.js').FindExportsAnalyzerEntry} FindExportsAnalyzerEntry
* @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot * @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../../../types/index.js').SwcScope} SwcScope
* @typedef {import('../../../types/index.js').SwcBinding} SwcBinding
* @typedef {import('../../../types/index.js').SwcPath} SwcPath
* @typedef {import('../../../types/index.js').SwcVisitor} SwcVisitor
* @typedef {import('./helpers/track-down-identifier.js').RootFile} RootFile * @typedef {import('./helpers/track-down-identifier.js').RootFile} RootFile
* @typedef {object} RootFileMapEntry * @typedef {object} RootFileMapEntry
* @typedef {string} currentFileSpecifier this is the local name in the file we track from * @typedef {string} currentFileSpecifier this is the local name in the file we track from
@ -26,7 +32,7 @@ import { LogService } from '../core/LogService.js';
* @param {FindExportsSpecifierObj[]} transformedFile * @param {FindExportsSpecifierObj[]} transformedFile
*/ */
async function trackdownRoot(transformedFile, relativePath, projectPath) { async function trackdownRoot(transformedFile, relativePath, projectPath) {
const fullCurrentFilePath = pathLib.resolve(projectPath, relativePath); const fullCurrentFilePath = path.resolve(projectPath, relativePath);
for (const specObj of transformedFile) { for (const specObj of transformedFile) {
/** @type {RootFileMap} */ /** @type {RootFileMap} */
const rootFileMap = []; const rootFileMap = [];
@ -96,49 +102,47 @@ function cleanup(transformedFile) {
} }
/** /**
* @param {*} node
* @returns {string[]} * @returns {string[]}
*/ */
function getExportSpecifiers(node) { function getExportSpecifiers(node) {
// handles default [export const g = 4]; // handles default [export const g = 4];
if (node.declaration) { if (node.declaration) {
if (node.declaration.declarations) { if (node.declaration.declarations) {
return [node.declaration.declarations[0].id.name]; return [node.declaration.declarations[0].id.value];
} }
if (node.declaration.id) { if (node.declaration.identifier) {
return [node.declaration.id.name]; return [node.declaration.identifier.value];
} }
} }
// handles (re)named specifiers [export { x (as y)} from 'y']; // handles (re)named specifiers [export { x (as y)} from 'y'];
return node.specifiers.map(s => { return (node.specifiers || []).map(s => {
let specifier;
if (s.exported) { if (s.exported) {
// { x as y } // { x as y }
specifier = s.exported.name === 'default' ? '[default]' : s.exported.name; return s.exported.value === 'default' ? '[default]' : s.exported.value;
} else {
// { x }
specifier = s.local.name;
} }
return specifier; // { x }
return s.orig.value;
}); });
} }
/** /**
* @returns {object[]} * @returns {{local:string;exported:string;}|undefined[]}
*/ */
function getLocalNameSpecifiers(node) { function getLocalNameSpecifiers(node) {
return node.specifiers return (node.declaration?.declarations || node.specifiers || [])
.map(s => { .map(s => {
if (s.exported && s.local && s.exported.name !== s.local.name) { if (s.exported && s.orig && s.exported.value !== s.orig.value) {
return { return {
// if reserved keyword 'default' is used, translate it into 'providence keyword' // if reserved keyword 'default' is used, translate it into 'providence keyword'
local: s.local.name === 'default' ? '[default]' : s.local.name, local: s.orig.value === 'default' ? '[default]' : s.orig.value,
exported: s.exported.name, exported: s.exported.value,
}; };
} }
return undefined; return undefined;
}) })
.filter(s => s); .filter(Boolean);
} }
const isImportingSpecifier = pathOrNode => const isImportingSpecifier = pathOrNode =>
@ -146,10 +150,10 @@ const isImportingSpecifier = pathOrNode =>
/** /**
* Finds import specifiers and sources for a given ast result * Finds import specifiers and sources for a given ast result
* @param {File} babelAst * @param {SwcAstModule} swcAst
* @param {FindExportsConfig} config * @param {FindExportsConfig} config
*/ */
function findExportsPerAstFile(babelAst, { skipFileImports }) { function findExportsPerAstFile(swcAst, { skipFileImports }) {
LogService.debug(`Analyzer "find-exports": started findExportsPerAstFile method`); LogService.debug(`Analyzer "find-exports": started findExportsPerAstFile method`);
// Visit AST... // Visit AST...
@ -159,44 +163,52 @@ function findExportsPerAstFile(babelAst, { 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)
/** @type {{[key:string]:SwcBinding}} */
let globalScopeBindings; let globalScopeBindings;
babelTraverse.default(babelAst, { const exportHandler = (/** @type {SwcPath} */ astPath) => {
Program: { const exportSpecifiers = getExportSpecifiers(astPath.node);
enter(babelPath) { const localMap = getLocalNameSpecifiers(astPath.node);
const body = babelPath.get('body'); const source = astPath.node.source?.value;
if (body.length) { const entry = { exportSpecifiers, localMap, source, __tmp: { astPath } };
globalScopeBindings = body[0].scope.bindings; if (astPath.node.asserts) {
} entry.assertionType = astPath.node.asserts.properties[0].value?.value;
}, }
}, transformedFile.push(entry);
ExportNamedDeclaration(astPath) { };
const exportSpecifiers = getExportSpecifiers(astPath.node);
const localMap = getLocalNameSpecifiers(astPath.node); const exportDefaultHandler = (/** @type {SwcPath} */ astPath) => {
const source = astPath.node.source?.value; const exportSpecifiers = ['[default]'];
const entry = { exportSpecifiers, localMap, source, __tmp: { astPath } }; let source;
if (astPath.node.assertions?.length) { // Is it an inline declaration like "export default class X {};" ?
entry.assertionType = astPath.node.assertions[0].value?.value; if (
astPath.node.decl?.type === 'Identifier' ||
astPath.node.expression?.type === 'Identifier'
) {
// It is a reference to an identifier like "export { x } from 'y';"
const importOrDeclPath = getReferencedDeclaration({
referencedIdentifierName: astPath.node.decl?.value || astPath.node.expression.value,
globalScopeBindings,
});
if (isImportingSpecifier(importOrDeclPath)) {
source = importOrDeclPath.parentPath.node.source.value;
} }
transformedFile.push(entry); }
transformedFile.push({ exportSpecifiers, source, __tmp: { astPath } });
};
/** @type {SwcVisitor} */
const visitor = {
Module({ scope }) {
globalScopeBindings = scope.bindings;
}, },
ExportDefaultDeclaration(defaultExportPath) { ExportDeclaration: exportHandler,
const exportSpecifiers = ['[default]']; ExportNamedDeclaration: exportHandler,
let source; ExportDefaultDeclaration: exportDefaultHandler,
if (defaultExportPath.node.declaration?.type !== 'Identifier') { ExportDefaultExpression: exportDefaultHandler,
source = defaultExportPath.node.declaration.name; };
} else {
const importOrDeclPath = getReferencedDeclaration({ swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
referencedIdentifierName: defaultExportPath.node.declaration.name,
globalScopeBindings,
});
if (isImportingSpecifier(importOrDeclPath)) {
source = importOrDeclPath.parentPath.node.source.value;
}
}
transformedFile.push({ exportSpecifiers, source, __tmp: { astPath: defaultExportPath } });
},
});
if (!skipFileImports) { if (!skipFileImports) {
// Always add an entry for just the file 'relativePath' // Always add an entry for just the file 'relativePath'
@ -211,62 +223,34 @@ function findExportsPerAstFile(babelAst, { skipFileImports }) {
} }
export default class FindExportsAnalyzer extends Analyzer { export default class FindExportsAnalyzer extends Analyzer {
/** @type {AnalyzerName} */ static analyzerName = /** @type {AnalyzerName} */ ('find-exports');
static analyzerName = 'find-exports';
/** @type {'babel'|'swc-to-babel'} */ static requiredAst = /** @type {AnalyzerAst} */ ('swc');
requiredAst = 'swc-to-babel';
/** /**
* Finds export specifiers and sources * @typedef FindExportsConfig
* @param {FindExportsConfig} customConfig * @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
*/ */
async execute(customConfig = {}) { get config() {
/** return {
* @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, targetProjectPath: null,
skipFileImports: false, skipFileImports: false,
...customConfig, ...this._customConfig,
}; };
}
/** static async analyzeFile(ast, { relativePath, analyzerCfg }) {
* Prepare const projectPath = analyzerCfg.targetProjectPath;
*/
const cachedAnalyzerResult = this._prepare(cfg);
if (cachedAnalyzerResult) {
return cachedAnalyzerResult;
}
/** let transformedFile = findExportsPerAstFile(ast, analyzerCfg);
* Traverse
*/
const projectPath = cfg.targetProjectPath;
const traverseEntryFn = async (ast, { relativePath }) => { transformedFile = await normalizeSourcePaths(transformedFile, relativePath, projectPath);
let transformedFile = findExportsPerAstFile(ast, cfg); transformedFile = await trackdownRoot(transformedFile, relativePath, projectPath);
transformedFile = cleanup(transformedFile);
transformedFile = await normalizeSourcePaths(transformedFile, relativePath, projectPath); return { result: transformedFile };
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

@ -0,0 +1,189 @@
/* 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

@ -1,14 +1,15 @@
/* eslint-disable no-shadow, no-param-reassign */ /* eslint-disable no-shadow, no-param-reassign */
import babelTraverse from '@babel/traverse';
import { isRelativeSourcePath } from '../utils/relative-source-path.js'; import { isRelativeSourcePath } from '../utils/relative-source-path.js';
import { swcTraverse } from '../utils/swc-traverse.js';
import { normalizeSourcePaths } from './helpers/normalize-source-paths.js'; import { normalizeSourcePaths } from './helpers/normalize-source-paths.js';
import { Analyzer } from '../core/Analyzer.js'; import { Analyzer } from '../core/Analyzer.js';
import { LogService } from '../core/LogService.js'; import { LogService } from '../core/LogService.js';
/** /**
* @typedef {import('@babel/types').File} File * @typedef {import("@swc/core").Module} SwcAstModule
* @typedef {import('@babel/types').Node} Node * @typedef {import("@swc/core").Node} SwcNode
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
* @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst
* @typedef {import('../../../types/index.js').AnalyzerConfig} AnalyzerConfig * @typedef {import('../../../types/index.js').AnalyzerConfig} AnalyzerConfig
* @typedef {import('../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult * @typedef {import('../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../../../types/index.js').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry * @typedef {import('../../../types/index.js').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry
@ -16,117 +17,86 @@ import { LogService } from '../core/LogService.js';
*/ */
/** /**
* Options that allow to filter 'on a file basis'. * @param {SwcNode} node
* 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) { function getImportOrReexportsSpecifiers(node) {
return node.specifiers.map(s => { return node.specifiers.map(s => {
if ( if (
s.type === 'ImportDefaultSpecifier' || s.type === 'ImportDefaultSpecifier' ||
s.type === 'ExportDefaultSpecifier' || s.type === 'ExportDefaultSpecifier' ||
(s.type === 'ExportSpecifier' && s.exported?.name === 'default') (s.type === 'ExportSpecifier' && s.exported?.value === 'default')
) { ) {
return '[default]'; return '[default]';
} }
if (s.type === 'ImportNamespaceSpecifier' || s.type === 'ExportNamespaceSpecifier') { if (s.type === 'ImportNamespaceSpecifier' || s.type === 'ExportNamespaceSpecifier') {
return '[*]'; return '[*]';
} }
if ((s.imported && s.type === 'ImportNamespaceSpecifier') || s.type === 'ImportSpecifier') { const importedValue = s.imported?.value || s.orig?.value || s.exported?.value || s.local?.value;
return s.imported.name; return importedValue;
}
if (s.exported && s.type === 'ExportNamespaceSpecifier') {
return s.exported.name;
}
return s.local.name;
}); });
} }
/** /**
* Finds import specifiers and sources * Finds import specifiers and sources
* @param {File} babelAst * @param {SwcAstModule} swcAst
*/ */
function findImportsPerAstFile(babelAst, context) { function findImportsPerAstFile(swcAst, context) {
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
// Visit AST... // Visit AST...
/** @type {Partial<FindImportsAnalyzerEntry>[]} */ /** @type {Partial<FindImportsAnalyzerEntry>[]} */
const transformedFile = []; const transformedFile = [];
babelTraverse.default(babelAst, {
ImportDeclaration(path) { swcTraverse(swcAst, {
const importSpecifiers = getImportOrReexportsSpecifiers(path.node); ImportDeclaration({ node }) {
const importSpecifiers = getImportOrReexportsSpecifiers(node);
if (!importSpecifiers.length) { if (!importSpecifiers.length) {
importSpecifiers.push('[file]'); // apparently, there was just a file import importSpecifiers.push('[file]'); // apparently, there was just a file import
} }
const source = path.node.source.value; const source = node.source.value;
const entry = /** @type {Partial<FindImportsAnalyzerEntry>} */ ({ importSpecifiers, source }); const entry = /** @type {Partial<FindImportsAnalyzerEntry>} */ ({ importSpecifiers, source });
if (path.node.assertions?.length) { if (node.asserts) {
entry.assertionType = path.node.assertions[0].value?.value; entry.assertionType = node.asserts.properties[0].value?.value;
}
transformedFile.push(entry);
},
ExportNamedDeclaration({ node }) {
if (!node.source) {
return; // we are dealing with a regular export, not a reexport
}
const importSpecifiers = getImportOrReexportsSpecifiers(node);
const source = node.source.value;
const entry = /** @type {Partial<FindImportsAnalyzerEntry>} */ ({ importSpecifiers, source });
if (node.asserts) {
entry.assertionType = node.asserts.properties[0].value?.value;
} }
transformedFile.push(entry); transformedFile.push(entry);
}, },
// Dynamic imports // Dynamic imports
CallExpression(path) { CallExpression({ node }) {
if (path.node.callee?.type !== 'Import') { if (node.callee?.type !== 'Import') {
return; return;
} }
// TODO: check for specifiers catched via obj destructuring? // TODO: check for specifiers catched via obj destructuring?
// TODO: also check for ['file'] // TODO: also check for ['file']
const importSpecifiers = ['[default]']; const importSpecifiers = ['[default]'];
let source = path.node.arguments[0].value; const dynamicImportExpression = node.arguments[0].expression;
if (!source) { const source =
// TODO: with advanced retrieval, we could possibly get the value dynamicImportExpression.type === 'StringLiteral'
source = '[variable]'; ? dynamicImportExpression.value
} : '[variable]';
transformedFile.push({ importSpecifiers, source }); 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; return transformedFile;
} }
export default class FindImportsAnalyzer extends Analyzer { export default class FindImportsSwcAnalyzer extends Analyzer {
/** @type {AnalyzerName} */ static analyzerName = /** @type {AnalyzerName} */ ('find-imports');
static analyzerName = 'find-imports';
/** @type {'babel'|'swc-to-babel'} */ static requiredAst = /** @type {AnalyzerAst} */ ('swc');
requiredAst = 'swc-to-babel';
/** /**
* Finds import specifiers and sources * Finds import specifiers and sources
@ -158,8 +128,8 @@ export default class FindImportsAnalyzer extends Analyzer {
/** /**
* Traverse * Traverse
*/ */
const queryOutput = await this._traverse(async (ast, context) => { const queryOutput = await this._traverse(async (swcAst, context) => {
let transformedFile = findImportsPerAstFile(ast, context); let transformedFile = findImportsPerAstFile(swcAst, context);
// Post processing based on configuration... // Post processing based on configuration...
transformedFile = await normalizeSourcePaths( transformedFile = await normalizeSourcePaths(
transformedFile, transformedFile,
@ -168,19 +138,12 @@ export default class FindImportsAnalyzer extends Analyzer {
); );
if (!cfg.keepInternalSources) { if (!cfg.keepInternalSources) {
transformedFile = options.onlyExternalSources(transformedFile); transformedFile = transformedFile.filter(entry => !isRelativeSourcePath(entry.source));
} }
return { result: transformedFile }; return { result: transformedFile };
}); });
// if (cfg.sortBySpecifier) {
// queryOutput = sortBySpecifier.execute(queryOutput, {
// ...cfg,
// specifiersKey: 'importSpecifiers',
// });
// }
/** /**
* Finalize * Finalize
*/ */

View file

@ -0,0 +1,330 @@
import fs from 'fs';
import pathLib from 'path';
import babelTraverse from '@babel/traverse';
import { isRelativeSourcePath, toRelativeSourcePath } from '../../utils/relative-source-path.js';
import { InputDataService } from '../../core/InputDataService.js';
import { resolveImportPath } from '../../utils/resolve-import-path.js';
import { AstService } from '../../core/AstService.js';
import { LogService } from '../../core/LogService.js';
import { memoize } from '../../utils/memoize.js';
/**
* @typedef {import('../../../../types/index.js').RootFile} RootFile
* @typedef {import('../../../../types/index.js').SpecifierSource} SpecifierSource
* @typedef {import('../../../../types/index.js').IdentifierName} IdentifierName
* @typedef {import('../../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('@babel/traverse').NodePath} NodePath
*/
/**
* @param {string} source
* @param {string} projectName
*/
function isSelfReferencingProject(source, projectName) {
return source.startsWith(`${projectName}`);
}
/**
* @param {string} source
* @param {string} projectName
*/
function isExternalProject(source, projectName) {
return (
!source.startsWith('#') &&
!isRelativeSourcePath(source) &&
!isSelfReferencingProject(source, projectName)
);
}
/**
* Other than with import, no binding is created for MyClass by Babel(?)
* This means 'path.scope.getBinding('MyClass')' returns undefined
* and we have to find a different way to retrieve this value.
* @param {NodePath} astPath Babel ast traversal path
* @param {IdentifierName} identifierName the name that should be tracked (and that exists inside scope of astPath)
*/
function getBindingAndSourceReexports(astPath, identifierName) {
// Get to root node of file and look for exports like `export { identifierName } from 'src';`
let source;
let bindingType;
let bindingPath;
let curPath = astPath;
while (curPath.parentPath) {
curPath = curPath.parentPath;
}
const rootPath = curPath;
rootPath.traverse({
ExportSpecifier(astPath) {
// eslint-disable-next-line arrow-body-style
const found =
astPath.node.exported.name === identifierName || astPath.node.local.name === identifierName;
if (found) {
bindingPath = astPath;
bindingType = 'ExportSpecifier';
source = astPath.parentPath.node.source
? astPath.parentPath.node.source.value
: '[current]';
astPath.stop();
}
},
});
return [source, bindingType, bindingPath];
}
/**
* Retrieves source (like '@lion/core') and importedIdentifierName (like 'lit') from ast for
* current file.
* We might be an import that was locally renamed.
* Since we are traversing, we are interested in the imported name. Or in case of a re-export,
* the local name.
* @param {NodePath} astPath Babel ast traversal path
* @param {string} identifierName the name that should be tracked (and that exists inside scope of astPath)
* @returns {{ source:string, importedIdentifierName:string }}
*/
export function getImportSourceFromAst(astPath, identifierName) {
let source;
let importedIdentifierName;
const binding = astPath.scope.getBinding(identifierName);
let bindingType = binding?.path.type;
let bindingPath = binding?.path;
const matchingTypes = ['ImportSpecifier', 'ImportDefaultSpecifier', 'ExportSpecifier'];
if (bindingType && matchingTypes.includes(bindingType)) {
source = binding?.path?.parentPath?.node?.source?.value;
} else {
// no binding
[source, bindingType, bindingPath] = getBindingAndSourceReexports(astPath, identifierName);
}
const shouldLookForDefaultExport = bindingType === 'ImportDefaultSpecifier';
if (shouldLookForDefaultExport) {
importedIdentifierName = '[default]';
} else if (source) {
const { node } = bindingPath;
importedIdentifierName = (node.imported && node.imported.name) || node.local.name;
}
return { source, importedIdentifierName };
}
/**
* @typedef {(source:SpecifierSource,identifierName:IdentifierName,currentFilePath:PathFromSystemRoot,rootPath:PathFromSystemRoot,projectName?: string,depth?:number) => Promise<RootFile>} TrackDownIdentifierFn
*/
/**
* Follows the full path of an Identifier until its declaration ('root file') is found.
* @example
*```js
* // 1. Starting point
* // target-proj/my-comp-import.js
* import { MyComp as TargetComp } from 'ref-proj';
*
* // 2. Intermediate stop: a re-export
* // ref-proj/exportsIndex.js (package.json has main: './exportsIndex.js')
* export { RefComp as MyComp } from './src/RefComp.js';
*
* // 3. End point: our declaration
* // ref-proj/src/RefComp.js
* export class RefComp extends LitElement {...}
*```
*
* -param {SpecifierSource} source an importSpecifier source, like 'ref-proj' or '../file'
* -param {IdentifierName} identifierName imported reference/Identifier name, like 'MyComp'
* -param {PathFromSystemRoot} currentFilePath file path, like '/path/to/target-proj/my-comp-import.js'
* -param {PathFromSystemRoot} rootPath dir path, like '/path/to/target-proj'
* -param {string} [projectName] like 'target-proj' or '@lion/input'
* -returns {Promise<RootFile>} file: path of file containing the binding (exported declaration),
* like '/path/to/ref-proj/src/RefComp.js'
*/
/** @type {TrackDownIdentifierFn} */
// eslint-disable-next-line import/no-mutable-exports
export let trackDownIdentifier;
/** @type {TrackDownIdentifierFn} */
async function trackDownIdentifierFn(
source,
identifierName,
currentFilePath,
rootPath,
projectName,
depth = 0,
) {
let rootFilePath; // our result path
let rootSpecifier; // the name under which it was imported
if (!projectName) {
// eslint-disable-next-line no-param-reassign
projectName = InputDataService.getPackageJson(rootPath)?.name;
}
if (projectName && isExternalProject(source, projectName)) {
// So, it is an external ref like '@lion/core' or '@open-wc/scoped-elements/index.js'
// At this moment in time, we don't know if we have file system access to this particular
// project. Therefore, we limit ourselves to tracking down local references.
// In case this helper is used inside an analyzer like 'match-subclasses', the external
// (search-target) project can be accessed and paths can be resolved to local ones,
// just like in 'match-imports' analyzer.
/** @type {RootFile} */
const result = { file: source, specifier: identifierName };
return result;
}
const resolvedSourcePath = await resolveImportPath(source, currentFilePath);
LogService.debug(`[trackDownIdentifier] ${resolvedSourcePath}`);
const allowedJsModuleExtensions = ['.mjs', '.js'];
if (!allowedJsModuleExtensions.includes(pathLib.extname(resolvedSourcePath))) {
// We have an import assertion
return /** @type { RootFile } */ {
file: toRelativeSourcePath(resolvedSourcePath, rootPath),
specifier: '[default]',
};
}
const code = fs.readFileSync(resolvedSourcePath, 'utf8');
const babelAst = AstService.getAst(code, 'swc-to-babel', { filePath: resolvedSourcePath });
const shouldLookForDefaultExport = identifierName === '[default]';
let reexportMatch = false; // named specifier declaration
let exportMatch;
let pendingTrackDownPromise;
babelTraverse.default(babelAst, {
ExportDefaultDeclaration(astPath) {
if (!shouldLookForDefaultExport) {
return;
}
let newSource;
if (astPath.node.declaration.type === 'Identifier') {
newSource = getImportSourceFromAst(astPath, astPath.node.declaration.name).source;
}
if (newSource) {
pendingTrackDownPromise = trackDownIdentifier(
newSource,
'[default]',
resolvedSourcePath,
rootPath,
projectName,
depth + 1,
);
} else {
// We found our file!
rootSpecifier = identifierName;
rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath);
}
astPath.stop();
},
ExportNamedDeclaration: {
enter(astPath) {
if (reexportMatch || shouldLookForDefaultExport) {
return;
}
// Are we dealing with a re-export ?
if (astPath.node.specifiers?.length) {
exportMatch = astPath.node.specifiers.find(s => s.exported.name === identifierName);
if (exportMatch) {
const localName = exportMatch.local.name;
let newSource;
if (astPath.node.source) {
/**
* @example
* export { x } from 'y'
*/
newSource = astPath.node.source.value;
} else {
/**
* @example
* import { x } from 'y'
* export { x }
*/
newSource = getImportSourceFromAst(astPath, identifierName).source;
if (!newSource || newSource === '[current]') {
/**
* @example
* const x = 12;
* export { x }
*/
return;
}
}
reexportMatch = true;
pendingTrackDownPromise = trackDownIdentifier(
newSource,
localName,
resolvedSourcePath,
rootPath,
projectName,
depth + 1,
);
astPath.stop();
}
}
},
exit(astPath) {
if (!reexportMatch) {
// We didn't find a re-exported Identifier, that means the reference is declared
// in current file...
rootSpecifier = identifierName;
rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath);
if (exportMatch) {
astPath.stop();
}
}
},
},
});
if (pendingTrackDownPromise) {
// We can't handle promises inside Babel traverse, so we do it here...
const resObj = await pendingTrackDownPromise;
rootFilePath = resObj.file;
rootSpecifier = resObj.specifier;
}
return /** @type { RootFile } */ { file: rootFilePath, specifier: rootSpecifier };
}
trackDownIdentifier = memoize(trackDownIdentifierFn);
/**
* @param {NodePath} astPath
* @param {string} identifierNameInScope
* @param {PathFromSystemRoot} fullCurrentFilePath
* @param {PathFromSystemRoot} projectPath
* @param {string} [projectName]
*/
async function trackDownIdentifierFromScopeFn(
astPath,
identifierNameInScope,
fullCurrentFilePath,
projectPath,
projectName,
) {
const sourceObj = getImportSourceFromAst(astPath, identifierNameInScope);
/** @type {RootFile} */
let rootFile;
if (sourceObj.source) {
rootFile = await trackDownIdentifier(
sourceObj.source,
sourceObj.importedIdentifierName,
fullCurrentFilePath,
projectPath,
projectName,
);
} else {
const specifier = sourceObj.importedIdentifierName || identifierNameInScope;
rootFile = { file: '[current]', specifier };
}
return rootFile;
}
export const trackDownIdentifierFromScope = memoize(trackDownIdentifierFromScopeFn);

View file

@ -1,11 +1,10 @@
import fs from 'fs'; import fs from 'fs';
import pathLib from 'path'; import path from 'path';
import babelTraverse from '@babel/traverse'; import { swcTraverse } from '../../utils/swc-traverse.js';
import { isRelativeSourcePath, toRelativeSourcePath } from '../../utils/relative-source-path.js'; import { isRelativeSourcePath, toRelativeSourcePath } from '../../utils/relative-source-path.js';
import { InputDataService } from '../../core/InputDataService.js'; import { InputDataService } from '../../core/InputDataService.js';
import { resolveImportPath } from '../../utils/resolve-import-path.js'; import { resolveImportPath } from '../../utils/resolve-import-path.js';
import { AstService } from '../../core/AstService.js'; import { AstService } from '../../core/AstService.js';
import { LogService } from '../../core/LogService.js';
import { memoize } from '../../utils/memoize.js'; import { memoize } from '../../utils/memoize.js';
/** /**
@ -13,7 +12,7 @@ import { memoize } from '../../utils/memoize.js';
* @typedef {import('../../../../types/index.js').SpecifierSource} SpecifierSource * @typedef {import('../../../../types/index.js').SpecifierSource} SpecifierSource
* @typedef {import('../../../../types/index.js').IdentifierName} IdentifierName * @typedef {import('../../../../types/index.js').IdentifierName} IdentifierName
* @typedef {import('../../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot * @typedef {import('../../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('@babel/traverse').NodePath} NodePath * @typedef {import('../../../../types/index.js').SwcPath} SwcPath
*/ */
/** /**
@ -40,7 +39,7 @@ 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 {NodePath} astPath Babel ast traversal path * @param {SwcPath} astPath 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(astPath, identifierName) {
@ -54,18 +53,21 @@ function getBindingAndSourceReexports(astPath, identifierName) {
curPath = curPath.parentPath; curPath = curPath.parentPath;
} }
const rootPath = curPath; const rootPath = curPath;
rootPath.traverse({
ExportSpecifier(path) { swcTraverse(rootPath.node, {
ExportSpecifier(astPath) {
// eslint-disable-next-line arrow-body-style // eslint-disable-next-line arrow-body-style
const found = const found =
// @ts-expect-error astPath.node.orig?.value === identifierName ||
path.node.exported.name === identifierName || path.node.local.name === identifierName; astPath.node.exported?.value === identifierName ||
astPath.node.local?.value === identifierName;
if (found) { if (found) {
bindingPath = path; bindingPath = astPath;
bindingType = 'ExportSpecifier'; bindingType = 'ExportSpecifier';
// @ts-expect-error source = astPath.parentPath.node.source
source = path.parentPath.node.source ? path.parentPath.node.source.value : '[current]'; ? astPath.parentPath.node.source.value
path.stop(); : '[current]';
astPath.stop();
} }
}, },
}); });
@ -78,7 +80,7 @@ function getBindingAndSourceReexports(astPath, identifierName) {
* We might be an import that was locally renamed. * We might be an import that was locally renamed.
* Since we are traversing, we are interested in the imported name. Or in case of a re-export, * Since we are traversing, we are interested in the imported name. Or in case of a re-export,
* the local name. * the local name.
* @param {NodePath} astPath Babel ast traversal path * @param {SwcPath} astPath Babel ast traversal path
* @param {string} identifierName the name that should be tracked (and that exists inside scope of astPath) * @param {string} identifierName the name that should be tracked (and that exists inside scope of astPath)
* @returns {{ source:string, importedIdentifierName:string }} * @returns {{ source:string, importedIdentifierName:string }}
*/ */
@ -86,13 +88,12 @@ export function getImportSourceFromAst(astPath, identifierName) {
let source; let source;
let importedIdentifierName; let importedIdentifierName;
const binding = astPath.scope.getBinding(identifierName); const binding = astPath.scope.bindings[identifierName];
let bindingType = binding?.path.type; let bindingType = binding?.path.type;
let bindingPath = binding?.path; let bindingPath = binding?.path;
const matchingTypes = ['ImportSpecifier', 'ImportDefaultSpecifier', 'ExportSpecifier']; const matchingTypes = ['ImportSpecifier', 'ImportDefaultSpecifier', 'ExportSpecifier'];
if (bindingType && matchingTypes.includes(bindingType)) { if (bindingType && matchingTypes.includes(bindingType)) {
// @ts-expect-error
source = binding?.path?.parentPath?.node?.source?.value; source = binding?.path?.parentPath?.node?.source?.value;
} else { } else {
// no binding // no binding
@ -103,10 +104,10 @@ export function getImportSourceFromAst(astPath, identifierName) {
if (shouldLookForDefaultExport) { if (shouldLookForDefaultExport) {
importedIdentifierName = '[default]'; importedIdentifierName = '[default]';
} else if (source) { } else if (source) {
// @ts-expect-error
const { node } = bindingPath; const { node } = bindingPath;
importedIdentifierName = (node.imported && node.imported.name) || node.local.name; importedIdentifierName = node.orig?.value || node.imported?.value || node.local?.value;
} }
return { source, importedIdentifierName }; return { source, importedIdentifierName };
} }
@ -174,17 +175,26 @@ async function trackDownIdentifierFn(
const resolvedSourcePath = await resolveImportPath(source, currentFilePath); const resolvedSourcePath = await resolveImportPath(source, currentFilePath);
LogService.debug(`[trackDownIdentifier] ${resolvedSourcePath}`); // if (resolvedSourcePath === null) {
// LogService.error(`[trackDownIdentifier] ${resolvedSourcePath} not found`);
// }
// if (resolvedSourcePath === '[node-builtin]') {
// LogService.error(`[trackDownIdentifier] ${resolvedSourcePath} not found`);
// }
const allowedJsModuleExtensions = ['.mjs', '.js']; const allowedJsModuleExtensions = ['.mjs', '.js'];
if (!allowedJsModuleExtensions.includes(pathLib.extname(resolvedSourcePath))) { if (
!allowedJsModuleExtensions.includes(path.extname(/** @type {string} */ (resolvedSourcePath)))
) {
// We have an import assertion // We have an import assertion
return /** @type { RootFile } */ { return /** @type { RootFile } */ {
file: toRelativeSourcePath(resolvedSourcePath, rootPath), file: toRelativeSourcePath(/** @type {string} */ (resolvedSourcePath), rootPath),
specifier: '[default]', specifier: '[default]',
}; };
} }
const code = fs.readFileSync(resolvedSourcePath, 'utf8'); const code = fs.readFileSync(/** @type {string} */ (resolvedSourcePath), 'utf8');
const babelAst = AstService.getAst(code, 'swc-to-babel', { filePath: resolvedSourcePath }); const swcAst = AstService._getSwcAst(code);
const shouldLookForDefaultExport = identifierName === '[default]'; const shouldLookForDefaultExport = identifierName === '[default]';
@ -192,96 +202,111 @@ async function trackDownIdentifierFn(
let exportMatch; let exportMatch;
let pendingTrackDownPromise; let pendingTrackDownPromise;
babelTraverse.default(babelAst, { const handleExportDefaultDeclOrExpr = astPath => {
ExportDefaultDeclaration(path) { if (!shouldLookForDefaultExport) {
if (!shouldLookForDefaultExport) { return;
}
let newSource;
if (
astPath.node.expression?.type === 'Identifier' ||
astPath.node.declaration?.type === 'Identifier'
) {
newSource = getImportSourceFromAst(astPath, astPath.node.expression.value).source;
}
if (newSource) {
pendingTrackDownPromise = trackDownIdentifier(
newSource,
'[default]',
/** @type {PathFromSystemRoot} */ (resolvedSourcePath),
rootPath,
projectName,
depth + 1,
);
} else {
// We found our file!
rootSpecifier = identifierName;
rootFilePath = toRelativeSourcePath(
/** @type {PathFromSystemRoot} */ (resolvedSourcePath),
rootPath,
);
}
astPath.stop();
};
const handleExportDeclOrNamedDecl = {
enter(astPath) {
if (reexportMatch || shouldLookForDefaultExport) {
return; return;
} }
let newSource; // Are we dealing with a re-export ?
if (path.node.declaration.type === 'Identifier') { if (astPath.node.specifiers?.length) {
newSource = getImportSourceFromAst(path, path.node.declaration.name).source; exportMatch = astPath.node.specifiers.find(
} s => s.orig?.value === identifierName || s.exported?.value === identifierName,
if (newSource) {
pendingTrackDownPromise = trackDownIdentifier(
newSource,
'[default]',
resolvedSourcePath,
rootPath,
projectName,
depth + 1,
); );
} else {
// We found our file!
rootSpecifier = identifierName;
rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath);
}
path.stop();
},
ExportNamedDeclaration: {
enter(path) {
if (reexportMatch || shouldLookForDefaultExport) {
return;
}
// Are we dealing with a re-export ? if (exportMatch) {
if (path.node.specifiers?.length) { const localName = exportMatch.orig.value;
exportMatch = path.node.specifiers.find(s => s.exported.name === identifierName); let newSource;
if (astPath.node.source) {
/**
* @example
* export { x } from 'y'
*/
newSource = astPath.node.source.value;
} else {
/**
* @example
* import { x } from 'y'
* export { x }
*/
newSource = getImportSourceFromAst(astPath, identifierName).source;
if (exportMatch) { if (!newSource || newSource === '[current]') {
const localName = exportMatch.local.name;
let newSource;
if (path.node.source) {
/** /**
* @example * @example
* export { x } from 'y' * const x = 12;
*/
newSource = path.node.source.value;
} else {
/**
* @example
* import { x } from 'y'
* export { x } * export { x }
*/ */
newSource = getImportSourceFromAst(path, identifierName).source; return;
if (!newSource || newSource === '[current]') {
/**
* @example
* const x = 12;
* export { x }
*/
return;
}
} }
reexportMatch = true;
pendingTrackDownPromise = trackDownIdentifier(
newSource,
localName,
resolvedSourcePath,
rootPath,
projectName,
depth + 1,
);
path.stop();
} }
reexportMatch = true;
pendingTrackDownPromise = trackDownIdentifier(
newSource,
localName,
resolvedSourcePath,
rootPath,
projectName,
depth + 1,
);
astPath.stop();
} }
}, }
exit(path) {
if (!reexportMatch) {
// We didn't find a re-exported Identifier, that means the reference is declared
// in current file...
rootSpecifier = identifierName;
rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath);
if (exportMatch) {
path.stop();
}
}
},
}, },
}); exit(astPath) {
if (!reexportMatch) {
// We didn't find a re-exported Identifier, that means the reference is declared
// in current file...
rootSpecifier = identifierName;
rootFilePath = toRelativeSourcePath(resolvedSourcePath, rootPath);
if (exportMatch) {
astPath.stop();
}
}
},
};
const visitor = {
ExportDefaultDeclaration: handleExportDefaultDeclOrExpr,
ExportDefaultExpression: handleExportDefaultDeclOrExpr,
ExportNamedDeclaration: handleExportDeclOrNamedDecl,
ExportDeclaration: handleExportDeclOrNamedDecl,
};
swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
if (pendingTrackDownPromise) { if (pendingTrackDownPromise) {
// We can't handle promises inside Babel traverse, so we do it here... // We can't handle promises inside Babel traverse, so we do it here...
@ -296,7 +321,7 @@ async function trackDownIdentifierFn(
trackDownIdentifier = memoize(trackDownIdentifierFn); trackDownIdentifier = memoize(trackDownIdentifierFn);
/** /**
* @param {NodePath} astPath * @param {SwcPath} astPath
* @param {string} identifierNameInScope * @param {string} identifierNameInScope
* @param {PathFromSystemRoot} fullCurrentFilePath * @param {PathFromSystemRoot} fullCurrentFilePath
* @param {PathFromSystemRoot} projectPath * @param {PathFromSystemRoot} projectPath

View file

@ -19,6 +19,7 @@ import { transformIntoIterableFindImportsOutput } from './helpers/transform-into
* @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
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
* @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst
*/ */
/** /**
@ -155,8 +156,9 @@ async function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerRes
} }
export default class MatchImportsAnalyzer extends Analyzer { export default class MatchImportsAnalyzer extends Analyzer {
/** @type {AnalyzerName} */ static analyzerName = /** @type {AnalyzerName} */ ('match-imports');
static analyzerName = 'match-imports';
static requiredAst = /** @type {AnalyzerAst} */ ('swc');
static requiresReference = true; static requiresReference = true;

View file

@ -6,11 +6,12 @@ import { QueryService } from './QueryService.js';
import { ReportService } from './ReportService.js'; import { ReportService } from './ReportService.js';
import { InputDataService } from './InputDataService.js'; import { InputDataService } from './InputDataService.js';
import { toPosixPath } from '../utils/to-posix-path.js'; import { toPosixPath } from '../utils/to-posix-path.js';
import { memoize } from '../utils/memoize.js';
import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-from-root.js'; import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-from-root.js';
/** /**
* @typedef {import("@swc/core").Module} SwcAstModule
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
* @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst
* @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../../../types/index.js').QueryOutput} QueryOutput * @typedef {import('../../../types/index.js').QueryOutput} QueryOutput
* @typedef {import('../../../types/index.js').ProjectInputData} ProjectInputData * @typedef {import('../../../types/index.js').ProjectInputData} ProjectInputData
@ -25,12 +26,13 @@ import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-fro
* Analyzes one entry: the callback can traverse a given ast for each entry * Analyzes one entry: the callback can traverse a given ast for each entry
* @param {ProjectInputDataWithMeta} projectData * @param {ProjectInputDataWithMeta} projectData
* @param {function} astAnalysis * @param {function} astAnalysis
* @param {object} analyzerCfg
*/ */
async function analyzePerAstFile(projectData, astAnalysis) { async function analyzePerAstFile(projectData, astAnalysis, analyzerCfg) {
const entries = []; const entries = [];
for (const { file, ast, context: astContext } of projectData.entries) { for (const { file, ast, context: astContext } of projectData.entries) {
const relativePath = getFilePathRelativeFromRoot(file, projectData.project.path); const relativePath = getFilePathRelativeFromRoot(file, projectData.project.path);
const context = { code: astContext.code, relativePath, projectData }; const context = { code: astContext.code, relativePath, projectData, analyzerCfg };
LogService.debug(`${pathLib.resolve(projectData.project.path, file)}`); LogService.debug(`${pathLib.resolve(projectData.project.path, file)}`);
const { result, meta } = await astAnalysis(ast, context); const { result, meta } = await astAnalysis(ast, context);
entries.push({ file: relativePath, meta, result }); entries.push({ file: relativePath, meta, result });
@ -86,8 +88,8 @@ function ensureAnalyzerResultFormat(queryOutput, cfg, analyzer) {
const aResult = { const aResult = {
queryOutput, queryOutput,
analyzerMeta: { analyzerMeta: {
name: analyzer.name, name: analyzer.constructor.analyzerName,
requiredAst: analyzer.requiredAst, requiredAst: analyzer.constructor.requiredAst,
identifier, identifier,
...optional, ...optional,
configuration: cfg, configuration: cfg,
@ -131,30 +133,28 @@ function ensureAnalyzerResultFormat(queryOutput, cfg, analyzer) {
* @typedef {(referencePath:PathFromSystemRoot,targetPath:PathFromSystemRoot) => {compatible:boolean; reason?:string}} CheckForMatchCompatibilityFn * @typedef {(referencePath:PathFromSystemRoot,targetPath:PathFromSystemRoot) => {compatible:boolean; reason?:string}} CheckForMatchCompatibilityFn
* @type {CheckForMatchCompatibilityFn} * @type {CheckForMatchCompatibilityFn}
*/ */
const checkForMatchCompatibility = memoize( const checkForMatchCompatibility = (
( /** @type {PathFromSystemRoot} */ referencePath,
/** @type {PathFromSystemRoot} */ referencePath, /** @type {PathFromSystemRoot} */ targetPath,
/** @type {PathFromSystemRoot} */ targetPath, ) => {
) => { // const refFile = pathLib.resolve(referencePath, 'package.json');
// const refFile = pathLib.resolve(referencePath, 'package.json'); const referencePkg = InputDataService.getPackageJson(referencePath);
const referencePkg = InputDataService.getPackageJson(referencePath); // const targetFile = pathLib.resolve(targetPath, 'package.json');
// const targetFile = pathLib.resolve(targetPath, 'package.json'); const targetPkg = InputDataService.getPackageJson(targetPath);
const targetPkg = InputDataService.getPackageJson(targetPath);
const allTargetDeps = [ const allTargetDeps = [
...Object.entries(targetPkg?.devDependencies || {}), ...Object.entries(targetPkg?.devDependencies || {}),
...Object.entries(targetPkg?.dependencies || {}), ...Object.entries(targetPkg?.dependencies || {}),
]; ];
const importEntry = allTargetDeps.find(([name]) => referencePkg?.name === name); const importEntry = allTargetDeps.find(([name]) => referencePkg?.name === name);
if (!importEntry) { if (!importEntry) {
return { compatible: false, reason: 'no-dependency' }; return { compatible: false, reason: 'no-dependency' };
} }
if (referencePkg?.version && !semver.satisfies(referencePkg.version, importEntry[1])) { if (referencePkg?.version && !semver.satisfies(referencePkg.version, importEntry[1])) {
return { compatible: false, reason: 'no-matched-version' }; return { compatible: false, reason: 'no-matched-version' };
} }
return { compatible: true }; return { compatible: true };
}, };
);
/** /**
* If in json format, 'unwind' to be compatible for analysis... * If in json format, 'unwind' to be compatible for analysis...
@ -169,13 +169,21 @@ function unwindJsonResult(targetOrReferenceProjectResult) {
export class Analyzer { export class Analyzer {
static requiresReference = false; static requiresReference = false;
/** @type {AnalyzerAst} */
static requiredAst = 'babel';
/** @type {AnalyzerName} */ /** @type {AnalyzerName} */
static analyzerName = ''; static analyzerName = '';
name = /** @type {typeof Analyzer} */ (this.constructor).analyzerName; name = /** @type {typeof Analyzer} */ (this.constructor).analyzerName;
/** @type {'babel'|'swc-to-babel'} */ _customConfig = {};
requiredAst = 'babel';
get config() {
return {
...this._customConfig,
};
}
/** /**
* In a MatchAnalyzer, two Analyzers (a reference and targer) are run. * In a MatchAnalyzer, two Analyzers (a reference and targer) are run.
@ -335,33 +343,36 @@ export class Analyzer {
*/ */
const astDataProjects = await QueryService.addAstToProjectsData( const astDataProjects = await QueryService.addAstToProjectsData(
finalTargetData, finalTargetData,
this.requiredAst, this.constructor.requiredAst,
); );
return analyzePerAstFile(astDataProjects[0], traverseEntryFn); return analyzePerAstFile(astDataProjects[0], traverseEntryFn, this.config);
} }
async execute(customConfig = {}) { /**
LogService.debug(`Analyzer "${this.name}": started execute method`); * Finds export specifiers and sources
* @param {FindExportsConfig} customConfig
const cfg = { */
targetProjectPath: null, async execute(customConfig) {
referenceProjectPath: null, this._customConfig = customConfig;
suppressNonCriticalLogs: false, const cfg = this.config;
...customConfig,
};
/** /**
* Prepare * Prepare
*/ */
const analyzerResult = this._prepare(cfg); const cachedAnalyzerResult = this._prepare(cfg);
if (analyzerResult) { if (cachedAnalyzerResult) {
return analyzerResult; return cachedAnalyzerResult;
} }
/** /**
* Traverse * Traverse
*/ */
const queryOutput = await this._traverse(() => {}); const queryOutput = await this._traverse({
// @ts-ignore
traverseEntryFn: this.constructor.analyzeFile,
filePaths: cfg.targetFilePaths,
projectPath: cfg.targetProjectPath,
});
/** /**
* Finalize * Finalize

View file

@ -52,6 +52,29 @@ export class AstService {
return guardedSwcToBabel(ast, code); return guardedSwcToBabel(ast, code);
} }
/**
* Compiles an array of file paths using swc.
* @param {string} code
* @param {ParserOptions} parserOptions
* @returns {SwcAstModule}
*/
static _getSwcAst(code, parserOptions = {}) {
const ast = swc.parseSync(code, {
syntax: 'typescript',
target: 'es2022',
...parserOptions,
});
return ast;
}
/**
* Compensates for swc span bug: https://github.com/swc-project/swc/issues/1366#issuecomment-1516539812
* @returns {number}
*/
static _getSwcOffset() {
return swc.parseSync('').span.end;
}
/** /**
* Combines all script tags as if it were one js file. * Combines all script tags as if it were one js file.
* @param {string} htmlCode * @param {string} htmlCode
@ -77,9 +100,9 @@ export class AstService {
/** /**
* Returns the Babel AST * Returns the Babel AST
* @param { string } code * @param { string } code
* @param { 'babel'|'swc-to-babel'} astType * @param { 'babel'|'swc-to-babel'|'swc'} astType
* @param { {filePath?: PathFromSystemRoot} } options * @param { {filePath?: PathFromSystemRoot} } options
* @returns {File|undefined} * @returns {File|undefined|SwcAstModule}
*/ */
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
static getAst(code, astType, { filePath } = {}) { static getAst(code, astType, { filePath } = {}) {
@ -91,6 +114,9 @@ export class AstService {
if (astType === 'swc-to-babel') { if (astType === 'swc-to-babel') {
return this._getSwcToBabelAst(code); return this._getSwcToBabelAst(code);
} }
if (astType === 'swc') {
return this._getSwcAst(code);
}
throw new Error(`astType "${astType}" not supported.`); throw new Error(`astType "${astType}" not supported.`);
} catch (e) { } catch (e) {
LogService.error(`Error when parsing "${filePath}":/n${e}`); LogService.error(`Error when parsing "${filePath}":/n${e}`);

View file

@ -12,6 +12,8 @@ import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-fro
import { toPosixPath } from '../utils/to-posix-path.js'; import { toPosixPath } from '../utils/to-posix-path.js';
import { memoize } from '../utils/memoize.js'; import { memoize } from '../utils/memoize.js';
// const memoize = fn => fn;
/** /**
* @typedef {import('../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult * @typedef {import('../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../../../types/index.js').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry * @typedef {import('../../../types/index.js').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry
@ -309,13 +311,13 @@ export class InputDataService {
try { try {
const pkgJson = getPackageJson(projectPath); const pkgJson = getPackageJson(projectPath);
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
project.mainEntry = this.__normalizeMainEntry(pkgJson.main || './index.js'); project.mainEntry = this.__normalizeMainEntry(pkgJson?.main || './index.js');
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
project.name = pkgJson.name; project.name = pkgJson?.name;
// TODO: also add meta info whether we are in a monorepo or not. // TODO: also add meta info whether we are in a monorepo or not.
// We do this by checking whether there is a lerna.json on root level. // We do this by checking whether there is a lerna.json on root level.
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
project.version = pkgJson.version; project.version = pkgJson?.version;
} catch (e) { } catch (e) {
LogService.warn(/** @type {string} */ (e)); LogService.warn(/** @type {string} */ (e));
} }
@ -422,6 +424,10 @@ export class InputDataService {
.filter(dirPath => fs.lstatSync(dirPath).isDirectory()); .filter(dirPath => fs.lstatSync(dirPath).isDirectory());
} }
static set targetProjectPaths(v) {
this.__targetProjectPaths = ensureArray(v);
}
/** /**
* @type {PathFromSystemRoot[]} a list of strings representing all entry paths for projects we want to query * @type {PathFromSystemRoot[]} a list of strings representing all entry paths for projects we want to query
*/ */
@ -446,10 +452,6 @@ export class InputDataService {
this.__referenceProjectPaths = ensureArray(v); this.__referenceProjectPaths = ensureArray(v);
} }
static set targetProjectPaths(v) {
this.__targetProjectPaths = ensureArray(v);
}
/** /**
* @type {GatherFilesConfig} * @type {GatherFilesConfig}
*/ */
@ -620,12 +622,12 @@ export class InputDataService {
static getMonoRepoPackages(rootPath) { static getMonoRepoPackages(rootPath) {
// [1] Look for npm/yarn workspaces // [1] Look for npm/yarn workspaces
const pkgJson = getPackageJson(rootPath); const pkgJson = getPackageJson(rootPath);
if (pkgJson && pkgJson.workspaces) { if (pkgJson?.workspaces) {
return getPathsFromGlobList(pkgJson.workspaces, rootPath); return getPathsFromGlobList(pkgJson.workspaces, rootPath);
} }
// [2] Look for lerna packages // [2] Look for lerna packages
const lernaJson = getLernaJson(rootPath); const lernaJson = getLernaJson(rootPath);
if (lernaJson && lernaJson.packages) { if (lernaJson?.packages) {
return getPathsFromGlobList(lernaJson.packages, rootPath); return getPathsFromGlobList(lernaJson.packages, rootPath);
} }
// TODO: support forward compatibility for npm? // TODO: support forward compatibility for npm?

View file

@ -3,8 +3,10 @@ import path from 'path';
import { AstService } from './AstService.js'; import { AstService } from './AstService.js';
import { LogService } from './LogService.js'; import { LogService } from './LogService.js';
import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-from-root.js'; import { getFilePathRelativeFromRoot } from '../utils/get-file-path-relative-from-root.js';
import { memoize } from '../utils/memoize.js';
import { getCurrentDir } from '../utils/get-current-dir.js'; import { getCurrentDir } from '../utils/get-current-dir.js';
// import { memoize } from '../utils/memoize.js';
const memoize = fn => fn;
/** /**
* @typedef {import('./Analyzer.js').Analyzer} Analyzer * @typedef {import('./Analyzer.js').Analyzer} Analyzer
@ -20,6 +22,7 @@ import { getCurrentDir } from '../utils/get-current-dir.js';
* @typedef {import('../../../types/index.js').ProjectInputData} ProjectInputData * @typedef {import('../../../types/index.js').ProjectInputData} ProjectInputData
* @typedef {import('../../../types/index.js').AnalyzerConfig} AnalyzerConfig * @typedef {import('../../../types/index.js').AnalyzerConfig} AnalyzerConfig
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName * @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
* @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst
* @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot * @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../../../types/index.js').GatherFilesConfig} GatherFilesConfig * @typedef {import('../../../types/index.js').GatherFilesConfig} GatherFilesConfig
* @typedef {import('../../../types/index.js').AnalyzerQueryResult} AnalyzerQueryResult * @typedef {import('../../../types/index.js').AnalyzerQueryResult} AnalyzerQueryResult
@ -239,7 +242,7 @@ export class QueryService {
/** /**
* @param {ProjectInputData[]} projectsData * @param {ProjectInputData[]} projectsData
* @param {'babel'|'swc-to-babel'} requiredAst * @param {AnalyzerAst} requiredAst
*/ */
static async addAstToProjectsData(projectsData, requiredAst) { static async addAstToProjectsData(projectsData, requiredAst) {
return projectsData.map(projectData => { return projectsData.map(projectData => {

View file

@ -1,7 +1,8 @@
import fs from 'fs'; import fs from 'fs';
import pathLib from 'path'; import pathLib from 'path';
import { getHash } from '../utils/get-hash.js'; import { getHash } from '../utils/get-hash.js';
import { memoize } from '../utils/memoize.js'; // import { memoize } from '../utils/memoize.js';
const memoize = fn => fn;
/** /**
* @typedef {import('../../../types/index.js').Project} Project * @typedef {import('../../../types/index.js').Project} Project

View file

@ -0,0 +1,188 @@
import fs from 'fs';
import path from 'path';
import babelTraversePkg from '@babel/traverse';
import { AstService } from '../core/AstService.js';
import { trackDownIdentifier } from '../analyzers/helpers/track-down-identifier.js';
import { toPosixPath } from './to-posix-path.js';
/**
* @typedef {import('@babel/types').Node} Node
* @typedef {import('@babel/traverse').NodePath} NodePath
* @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
*/
/**
* @param {{rootPath:PathFromSystemRoot; localPath:PathRelativeFromProjectRoot}} opts
* @returns
*/
export function getFilePathOrExternalSource({ rootPath, localPath }) {
if (!localPath.startsWith('.')) {
// We are not resolving external files like '@lion/input-amount/x.js',
// but we give a 100% score if from and to are same here..
return localPath;
}
return toPosixPath(path.resolve(rootPath, localPath));
}
/**
* Assume we had:
* ```js
* const x = 88;
* const y = x;
* export const myIdentifier = y;
* ```
* - 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)
* - is it a non ref declaration? Return the path of the node
* @param {{ referencedIdentifierName:string, globalScopeBindings:BabelBinding; }} opts
* @returns {NodePath}
*/
export function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) {
const [, refDeclaratorBinding] = Object.entries(globalScopeBindings).find(
([key]) => key === referencedIdentifierName,
);
if (
refDeclaratorBinding.path.type === 'ImportSpecifier' ||
refDeclaratorBinding.path.type === 'ImportDefaultSpecifier'
) {
return refDeclaratorBinding.path;
}
if (refDeclaratorBinding.path.node.init.type === 'Identifier') {
return getReferencedDeclaration({
referencedIdentifierName: refDeclaratorBinding.path.node.init.name,
globalScopeBindings,
});
}
return refDeclaratorBinding.path.get('init');
}
/**
* @example
* ```js
* // ------ input file --------
* const x = 88;
* const y = x;
* export const myIdentifier = y;
* // --------------------------
*
* await getSourceCodeFragmentOfDeclaration(code) // finds "88"
* ```
*
* @param {{ filePath: PathFromSystemRoot; exportedIdentifier: string; projectRootPath: PathFromSystemRoot }} opts
* @returns {Promise<{ sourceNodePath: string; sourceFragment: string|null; externalImportSource: string; }>}
*/
export async function getSourceCodeFragmentOfDeclaration({
filePath,
exportedIdentifier,
projectRootPath,
}) {
const code = fs.readFileSync(filePath, 'utf8');
// TODO: fix swc-to-babel lib to make this compatible with 'swc-to-babel' mode of getAst
const babelAst = AstService.getAst(code, 'babel', { filePath });
/** @type {NodePath} */
let finalNodePath;
babelTraversePkg.default(babelAst, {
Program(astPath) {
astPath.stop();
// Situations
// - Identifier is part of default export (in this case 'exportedIdentifier' is '[default]' )
// - declared right away (for instance a class)
// - referenced (possibly recursively) by other declaration
// - Identifier is part of a named export
// - declared right away
// - referenced (possibly recursively) by other declaration
const globalScopeBindings = astPath.get('body')[0].scope.bindings;
if (exportedIdentifier === '[default]') {
const defaultExportPath = astPath
.get('body')
.find(child => child.node.type === 'ExportDefaultDeclaration');
// @ts-expect-error
const isReferenced = defaultExportPath?.node.declaration?.type === 'Identifier';
if (!isReferenced) {
finalNodePath = defaultExportPath.get('declaration');
} else {
finalNodePath = getReferencedDeclaration({
referencedIdentifierName: defaultExportPath.node.declaration.name,
globalScopeBindings,
});
}
} else {
const variableDeclaratorPath = astPath.scope.getBinding(exportedIdentifier).path;
const varDeclNode = variableDeclaratorPath.node;
const isReferenced = varDeclNode.init?.type === 'Identifier';
const contentPath = varDeclNode.init
? variableDeclaratorPath.get('init')
: variableDeclaratorPath;
const name = varDeclNode.init
? varDeclNode.init.name
: varDeclNode.id?.name || varDeclNode.imported.name;
if (!isReferenced) {
// it must be an exported declaration
finalNodePath = contentPath;
} else {
finalNodePath = getReferencedDeclaration({
referencedIdentifierName: name,
globalScopeBindings,
});
}
}
},
});
if (finalNodePath.type === 'ImportSpecifier') {
const importDeclNode = finalNodePath.parentPath.node;
const source = importDeclNode.source.value;
const identifierName = finalNodePath.node.imported.name;
const currentFilePath = filePath;
const rootFile = await trackDownIdentifier(
source,
identifierName,
currentFilePath,
projectRootPath,
);
const filePathOrSrc = getFilePathOrExternalSource({
rootPath: projectRootPath,
localPath: rootFile.file,
});
// TODO: allow resolving external project file paths
if (!filePathOrSrc.startsWith('/')) {
// So we have external project; smth like '@lion/input/x.js'
return {
sourceNodePath: finalNodePath,
sourceFragment: null,
externalImportSource: filePathOrSrc,
};
}
return getSourceCodeFragmentOfDeclaration({
filePath: filePathOrSrc,
exportedIdentifier: rootFile.specifier,
projectRootPath,
});
}
return {
sourceNodePath: finalNodePath,
sourceFragment: code.slice(
finalNodePath.node?.loc?.start.index,
finalNodePath.node?.loc?.end.index,
),
externalImportSource: null,
};
}

View file

@ -1,6 +1,6 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import babelTraversePkg from '@babel/traverse'; import { swcTraverse, getPathFromNode } from './swc-traverse.js';
import { AstService } from '../core/AstService.js'; import { AstService } from '../core/AstService.js';
import { trackDownIdentifier } from '../analyzers/helpers/track-down-identifier.js'; import { trackDownIdentifier } from '../analyzers/helpers/track-down-identifier.js';
import { toPosixPath } from './to-posix-path.js'; import { toPosixPath } from './to-posix-path.js';
@ -8,6 +8,7 @@ import { toPosixPath } from './to-posix-path.js';
/** /**
* @typedef {import('@babel/types').Node} Node * @typedef {import('@babel/types').Node} Node
* @typedef {import('@babel/traverse').NodePath} NodePath * @typedef {import('@babel/traverse').NodePath} NodePath
* @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
*/ */
@ -33,28 +34,32 @@ export function getFilePathOrExternalSource({ rootPath, localPath }) {
* export const myIdentifier = y; * export const myIdentifier = y;
* ``` * ```
* - We started in getSourceCodeFragmentOfDeclaration (looking 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', globalScopeBindings: {x: SwcBinding; y: SwcBinding} }
* - 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)
* - 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:BabelBinding; }} opts * @param {{ referencedIdentifierName:string, globalScopeBindings:{[key:string]:SwcBinding}; }} opts
* @returns {NodePath} * @returns {NodePath}
*/ */
export function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) { export function getReferencedDeclaration({ referencedIdentifierName, globalScopeBindings }) {
const [, refDeclaratorBinding] = Object.entries(globalScopeBindings).find( // We go from referencedIdentifierName 'y' to binding (VariableDeclarator path) 'y';
([key]) => key === referencedIdentifierName, // const [, refDeclaratorBinding] =
); // Object.entries(globalScopeBindings).find(([key]) => key === referencedIdentifierName) || [];
if ( const refDeclaratorBinding = globalScopeBindings[referencedIdentifierName];
refDeclaratorBinding.path.type === 'ImportSpecifier' ||
refDeclaratorBinding.path.type === 'ImportDefaultSpecifier' // We provided a referencedIdentifierName that is not in the globalScopeBindings
) { if (!refDeclaratorBinding) {
return null;
}
if (['ImportSpecifier', 'ImportDefaultSpecifier'].includes(refDeclaratorBinding.path.node.type)) {
return refDeclaratorBinding.path; return refDeclaratorBinding.path;
} }
if (refDeclaratorBinding.path.node.init.type === 'Identifier') { if (refDeclaratorBinding.identifier.init.type === 'Identifier') {
return getReferencedDeclaration({ return getReferencedDeclaration({
referencedIdentifierName: refDeclaratorBinding.path.node.init.name, referencedIdentifierName: refDeclaratorBinding.identifier.init.value,
globalScopeBindings, globalScopeBindings,
}); });
} }
@ -83,70 +88,78 @@ export async function getSourceCodeFragmentOfDeclaration({
projectRootPath, projectRootPath,
}) { }) {
const code = fs.readFileSync(filePath, 'utf8'); const code = fs.readFileSync(filePath, 'utf8');
// compensate for swc span bug: https://github.com/swc-project/swc/issues/1366#issuecomment-1516539812
const offset = AstService._getSwcOffset();
// 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 babelAst = AstService.getAst(code, 'babel', { filePath }); const swcAst = AstService._getSwcAst(code);
/** @type {NodePath} */ /** @type {NodePath} */
let finalNodePath; let finalNodePath;
babelTraversePkg.default(babelAst, { swcTraverse(
Program(astPath) { swcAst,
astPath.stop(); {
Module(astPath) {
astPath.stop();
// Situations // Situations
// - Identifier is part of default export (in this case 'exportedIdentifier' is '[default]' ) // - Identifier is part of default export (in this case 'exportedIdentifier' is '[default]' )
// - declared right away (for instance a class) // - declared right away (for instance a class)
// - referenced (possibly recursively) by other declaration // - referenced (possibly recursively) by other declaration
// - Identifier is part of a named export // - Identifier is part of a named export
// - declared right away // - declared right away
// - referenced (possibly recursively) by other declaration // - referenced (possibly recursively) by other declaration
const globalScopeBindings = astPath.get('body')[0].scope.bindings; const globalScopeBindings = getPathFromNode(astPath.node.body?.[0])?.scope.bindings;
if (exportedIdentifier === '[default]') { if (exportedIdentifier === '[default]') {
const defaultExportPath = astPath const defaultExportPath = getPathFromNode(
.get('body') astPath.node.body.find(child =>
.find(child => child.node.type === 'ExportDefaultDeclaration'); ['ExportDefaultDeclaration', 'ExportDefaultExpression'].includes(child.type),
// @ts-expect-error ),
const isReferenced = defaultExportPath?.node.declaration?.type === 'Identifier'; );
const isReferenced = defaultExportPath?.node.expression?.type === 'Identifier';
if (!isReferenced) { if (!isReferenced) {
finalNodePath = defaultExportPath.get('declaration'); finalNodePath = defaultExportPath.get('decl') || defaultExportPath.get('expression');
} else {
finalNodePath = getReferencedDeclaration({
referencedIdentifierName: defaultExportPath.node.expression.value,
globalScopeBindings,
});
}
} else { } else {
finalNodePath = getReferencedDeclaration({ const variableDeclaratorPath = astPath.scope.bindings[exportedIdentifier].path;
referencedIdentifierName: defaultExportPath.node.declaration.name, const varDeclNode = variableDeclaratorPath.node;
globalScopeBindings, const isReferenced = varDeclNode.init?.type === 'Identifier';
}); const contentPath = varDeclNode.init
} ? variableDeclaratorPath.get('init')
} else { : variableDeclaratorPath;
const variableDeclaratorPath = astPath.scope.getBinding(exportedIdentifier).path;
const varDeclNode = variableDeclaratorPath.node;
const isReferenced = varDeclNode.init?.type === 'Identifier';
const contentPath = varDeclNode.init
? variableDeclaratorPath.get('init')
: variableDeclaratorPath;
const name = varDeclNode.init const name = varDeclNode.init
? varDeclNode.init.name ? varDeclNode.init.value
: varDeclNode.id?.name || varDeclNode.imported.name; : varDeclNode.id?.value || varDeclNode.imported?.value || varDeclNode.orig?.value;
if (!isReferenced) { if (!isReferenced) {
// it must be an exported declaration // it must be an exported declaration
finalNodePath = contentPath; finalNodePath = contentPath;
} else { } else {
finalNodePath = getReferencedDeclaration({ finalNodePath = getReferencedDeclaration({
referencedIdentifierName: name, referencedIdentifierName: name,
globalScopeBindings, globalScopeBindings,
}); });
}
} }
} },
}, },
}); { needsAdvancedPaths: true },
);
if (finalNodePath.type === 'ImportSpecifier') { if (finalNodePath.type === 'ImportSpecifier') {
const importDeclNode = finalNodePath.parentPath.node; const importDeclNode = finalNodePath.parentPath.node;
const source = importDeclNode.source.value; const source = importDeclNode.source.value;
const identifierName = finalNodePath.node.imported.name; const identifierName = finalNodePath.node.imported?.value || finalNodePath.node.local?.value;
const currentFilePath = filePath; const currentFilePath = filePath;
const rootFile = await trackDownIdentifier( const rootFile = await trackDownIdentifier(
@ -180,9 +193,10 @@ export async function getSourceCodeFragmentOfDeclaration({
return { return {
sourceNodePath: finalNodePath, sourceNodePath: finalNodePath,
sourceFragment: code.slice( sourceFragment: code.slice(
finalNodePath.node?.loc?.start.index, finalNodePath.node?.span?.start - 1 - offset,
finalNodePath.node?.loc?.end.index, finalNodePath.node?.span?.end - 1 - offset,
), ),
// sourceFragment: finalNodePath.node?.raw || finalNodePath.node?.value,
externalImportSource: null, externalImportSource: null,
}; };
} }

View file

@ -66,9 +66,9 @@ async function resolveImportPathFn(importee, importer, opts) {
); );
if (!result?.id) { if (!result?.id) {
LogService.warn( // LogService.warn(
`[resolveImportPath] importee ${importee} not found in filesystem for importer '${importer}'.`, // `[resolveImportPath] importee ${importee} not found in filesystem for importer '${importer}'.`,
); // );
return null; return null;
} }
return toPosixPath(result.id); return toPosixPath(result.id);

View file

@ -0,0 +1,359 @@
/**
* @typedef {import('@swc/core').Module} SwcAstModule
* @typedef {import('@swc/core').Node} SwcNode
* @typedef {import('@swc/core').VariableDeclarator} SwcVariableDeclarator
* @typedef {import('@swc/core').Identifier} SwcIdentifierNode
* @typedef {import('../../../types/index.js').SwcPath} SwcPath
* @typedef {import('../../../types/index.js').SwcScope} SwcScope
* @typedef {import('../../../types/index.js').SwcVisitor} SwcVisitor
* @typedef {import('../../../types/index.js').SwcBinding} SwcBinding
* @typedef {import('../../../types/index.js').SwcTraversalContext} SwcTraversalContext
*/
/**
* Contains all node info, to create paths from
* @type {WeakMap<SwcNode,SwcPath>}
*/
const swcPathCache = new WeakMap();
const fnTypes = [
'FunctionDeclaration',
'FunctionExpression',
'ArrowFunctionExpression',
'ClassMethod',
'Constructor',
];
const nonBlockParentTypes = [...fnTypes, 'SwitchStatement', 'ClassDeclaration'];
/**
* @param {SwcPath} swcPath
* @param {SwcScope} currentScope
* @param {SwcTraversalContext} traversalContext
* @returns {SwcScope|null}
*/
function getNewScope(swcPath, currentScope, traversalContext) {
const { node, parent } = swcPath;
// const hasNonBlockParent = (/** @type {SwcNode} */ nd) => nonBlockParentTypes.includes(nd.type);
const isFn = (/** @type {SwcNode} */ nd) => nd && fnTypes.includes(nd.type);
const isIsolatedBlockStatement = !isFn(parent) && node.type === 'BlockStatement';
// Create new scope...
if (nonBlockParentTypes.includes(node.type) || isIsolatedBlockStatement) {
// eslint-disable-next-line no-param-reassign
traversalContext.scopeId += 1;
return {
id: traversalContext.scopeId,
parentScope: currentScope,
path: swcPath,
bindings: {},
_pendingRefsWithoutBinding: [],
_isIsolatedBlockStatement: isIsolatedBlockStatement,
};
}
return null;
}
/**
* @param {SwcNode} node
*/
export function getPathFromNode(node) {
return swcPathCache.get(node);
}
/**
* @param {SwcNode} node
* @param {SwcNode|null} parent
* @param {Function} stop
* @param {SwcScope} [scope]
* @returns {SwcPath}
*/
function createSwcPath(node, parent, stop, scope) {
const swcPath = {
node,
parent,
stop,
// TODO: "pre-traverse" the missing scope parts instead via getter that adds refs and bindings for current scope
scope,
parentPath: parent ? getPathFromNode(parent) : null,
get(/** @type {string} */ name) {
const swcPathForNode = getPathFromNode(node[name]);
if (node[name] && !swcPathForNode) {
// throw new Error(
// `[swcTraverse]: Use {needsAdvancedPaths: true} to find path for node: ${node[name]}`,
// );
// TODO: "pre-traverse" the missing path parts instead
}
return swcPathForNode;
},
get type() {
return node.type;
},
};
swcPathCache.set(node, swcPath);
return swcPath;
}
/**
* Is the node:
* - a declaration (like "const a = 1")?
* - an import specifier (like "import { a } from 'b'")?
* Handy to know if the parents of Identifiers mark a binding
* @param {SwcNode} parent
* @param {string} identifierValue
*/
function isBindingNode(parent, identifierValue) {
if (parent.type === 'VariableDeclarator') {
// @ts-ignore
return parent.id.value === identifierValue;
}
return [
'ClassDeclaration',
'FunctionDeclaration',
'ArrowFunctionExpression',
'ImportSpecifier',
'ImportDefaultSpecifier',
].includes(parent.type);
}
/**
* Is the node:
* - a declaration (like "const a = 1")?
* - an import specifier (like "import { a } from 'b'")?
* Handy to know if the parents of Identifiers mark a binding
* @param {SwcNode} parent
*/
function isBindingRefNode(parent) {
return ![
'ClassMethod',
'Constructor',
'MemberExpression',
'KeyValueProperty',
'SwitchStatement',
'MethodProperty',
].includes(parent.type);
}
/**
* @param {SwcPath} swcPathForIdentifier
* @returns {void}
*/
function addPotentialBindingOrRefToScope(swcPathForIdentifier) {
const { node, parent, scope, parentPath } = swcPathForIdentifier;
if (node.type !== 'Identifier') {
return;
}
// const parentPath = getPathFromNode(parent);
if (isBindingNode(parent, node.value)) {
/** @type {SwcBinding} */
const binding = {
identifier: parent,
// kind: 'var',
refs: [],
path: swcPathForIdentifier.parentPath,
};
let scopeBindingBelongsTo = scope;
const isVarInIsolatedBlock =
scope._isIsolatedBlockStatement &&
swcPathForIdentifier.parentPath.parentPath.node.kind === 'var';
const hasNonBlockParent = nonBlockParentTypes.includes(parent.type);
if (isVarInIsolatedBlock || hasNonBlockParent) {
scopeBindingBelongsTo = scope.parentScope || scope;
}
if (scopeBindingBelongsTo._pendingRefsWithoutBinding.includes(parentPath)) {
binding.refs.push(parentPath);
scopeBindingBelongsTo._pendingRefsWithoutBinding.splice(
scopeBindingBelongsTo._pendingRefsWithoutBinding.indexOf(parentPath),
1,
);
}
const idName = node.value || node.local?.value || node.orig?.value;
// eslint-disable-next-line no-param-reassign
scopeBindingBelongsTo.bindings[idName] = binding;
// Align with Babel... => in example `class Q {}`, Q has binding to root scope and ClassDeclaration scope
if (parent.type === 'ClassDeclaration') {
scope.bindings[idName] = binding;
}
}
// In other cases, we are dealing with a reference that must be bound to a binding
else if (isBindingRefNode(parent)) {
const binding = scope.bindings[node.value];
if (binding) {
binding.refs.push(parentPath);
} else {
// we are referencing a variable that is not declared in this scope or any parent scope
// It might be hoisted, so we might find it later. For now, store it as a pending reference
scope._pendingRefsWithoutBinding.push(parentPath);
}
}
}
/**
* Is the node is the root of the ast?
* in Babel, this is the equivalent of Program
* @param {SwcNode} node
* @returns {boolean}
*/
function isRootNode(node) {
return node.type === 'Module' || node.type === 'Script';
}
/**
* @param {{node: SwcNode; }} node
* @param {(data:{child:SwcNode}) => void} callback
*/
const loopChildren = ({ node }, callback) => {
for (const [childKey, childVal] of Object.entries(node)) {
if (childKey === 'span') {
// eslint-disable-next-line no-continue
continue;
}
if (Array.isArray(childVal)) {
for (const childValElem of childVal) {
callback({ child: childValElem });
}
} else if (typeof childVal === 'object') {
callback({ child: childVal });
}
}
};
/**
* @param {SwcPath} swcPath
* @param {SwcVisitor} visitor
* @param {SwcTraversalContext} traversalContext
*/
function visit(swcPath, visitor, traversalContext) {
if (visitor.enter) {
// @ts-expect-error
visitor.enter(swcPath);
}
if (isRootNode(swcPath.node) && visitor.root) {
// @ts-expect-error
visitor.root(swcPath);
}
// Later, consider https://github.com/babel/babel/blob/b1e73d6f961065c56427ffa89c130beea8321d3b/packages/babel-traverse/src/traverse-node.ts#L28
if (typeof visitor[swcPath.node.type] === 'function') {
// @ts-expect-error
visitor[swcPath.node.type](swcPath);
}
// @ts-expect-error
else if (visitor[swcPath.node.type]?.enter) {
// @ts-expect-error
visitor[swcPath.node.type].enter(swcPath);
}
// @ts-expect-error
if (visitor[swcPath.node.type]?.exit) {
// Let visitTree know that we should visit on exit
// @ts-expect-error
traversalContext.visitOnExitFns.push(() => visitor[swcPath.node.type].exit(swcPath));
}
}
/**
* Simple traversal for swc ast.
* @param {SwcAstModule} swcAst
* @param {SwcVisitor} visitor
* @param {object} config
* @param {boolean} [config.needsAdvancedPaths] needs a full traversal before starting the visitor, which is less performant. Only enable when path.get() is used
*/
export function swcTraverse(swcAst, visitor, { needsAdvancedPaths = false } = {}) {
/**
* For performance, the author of a visitor can call this to stop further traversal
*/
let isStopped = false;
const stop = () => {
isStopped = true;
};
/**
* @param {SwcNode} node
* @param {SwcNode|null} parent
* @param {SwcScope} scope
* @param {boolean} hasPreparedTree
* @param {SwcTraversalContext} traversalContext
*/
const handlePathAndScope = (node, parent, scope, hasPreparedTree, traversalContext) => {
if (hasPreparedTree) {
const swcPath = /** @type {SwcPath} */ (swcPathCache.get(node));
return {
swcPath,
newOrCurScope: getNewScope(swcPath, scope, traversalContext) || scope,
};
}
// `needsAdvancedPaths` was false
const swcPath = createSwcPath(node, parent, stop);
// We create scopes ourselves, since paths are not prepared yet...
const newOrCurScope = getNewScope(swcPath, scope, traversalContext) || scope;
swcPath.scope = newOrCurScope;
addPotentialBindingOrRefToScope(swcPath);
return { newOrCurScope, swcPath };
};
/**
* @param {SwcNode} node
* @param {SwcNode|null} parent
* @param {SwcScope} scope
* @param {SwcTraversalContext} traversalContext
* @param {{haltCondition?: (node: SwcNode) => boolean;}} [config]
*/
const prepareTree = (node, parent, scope, traversalContext, { haltCondition } = {}) => {
if (!node?.type) {
return;
}
const { newOrCurScope } = handlePathAndScope(node, parent, scope, false, traversalContext);
loopChildren({ node }, ({ child }) => {
prepareTree(child, node, newOrCurScope, traversalContext, { haltCondition });
});
};
/**
* @param {SwcNode} node
* @param {SwcNode|null} parent
* @param {SwcScope} scope
* @param {{hasPreparedTree?: boolean;}} config
* @param {SwcTraversalContext} traversalContext
*/
const visitTree = (node, parent, scope, config, traversalContext) => {
if (!node?.type || isStopped) {
return;
}
const { hasPreparedTree = false } = config || {};
const { swcPath } = handlePathAndScope(node, parent, scope, hasPreparedTree, traversalContext);
visit(swcPath, visitor, traversalContext);
loopChildren({ node }, ({ child }) => {
visitTree(child, node, swcPath.scope, config, traversalContext);
});
};
const traversalContext = { visitOnExitFns: [], scopeId: 0 };
// https://developer.mozilla.org/en-US/docs/Glossary/Scope
/** @type {SwcScope} */
const initialScope = {
id: traversalContext.scopeId,
bindings: {},
path: null,
_pendingRefsWithoutBinding: [],
_isIsolatedBlockStatement: false,
};
if (needsAdvancedPaths) {
// Do one full traversal to prepare advanced path functionality like path.get() and path.scope.bindings
// TODO: improve with on the fly, partial tree traversal for best performance
prepareTree(swcAst, null, initialScope, traversalContext);
}
visitTree(swcAst, null, initialScope, { hasPreparedTree: needsAdvancedPaths }, traversalContext);
// @ts-ignore
traversalContext.visitOnExitFns.reverse().forEach(fn => fn());
}

View file

@ -13,7 +13,7 @@ import { cli } from '../../src/cli/cli.js';
import { _promptAnalyzerMenuModule } from '../../src/cli/prompt-analyzer-menu.js'; import { _promptAnalyzerMenuModule } from '../../src/cli/prompt-analyzer-menu.js';
import { memoizeConfig } from '../../src/program/utils/memoize.js'; import { memoizeConfig } from '../../src/program/utils/memoize.js';
import { _extendDocsModule } from '../../src/cli/launch-providence-with-extend-docs.js'; import { _extendDocsModule } from '../../src/cli/launch-providence-with-extend-docs.js';
import { dashboardServer } from '../../dashboard/server.js'; import { dashboardServer } from '../../src/dashboard/server.js';
import { setupAnalyzerTest } from '../../test-helpers/setup-analyzer-test.js'; import { setupAnalyzerTest } from '../../test-helpers/setup-analyzer-test.js';
/** /**

View file

@ -4,9 +4,10 @@ import pathLib from 'path';
import sinon from 'sinon'; import sinon from 'sinon';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { expect } from 'chai'; import { expect } from 'chai';
import { it } from 'mocha';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { createTestServer } from '@web/dev-server-core/test-helpers'; import { createTestServer } from '@web/dev-server-core/test-helpers';
import { createDashboardServerConfig } from '../../dashboard/server.js'; import { createDashboardServerConfig } from '../../src/dashboard/server.js';
import { ReportService } from '../../src/program/core/ReportService.js'; import { ReportService } from '../../src/program/core/ReportService.js';
import { providenceConfUtil } from '../../src/program/utils/providence-conf-util.js'; import { providenceConfUtil } from '../../src/program/utils/providence-conf-util.js';
@ -57,7 +58,7 @@ describe('Dashboard Server', () => {
describe('Index', () => { describe('Index', () => {
it(`returns an index on '/'`, async () => { it(`returns an index on '/'`, async () => {
const response = await fetch(`${host}/dashboard`); const response = await fetch(`${host}/src/dashboard`);
const responseText = await response.text(); const responseText = await response.text();
expect(response.status).to.equal(200); expect(response.status).to.equal(200);
expect(responseText).to.include('<title>Providence dashboard</title>'); expect(responseText).to.include('<title>Providence dashboard</title>');
@ -66,7 +67,7 @@ describe('Dashboard Server', () => {
describe('App assets', () => { describe('App assets', () => {
it(`returns (static) js assets via app/*`, async () => { it(`returns (static) js assets via app/*`, async () => {
const response = await fetch(`${host}/dashboard/app/p-board.js`); const response = await fetch(`${host}/src/dashboard/app/p-board.js`);
expect(response.status).to.equal(200); expect(response.status).to.equal(200);
}); });
}); });

View file

@ -0,0 +1,220 @@
{
"meta": {
"searchType": "ast-analyzer",
"analyzerMeta": {
"name": "find-classes",
"requiredAst": "babel",
"identifier": "importing-target-project_0.0.2-target-mock__-905964591",
"targetProject": {
"mainEntry": "./target-src/match-imports/root-level-imports.js",
"name": "importing-target-project",
"version": "0.0.2-target-mock",
"commitHash": "[not-a-git-root]"
},
"configuration": {
"gatherFilesConfig": {},
"skipCheckMatchCompatibility": false,
"addSystemPathsInResult": false
}
}
},
"queryOutput": [
{
"file": "./target-src/find-customelements/multiple.js",
"result": [
{
"name": null,
"isMixin": true,
"superClasses": [
{
"name": "HTMLElement",
"isMixin": false,
"rootFile": {
"file": "[current]",
"specifier": "HTMLElement"
}
}
],
"members": {
"props": [],
"methods": []
}
},
{
"name": "ExtendedOnTheFly",
"isMixin": false,
"superClasses": [
{
"isMixin": true,
"rootFile": {
"file": "[current]"
}
},
{
"isMixin": false,
"rootFile": {
"file": "[current]"
}
}
],
"members": {
"props": [],
"methods": []
}
}
]
},
{
"file": "./target-src/match-subclasses/ExtendedComp.js",
"result": [
{
"name": "ExtendedComp",
"isMixin": false,
"superClasses": [
{
"name": "MyCompMixin",
"isMixin": true,
"rootFile": {
"file": "exporting-ref-project",
"specifier": "[default]"
}
},
{
"name": "RefClass",
"isMixin": false,
"rootFile": {
"file": "exporting-ref-project",
"specifier": "RefClass"
}
}
],
"members": {
"props": [
{
"name": "getterSetter",
"accessType": "public",
"kind": [
"get",
"set"
]
},
{
"name": "staticGetterSetter",
"accessType": "public",
"static": true,
"kind": [
"get",
"set"
]
},
{
"name": "attributes",
"accessType": "public",
"static": true,
"kind": [
"get"
]
},
{
"name": "styles",
"accessType": "public",
"static": true,
"kind": [
"get"
]
},
{
"name": "updateComplete",
"accessType": "public",
"kind": [
"get"
]
},
{
"name": "localizeNamespaces",
"accessType": "public",
"static": true,
"kind": [
"get"
]
},
{
"name": "slots",
"accessType": "public",
"kind": [
"get"
]
}
],
"methods": [
{
"name": "method",
"accessType": "public"
},
{
"name": "_protectedMethod",
"accessType": "protected"
},
{
"name": "__privateMethod",
"accessType": "private"
},
{
"name": "$protectedMethod",
"accessType": "protected"
},
{
"name": "$$privateMethod",
"accessType": "private"
},
{
"name": "constructor",
"accessType": "public"
},
{
"name": "connectedCallback",
"accessType": "public"
},
{
"name": "disconnectedCallback",
"accessType": "public"
},
{
"name": "requestUpdate",
"accessType": "public"
},
{
"name": "createRenderRoot",
"accessType": "public"
},
{
"name": "render",
"accessType": "public"
},
{
"name": "updated",
"accessType": "public"
},
{
"name": "firstUpdated",
"accessType": "public"
},
{
"name": "update",
"accessType": "public"
},
{
"name": "shouldUpdate",
"accessType": "public"
},
{
"name": "onLocaleUpdated",
"accessType": "public"
}
]
}
}
]
}
]
}

View file

@ -0,0 +1,52 @@
{
"meta": {
"searchType": "ast-analyzer",
"analyzerMeta": {
"name": "find-customelements",
"requiredAst": "swc-to-babel",
"identifier": "importing-target-project_0.0.2-target-mock__61665553",
"targetProject": {
"mainEntry": "./target-src/match-imports/root-level-imports.js",
"name": "importing-target-project",
"version": "0.0.2-target-mock",
"commitHash": "[not-a-git-root]"
},
"configuration": {
"gatherFilesConfig": {},
"skipCheckMatchCompatibility": false,
"addSystemPathsInResult": false
}
}
},
"queryOutput": [
{
"file": "./target-src/find-customelements/multiple.js",
"result": [
{
"tagName": "ref-class",
"constructorIdentifier": "RefClass",
"rootFile": {
"file": "exporting-ref-project",
"specifier": "RefClass"
}
},
{
"tagName": "extended-comp",
"constructorIdentifier": "ExtendedComp",
"rootFile": {
"file": "./target-src/match-subclasses/ExtendedComp.js",
"specifier": "ExtendedComp"
}
},
{
"tagName": "on-the-fly",
"constructorIdentifier": "[inline]",
"rootFile": {
"file": "[current]",
"specifier": "[inline]"
}
}
]
}
]
}

View file

@ -0,0 +1,194 @@
{
"meta": {
"searchType": "ast-analyzer",
"analyzerMeta": {
"name": "find-exports",
"requiredAst": "swc-to-babel",
"identifier": "exporting-ref-project_1.0.0__-42206859",
"targetProject": {
"mainEntry": "./index.js",
"name": "exporting-ref-project",
"version": "1.0.0",
"commitHash": "[not-a-git-root]"
},
"configuration": {
"skipFileImports": false,
"gatherFilesConfig": {},
"skipCheckMatchCompatibility": false,
"addSystemPathsInResult": false
}
}
},
"queryOutput": [
{
"file": "./index.js",
"result": [
{
"exportSpecifiers": [
"[default]"
],
"source": "./ref-src/core.js",
"normalizedSource": "./ref-src/core.js",
"rootFileMap": [
{
"currentFileSpecifier": "[default]",
"rootFile": {
"file": "./ref-src/core.js",
"specifier": "[default]"
}
}
]
},
{
"exportSpecifiers": [
"RefClass",
"RefRenamedClass"
],
"localMap": [
{
"local": "RefClass",
"exported": "RefRenamedClass"
}
],
"source": "./ref-src/core.js",
"normalizedSource": "./ref-src/core.js",
"rootFileMap": [
{
"currentFileSpecifier": "RefClass",
"rootFile": {
"file": "./ref-src/core.js",
"specifier": "RefClass"
}
},
{
"currentFileSpecifier": "RefRenamedClass",
"rootFile": {
"file": "./ref-src/core.js",
"specifier": "RefClass"
}
}
]
},
{
"exportSpecifiers": [
"[file]"
],
"rootFileMap": [
null
]
}
]
},
{
"file": "./not-imported.js",
"result": [
{
"exportSpecifiers": [
"notImported"
],
"localMap": [],
"rootFileMap": [
{
"currentFileSpecifier": "notImported",
"rootFile": {
"file": "[current]",
"specifier": "notImported"
}
}
]
},
{
"exportSpecifiers": [
"[file]"
],
"rootFileMap": [
null
]
}
]
},
{
"file": "./ref-component.js",
"result": [
{
"exportSpecifiers": [
"[file]"
],
"rootFileMap": [
null
]
}
]
},
{
"file": "./ref-src/core.js",
"result": [
{
"exportSpecifiers": [
"RefClass"
],
"localMap": [],
"rootFileMap": [
{
"currentFileSpecifier": "RefClass",
"rootFile": {
"file": "[current]",
"specifier": "RefClass"
}
}
]
},
{
"exportSpecifiers": [
"[default]"
],
"rootFileMap": [
{
"currentFileSpecifier": "[default]",
"rootFile": {
"file": "[current]",
"specifier": "[default]"
}
}
]
},
{
"exportSpecifiers": [
"[file]"
],
"rootFileMap": [
null
]
}
]
},
{
"file": "./ref-src/folder/index.js",
"result": [
{
"exportSpecifiers": [
"resolvePathCorrect"
],
"localMap": [],
"rootFileMap": [
{
"currentFileSpecifier": "resolvePathCorrect",
"rootFile": {
"file": "[current]",
"specifier": "resolvePathCorrect"
}
}
]
},
{
"exportSpecifiers": [
"[file]"
],
"rootFileMap": [
null
]
}
]
}
]
}

View file

@ -0,0 +1,204 @@
{
"meta": {
"searchType": "ast-analyzer",
"analyzerMeta": {
"name": "find-imports",
"requiredAst": "swc-to-babel",
"identifier": "importing-target-project_0.0.2-target-mock__349742630",
"targetProject": {
"mainEntry": "./target-src/match-imports/root-level-imports.js",
"name": "importing-target-project",
"version": "0.0.2-target-mock",
"commitHash": "[not-a-git-root]"
},
"configuration": {
"keepInternalSources": false,
"gatherFilesConfig": {},
"skipCheckMatchCompatibility": false,
"addSystemPathsInResult": false
}
}
},
"queryOutput": [
{
"file": "./target-src/find-customelements/multiple.js",
"result": [
{
"importSpecifiers": [
"RefClass"
],
"source": "exporting-ref-project",
"normalizedSource": "exporting-ref-project"
}
]
},
{
"file": "./target-src/find-imports/all-notations.js",
"result": [
{
"importSpecifiers": [
"[file]"
],
"source": "imported/source",
"normalizedSource": "imported/source"
},
{
"importSpecifiers": [
"[default]"
],
"source": "imported/source-a",
"normalizedSource": "imported/source-a"
},
{
"importSpecifiers": [
"b"
],
"source": "imported/source-b",
"normalizedSource": "imported/source-b"
},
{
"importSpecifiers": [
"c",
"d"
],
"source": "imported/source-c",
"normalizedSource": "imported/source-c"
},
{
"importSpecifiers": [
"[default]",
"f",
"g"
],
"source": "imported/source-d",
"normalizedSource": "imported/source-d"
},
{
"importSpecifiers": [
"[default]"
],
"source": "my/source-e",
"normalizedSource": "my/source-e"
},
{
"importSpecifiers": [
"[default]"
],
"source": "[variable]",
"normalizedSource": "[variable]"
},
{
"importSpecifiers": [
"[*]"
],
"source": "imported/source-g",
"normalizedSource": "imported/source-g"
}
]
},
{
"file": "./target-src/match-imports/deep-imports.js",
"result": [
{
"importSpecifiers": [
"RefClass"
],
"source": "exporting-ref-project/ref-src/core.js",
"normalizedSource": "exporting-ref-project/ref-src/core.js"
},
{
"importSpecifiers": [
"[default]"
],
"source": "exporting-ref-project/ref-src/core.js",
"normalizedSource": "exporting-ref-project/ref-src/core.js"
},
{
"importSpecifiers": [
"nonMatched"
],
"source": "unknown-project/xyz.js",
"normalizedSource": "unknown-project/xyz.js"
},
{
"importSpecifiers": [
"[file]"
],
"source": "exporting-ref-project/ref-component",
"normalizedSource": "exporting-ref-project/ref-component"
},
{
"importSpecifiers": [
"resolvePathCorrect"
],
"source": "exporting-ref-project/ref-src/folder",
"normalizedSource": "exporting-ref-project/ref-src/folder"
},
{
"importSpecifiers": [
"[*]"
],
"source": "exporting-ref-project/ref-src/core.js",
"normalizedSource": "exporting-ref-project/ref-src/core.js"
}
]
},
{
"file": "./target-src/match-imports/root-level-imports.js",
"result": [
{
"importSpecifiers": [
"RefClass"
],
"source": "exporting-ref-project",
"normalizedSource": "exporting-ref-project"
},
{
"importSpecifiers": [
"RefRenamedClass"
],
"source": "exporting-ref-project",
"normalizedSource": "exporting-ref-project"
},
{
"importSpecifiers": [
"[default]"
],
"source": "exporting-ref-project",
"normalizedSource": "exporting-ref-project"
},
{
"importSpecifiers": [
"nonMatched"
],
"source": "unknown-project",
"normalizedSource": "unknown-project"
}
]
},
{
"file": "./target-src/match-subclasses/ExtendedComp.js",
"result": [
{
"importSpecifiers": [
"RefClass"
],
"source": "exporting-ref-project",
"normalizedSource": "exporting-ref-project"
}
]
},
{
"file": "./target-src/match-subclasses/internalProxy.js",
"result": [
{
"importSpecifiers": [
"[default]"
],
"source": "exporting-ref-project",
"normalizedSource": "exporting-ref-project"
}
]
}
]
}

View file

@ -0,0 +1,94 @@
{
"meta": {
"searchType": "ast-analyzer",
"analyzerMeta": {
"name": "match-imports",
"requiredAst": "babel",
"identifier": "importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__142861209",
"targetProject": {
"mainEntry": "./target-src/match-imports/root-level-imports.js",
"name": "importing-target-project",
"version": "0.0.2-target-mock",
"commitHash": "[not-a-git-root]"
},
"referenceProject": {
"mainEntry": "./index.js",
"name": "exporting-ref-project",
"version": "1.0.0",
"commitHash": "[not-a-git-root]"
},
"configuration": {
"gatherFilesConfig": {},
"prefix": null,
"skipCheckMatchCompatibility": false,
"addSystemPathsInResult": false
}
}
},
"queryOutput": [
{
"name": "[default]",
"variable": {
"from": "[default]",
"to": "ExtendedComp",
"paths": [
{
"from": "./index.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "./ref-src/core.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "exporting-ref-project/index.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "exporting-ref-project/ref-src/core.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
}
]
}
},
{
"name": "RefClass",
"variable": {
"from": "RefClass",
"to": "ExtendedComp",
"paths": [
{
"from": "./index.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "./ref-src/core.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "exporting-ref-project/index.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "exporting-ref-project/ref-src/core.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
}
]
},
"tag": {
"from": "ref-component",
"to": "extended-comp",
"paths": [
{
"from": "./ref-component.js",
"to": "./target-src/find-customelements/multiple.js"
},
{
"from": "exporting-ref-project/ref-component.js",
"to": "./target-src/find-customelements/multiple.js"
}
]
}
}
]
}

View file

@ -0,0 +1,94 @@
{
"meta": {
"searchType": "ast-analyzer",
"analyzerMeta": {
"name": "match-paths",
"requiredAst": "babel",
"identifier": "importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__142861209",
"targetProject": {
"mainEntry": "./target-src/match-imports/root-level-imports.js",
"name": "importing-target-project",
"version": "0.0.2-target-mock",
"commitHash": "[not-a-git-root]"
},
"referenceProject": {
"mainEntry": "./index.js",
"name": "exporting-ref-project",
"version": "1.0.0",
"commitHash": "[not-a-git-root]"
},
"configuration": {
"gatherFilesConfig": {},
"prefix": null,
"skipCheckMatchCompatibility": false,
"addSystemPathsInResult": false
}
}
},
"queryOutput": [
{
"name": "[default]",
"variable": {
"from": "[default]",
"to": "ExtendedComp",
"paths": [
{
"from": "./index.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "./ref-src/core.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "exporting-ref-project/index.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "exporting-ref-project/ref-src/core.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
}
]
}
},
{
"name": "RefClass",
"variable": {
"from": "RefClass",
"to": "ExtendedComp",
"paths": [
{
"from": "./index.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "./ref-src/core.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "exporting-ref-project/index.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
},
{
"from": "exporting-ref-project/ref-src/core.js",
"to": "./target-src/match-subclasses/ExtendedComp.js"
}
]
},
"tag": {
"from": "ref-component",
"to": "extended-comp",
"paths": [
{
"from": "./ref-component.js",
"to": "./target-src/find-customelements/multiple.js"
},
{
"from": "exporting-ref-project/ref-component.js",
"to": "./target-src/find-customelements/multiple.js"
}
]
}
}
]
}

View file

@ -0,0 +1,67 @@
{
"meta": {
"searchType": "ast-analyzer",
"analyzerMeta": {
"name": "match-subclasses",
"requiredAst": "babel",
"identifier": "importing-target-project_0.0.2-target-mock_+_exporting-ref-project_1.0.0__1982316146",
"targetProject": {
"mainEntry": "./target-src/match-imports/root-level-imports.js",
"name": "importing-target-project",
"version": "0.0.2-target-mock",
"commitHash": "[not-a-git-root]"
},
"referenceProject": {
"mainEntry": "./index.js",
"name": "exporting-ref-project",
"version": "1.0.0",
"commitHash": "[not-a-git-root]"
},
"configuration": {
"gatherFilesConfig": {},
"skipCheckMatchCompatibility": false,
"addSystemPathsInResult": false
}
}
},
"queryOutput": [
{
"exportSpecifier": {
"name": "[default]",
"project": "exporting-ref-project",
"filePath": "./index.js",
"id": "[default]::./index.js::exporting-ref-project"
},
"matchesPerProject": [
{
"project": "importing-target-project",
"files": [
{
"file": "./target-src/match-subclasses/ExtendedComp.js",
"identifier": "ExtendedComp"
}
]
}
]
},
{
"exportSpecifier": {
"name": "RefClass",
"project": "exporting-ref-project",
"filePath": "./index.js",
"id": "RefClass::./index.js::exporting-ref-project"
},
"matchesPerProject": [
{
"project": "importing-target-project",
"files": [
{
"file": "./target-src/match-subclasses/ExtendedComp.js",
"identifier": "ExtendedComp"
}
]
}
]
}
]
}

View file

@ -0,0 +1,330 @@
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

@ -21,7 +21,7 @@ describe('Analyzer "find-exports"', async () => {
}; };
describe('Export notations', () => { describe('Export notations', () => {
it(`supports [export const x = 0] (named specifier)`, async () => { it(`supports "export const x = 0;" (named specifier)`, async () => {
mockProject([`export const x = 0`]); mockProject([`export const x = 0`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstResult = getEntry(queryResults[0]).result[0]; const firstResult = getEntry(queryResults[0]).result[0];
@ -30,7 +30,7 @@ describe('Analyzer "find-exports"', async () => {
expect(firstResult.source).to.be.undefined; expect(firstResult.source).to.be.undefined;
}); });
it(`supports [export default class X {}] (default export)`, async () => { it(`supports "export default class X {};" (default export)`, async () => {
mockProject([`export default class X {}`]); mockProject([`export default class X {}`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstResult = getEntry(queryResults[0]).result[0]; const firstResult = getEntry(queryResults[0]).result[0];
@ -38,16 +38,16 @@ describe('Analyzer "find-exports"', async () => {
expect(firstResult.source).to.be.undefined; expect(firstResult.source).to.be.undefined;
}); });
it(`supports [export default fn(){}] (default export)`, async () => { it(`supports "export default x => x * 3;" (default function export)`, async () => {
mockProject([`export default x => x * 3`]); mockProject([`export default x => x * 3`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstResult = getEntry(queryResults[0]).result[0]; const firstResult = getEntry(queryResults[0]).result[0];
expect(firstResult.exportSpecifiers).to.eql(['[default]']); expect(firstResult.exportSpecifiers).to.eql(['[default]']);
expect(firstResult.source).to.equal(undefined); expect(firstResult.source).to.be.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;',
'./file-with-default-re-export.js': './file-with-default-re-export.js':
@ -82,7 +82,7 @@ describe('Analyzer "find-exports"', async () => {
}); });
}); });
it(`supports [import {x} from 'y'; export default x] (named re-export as default)`, async () => { it(`supports "import {x} from 'y'; export default x;" (named re-export as default)`, async () => {
mockProject([`import {x} from 'y'; export default x;`]); mockProject([`import {x} from 'y'; export default x;`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]); const firstEntry = getEntry(queryResults[0]);
@ -91,7 +91,7 @@ describe('Analyzer "find-exports"', async () => {
expect(firstEntry.result[0].source).to.equal('y'); expect(firstEntry.result[0].source).to.equal('y');
}); });
it(`supports [import x from 'y'; export default x] (default re-export as default)`, async () => { it(`supports "import x from 'y'; export default x" (default re-export as default)`, async () => {
mockProject([`import x from 'y'; export default x;`]); mockProject([`import x from 'y'; export default x;`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]); const firstEntry = getEntry(queryResults[0]);
@ -100,7 +100,7 @@ describe('Analyzer "find-exports"', async () => {
expect(firstEntry.result[0].source).to.equal('y'); 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'`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]); const firstEntry = getEntry(queryResults[0]);
@ -169,7 +169,7 @@ describe('Analyzer "find-exports"', async () => {
]); ]);
}); });
it(`supports [export { x, y } from 'my/source'] (multiple re-exported named specifiers)`, async () => { it(`supports "export { x, y } from 'my/source';" (multiple re-exported named specifiers)`, async () => {
mockProject([`export { x, y } from 'my/source'`]); mockProject([`export { x, y } from 'my/source'`]);
const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]); const firstEntry = getEntry(queryResults[0]);
@ -185,6 +185,7 @@ describe('Analyzer "find-exports"', async () => {
'./index.js': `export { InBetweenComp as MyComp } from './src/inbetween.js'`, './index.js': `export { InBetweenComp as MyComp } from './src/inbetween.js'`,
}); });
const queryResults = await providence(findExportsQueryConfig, _providenceCfg); const queryResults = await providence(findExportsQueryConfig, _providenceCfg);
const firstEntry = getEntry(queryResults[0]); const firstEntry = getEntry(queryResults[0]);
const secondEntry = getEntry(queryResults[0], 1); const secondEntry = getEntry(queryResults[0], 1);
const thirdEntry = getEntry(queryResults[0], 2); const thirdEntry = getEntry(queryResults[0], 2);

View file

@ -1,15 +1,12 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { it } from 'mocha'; import { it } from 'mocha';
import babelTraverse from '@babel/traverse'; import { swcTraverse } from '../../../../src/program/utils/swc-traverse.js';
import { import {
trackDownIdentifier, trackDownIdentifier,
trackDownIdentifierFromScope, trackDownIdentifierFromScope,
} from '../../../../src/program/analyzers/helpers/track-down-identifier.js'; } from '../../../../src/program/analyzers/helpers/track-down-identifier.js';
import { AstService } from '../../../../src/program/core/AstService.js'; import { AstService } from '../../../../src/program/core/AstService.js';
import { import { mockProject } from '../../../../test-helpers/mock-project-helpers.js';
mockProject,
restoreMockedProjects,
} from '../../../../test-helpers/mock-project-helpers.js';
import { setupAnalyzerTest } from '../../../../test-helpers/setup-analyzer-test.js'; import { setupAnalyzerTest } from '../../../../test-helpers/setup-analyzer-test.js';
/** /**
@ -285,9 +282,6 @@ describe('trackdownIdentifier', () => {
specifier: 'IngAccordionInvokerButton', specifier: 'IngAccordionInvokerButton',
}); });
}); });
// TODO: improve perf
describe.skip('Caching', () => {});
}); });
describe('trackDownIdentifierFromScope', () => { describe('trackDownIdentifierFromScope', () => {
@ -299,7 +293,8 @@ describe('trackDownIdentifierFromScope', () => {
}; };
mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' }); mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' });
const ast = AstService._getBabelAst(projectFiles['./src/declarationOfMyClass.js']); // const ast = AstService._getBabelAst(projectFiles['./src/declarationOfMyClass.js']);
const ast = AstService._getSwcAst(projectFiles['./src/declarationOfMyClass.js']);
// Let's say we want to track down 'MyClass' in the code above // Let's say we want to track down 'MyClass' in the code above
const identifierNameInScope = 'MyClass'; const identifierNameInScope = 'MyClass';
@ -308,7 +303,12 @@ describe('trackDownIdentifierFromScope', () => {
/** @type {NodePath} */ /** @type {NodePath} */
let astPath; let astPath;
babelTraverse.default(ast, { // babelTraverse.default(ast, {
// ClassDeclaration(path) {
// astPath = path;
// },
// });
swcTraverse(ast, {
ClassDeclaration(path) { ClassDeclaration(path) {
astPath = path; astPath = path;
}, },
@ -344,7 +344,8 @@ describe('trackDownIdentifierFromScope', () => {
}; };
mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' }); mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' });
const ast = AstService._getBabelAst(projectFiles['./imported.js']); // const ast = AstService._getBabelAst(projectFiles['./imported.js']);
const ast = AstService._getSwcAst(projectFiles['./imported.js']);
// Let's say we want to track down 'MyClass' in the code above // Let's say we want to track down 'MyClass' in the code above
const identifierNameInScope = 'MyClass'; const identifierNameInScope = 'MyClass';
@ -353,7 +354,12 @@ describe('trackDownIdentifierFromScope', () => {
/** @type {NodePath} */ /** @type {NodePath} */
let astPath; let astPath;
babelTraverse.default(ast, { // babelTraverse.default(ast, {
// ImportDeclaration(path) {
// astPath = path;
// },
// });
swcTraverse(ast, {
ImportDeclaration(path) { ImportDeclaration(path) {
astPath = path; astPath = path;
}, },
@ -372,7 +378,7 @@ describe('trackDownIdentifierFromScope', () => {
}); });
}); });
it(`tracks down extended classes from a reexport`, async () => { it(`tracks down extended classes from a re-export`, async () => {
const projectFiles = { const projectFiles = {
'./src/classes.js': ` './src/classes.js': `
export class El1 extends HTMLElement {} export class El1 extends HTMLElement {}
@ -386,7 +392,8 @@ describe('trackDownIdentifierFromScope', () => {
}; };
mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' }); mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' });
const ast = AstService._getBabelAst(projectFiles['./imported.js']); // const ast = AstService._getBabelAst(projectFiles['./imported.js']);
const ast = AstService._getSwcAst(projectFiles['./imported.js']);
// Let's say we want to track down 'MyClass' in the code above // Let's say we want to track down 'MyClass' in the code above
const identifierNameInScope = 'El1'; const identifierNameInScope = 'El1';
@ -395,7 +402,12 @@ describe('trackDownIdentifierFromScope', () => {
/** @type {NodePath} */ /** @type {NodePath} */
let astPath; let astPath;
babelTraverse.default(ast, { // babelTraverse.default(ast, {
// ClassDeclaration(path) {
// astPath = path;
// },
// });
swcTraverse(ast, {
ClassDeclaration(path) { ClassDeclaration(path) {
astPath = path; astPath = path;
}, },

View file

@ -25,9 +25,9 @@ describe('Analyzer', async () => {
}); });
it('has a "requiredAst" string', async () => { it('has a "requiredAst" string', async () => {
expect(typeof dummyAnalyzer.requiredAst).to.equal('string'); expect(typeof dummyAnalyzer.constructor.requiredAst).to.equal('string');
const allowedAsts = ['babel']; const allowedAsts = ['babel'];
expect(allowedAsts).to.include(dummyAnalyzer.requiredAst); expect(allowedAsts).to.include(dummyAnalyzer.constructor.requiredAst);
}); });
it('has a "requiresReference" boolean', async () => { it('has a "requiresReference" boolean', async () => {

View file

@ -88,8 +88,7 @@ describe('getSourceCodeFragmentOfDeclaration', () => {
it('handles class declarations', async () => { it('handles class declarations', async () => {
const fakeFs = { const fakeFs = {
'/my/proj/exports/ajax.js': ` '/my/proj/exports/ajax.js': `
import { AjaxClass as LionAjaxClass } from '../_legacy/ajax/index.js'; import { AjaxClass as LionAjaxClass } from 'some-external-package';
export class AjaxClass extends LionAjaxClass {} export class AjaxClass extends LionAjaxClass {}
`, `,
}; };

View file

@ -0,0 +1,454 @@
import { expect } from 'chai';
import { it } from 'mocha';
// @ts-ignore
import babelTraversePkg from '@babel/traverse';
import { swcTraverse } from '../../../src/program/utils/swc-traverse.js';
import { AstService } from '../../../src/program/core/AstService.js';
/**
* @typedef {import('@swc/core').Module} SwcAstModule
* @typedef {import('../../../types/index.js').SwcPath} SwcPath
* @typedef {import('../../../types/index.js').SwcScope} SwcScope
*/
/**
* @param {SwcAstModule} swcAst
*/
function gatherAllScopes(swcAst) {
/** @type {SwcScope[]} */
const swcScopes = [];
swcTraverse(swcAst, {
enter({ scope }) {
if (!swcScopes.includes(scope)) {
swcScopes.push(scope);
}
},
});
return swcScopes;
}
describe('swcTraverse', () => {
describe('Visitor', () => {
it('traverses an swc AST based on <Node.type> visitor', async () => {
const code = `import x from 'y';`;
const swcAst = await AstService._getSwcAst(code);
let foundImportDeclarationPath;
const visitor = {
ImportDeclaration(/** @type {SwcPath} */ path) {
foundImportDeclarationPath = path;
},
};
swcTraverse(swcAst, visitor);
expect(foundImportDeclarationPath).to.not.be.undefined;
});
it('supports "enter" as a generic arrival handler', async () => {
const code = `import x from 'y';`;
const swcAst = await AstService._getSwcAst(code);
/** @type {string[]} */
const foundTypes = [];
const visitor = {
/**
* @param {any} path
*/
enter(path) {
foundTypes.push(path.node.type);
},
};
swcTraverse(swcAst, visitor);
expect(foundTypes).to.eql([
'Module',
'ImportDeclaration',
'ImportDefaultSpecifier',
'Identifier',
'StringLiteral',
]);
});
it('supports "enter" and "exit" as generic handlers inside <Node.type> handlers', async () => {
const code = `import x from 'y';`;
const swcAst = await AstService._getSwcAst(code);
/** @type {string[]} */
const visitedPaths = [];
const visitor = {
/**
* @param {any} path
*/
ImportDeclaration: {
enter(path) {
visitedPaths.push({ path, phase: 'enter' });
},
exit(path) {
visitedPaths.push({ path, phase: 'exit' });
},
},
};
swcTraverse(swcAst, visitor);
expect(visitedPaths[0].path).to.equal(visitedPaths[1].path);
expect(visitedPaths[0].phase).to.equal('enter');
expect(visitedPaths[1].phase).to.equal('exit');
});
it('supports "root" as alternative for Program', async () => {
const code = `import x from 'y';`;
const swcAst = await AstService._getSwcAst(code);
let rootPath;
const visitor = {
/**
* @param {any} path
*/
root(path) {
rootPath = path;
},
};
swcTraverse(swcAst, visitor);
// TODO: also add case for Script
expect(rootPath.node.type).to.equal('Module');
});
});
describe.skip('Paths', () => {
it(`adds {
node: SwcNode;
parent: SwcNode;
stop: function;
scope: SwcScope;
parentPath: SwcPath;
}`, async () => {});
it('supports getPathFromNode', async () => {});
});
describe('Scopes', () => {
describe('Lexical scoping', () => {
it('creates scopes for blocks', async () => {
const code = `
const globalScope = 0;
{
const middleScope = 1;
{
const deepestScope = 2;
}
}
const alsoGlobalScope = 3;
`;
const swcAst = await AstService._getSwcAst(code);
/** @type {SwcPath[]} */
const declaratorPaths = [];
const visitor = {
/**
* @param {any} path
*/
VariableDeclarator(path) {
declaratorPaths.push(path);
},
};
swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
expect(declaratorPaths[0].scope.id).to.equal(0);
expect(declaratorPaths[1].scope.id).to.equal(1);
expect(declaratorPaths[2].scope.id).to.equal(2);
expect(declaratorPaths[0].node.id.value).to.equal('globalScope');
expect(Object.keys(declaratorPaths[0].scope.bindings)).to.eql([
'globalScope',
'alsoGlobalScope',
]);
// 0 and 3 are the same scope
expect(declaratorPaths[0].scope).to.equal(declaratorPaths[3].scope);
// Scope bindings refer to Declarator nodes
expect(declaratorPaths[0].scope.bindings.globalScope.identifier).to.equal(
declaratorPaths[0].node,
);
expect(declaratorPaths[0].scope.bindings.alsoGlobalScope.identifier).to.equal(
declaratorPaths[3].node,
);
expect(Object.keys(declaratorPaths[1].scope.bindings)).to.eql(['middleScope']);
expect(Object.keys(declaratorPaths[2].scope.bindings)).to.eql(['deepestScope']);
});
it('creates scopes for nested FunctionDeclaration', async () => {
const code = `
function globalFn() {
function middleScope() {
function deepestScope() {
}
}
}
`;
const swcAst = await AstService._getSwcAst(code);
/** @type {SwcPath[]} */
const declaratorPaths = [];
const visitor = {
FunctionDeclaration(/** @type {any} */ path) {
declaratorPaths.push(path);
},
};
swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
const scopes = gatherAllScopes(swcAst);
expect(scopes[1].path?.node).to.equal(declaratorPaths[0].node);
expect(scopes[2].path?.node).to.equal(declaratorPaths[1].node);
expect(scopes[3].path?.node).to.equal(declaratorPaths[2].node);
});
it('creates scopes for ClassDeclaration', async () => {
const code = `
class X extends HTMLElement {
constructor() {
var x = 1;
}
method() {
window.alert('hi');
}
}
`;
const swcAst = await AstService._getSwcAst(code);
/** @type {SwcPath[]} */
const declaratorPaths = [];
const visitor = {
VariableDeclarator(/** @type {any} */ path) {
declaratorPaths.push(path);
},
};
swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
expect(declaratorPaths[0].scope.id).to.equal(2);
});
it('creates scopes SwitchStatement', async () => {
const code = `
const myCases = { a: true };
switch (myCases) {
case myCases.a:
const x = 1;
break;
default:
}`;
const swcAst = await AstService._getSwcAst(code);
/** @type {SwcPath[]} */
const declaratorPaths = [];
const visitor = {
VariableDeclarator(/** @type {any} */ path) {
declaratorPaths.push(path);
},
};
swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
expect(declaratorPaths[0].node.id.value).to.equal('myCases');
expect(declaratorPaths[1].node.id.value).to.equal('x');
expect(declaratorPaths[0].scope.id).to.equal(0);
expect(declaratorPaths[1].scope.id).to.equal(1);
});
it('creates scopes for ObjectExpression', async () => {
const code = `
export default {
toString(dateObj, opt = {}) {},
};
`;
const swcAst = await AstService._getSwcAst(code);
/** @type {SwcPath[]} */
const results = [];
const visitor = {
MethodProperty(/** @type {any} */ path) {
results.push(path);
},
};
swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
expect(results[0].node.key.value).to.equal('toString');
expect(results[0].scope.id).to.equal(0);
});
it('works for KeyValueProperty', async () => {
const code = `
export const x = {
y:() => {
const z = 1;
},
};
`;
const swcAst = await AstService._getSwcAst(code);
/** @type {SwcPath[]} */
const declaratorPaths = [];
const visitor = {
VariableDeclarator(/** @type {any} */ path) {
declaratorPaths.push(path);
},
};
swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
expect(declaratorPaths[0].node.id.value).to.equal('x');
expect(declaratorPaths[1].node.id.value).to.equal('z');
expect(declaratorPaths[0].scope.id).to.equal(0);
expect(declaratorPaths[1].scope.id).to.equal(1);
});
});
describe('Bindings', () => {
it('binds const and lets to block scope', async () => {
const code = `
const globalScope = 0;
{
let middleScope = 1;
{
const deepestScope = 2;
}
}
let alsoGlobalScope = 3;
`;
const swcAst = await AstService._getSwcAst(code);
/** @type {SwcPath[]} */
const declaratorPaths = [];
const visitor = {
VariableDeclarator(/** @type {SwcPath} */ path) {
declaratorPaths.push(path);
},
FunctionDeclaration(/** @type {SwcPath} */ path) {
declaratorPaths.push(path);
},
};
swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
expect(Object.keys(declaratorPaths[0].scope.bindings)).to.eql([
'globalScope',
'alsoGlobalScope',
]);
// Scope bindings refer to Declarator nodes
expect(declaratorPaths[0].scope.bindings.globalScope.identifier).to.equal(
declaratorPaths[0].node,
);
expect(declaratorPaths[0].scope.bindings.alsoGlobalScope.identifier).to.equal(
declaratorPaths[3].node,
);
});
it('binds vars to function scope', async () => {
const code = `
var globalScope = 0;
{
var stillGlobalScope = 1;
function middleScope() {
var insideFnScope = 2;
}
}
`;
const swcAst = await AstService._getSwcAst(code);
/** @type {SwcPath[]} */
const declaratorPaths = [];
const visitor = {
VariableDeclarator(/** @type {SwcPath} */ path) {
declaratorPaths.push(path);
},
};
swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
expect(Object.keys(declaratorPaths[0].scope.bindings)).to.eql([
'globalScope',
'stillGlobalScope',
]);
expect(Object.keys(declaratorPaths[1].scope.bindings)).to.eql(['middleScope']);
expect(Object.keys(declaratorPaths[2].scope.bindings)).to.eql(['insideFnScope']);
});
});
describe.skip('References', () => {});
});
describe('Babel compatibility', () => {
const babelTraverse = babelTraversePkg.default;
/**
* @param {string} code
*/
async function compareScopeResultsWithBabel(code) {
const swcAst = await AstService._getSwcAst(code);
const babelAst = await AstService._getBabelAst(code);
/**
* @type {any[]}
*/
const babelScopes = [];
babelTraverse(babelAst, {
// @ts-ignore
enter({ scope }) {
if (!babelScopes.includes(scope)) {
babelScopes.push(scope);
}
},
});
/** @type {SwcScope[]} */
const swcScopes = [];
swcTraverse(swcAst, {
enter({ scope }) {
if (!swcScopes.includes(scope)) {
swcScopes.push(scope);
}
},
});
const babelRootScopeIdOffset = babelScopes[0].uid;
expect(babelScopes.length).to.equal(swcScopes.length);
for (let i = 0; i < babelScopes.length; i += 1) {
expect(babelScopes[i].uid - babelRootScopeIdOffset).to.equal(swcScopes[i].id);
expect(Object.keys(babelScopes[i].bindings)).to.eql(Object.keys(swcScopes[i].bindings));
// expect(babelScopes[i].references).to.eql(swcResults[i].references);
}
}
it('handles all kinds of lexical scopes and bindings in a similar way', async () => {
const code = `
const globalScope = 0;
function fn() {
let middleScope = 2;
function fn() {
var parentScope = 3;
}
}
const alsoGlobalScope = 4;
{
const myCases = { a: true };
{
switch (myCases) {
case myCases.a:
const x = 1;
break;
default:
};
}
}
class Q {
constructor() {
}
}
`;
await compareScopeResultsWithBabel(code);
});
});
});

View file

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

View file

@ -3,7 +3,7 @@ import {
PathFromSystemRoot, PathFromSystemRoot,
QueryType, QueryType,
QueryResult, QueryResult,
RequiredAst, AnalyzerAst,
ImportOrExportId, ImportOrExportId,
Project, Project,
GatherFilesConfig, GatherFilesConfig,
@ -16,6 +16,8 @@ import {
*/ */
export type AnalyzerName = `${'find' | 'match'}-${string}` | ''; export type AnalyzerName = `${'find' | 'match'}-${string}` | '';
export type AnalyzerAst = 'babel' | 'swc-to-babel' | 'swc';
// TODO: make sure that data structures of JSON output (generated in ReportService) // TODO: make sure that data structures of JSON output (generated in ReportService)
// and data structure generated in Analyzer.prototype._finalize match exactly (move logic from ReportSerivce to _finalize) // and data structure generated in Analyzer.prototype._finalize match exactly (move logic from ReportSerivce to _finalize)
// so that these type definitions can be used to generate a json schema: https://www.npmjs.com/package/typescript-json-schema // so that these type definitions can be used to generate a json schema: https://www.npmjs.com/package/typescript-json-schema
@ -27,7 +29,7 @@ export interface Meta {
export interface AnalyzerMeta { export interface AnalyzerMeta {
name: AnalyzerName; name: AnalyzerName;
requiredAst: RequiredAst; requiredAst: AnalyzerAst;
/* a unique hash based on target, reference and configuration */ /* a unique hash based on target, reference and configuration */
identifier: ImportOrExportId; identifier: ImportOrExportId;
/* target project meta object */ /* target project meta object */

View file

@ -53,11 +53,6 @@ export type RootFile = {
specifier: SpecifierName; specifier: SpecifierName;
}; };
/**
* Required ast for the analysis. Currently, only Babel is supported
*/
export type RequiredAst = 'babel';
/** /**
* Name entry found in package.json * Name entry found in package.json
*/ */
@ -186,4 +181,9 @@ export type PackageJson = {
dependencies?: { [dependency: string]: string }; dependencies?: { [dependency: string]: string };
devDependencies?: { [dependency: string]: string }; devDependencies?: { [dependency: string]: string };
workspaces?: string[]; workspaces?: string[];
main?: string;
};
export type LernaJson = {
packages: string[];
}; };

View file

@ -1,3 +1,4 @@
export * from './core/index.js'; export * from './core/index.js';
export * from './analyzers/index.js'; export * from './analyzers/index.js';
export * from './misc.js'; export * from './misc.js';
export * from './utils/index.js';

View file

@ -0,0 +1,31 @@
export type SwcScope = {
id: number;
parentScope?: Scope;
bindings: { [key: string]: Binding };
path: SwcPath | null;
_pendingRefsWithoutBinding: SwcNode[];
_isIsolatedBlockStatement: boolean;
};
/* Binding https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#toc-bindings */
export type SwcBinding = {
identifier: SwcNode;
// kind: string;
refs: SwcNode[];
path: SwcPath;
};
export type SwcPath = {
node: SwcNode;
parent: SwcNode;
stop: function;
scope: SwcScope;
parentPath: SwcPath;
};
type SwcVisitorFn = (swcPath: SwcPath) => void;
export type SwcVisitor = {
[key: string]: SwcVisitorFn | { enter?: SwcVisitorFn; leave?: SwcVisitorFn };
};
export type SwcTraversalContext = { visitOnExitFns: (() => void)[]; scopeId: number };