Skip to content

Commit

Permalink
feat: static source code, fix #238
Browse files Browse the repository at this point in the history
  • Loading branch information
Akryum committed Oct 3, 2022
1 parent 1717f02 commit 8887c70
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 28 deletions.
158 changes: 133 additions & 25 deletions packages/histoire-app/src/app/components/panel/StorySourceCode.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { computed, markRaw, onMounted, ref, shallowRef, watch, watchEffect } from 'vue'
import { computed, markRaw, nextTick, onMounted, ref, shallowRef, watch, watchEffect } from 'vue'
import { Icon } from '@iconify/vue'
import { getHighlighter, Highlighter, setCDN } from 'shiki'
import { HstCopyIcon } from '@histoire/controls'
Expand All @@ -25,53 +25,108 @@ watchEffect(async () => {
}
})
const sourceCode = ref('')
const highlighter = shallowRef<Highlighter>()
const error = ref<string>(null)
onMounted(async () => {
setCDN('https://unpkg.com/[email protected]/')
highlighter.value = await getHighlighter({
langs: [
'html',
'jsx',
],
themes: [
'github-light',
'github-dark',
],
})
})
const dynamicSourceCode = ref('')
const error = ref<string>(null)
watch(() => [props.variant, generateSourceCodeFn.value], async () => {
if (!generateSourceCodeFn.value) return
error.value = null
dynamicSourceCode.value = ''
try {
if (props.variant.source) {
sourceCode.value = props.variant.source
dynamicSourceCode.value = props.variant.source
} else if (props.variant.slots?.().source) {
const source = props.variant.slots?.().source()[0].children
if (source) {
sourceCode.value = await unindent(source)
dynamicSourceCode.value = await unindent(source)
}
} else {
sourceCode.value = await generateSourceCodeFn.value(props.variant)
dynamicSourceCode.value = await generateSourceCodeFn.value(props.variant)
}
} catch (e) {
console.error(e)
error.value = e.message
}
// Auto-switch
if (!dynamicSourceCode.value) {
displayedSource.value = 'static'
}
}, {
deep: true,
immediate: true,
})
const sourceHtml = computed(() => sourceCode.value
? highlighter.value?.codeToHtml(sourceCode.value, {
// Static file source
const staticSourceCode = ref('')
watch(() => [props.story, props.story?.file?.source], async () => {
staticSourceCode.value = ''
const sourceLoader = props.story.file?.source
if (sourceLoader) {
staticSourceCode.value = (await sourceLoader()).default
}
}, {
immediate: true,
})
const displayedSource = ref<'dynamic' | 'static'>('dynamic')
const displayedSourceCode = computed(() => {
if (displayedSource.value === 'dynamic') {
return dynamicSourceCode.value
}
return staticSourceCode.value
})
// HTML render
onMounted(async () => {
setCDN('https://unpkg.com/[email protected]/')
highlighter.value = await getHighlighter({
langs: [
'html',
'jsx',
],
themes: [
'github-light',
'github-dark',
],
})
})
const sourceHtml = computed(() => displayedSourceCode.value
? highlighter.value?.codeToHtml(displayedSourceCode.value, {
lang: 'html',
theme: isDark.value ? 'github-dark' : 'github-light',
})
: '')
// Scrolling
let lastScroll = 0
// Reset
watch(() => props.variant, () => {
lastScroll = 0
})
const scroller = ref<HTMLElement>()
function onScroll (event) {
if (sourceHtml.value) {
lastScroll = event.target.scrollTop
}
}
watch(sourceHtml, async () => {
await nextTick()
if (scroller.value) {
scroller.value.scrollTop = lastScroll
}
})
</script>

<template>
Expand All @@ -81,14 +136,63 @@ const sourceHtml = computed(() => sourceCode.value
<!-- Toolbar -->
<div
v-if="!error"
class="htw-h-10 htw-flex-none htw-border-b htw-border-solid htw-border-gray-150 dark:htw-border-gray-850 htw-px-4 htw-flex htw-items-center"
class="htw-h-10 htw-flex-none htw-border-b htw-border-solid htw-border-gray-150 dark:htw-border-gray-850 htw-px-4 htw-flex htw-items-center htw-gap-2"
>
<div class="htw-text-gray-900 dark:htw-text-gray-100">
Source
</div>
<div class="htw-flex-1" />

<!-- Display source modes -->
<div class="htw-flex htw-flex-none htw-gap-px htw-h-full htw-py-2">
<button
v-tooltip="!dynamicSourceCode ? 'Dynamic source code is not available' : displayedSource !== 'dynamic' ? 'Switch to dynamic source' : null"
class="htw-flex htw-items-center htw-gap-1 htw-h-full htw-px-1 htw-bg-gray-500/10 htw-rounded-l htw-transition-all htw-ease-[cubic-bezier(0,1,.6,1)] htw-duration-300 htw-overflow-hidden"
:class="[
displayedSource !== 'dynamic' ? 'htw-max-w-6 htw-opacity-70' : 'htw-max-w-[82px] htw-text-primary-600 dark:htw-text-primary-400',
dynamicSourceCode ? 'htw-cursor-pointer hover:htw-bg-gray-500/30 active:htw-bg-gray-600/50' : 'htw-opacity-50',
]"
@click="dynamicSourceCode && (displayedSource = 'dynamic')"
>
<Icon
icon="carbon:flash"
class="htw-w-4 htw-h-4 htw-flex-none"
/>
<span
class="transition-opacity duration-300"
:class="{
'opacity-0': displayedSource !== 'dynamic',
}"
>
Dynamic
</span>
</button>
<button
v-tooltip="!staticSourceCode ? 'Static source code is not available' : displayedSource !== 'static' ? 'Switch to static source' : null"
class="htw-flex htw-items-center htw-gap-1 htw-h-full htw-px-1 htw-bg-gray-500/10 htw-rounded-r htw-transition-all htw-ease-[cubic-bezier(0,1,.6,1)] htw-duration-300 htw-overflow-hidden"
:class="[
displayedSource !== 'static' ? 'htw-max-w-6 htw-opacity-70' : 'htw-max-w-[63px] htw-text-primary-600 dark:htw-text-primary-400',
staticSourceCode ? 'htw-cursor-pointer hover:htw-bg-gray-500/30 active:htw-bg-gray-600/50' : 'htw-opacity-50',
]"
@click="staticSourceCode && (displayedSource = 'static')"
>
<Icon
icon="carbon:document"
class="htw-w-4 htw-h-4 htw-flex-none"
/>
<span
class="transition-opacity duration-300"
:class="{
'opacity-0': displayedSource !== 'static',
}"
>
Static
</span>
</button>
</div>

