Skip to content

Commit

Permalink
feat: support script and setup script
Browse files Browse the repository at this point in the history
  • Loading branch information
tobiasdiez committed Nov 5, 2022
1 parent 946d56c commit db343dc
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 19 deletions.
6 changes: 5 additions & 1 deletion examples/vite/src/components/Button.stories.vue
@@ -1,7 +1,11 @@
<script lang='ts' setup>
import MyButton from './Button.vue'
</script>

<template>
<Stories>
<Story title="Primary">
<span>test</span>
<MyButton label="Button" />
</Story>
</Stories>
</template>
1 change: 1 addition & 0 deletions src/core/logger.ts
@@ -1,2 +1,3 @@
import consola from 'consola'
export const logger = consola.withTag('[storybook:vue]')
// logger.level = LogLevel.Debug
32 changes: 25 additions & 7 deletions src/core/transform.ts
@@ -1,4 +1,5 @@
import { compileTemplate, parse } from 'vue/compiler-sfc'
import type { SFCDescriptor, SFCScriptBlock } from 'vue/compiler-sfc'
import { compileScript, compileTemplate, parse, rewriteDefault } from 'vue/compiler-sfc'
import type { ElementNode } from '@vue/compiler-core'

/**
Expand All @@ -9,7 +10,17 @@ export function transform(code: string) {
if (descriptor.template === null)
throw new Error('No template found in SFC')

return transformTemplate(descriptor.template.content)
let result = ''
const resolvedScript = resolveScript(descriptor)
if (resolvedScript) {
result += rewriteDefault(resolvedScript.content, '_sfc_main')
result += '\n'
}
else {
result += 'const _sfc_main = {}\n'
}
result += transformTemplate(descriptor.template.content, resolvedScript)
return result

/*
return `
Expand Down Expand Up @@ -69,7 +80,7 @@ export function transform(code: string) {
*/
}

