Skip to content

Commit

Permalink
Merge pull request #8 from pocka/fix/valid-code-generation
Browse files Browse the repository at this point in the history
fix: Correct code injection
  • Loading branch information
pocka authored Dec 15, 2019
2 parents 5481265 + 5ef5656 commit 4dbe4e2
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 26 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
],
"dependencies": {
"clone": "^2.1.2",
"jscodeshift": "^0.7.0",
"loader-utils": "^1.2.3",
"querystring": "^0.2.0"
},
Expand Down
17 changes: 3 additions & 14 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const docgen = require('vue-docgen-api')
const loaderUtils = require('loader-utils')
const qs = require('querystring')

const inject = require('./inject')

module.exports = async function(content, map) {
const callback = this.async()
const queries = qs.parse(this.resourceQuery.slice(1))
Expand Down Expand Up @@ -36,20 +38,7 @@ module.exports = async function(content, map) {
infoOrPromise instanceof Promise ? await infoOrPromise : infoOrPromise
)

let fullExportStatement = ''
for (let i = 0; i < allInfo.length; i++) {
const info = allInfo[i]
const ident =
(info.exportName !== 'default' && info.exportName) || 'component'
const exportStatement = `;(${ident}.options = ${ident}.options || {}).__docgenInfo = ${JSON.stringify(
info
)}\n`
fullExportStatement += `${exportStatement}`
}

const js = content + '\n' + fullExportStatement

callback(null, js, map)
callback(null, inject(content, allInfo), map)
} catch (e) {
if (e instanceof Error) {
e.message =
Expand Down
99 changes: 99 additions & 0 deletions src/inject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const jscodeshift = require('jscodeshift')

/**
* Inject docgen results into components.
* @param content {string} - JS source code
* @param infoList {object[]} - A list of docgen result
* @return {string} - source code with docgen info
*/
const inject = (content, infoList) => {
// In order to inject info object to Component, we need to assign default
// exported expression to a variable.
const [normalizedCode, defaultExportAltName] = reDeclareDefaultExport(
content,
infoList
)

const infoInjectors = infoList
.map(info => {
const name =
!info.exportName || info.exportName === 'default'
? defaultExportAltName
: info.exportName

return `${name}.__docgenInfo = ${JSON.stringify(info)}`
})
.join('\n')

return normalizedCode + '\n' + infoInjectors
}

module.exports = inject

/**
* Replace default export with variable declaration and default export.
*
* ```js
* export default { props: ['foo'] }
*
* // to ...
*
* const __vuedocgen_export_0 = { props: ['foo'] }
* export default __vuedocgen_export_0
* ```
*
* @param content {string} - JS source code
* @param infoList {object[]} - A list of docgen result
* @return {[string, string]} - Modified source code and variable name to inject info
*/
const reDeclareDefaultExport = (content, infoList) => {
if (
!infoList.some(info => !info.exportName || info.exportName === 'default')
) {
return [content, null]
}

const ast = jscodeshift(content)

const defaultExportAltName = generateDefaultExportAltName(ast)

ast.find(jscodeshift.ExportDefaultDeclaration).forEach(path => {
const info = infoList.find(
info => info.exportName === 'default' || !info.exportName
)

if (!info) {
return
}

jscodeshift(path).replaceWith(
jscodeshift.variableDeclaration('const', [
jscodeshift.variableDeclarator(
jscodeshift.identifier(defaultExportAltName),
path.value.declaration
)
])
)

jscodeshift(path).insertAfter(
jscodeshift.exportDefaultDeclaration(
jscodeshift.identifier(defaultExportAltName)
)
)
})

return [ast.toSource(), defaultExportAltName]
}

/**
* Generate a name for default exported expression.
* @param ast {object}
* @return {string} - identifier
*/
const generateDefaultExportAltName = (ast, i = 0) => {
const name = `__vuedocgen_export_${i}`

const idents = ast.find(jscodeshift.Identifier, node => node.name === name)

return idents.length === 0 ? name : generateDefaultExportAltName(ast, i + 1)
}
23 changes: 23 additions & 0 deletions test/__snapshots__/loader.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,29 @@ Object {
}
`;

exports[`Injects docgen result for default-exported non-SFC 1`] = `
Object {
"description": "",
"displayName": "basicDefault.vue",
"exportName": "default",
"props": Array [
Object {
"defaultValue": Object {
"func": false,
"value": "-1",
},
"description": "Foo the number.",
"name": "foo",
"tags": Object {},
"type": Object {
"name": "number",
},
},
],
"tags": Object {},
}
`;

exports[`Injects docgen result for non-SFC 1`] = `
Object {
"description": "",
Expand Down
3 changes: 0 additions & 3 deletions test/fixtures/basic.vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,3 @@ const MyButton = Vue.extend({
})

export { MyButton }

// NOTE: vue-docgen-api cannot handle default exports (is it expected behavor or bug? idk...)
// export default MyButton
14 changes: 14 additions & 0 deletions test/fixtures/basicDefault.vue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Vue from 'vue'

export default Vue.extend({
props: {
/**
* Foo the number.
*/
foo: {
type: Number,
default: -1
}
},
template: '<span>{{foo}}</span>'
})
5 changes: 0 additions & 5 deletions test/fixtures/basicMulti.vue.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Vue from 'vue'

export const MyButton1 = {
props: {
/**
Expand All @@ -25,6 +23,3 @@ export const MyButton2 = {
},
template: '<span>{{foo}}</span>'
}

// NOTE: vue-docgen-api cannot handle default exports (is it expected behavor or bug? idk...)
// export default MyButton
18 changes: 15 additions & 3 deletions test/loader.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ it('Injects docgen result for non-SFC', async () => {

const Component = await renderComponent(output, fixture, mod => mod.MyButton)

expect(Component.options.__docgenInfo).toMatchSnapshot()
expect(Component.__docgenInfo).toMatchSnapshot()
})

it('Injects docgen result non-SFC with multiple exports', async () => {
Expand All @@ -37,6 +37,18 @@ it('Injects docgen result non-SFC with multiple exports', async () => {
renderComponent(output, fixture, mod => mod.MyButton2)
])

expect(components[0].options.__docgenInfo).toMatchSnapshot()
expect(components[1].options.__docgenInfo).toMatchSnapshot()
expect(components[0].__docgenInfo).toMatchSnapshot()
expect(components[1].__docgenInfo).toMatchSnapshot()
})

it('Injects docgen result for default-exported non-SFC', async () => {
const fixture = './fixtures/basicDefault.vue.js'

const stats = await compiler(fixture)
const output = stats.toJson().modules.find(mod => mod.name.includes(fixture))
.source

const Component = await renderComponent(output, fixture, mod => mod.default)

expect(Component.__docgenInfo).toMatchSnapshot()
})
2 changes: 1 addition & 1 deletion test/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export async function renderComponent(
await render(Component)

// Does the component have docgen info?
expect(Component).toHaveProperty('options.__docgenInfo')
expect(Component).toHaveProperty('__docgenInfo')

return Component
}

0 comments on commit 4dbe4e2

Please sign in to comment.