<HstCopyIcon
:content="sourceCode"
:content="displayedSourceCode"
class="htw-flex-none"
/>
</div>
Expand All @@ -100,7 +204,7 @@ const sourceHtml = computed(() => sourceCode.value
Error: {{ error }}
</div>

<BaseEmpty v-else-if="!sourceCode">
<BaseEmpty v-else-if="!displayedSourceCode">
<Icon
icon="carbon:code-hide"
class="htw-w-8 htw-h-8 htw-opacity-50 htw-mb-6"
Expand All @@ -110,16 +214,20 @@ const sourceHtml = computed(() => sourceCode.value

<textarea
v-else-if="!sourceHtml"
ref="scroller"
class="__histoire-code-placeholder htw-w-full htw-h-full htw-p-4 htw-outline-none htw-bg-transparent htw-resize-none htw-m-0"
:value="sourceCode"
:value="displayedSourceCode"
readonly
data-test-id="story-source-code"
@scroll="onScroll"
/>
<!-- eslint-disable vue/no-v-html -->
<div
v-else
ref="scroller"
class="htw-w-full htw-h-full htw-overflow-auto"
data-test-id="story-source-code"
@scroll="onScroll"
>
<div
class="__histoire-code htw-p-4 htw-w-fit"
Expand Down
3 changes: 2 additions & 1 deletion packages/histoire-shared/src/types/story.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface StoryFile {
story: Story
path: string[]
filePath: string
source: () => Promise<{ default: string }>
}

export type StoryLayout = {
Expand Down Expand Up @@ -59,7 +60,7 @@ export interface Variant {
icon?: string
iconColor?: string
setupApp?: (payload: any) => unknown
slots?: () => { default: any, controls: any }
slots?: () => { default: any, controls: any, source: any }
state: any
source?: string
responsiveDisabled?: boolean
Expand Down
7 changes: 6 additions & 1 deletion packages/histoire/src/node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,12 @@ export async function createServer (ctx: Context, port: number) {
console.log('Collect stories start', changedFile?.path ?? 'all')
const time = Date.now()
if (changedFile && !queuedAll) {
await Promise.all(queuedFiles.map(storyFile => executeStoryFile(storyFile)))
await Promise.all(queuedFiles.map(async storyFile => {
await executeStoryFile(storyFile)
if (storyFile.story) {
await invalidateModule(`/__resolved__virtual:story-source:${storyFile.story.id}`)
}
}))
} else {
// Full update

Expand Down
22 changes: 21 additions & 1 deletion packages/histoire/src/node/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
loadConfigFromFile as loadViteConfigFromFile,
} from 'vite'
import { lookup as lookupMime } from 'mrmime'
import fs from 'fs-extra'
import { APP_PATH, TEMP_PATH } from './alias.js'
import { Context } from './context.js'
import { notifyStoryChange } from './stories.js'
Expand Down Expand Up @@ -234,6 +235,11 @@ export async function getViteConfigWithPlugins (isServer: boolean, ctx: Context)
if (id.startsWith('virtual:story:')) {
return `\0${id}`
}
if (id.startsWith('virtual:story-source:')) {
return `/__resolved__${id}`
// @TODO
// return `\0${id}`
}
},

async load (id) {
Expand All @@ -260,7 +266,7 @@ ${resolvedStories.map((file, index) => {
}
return supportPlugin.importStoryComponent(file, index)
}).filter(Boolean).join('\n')}
export let files = [${files.map((file) => `{${JSON.stringify(file).slice(1, -1)}, component: Comp${file.index}}`).join(',\n')}]
export let files = [${files.map((file) => `{${JSON.stringify(file).slice(1, -1)}, component: Comp${file.index}, source: () => import('virtual:story-source:${file.story.id}')}`).join(',\n')}]
export let tree = ${JSON.stringify(makeTree(ctx.config, resolvedStories))}
const handlers = []
export function onUpdate (cb) {
Expand Down Expand Up @@ -381,6 +387,20 @@ if (import.meta.hot) {
return storyFile.moduleCode
}
}

if (id.startsWith('/__resolved__virtual:story-source:')) {
const storyId = id.slice('/__resolved__virtual:story-source:'.length)
const storyFile = ctx.storyFiles.find(f => f.story?.id === storyId)
if (storyFile) {
let source: string
if (storyFile.virtual) {
source = storyFile.moduleCode
} else {
source = await fs.readFile(resolve(ctx.root, storyFile.relativePath), 'utf-8')
}
return `export default ${JSON.stringify(source)}`
}
}
},

handleHotUpdate (updateContext) {
Expand Down

0 comments on commit 8887c70

Please sign in to comment.