Skip to content

Commit 42d563c

Browse files
authored
feat: support relative import inside slides (#1744)
1 parent 3750654 commit 42d563c

File tree

10 files changed

+441
-420
lines changed

10 files changed

+441
-420
lines changed

packages/client/composables/useSlideInfo.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function useSlideInfo(no: number): UseSlideInfo {
1717
update: async () => {},
1818
}
1919
}
20-
const url = `/@slidev/slide/${no}.json`
20+
const url = `/__slidev/slides/${no}.json`
2121
const { data: info, execute } = useFetch(url).json<SlideInfo>().get()
2222

2323
execute()

packages/slidev/node/options.ts

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import path from 'node:path'
12
import { uniq } from '@antfu/utils'
23
import Debug from 'debug'
34
import type { ResolvedSlidevOptions, ResolvedSlidevUtils, SlidevData, SlidevEntryOptions } from '@slidev/types'
45
import mm from 'micromatch'
6+
import fg from 'fast-glob'
57
import { parser } from './parser'
68
import { getThemeMeta, resolveTheme } from './integrations/themes'
79
import { resolveAddons } from './integrations/addons'
@@ -58,16 +60,45 @@ export async function resolveOptions(
5860
themeRoots,
5961
addonRoots,
6062
roots,
61-
utils: createDataUtils(data),
63+
utils: createDataUtils(data, rootsInfo.clientRoot, roots),
6264
}
6365

6466
return resolved
6567
}
6668

