| 1 | import type { Plugin } from 'vite' |
| 2 | import { templateImportContextUtils, templateInitContext, templateInjectionMarker } from './common' |
| 3 | |
| 4 | const RE_EXPORT_DEFAULT_OBJECT = /export\s+default\s+\{/ |
| 5 | const RE_INJECT_OPTION = /.*inject\s*:\s*([[{])/ |
| 6 | |
| 7 | /** |
| 8 | * Inject `$slidev` into the script block of a Vue component |
| 9 | */ |
| 10 | export function createContextInjectionPlugin(): Plugin { |
| 11 | return { |
| 12 | name: 'slidev:context-injection', |
| 13 | transform: { |
| 14 | // TODO: static filter |
| 15 | async handler(code, id) { |
| 16 | if (!id.endsWith('.vue') || id.includes('/@slidev/client/') || id.includes('/packages/client/')) |
| 17 | return |
| 18 | if (code.includes(templateInjectionMarker) || code.includes('useSlideContext()')) |
| 19 | return code // Assume that the context is already imported and used |
| 20 | const imports = [ |
| 21 | templateImportContextUtils, |
| 22 | templateInitContext, |
| 23 | templateInjectionMarker, |
| 24 | ] |
| 25 | |
| 26 | // Find all <script> blocks |
| 27 | const matchScripts = [...code.matchAll(/<script([^>]*)>/g)] |
| 28 | // Find the <script ... setup> block |
| 29 | const setupScriptMatch = [...code.matchAll(/<script([^>]*)setup([^>]*)>/g)].at(0) |
| 30 | if (setupScriptMatch) { |
| 31 | // Only inject into the <script setup> block |
| 32 | const setupTag = setupScriptMatch[0] |
| 33 | const setupTagIndex = setupScriptMatch.index || 0 |
| 34 | const setupTagEnd = setupTagIndex + setupTag.length |
| 35 | // Insert imports right after the <script setup ...> tag |
| 36 | return `${code.slice(0, setupTagEnd)}\n${imports.join('\n')}\n${code.slice(setupTagEnd)}` |
| 37 | } |
| 38 | else if (!setupScriptMatch && matchScripts.length === 1) { |
| 39 | // not a setup script |
| 40 | const matchExport = code.match(RE_EXPORT_DEFAULT_OBJECT) |
| 41 | if (matchExport) { |
| 42 | // script exports a component |
| 43 | const exportIndex = (matchExport.index || 0) + matchExport[0].length |
| 44 | let component = code.slice(exportIndex) |
| 45 | component = component.slice(0, component.indexOf('</script>')) |
| 46 | |
| 47 | const scriptIndex = (matchScripts[0].index || 0) + matchScripts[0][0].length |
| 48 | const provideImport = '\nimport { injectionSlidevContext } from "@slidev/client/constants.ts"\n' |
| 49 | code = `${code.slice(0, scriptIndex)}${provideImport}${code.slice(scriptIndex)}` |
| 50 | |
| 51 | let injectIndex = exportIndex + provideImport.length |
| 52 | let injectObject = '$slidev: { from: injectionSlidevContext },' |
| 53 | const matchInject = component.match(RE_INJECT_OPTION) |
| 54 | if (matchInject) { |
| 55 | // component has a inject option |
| 56 | injectIndex += (matchInject.index || 0) + matchInject[0].length |
| 57 | if (matchInject[1] === '[') { |
| 58 | // inject option in array |
| 59 | let injects = component.slice((matchInject.index || 0) + matchInject[0].length) |
| 60 | const injectEndIndex = injects.indexOf(']') |
| 61 | injects = injects.slice(0, injectEndIndex) |
| 62 | injectObject += injects.split(',').map(inject => `${inject}: {from: ${inject}}`).join(',') |
| 63 | return `${code.slice(0, injectIndex - 1)}{\n${injectObject}\n}${code.slice(injectIndex + injectEndIndex + 1)}` |
| 64 | } |
| 65 | else { |
| 66 | // inject option in object |
| 67 | return `${code.slice(0, injectIndex)}\n${injectObject}\n${code.slice(injectIndex)}` |
| 68 | } |
| 69 | } |
| 70 | // add inject option |
| 71 | return `${code.slice(0, injectIndex)}\ninject: { ${injectObject} },\n${code.slice(injectIndex)}` |
| 72 | } |
| 73 | } |
| 74 | // no setup script and not a vue component |
| 75 | return `<script setup>\n${imports.join('\n')}\n</script>\n${code}` |
| 76 | }, |
| 77 | }, |
| 78 | } |
| 79 | } |
| 80 |