Skip to content

Commit

Permalink
feat: vanilla js (no-framework) support plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
Akryum committed Aug 9, 2022
1 parent c664bad commit 31f1bce
Show file tree
Hide file tree
Showing 11 changed files with 432 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/histoire-shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './codegen/index.js'
export * from './types.js'
export * from './state.js'
export * from './type-utils.js'
1 change: 1 addition & 0 deletions packages/histoire-shared/src/type-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Awaitable<T> = Promise<T> | T
28 changes: 26 additions & 2 deletions packages/histoire-shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@ export type StoryLayout = {
width?: number | string
}

export interface CommonProps {
id?: string
title?: string
icon?: string
iconColor?: string
}

export interface InheritedProps {
setupApp?: (payload: any) => unknown
source?: string
responsiveDisabled?: boolean
autoPropsDisabled?: boolean
}

export interface VariantProps extends CommonProps, InheritedProps {
// No additional properties
}

export interface StoryProps extends CommonProps, InheritedProps {
group?: string
layout?: StoryLayout
docsOnly?: boolean
}

export interface Story {
id: string
title: string
Expand All @@ -25,7 +49,7 @@ export interface Story {
docsOnly?: boolean
file?: StoryFile
lastSelectedVariant?: Variant
slots?: () => Readonly<any>
slots?: () => any
}

export interface Variant {
Expand All @@ -34,7 +58,7 @@ export interface Variant {
icon?: string
iconColor?: string
setupApp?: (payload: any) => unknown
slots?: () => Readonly<any>
slots?: () => { default: any, controls: any }
state: any
source?: string
responsiveDisabled?: boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
defineComponent as _defineComponent,
PropType as _PropType,
} from '@histoire/vendors/vue'
import type { Story } from '@histoire/shared'
import type { StoryOptions, VariantOptions } from './types'

export default _defineComponent({
name: 'MountStory',

props: {
story: {
type: Object as _PropType<Story>,
required: true,
},
},

setup (props) {
const options = props.story.file.component as StoryOptions

let rawVariants: VariantOptions[] = []

if (options.onMount) {
rawVariants = [{
id: '_default',
title: 'default',
onMount: options.onMount,
onMountControls: options.onMountControls,
}]
} else {
rawVariants = options.variants
}

for (const index in props.story.variants) {
const rawVariant = rawVariants[index]
Object.assign(props.story.variants[index], {
slots: () => ({ default: rawVariant.onMount, controls: rawVariant.onMountControls }),
source: rawVariant.source,
responsiveDisabled: rawVariant.responsiveDisabled,
autoPropsDisabled: rawVariant.autoPropsDisabled,
setupApp: rawVariant.setupApp,
configReady: true,
})
}
},

render () {
return null
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/* eslint-disable vue/one-component-per-file */

import {
defineComponent as _defineComponent,
onBeforeUnmount as _onBeforeUnmount,
onMounted as _onMounted,
PropType as _PropType,
ref as _ref,
watch as _watch,
h as _h,
} from '@histoire/vendors/vue'
import type { Story, Variant } from '@histoire/shared'
// @ts-expect-error virtual module id
import * as setup from '$histoire-setup'
// @ts-expect-error virtual module id
import * as generatedSetup from '$histoire-generated-global-setup'
import type { App, MountApi, VanillaApi } from './types'

export default _defineComponent({
name: 'RenderStory',

props: {
variant: {
type: Object as _PropType<Variant>,
required: true,
},

story: {
type: Object as _PropType<Story>,
required: true,
},

slotName: {
type: String,
default: 'default',
},
},

emits: {
ready: () => true,
},

setup (props, { emit }) {
const sandbox = _ref<HTMLDivElement>()
let mounting = false
let app: App
let appHooks: Record<'onUpdate' | 'onUnmount', (() => unknown)[]>

async function unmountVariant () {
if (app) {
await app.onUnmount?.()
if (appHooks) {
for (const hook of appHooks.onUnmount) {
await hook()
}
}

app.el.parentNode.removeChild(app.el)
app = null
}
}

async function mountVariant () {
if (mounting) return
mounting = true

await unmountVariant()

app = {
el: document.createElement('div'),
}

if (typeof generatedSetup?.setupVanilla === 'function') {
await generatedSetup.setupVanilla({
app,
story: props.story,
variant: props.variant,
})
}

if (typeof setup?.setupVanilla === 'function') {
await setup.setupVanilla({
app,
story: props.story,
variant: props.variant,
})
}

if (typeof props.variant.setupApp === 'function') {
await props.variant.setupApp({
app,
story: props.story,
variant: props.variant,
})
}

await app.onMount?.()

appHooks = {
onUpdate: [],
onUnmount: [],
}

const api: MountApi = {
el: app.el,
state: props.variant.state,
onUpdate: (cb) => {
appHooks.onUpdate.push(cb)
},
onUnmount: (cb) => {
appHooks.onUnmount.push(cb)
},
}

const onMount = props.variant.slots()[props.slotName] as VanillaApi['onMount'] | VanillaApi['onMountControls']
await onMount(api)

sandbox.value.appendChild(app.el)

emit('ready')
}

_onMounted(async () => {
if (props.variant.configReady) {
await mountVariant()
}
})

_watch(() => props.variant, async value => {
if (value.configReady && !mounting) {
if (!app) {
await mountVariant()
} else {
// @TODO check if need to refresh here
}
}
}, {
deep: true,
})

_watch(() => props.variant.state, async () => {
if (appHooks) {
for (const hook of appHooks.onUpdate) {
await hook()
}
}
}, {
deep: true,
})

_onBeforeUnmount(() => {
unmountVariant()
})

return {
sandbox,
}
},

render () {
return _h('div', {
ref: 'sandbox',
class: '__histoire-sandbox htw-overflow-auto',
})
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as MountStory } from './MountStory'
export { default as RenderStory } from './RenderStory'

export function generateSourceCode () {
// noop
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { ServerRunPayload, ServerStory, ServerVariant } from '@histoire/shared'
import type { StoryOptions, VariantOptions } from './types'

export async function run ({ file, storyData }: ServerRunPayload) {
const { default: Comp } = await import(file.moduleId)

const options = Comp as StoryOptions

let rawVariants: VariantOptions[] = []

if (options.onMount) {
// Implicit variant
rawVariants = [{
id: '_default',
title: 'default',
}]
} else {
rawVariants = options.variants
}

const story: ServerStory = {
id: options.id ?? file.id,
title: options.title ?? file.fileName,
group: options.group,
layout: options.layout ?? { type: 'single', iframe: true },
icon: options.icon,
iconColor: options.iconColor,
docsOnly: options.docsOnly ?? false,
variants: null,
}

const variants: ServerVariant[] = rawVariants.map((v, index) => ({
id: v.id ?? `${story.id}-${index}`,
title: v.title ?? 'untitled',
icon: v.icon,
iconColor: v.iconColor,
}))

story.variants = variants

storyData.push(story)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { dirname } from 'pathe'
import { fileURLToPath } from 'url'
import { Plugin } from '../../plugin.js'

const __dirname = dirname(fileURLToPath(import.meta.url))

export function vanillaSupport (): Plugin {
return {
name: 'builtin:vanilla-support',

config () {
return {
supportMatch: [
{
id: 'vanilla',
patterns: ['**/*.js'],
pluginIds: ['vanilla'],
},
],
}
},

supportPlugin: {
id: 'vanilla',
moduleName: __dirname,
setupFn: 'setupVanilla',
importStoryComponent: (file, index) => `import Comp${index} from '${file.path}'`,
},

// onDev (api) {
// // Test vanilla story
// api.addStoryFile(resolve(__dirname, './vanilla.story.js'))
// },
}
}
Loading

0 comments on commit 31f1bce

Please sign in to comment.