67-
export function createDataUtils(data: SlidevData): ResolvedSlidevUtils {
69+
export function createDataUtils(data: SlidevData, clientRoot: string, roots: string[]): ResolvedSlidevUtils {
6870
const monacoTypesIgnorePackagesMatches = (data.config.monacoTypesIgnorePackages || [])
6971
.map(i => mm.matcher(i))
72+
73+
let _layouts_cache_time = 0
74+
let _layouts_cache: Record<string, string> = {}
75+
7076
return {
7177
isMonacoTypesIgnored: pkg => monacoTypesIgnorePackagesMatches.some(i => i(pkg)),
78+
getLayouts: async () => {
79+
const now = Date.now()
80+
if (now - _layouts_cache_time < 2000)
81+
return _layouts_cache
82+
83+
const layouts: Record<string, string> = {}
84+
85+
for (const root of [clientRoot, ...roots]) {
86+
const layoutPaths = await fg('layouts/**/*.{vue,ts}', {
87+
cwd: root,
88+
absolute: true,
89+
suppressErrors: true,
90+
})
91+
92+
for (const layoutPath of layoutPaths) {
93+
const layoutName = path.basename(layoutPath).replace(/\.\w+$/, '')
94+
layouts[layoutName] = layoutPath
95+
}
96+
}
97+
98+
_layouts_cache_time = now
99+
_layouts_cache = layouts
100+
101+
return layouts
102+
},
72103
}
73104
}

packages/slidev/node/virtual/slides.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ export const templateSlides: VirtualModuleTemplate = {
2323
.map((_, idx) => {
2424
const no = idx + 1
2525
statements.push(
26-
`import { meta as f${no} } from '${VIRTUAL_SLIDE_PREFIX}${no}.frontmatter'`,
26+
`import { meta as f${no} } from '${VIRTUAL_SLIDE_PREFIX}${no}/frontmatter'`,
2727
// For some unknown reason, import error won't be caught by the error component. Catch it here.
2828
`const load${no} = async () => {`,
29-
` try { return componentsCache[${idx}] ??= await import('${VIRTUAL_SLIDE_PREFIX}${no}.md') }`,
29+
` try { return componentsCache[${idx}] ??= await import('${VIRTUAL_SLIDE_PREFIX}${no}/md') }`,
3030
` catch (e) { return SlideError }`,
3131
`}`,
3232
)

packages/slidev/node/vite/common.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const regexSlideReqPath = /^\/__slidev\/slides\/(\d+)\.json$/
2+
export const regexSlideFacadeId = /^\/@slidev\/slides\/(\d+)\/(md|frontmatter)($|\?)/
3+
export const regexSlideSourceId = /__slidev_(\d+)\.(md|frontmatter)$/
4+
5+
export const templateInjectionMarker = '/* @slidev-injection */'
6+
export const templateImportContextUtils = `import { useSlideContext as _useSlideContext, frontmatterToProps as _frontmatterToProps } from "@slidev/client/context.ts"`
7+
export const templateInitContext = `const { $slidev, $nav, $clicksContext, $clicks, $page, $renderContext, $frontmatter } = _useSlideContext()`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Plugin } from 'vite'
2+
import { templateImportContextUtils, templateInitContext, templateInjectionMarker } from './common'
3+
4+
/**
5+
* Inject `$slidev` into the script block of a Vue component
6+
*/
7+
export function createContextInjectionPlugin(): Plugin {
8+
return {
9+
name: 'slidev:context-injection',
10+
async transform(code, id) {
11+
if (!id.endsWith('.vue') || id.includes('/@slidev/client/') || id.includes('/packages/client/'))
12+
return
13+
if (code.includes(templateInjectionMarker) || code.includes('useSlideContext()'))
14+
return code // Assume that the context is already imported and used
15+
const imports = [
16+
templateImportContextUtils,
17+
templateInitContext,
18+
templateInjectionMarker,
19+
]
20+
// eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/optimal-quantifier-concatenation
21+
const matchScript = code.match(/<script((?!setup).)*(setup)?.*>/)
22+
if (matchScript && matchScript[2]) {
23+
// setup script
24+
return code.replace(/(<script.*>)/g, `$1\n${imports.join('\n')}\n`)
25+
}
26+
else if (matchScript && !matchScript[2]) {
27+
// not a setup script
28+
const matchExport = code.match(/export\s+default\s+\{/)
29+
if (matchExport) {
30+
// script exports a component
31+
const exportIndex = (matchExport.index || 0) + matchExport[0].length
32+
let component = code.slice(exportIndex)
33+
component = component.slice(0, component.indexOf('</script>'))
34+
35+
const scriptIndex = (matchScript.index || 0) + matchScript[0].length
36+
const provideImport = '\nimport { injectionSlidevContext } from "@slidev/client/constants.ts"\n'
37+
code = `${code.slice(0, scriptIndex)}${provideImport}${code.slice(scriptIndex)}`
38+
39+
let injectIndex = exportIndex + provideImport.length
40+
let injectObject = '$slidev: { from: injectionSlidevContext },'
41+
const matchInject = component.match(/.*inject\s*:\s*([[{])/)
42+
if (matchInject) {
43+
// component has a inject option
44+
injectIndex += (matchInject.index || 0) + matchInject[0].length
45+
if (matchInject[1] === '[') {
46+
// inject option in array
47+
let injects = component.slice((matchInject.index || 0) + matchInject[0].length)
48+
const injectEndIndex = injects.indexOf(']')
49+
injects = injects.slice(0, injectEndIndex)
50+
injectObject += injects.split(',').map(inject => `${inject}: {from: ${inject}}`).join(',')
51+
return `${code.slice(0, injectIndex - 1)}{\n${injectObject}\n}${code.slice(injectIndex + injectEndIndex + 1)}`
52+
}
53+
else {
54+
// inject option in object
55+
return `${code.slice(0, injectIndex)}\n${injectObject}\n${code.slice(injectIndex)}`
56+
}
57+
}
58+
// add inject option
59+
return `${code.slice(0, injectIndex)}\ninject: { ${injectObject} },\n${code.slice(injectIndex)}`
60+
}
61+
}
62+
// no setup script and not a vue component
63+
return `<script setup>\n${imports.join('\n')}\n</script>\n${code}`
64+
},
65+
}
66+
}

packages/slidev/node/vite/hmrPatch.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Plugin } from 'vite'
2+
import { regexSlideSourceId } from './common'
3+
4+
/**
5+
* force reload slide component to ensure v-click resolves correctly
6+
*/
7+
export function createHmrPatchPlugin(): Plugin {
8+
return {
9+
name: 'slidev:slide-transform:post',
10+
transform(code, id) {
11+
if (!id.match(regexSlideSourceId))
12+
return
13+
const replaced = code.replace('if (_rerender_only)', 'if (false)')
14+
if (replaced !== code)
15+
return replaced
16+
},
17+
}
18+
}

packages/slidev/node/vite/index.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { join } from 'node:path'
22
import { existsSync } from 'node:fs'
33
import process from 'node:process'
4-
import { fileURLToPath } from 'node:url'
54
import type { Plugin } from 'vite'
65
import Icons from 'unplugin-icons/vite'
76
import IconsResolver from 'unplugin-icons/resolver'
@@ -18,6 +17,9 @@ import { createVueCompilerFlagsPlugin } from './compilerFlagsVue'
1817
import { createMonacoTypesLoader } from './monacoTypes'
1918
import { createVuePlugin } from './vue'
2019
import { createMonacoWriter } from './monacoWrite'
20+
import { createLayoutWrapperPlugin } from './layoutWrapper'
21+
import { createContextInjectionPlugin } from './contextInjection'
22+
import { createHmrPatchPlugin } from './hmrPatch'
2123

2224
export async function ViteSlidevPlugin(
2325
options: ResolvedSlidevOptions,
@@ -44,11 +46,12 @@ export async function ViteSlidevPlugin(
4446
const publicRoots = [...themeRoots, ...addonRoots].map(i => join(i, 'public')).filter(existsSync)
4547

4648
const plugins = [
49+
createSlidesLoader(options, serverOptions),
4750
createMarkdownPlugin(options, pluginOptions),
48-
51+
createLayoutWrapperPlugin(options),
52+
createContextInjectionPlugin(),
4953
createVuePlugin(options, pluginOptions),
50-
createSlidesLoader(options, pluginOptions, serverOptions),
51-
createMonacoWriter(options),
54+
createHmrPatchPlugin(),
5255

5356
Components({
5457
extensions: ['vue', 'md', 'js', 'ts', 'jsx', 'tsx'],
@@ -78,7 +81,7 @@ export async function ViteSlidevPlugin(
7881

7982
Icons({
8083
defaultClass: 'slidev-icon',
81-
collectionsNodeResolvePath: fileURLToPath(import.meta.url),
84+
collectionsNodeResolvePath: options.cliRoot,
8285
...iconsOptions,
8386
}),
8487

@@ -120,6 +123,7 @@ export async function ViteSlidevPlugin(
120123

121124
createConfigPlugin(options),
122125
createMonacoTypesLoader(options),
126+
createMonacoWriter(options),
123127
createVueCompilerFlagsPlugin(options),
124128
createUnocssPlugin(options, pluginOptions),
125129

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { ResolvedSlidevOptions } from '@slidev/types'
2+
import type { Plugin } from 'vite'
3+
import { bold, gray, red, yellow } from 'kolorist'
4+
import { toAtFS } from '../resolver'
5+
import { regexSlideSourceId, templateImportContextUtils, templateInitContext, templateInjectionMarker } from './common'
6+
7+
export function createLayoutWrapperPlugin(
8+
{ data, utils }: ResolvedSlidevOptions,
9+
): Plugin {
10+
return {
11+
name: 'slidev:layout-wrapper',
12+
async transform(code, id) {
13+
const match = id.match(regexSlideSourceId)
14+
if (!match)
15+
return
16+
const [, no, type] = match
17+
if (type !== 'md')
18+
return
19+
const index = +no - 1
20+
const layouts = await utils.getLayouts()
21+
const rawLayoutName = data.slides[index]?.frontmatter?.layout ?? data.slides[0]?.frontmatter?.default?.layout
22+
let layoutName = rawLayoutName || (index === 0 ? 'cover' : 'default')
23+
if (!layouts[layoutName]) {
24+
console.error(red(`\nUnknown layout "${bold(layoutName)}".${yellow(' Available layouts are:')}`)
25+
+ Object.keys(layouts).map((i, idx) => (idx % 3 === 0 ? '\n ' : '') + gray(i.padEnd(15, ' '))).join(' '))
26+
console.error()
27+
layoutName = 'default'
28+
}
29+
30+
const setupTag = code.match(/^<script setup.*>/m)
31+
if (!setupTag)
32+
throw new Error(`[Slidev] Internal error: <script setup> block not found in slide ${index + 1}.`)
33+
34+
const templatePart = code.slice(0, setupTag.index!)
35+
const scriptPart = code.slice(setupTag.index!)
36+
37+
const bodyStart = templatePart.indexOf('<template>') + 10
38+
const bodyEnd = templatePart.lastIndexOf('</template>')
39+
let body = code.slice(bodyStart, bodyEnd).trim()
40+
if (body.startsWith('<div>') && body.endsWith('</div>'))
41+
body = body.slice(5, -6)
42+
43+
return [
44+
templatePart.slice(0, bodyStart),
45+
`<InjectedLayout v-bind="_frontmatterToProps($frontmatter,${index})">\n${body}\n</InjectedLayout>`,
46+
templatePart.slice(bodyEnd),
47+
scriptPart.slice(0, setupTag[0].length),
48+
`import InjectedLayout from "${toAtFS(layouts[layoutName])}"`,
49+
templateImportContextUtils,
50+
templateInitContext,
51+
'$clicksContext.setup()',
52+
templateInjectionMarker,
53+
scriptPart.slice(setupTag[0].length),
54+
].join('\n')
55+
},
56+
}
57+
}

0 commit comments

Comments
 (0)