feat(providence-analytics): support import assertions

This commit is contained in:
Thijs Louisse 2022-09-27 14:32:55 +02:00 committed by Thomas Allmer
parent 1f87043e82
commit 8890cc0dea
8 changed files with 101 additions and 14 deletions

View file

@ -33,6 +33,7 @@
"@babel/parser": "^7.5.5", "@babel/parser": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-syntax-export-default-from": "^7.18.6", "@babel/plugin-syntax-export-default-from": "^7.18.6",
"@babel/plugin-syntax-import-assertions": "^7.18.6",
"@babel/register": "^7.5.5", "@babel/register": "^7.5.5",
"@babel/traverse": "^7.5.5", "@babel/traverse": "^7.5.5",
"@babel/types": "^7.9.0", "@babel/types": "^7.9.0",

View file

@ -170,7 +170,11 @@ function findExportsPerAstEntry(ast, { skipFileImports }) {
const exportSpecifiers = getExportSpecifiers(path.node); const exportSpecifiers = getExportSpecifiers(path.node);
const localMap = getLocalNameSpecifiers(path.node); const localMap = getLocalNameSpecifiers(path.node);
const source = path.node.source?.value; const source = path.node.source?.value;
transformedEntry.push({ exportSpecifiers, localMap, source, __tmp: { path } }); const entry = { exportSpecifiers, localMap, source, __tmp: { path } };
if (path.node.assertions?.length) {
entry.assertionType = path.node.assertions[0].value?.value;
}
transformedEntry.push(entry);
}, },
ExportDefaultDeclaration(defaultExportPath) { ExportDefaultDeclaration(defaultExportPath) {
const exportSpecifiers = ['[default]']; const exportSpecifiers = ['[default]'];

View file

@ -60,11 +60,15 @@ function findImportsPerAstEntry(ast) {
traverse(ast, { traverse(ast, {
ImportDeclaration(path) { ImportDeclaration(path) {
const importSpecifiers = getImportOrReexportsSpecifiers(path.node); const importSpecifiers = getImportOrReexportsSpecifiers(path.node);
if (importSpecifiers.length === 0) { 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 = path.node.source.value;
transformedEntry.push({ importSpecifiers, source }); const entry = { importSpecifiers, source };
if (path.node.assertions?.length) {
entry.assertionType = path.node.assertions[0].value?.value;
}
transformedEntry.push(entry);
}, },
// Dynamic imports // Dynamic imports
CallExpression(path) { CallExpression(path) {
@ -86,7 +90,11 @@ function findImportsPerAstEntry(ast) {
} }
const importSpecifiers = getImportOrReexportsSpecifiers(path.node); const importSpecifiers = getImportOrReexportsSpecifiers(path.node);
const source = path.node.source.value; const source = path.node.source.value;
transformedEntry.push({ importSpecifiers, source }); const entry = { importSpecifiers, source };
if (path.node.assertions?.length) {
entry.assertionType = path.node.assertions[0].value?.value;
}
transformedEntry.push(entry);
}, },
// ExportAllDeclaration(path) { // ExportAllDeclaration(path) {
// if (!path.node.source) { // if (!path.node.source) {
@ -97,6 +105,7 @@ function findImportsPerAstEntry(ast) {
// transformedEntry.push({ importSpecifiers, source }); // transformedEntry.push({ importSpecifiers, source });
// }, // },
}); });
return transformedEntry; return transformedEntry;
} }
@ -140,15 +149,16 @@ class FindImportsAnalyzer extends Analyzer {
const queryOutput = await this._traverse(async (ast, { relativePath }) => { const queryOutput = await this._traverse(async (ast, { relativePath }) => {
let transformedEntry = findImportsPerAstEntry(ast); let transformedEntry = findImportsPerAstEntry(ast);
// Post processing based on configuration... // Post processing based on configuration...
transformedEntry = await normalizeSourcePaths( transformedEntry = await normalizeSourcePaths(
transformedEntry, transformedEntry,
relativePath, relativePath,
cfg.targetProjectPath, cfg.targetProjectPath,
); );
if (!cfg.keepInternalSources) { if (!cfg.keepInternalSources) {
transformedEntry = options.onlyExternalSources(transformedEntry); transformedEntry = options.onlyExternalSources(transformedEntry);
} }
return { result: transformedEntry }; return { result: transformedEntry };
}); });

View file

@ -1,4 +1,5 @@
const fs = require('fs'); const fs = require('fs');
const pathLib = require('path');
const { default: traverse } = require('@babel/traverse'); const { default: traverse } = require('@babel/traverse');
const { const {
isRelativeSourcePath, isRelativeSourcePath,
@ -157,7 +158,16 @@ async function trackDownIdentifierFn(
* @type {PathFromSystemRoot} * @type {PathFromSystemRoot}
*/ */
const resolvedSourcePath = await resolveImportPath(source, currentFilePath); const resolvedSourcePath = await resolveImportPath(source, currentFilePath);
LogService.debug(`[trackDownIdentifier] ${resolvedSourcePath}`); 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 code = fs.readFileSync(resolvedSourcePath, 'utf8');
const ast = AstService.getAst(code, 'babel', { filePath: resolvedSourcePath }); const ast = AstService.getAst(code, 'babel', { filePath: resolvedSourcePath });
const shouldLookForDefaultExport = identifierName === '[default]'; const shouldLookForDefaultExport = identifierName === '[default]';

View file

@ -61,7 +61,13 @@ class AstService {
static _getBabelAst(code) { static _getBabelAst(code) {
const ast = babelParser.parse(code, { const ast = babelParser.parse(code, {
sourceType: 'module', sourceType: 'module',
plugins: ['importMeta', 'dynamicImport', 'classProperties', 'exportDefaultFrom'], plugins: [
'importMeta',
'dynamicImport',
'classProperties',
'exportDefaultFrom',
'importAssertions',
],
}); });
return ast; return ast;
} }

View file

@ -155,6 +155,46 @@ describe('Analyzer "find-exports"', () => {
expect(firstEntry.result[0].source).to.equal('my/source'); 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 from './styles.css' assert { type: "css" };`,
});
await providence(findExportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
const firstEntry = getEntry(queryResult);
expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('styles');
expect(firstEntry.result[0].source).to.equal('./styles.css');
expect(firstEntry.result[0].rootFileMap[0]).to.eql({
currentFileSpecifier: 'styles',
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;`,
});
await providence(findExportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
const firstEntry = getEntry(queryResult);
expect(firstEntry.result[0].exportSpecifiers.length).to.equal(1);
expect(firstEntry.result[0].exportSpecifiers[0]).to.equal('[default]');
expect(firstEntry.result[0].source).to.equal('./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 () => { it(`stores meta info(local name) of renamed specifiers`, async () => {
mockProject([`export { x as y } from 'my/source'`]); mockProject([`export { x as y } from 'my/source'`]);
await providence(findExportsQueryConfig, _providenceCfg); await providence(findExportsQueryConfig, _providenceCfg);

View file

@ -168,6 +168,7 @@ describe('Analyzer "find-imports"', () => {
expect(firstEntry.result[0].source).to.equal('my/source'); expect(firstEntry.result[0].source).to.equal('my/source');
}); });
// TODO: we can track [variable] down via trackdownId + getSourceCodeFragmentOfDeclaration
it(`supports [import(pathReference)] (dynamic imports with variable source)`, async () => { it(`supports [import(pathReference)] (dynamic imports with variable source)`, async () => {
mockProject([ mockProject([
` `
@ -183,6 +184,29 @@ describe('Analyzer "find-imports"', () => {
expect(firstEntry.result[0].source).to.equal('[variable]'); expect(firstEntry.result[0].source).to.equal('[variable]');
}); });
// import styles from "./styles.css" assert { type: "css" };
it(`supports [import styles from "@css/lib/styles.css" assert { type: "css" }] (import assertions)`, async () => {
mockProject([`import styles from "@css/lib/styles.css" assert { type: "css" };`]);
await providence(findImportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
const firstEntry = getEntry(queryResult);
console.log({ firstEntry });
expect(firstEntry.result[0].importSpecifiers[0]).to.equal('[default]');
expect(firstEntry.result[0].source).to.equal('@css/lib/styles.css');
expect(firstEntry.result[0].assertionType).to.equal('css');
});
it(`supports [export styles from "@css/lib/styles.css" assert { type: "css" }] (import assertions)`, async () => {
mockProject([`export styles from "@css/lib/styles.css" assert { type: "css" };`]);
await providence(findImportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
const firstEntry = getEntry(queryResult);
console.log({ firstEntry });
expect(firstEntry.result[0].importSpecifiers[0]).to.equal('[default]');
expect(firstEntry.result[0].source).to.equal('@css/lib/styles.css');
expect(firstEntry.result[0].assertionType).to.equal('css');
});
describe('Filter out false positives', () => { describe('Filter out false positives', () => {
it(`doesn't support [object.import('my/source')] (import method members)`, async () => { it(`doesn't support [object.import('my/source')] (import method members)`, async () => {
mockProject([`object.import('my/source')`]); mockProject([`object.import('my/source')`]);
@ -336,11 +360,4 @@ describe('Analyzer "find-imports"', () => {
]); ]);
}); });
}); });
// TODO: put this in the generic providence/analyzer part
describe.skip('With <script type="module"> inside .html', () => {
it('gets the source from script tags', async () => {});
it('gets the content from script tags', async () => {});
});
}); });

View file

@ -137,7 +137,6 @@ describe('trackdownIdentifier', () => {
}); });
}); });
// TODO: support import maps
it(`identifies import map entries as internal sources`, async () => { it(`identifies import map entries as internal sources`, async () => {
mockProject( mockProject(
{ {