function transformTemplate(content: string) {
function transformTemplate(content: string, resolvedScript?: SFCScriptBlock) {
const template = compileTemplate({
source: content,
filename: 'test.vue',
Expand All @@ -92,7 +103,7 @@ function transformTemplate(content: string) {
if (story.type !== 1 || story.tag !== 'Story')
continue

result += generateStoryImport(story)
result += generateStoryImport(story, resolvedScript)
}
return result
}
Expand All @@ -116,18 +127,25 @@ function extractTitle(node: ElementNode) {
}
}

function generateStoryImport(story: ElementNode) {
function generateStoryImport(story: ElementNode, resolvedScript?: SFCScriptBlock) {
const title = extractTitle(story)
if (!title)
throw new Error('Story is missing a title')
const storyTemplate = parse(story.loc.source.replace(/<Story/, '<template').replace(/<\/Story>/, '</template>')).descriptor.template?.content
if (storyTemplate === undefined)
throw new Error('No template found in Story')

const { code } = compileTemplate({ source: storyTemplate.trim(), filename: 'test.vue', id: 'test' })
const { code } = compileTemplate({ source: storyTemplate.trim(), filename: 'test.vue', id: 'test', compilerOptions: { bindingMetadata: resolvedScript?.bindings } })
const renderFunction = code.replace('export function render', `function render${title}`)

// Each named export is a story, has to return a Vue ComponentOptionsBase
return `
${renderFunction}
export const ${title} = render${title}`
export const ${title} = () => Object.assign({render: render${title}}, _sfc_main)`
}

// Minimal version of https://github.com/vitejs/vite/blob/57916a476924541dd7136065ceee37ae033ca78c/packages/plugin-vue/src/main.ts#L297
function resolveScript(descriptor: SFCDescriptor) {
if (descriptor.script || descriptor.scriptSetup)
return compileScript(descriptor, { id: 'test' })
}
8 changes: 5 additions & 3 deletions src/index.ts
@@ -1,4 +1,5 @@
import { createUnplugin } from 'unplugin'
import { parseVueRequest } from '@vitejs/plugin-vue'
import { transform as transformStories } from './core/transform'
import { logger } from './core/logger'
import type { Options } from './types'
Expand All @@ -10,6 +11,7 @@ export default createUnplugin<Options>(_options => ({
name: 'unplugin-starter',
enforce: 'pre',
async resolveId(source, importer, options) {
// logger.debug('resolveId', source)
if (source.endsWith(STORIES_PUBLIC_SUFFIX)) {
// Determine what the actual entry would have been. We need "skipSelf" to avoid an infinite loop.
// @ts-expect-error: private API
Expand All @@ -27,11 +29,11 @@ export default createUnplugin<Options>(_options => ({
}
},
transformInclude(id) {
logger.debug('transformInclude', id)
// logger.debug('transformInclude', id)
return id.endsWith(STORIES_INTERNAL_SUFFIX)
},
transform(code) {
logger.debug('transform', code)
transform(code, id) {
logger.debug('transform', id)
return transformStories(code)
},
}))
67 changes: 59 additions & 8 deletions test/index.test.ts
@@ -1,13 +1,13 @@
import { describe, expect, it } from 'vitest'
import { compileTemplate, parse } from 'vue/compiler-sfc'
import { transform } from '../src/core/transform'

describe('transform', () => {
it('handles one simple story', () => {
const code = '<template><Stories><Story title="Primary">hello</Story></Stories></template>'
const result = transform(code)
expect(result).toMatchInlineSnapshot(`
"export default {
"const _sfc_main = {}
export default {
//component: MyComponent,
//decorators: [ ... ],
Expand All @@ -18,14 +18,15 @@ describe('transform', () => {
function renderPrimary(_ctx, _cache) {
return \\"hello\\"
}
export const Primary = renderPrimary"
export const Primary = () => Object.assign({render: renderPrimary}, _sfc_main)"
`)
})
it('extracts title from Stories', () => {
const code = '<template><Stories title="test"><Story title="Primary">hello</Story></Stories></template>'
const result = transform(code)
expect(result).toMatchInlineSnapshot(`
"export default {
"const _sfc_main = {}
export default {
title: 'test',
//component: MyComponent,
//decorators: [ ... ],
Expand All @@ -36,7 +37,7 @@ describe('transform', () => {
function renderPrimary(_ctx, _cache) {
return \\"hello\\"
}
export const Primary = renderPrimary"
export const Primary = () => Object.assign({render: renderPrimary}, _sfc_main)"
`)
})
it('throws error if story does not have a title', () => {
Expand All @@ -53,7 +54,8 @@ describe('transform', () => {
</template>`
const result = transform(code)
expect(result).toMatchInlineSnapshot(`
"export default {
"const _sfc_main = {}
export default {
//component: MyComponent,
//decorators: [ ... ],
Expand All @@ -64,13 +66,62 @@ describe('transform', () => {
function renderPrimary(_ctx, _cache) {
return \\"hello\\"
}
export const Primary = renderPrimary
export const Primary = () => Object.assign({render: renderPrimary}, _sfc_main)
function renderSecondary(_ctx, _cache) {
return \\"world\\"
}
export const Secondary = renderSecondary"
export const Secondary = () => Object.assign({render: renderSecondary}, _sfc_main)"
`)
},
)
it('supports components defined in script setup', () => {
const code = `
<script setup>
const test = {
data() {
return { a: 123 }
},
template: '<span>{{a}}</span>'
}
</script>
<template>
<Stories>
<Story title="Primary"><test></test></Story>
</Stories>
</template>`
const result = transform(code)
expect(result).toMatchInlineSnapshot(`
"const _sfc_main = {
setup(__props, { expose }) {
expose();
const test = {
data() {
return { a: 123 }
},
template: '<span>{{a}}</span>'
}
const __returned__ = { test }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
}
export default {
//component: MyComponent,
//decorators: [ ... ],
//parameters: { ... }
}
import { openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
function renderPrimary(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock($setup[\\"test\\"]))
}
export const Primary = () => Object.assign({render: renderPrimary}, _sfc_main)"
`)
})
})

0 comments on commit db343dc

Please sign in to comment.