diff --git a/src/script-handlers/extendsHandler.ts b/src/script-handlers/extendsHandler.ts index 5ba1618..40c451d 100644 --- a/src/script-handlers/extendsHandler.ts +++ b/src/script-handlers/extendsHandler.ts @@ -3,8 +3,8 @@ import { NodePath } from 'ast-types' import * as path from 'path' import { Documentation } from '../Documentation' import { parseFile, ParseOptions } from '../parse' -import resolveAliases from '../utils/resolveAliases' -import resolvePathFrom from '../utils/resolvePathFrom' +import resolveImmediatelyExportedRequire from '../utils/adaptExportsToIEV' +import makePathResolver from '../utils/makePathResolver' import resolveRequired from '../utils/resolveRequired' /** @@ -31,12 +31,13 @@ export default function extendsHandler( const originalDirName = path.dirname(opt.filePath) + const pathResolver = makePathResolver(originalDirName, opt.aliases) + + resolveImmediatelyExportedRequire(pathResolver, extendsFilePath) + // only look for documentation in the current project not in node_modules if (/^\./.test(extendsFilePath[extendsVariableName].filePath)) { - const fullFilePath = resolvePathFrom( - resolveAliases(extendsFilePath[extendsVariableName].filePath, opt.aliases || {}), - originalDirName, - ) + const fullFilePath = pathResolver(extendsFilePath[extendsVariableName].filePath) parseFile(documentation, { ...opt, diff --git a/src/script-handlers/mixinsHandler.ts b/src/script-handlers/mixinsHandler.ts index 57889f7..ceb2462 100644 --- a/src/script-handlers/mixinsHandler.ts +++ b/src/script-handlers/mixinsHandler.ts @@ -4,8 +4,8 @@ import * as path from 'path' import Map from 'ts-map' import { Documentation } from '../Documentation' import { parseFile, ParseOptions } from '../parse' -import resolveAliases from '../utils/resolveAliases' -import resolvePathFrom from '../utils/resolvePathFrom' +import resolveImmediatelyExportedRequire from '../utils/adaptExportsToIEV' +import makePathResolver from '../utils/makePathResolver' import resolveRequired from '../utils/resolveRequired' /** @@ -21,6 +21,8 @@ export default function mixinsHandler( ) { const originalDirName = path.dirname(opt.filePath) + const pathResolver = makePathResolver(originalDirName, opt.aliases) + // filter only mixins const mixinVariableNames = getMixinsVariableNames(componentDefinition) @@ -31,14 +33,13 @@ export default function mixinsHandler( // get all require / import statements const mixinVarToFilePath = resolveRequired(astPath, mixinVariableNames) + resolveImmediatelyExportedRequire(pathResolver, mixinVarToFilePath) + // get each doc for each mixin using parse const files = new Map() for (const varName of Object.keys(mixinVarToFilePath)) { const { filePath, exportName } = mixinVarToFilePath[varName] - const fullFilePath = resolvePathFrom( - resolveAliases(filePath, opt.aliases || {}), - originalDirName, - ) + const fullFilePath = pathResolver(filePath) const vars = files.get(fullFilePath) || [] vars.push(exportName) files.set(fullFilePath, vars) diff --git a/src/utils/__tests__/adaptExportsToIEV.ts b/src/utils/__tests__/adaptExportsToIEV.ts new file mode 100644 index 0000000..1b7cda8 --- /dev/null +++ b/src/utils/__tests__/adaptExportsToIEV.ts @@ -0,0 +1,19 @@ +import adaptRequireWithIEV from '../adaptExportsToIEV' +import { ImportedVariableSet } from '../resolveRequired' + +jest.mock('../resolveImmediatelyExported') + +describe('adaptRequireWithIEV', () => { + let set: ImportedVariableSet + let mockResolver: jest.Mock + beforeEach(() => { + set = { test: { filePath: 'my/path', exportName: 'exportIt' } } + mockResolver = jest.fn() + }) + + it('should call the resolver', () => { + adaptRequireWithIEV(mockResolver, set) + + expect(mockResolver).toHaveBeenCalledWith('my/path') + }) +}) diff --git a/src/utils/__tests__/resolveImmediatelyExported.ts b/src/utils/__tests__/resolveImmediatelyExported.ts new file mode 100644 index 0000000..2381a1f --- /dev/null +++ b/src/utils/__tests__/resolveImmediatelyExported.ts @@ -0,0 +1,53 @@ +import babylon from '../../babel-parser' +import resolveImmediatelyExported from '../resolveImmediatelyExported' + +describe('resolveImmediatelyExported', () => { + it('should immediately exported varibles', () => { + const ast = babylon().parse('export { test } from "test/path";') + const varNames = resolveImmediatelyExported(ast, ['test']) + expect(varNames).toMatchObject({ + test: { filePath: 'test/path', exportName: 'test' }, + }) + }) + + it('should immediately exported varibles with aliases', () => { + const ast = babylon().parse('export { test as changedName } from "test/path";') + const varNames = resolveImmediatelyExported(ast, ['changedName']) + expect(varNames).toMatchObject({ + changedName: { filePath: 'test/path', exportName: 'test' }, + }) + }) + + it('should resolve immediately exported varibles in two steps', () => { + const ast = babylon().parse( + [ + 'import { test as middleName } from "test/path";', + 'export { middleName as changedName };', + ].join('\n'), + ) + const varNames = resolveImmediatelyExported(ast, ['changedName']) + expect(varNames).toMatchObject({ + changedName: { filePath: 'test/path', exportName: 'test' }, + }) + }) + + it('should return immediately exported varibles in two steps with default import', () => { + const ast = babylon().parse( + ['import test from "test/path";', 'export { test as changedName };'].join('\n'), + ) + const varNames = resolveImmediatelyExported(ast, ['changedName']) + expect(varNames).toMatchObject({ + changedName: { filePath: 'test/path', exportName: 'default' }, + }) + }) + + it('should return immediately exported varibles in two steps with default export', () => { + const ast = babylon().parse( + ['import { test } from "test/path";', 'export default test;'].join('\n'), + ) + const varNames = resolveImmediatelyExported(ast, ['default']) + expect(varNames).toMatchObject({ + default: { filePath: 'test/path', exportName: 'test' }, + }) + }) +}) diff --git a/src/utils/adaptExportsToIEV.ts b/src/utils/adaptExportsToIEV.ts new file mode 100644 index 0000000..c5e53f0 --- /dev/null +++ b/src/utils/adaptExportsToIEV.ts @@ -0,0 +1,58 @@ +import * as fs from 'fs' +import * as path from 'path' +import Map from 'ts-map' +import buildParser from '../babel-parser' +import cacher from './cacher' +import resolveImmediatelyExported from './resolveImmediatelyExported' +import { ImportedVariableSet } from './resolveRequired' + +// tslint:disable-next-line:no-var-requires +import recast = require('recast') + +export default function adaptExportsToIEV( + pathResolver: (path: string, originalDirNameOverride?: string) => string, + varToFilePath: ImportedVariableSet, +) { + // key: filepath, content: {key: localName, content: exportedName} + const filePathToVars = new Map>() + Object.keys(varToFilePath).forEach(k => { + const exportedVariable = varToFilePath[k] + const exportToLocalMap = + filePathToVars.get(exportedVariable.filePath) || new Map() + exportToLocalMap.set(k, exportedVariable.exportName) + filePathToVars.set(exportedVariable.filePath, exportToLocalMap) + }) + + filePathToVars.forEach((exportToLocal, filePath) => { + if (filePath && exportToLocal) { + const exportedVariableNames: string[] = [] + exportToLocal.forEach(exportedName => { + if (exportedName) { + exportedVariableNames.push(exportedName) + } + }) + try { + const fullFilePath = pathResolver(filePath) + const source = fs.readFileSync(fullFilePath, { + encoding: 'utf-8', + }) + const astRemote = cacher(() => recast.parse(source, { parser: buildParser() }), source) + const returnedVariables = resolveImmediatelyExported(astRemote, exportedVariableNames) + exportToLocal.forEach((exported, local) => { + if (exported && local) { + const aliasedVariable = returnedVariables[exported] + if (aliasedVariable) { + aliasedVariable.filePath = pathResolver( + aliasedVariable.filePath, + path.dirname(fullFilePath), + ) + varToFilePath[local] = aliasedVariable + } + } + }) + } catch (e) { + // ignore load errors + } + } + }) +} diff --git a/src/utils/makePathResolver.ts b/src/utils/makePathResolver.ts new file mode 100644 index 0000000..41592a6 --- /dev/null +++ b/src/utils/makePathResolver.ts @@ -0,0 +1,10 @@ +import resolveAliases from '../utils/resolveAliases' +import resolvePathFrom from '../utils/resolvePathFrom' + +export default function makePathResolver( + refDirName: string, + aliases?: { [alias: string]: string }, +): (filePath: string, originalDirNameOverride?: string) => string { + return (filePath: string, originalDirNameOverride?: string): string => + resolvePathFrom(resolveAliases(filePath, aliases || {}), originalDirNameOverride || refDirName) +} diff --git a/src/utils/resolveImmediatelyExported.ts b/src/utils/resolveImmediatelyExported.ts new file mode 100644 index 0000000..2d0b67e --- /dev/null +++ b/src/utils/resolveImmediatelyExported.ts @@ -0,0 +1,73 @@ +import * as bt from '@babel/types' +import { NodePath } from 'ast-types' +import { ImportedVariableSet } from './resolveRequired' + +// tslint:disable-next-line:no-var-requires +import recast = require('recast') + +export default function(ast: bt.File, variableFilter: string[]): ImportedVariableSet { + const variables: ImportedVariableSet = {} + + const importedVariablePaths: ImportedVariableSet = {} + + // get imported variable names and filepath + recast.visit(ast.program, { + visitImportDeclaration(astPath: NodePath) { + if (!astPath.node.source) { + return false + } + const filePath = astPath.node.source.value + + const specifiers = astPath.get('specifiers') + specifiers.each((s: NodePath) => { + const varName = s.node.local.name + const exportName = bt.isImportSpecifier(s.node) ? s.node.imported.name : 'default' + importedVariablePaths[varName] = { filePath, exportName } + }) + return false + }, + }) + + recast.visit(ast.program, { + visitExportNamedDeclaration(astPath: NodePath) { + const specifiers = astPath.get('specifiers') + if (astPath.node.source) { + const filePath = astPath.node.source.value + + specifiers.each((s: NodePath) => { + const varName = s.node.exported.name + const exportName = s.node.local.name + if (variableFilter.indexOf(varName) > -1) { + variables[varName] = { filePath, exportName } + } + }) + } else { + specifiers.each((s: NodePath) => { + const varName = s.node.exported.name + const middleName = s.node.local.name + const importedVar = importedVariablePaths[middleName] + if (importedVar && variableFilter.indexOf(varName) > -1) { + variables[varName] = importedVar + } + }) + } + + return false + }, + visitExportDefaultDeclaration(astPath: NodePath) { + if (variableFilter.indexOf('default') > -1) { + const middleNameDeclaration = astPath.node.declaration + if (bt.isIdentifier(middleNameDeclaration)) { + const middleName = middleNameDeclaration.name + const importedVar = importedVariablePaths[middleName] + if (importedVar) { + variables.default = importedVar + } + } + } + return false + }, + }) + + return variables +} diff --git a/src/utils/resolvePathFrom.ts b/src/utils/resolvePathFrom.ts index 57cde13..8470794 100644 --- a/src/utils/resolvePathFrom.ts +++ b/src/utils/resolvePathFrom.ts @@ -1,5 +1,24 @@ +const SUFFIXES = ['', '.js', '.ts', '.vue', '/index.js', '/index.ts'] + export default function resolvePathFrom(path: string, from: string): string { - return require.resolve(path, { - paths: [from], + let finalPath = '' + SUFFIXES.forEach(s => { + if (!finalPath.length) { + try { + finalPath = require.resolve(`${path}${s}`, { + paths: [from], + }) + } catch (e) { + // eat the error + } + } }) + + if (!finalPath.length) { + throw new Error( + `Neither '${path}.vue' nor '${path}.js', not even '${path}/index.js' or '${path}/index.ts' could be found in '${from}'`, + ) + } + + return finalPath } diff --git a/src/utils/resolveRequired.ts b/src/utils/resolveRequired.ts index 34a7945..7e227fa 100644 --- a/src/utils/resolveRequired.ts +++ b/src/utils/resolveRequired.ts @@ -4,11 +4,15 @@ import { NodePath } from 'ast-types' // tslint:disable-next-line:no-var-requires import recast = require('recast') -interface ImportedVariableToken { +interface ImportedVariable { filePath: string exportName: string } +export interface ImportedVariableSet { + [key: string]: ImportedVariable +} + /** * * @param ast @@ -17,8 +21,8 @@ interface ImportedVariableToken { export default function resolveRequired( ast: bt.File, varNameFilter?: string[], -): { [key: string]: ImportedVariableToken } { - const varToFilePath: { [key: string]: ImportedVariableToken } = {} +): ImportedVariableSet { + const varToFilePath: ImportedVariableSet = {} recast.visit(ast.program, { visitImportDeclaration(astPath: NodePath) { @@ -37,8 +41,9 @@ export default function resolveRequired( if (!varNameFilter || varNameFilter.indexOf(localVariableName) > -1) { const nodeSource = (astPath.get('source') as NodePath).node if (bt.isStringLiteral(nodeSource)) { + const filePath = nodeSource.value varToFilePath[localVariableName] = { - filePath: nodeSource.value, + filePath, exportName, } } diff --git a/tests/components/mixin-resolved/button.vue b/tests/components/mixin-resolved/button.vue new file mode 100644 index 0000000..3955931 --- /dev/null +++ b/tests/components/mixin-resolved/button.vue @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/tests/components/mixin-resolved/resolved-mixin.test.ts b/tests/components/mixin-resolved/resolved-mixin.test.ts new file mode 100644 index 0000000..92f39ff --- /dev/null +++ b/tests/components/mixin-resolved/resolved-mixin.test.ts @@ -0,0 +1,31 @@ +import * as path from 'path' + +import { ComponentDoc, PropDescriptor } from '../../../src/Documentation' +import { parse } from '../../../src/main' +const button = path.join(__dirname, './button.vue') +let docButton: ComponentDoc + +describe('tests button', () => { + beforeAll(done => { + docButton = parse(button, { + '@mixins': path.resolve(__dirname, '../../mixins'), + }) + done() + }) + + describe('props', () => { + let props: { [propName: string]: PropDescriptor } + + beforeAll(() => { + props = docButton.props ? docButton.props : {} + }) + + it('should return the "color" prop description from passthrough exported mixin', () => { + expect(props.color.description).toEqual('Another Mixins Error') + }) + + it('should return the "propsAnother" prop description from a vue file mixin', () => { + expect(props.propsAnother.description).toEqual('Example prop in vue file') + }) + }) +}) diff --git a/tests/mixins/another/mix.vue b/tests/mixins/another/mix.vue new file mode 100644 index 0000000..6cf289c --- /dev/null +++ b/tests/mixins/another/mix.vue @@ -0,0 +1,13 @@ + diff --git a/tests/mixins/index.js b/tests/mixins/index.js new file mode 100644 index 0000000..90e53b4 --- /dev/null +++ b/tests/mixins/index.js @@ -0,0 +1,2 @@ +export { default as anotherMixin } from './anotherMixin' +export { default as myMixin } from './another/mix'