|
| 1 | +import { store, File } from './store' |
| 2 | +import { |
| 3 | + parse, |
| 4 | + compileTemplate, |
| 5 | + compileStyleAsync, |
| 6 | + compileScript, |
| 7 | + rewriteDefault, |
| 8 | + SFCDescriptor, |
| 9 | + BindingMetadata |
| 10 | +} from '@vue/compiler-sfc' |
| 11 | + |
| 12 | +export const MAIN_FILE = 'App.vue' |
| 13 | +export const COMP_IDENTIFIER = `__sfc__` |
| 14 | + |
| 15 | +// @ts-ignore |
| 16 | +export const SANDBOX_VUE_URL = import.meta.env.PROD |
| 17 | + ? '/vue.runtime.esm-browser.js' // to be copied on build |
| 18 | + : '/src/vue-dev-proxy' |
| 19 | + |
| 20 | +export async function compileFile({ filename, code, compiled }: File) { |
| 21 | + if (!code.trim()) { |
| 22 | + return |
| 23 | + } |
| 24 | + |
| 25 | + if (filename.endsWith('.js')) { |
| 26 | + compiled.js = compiled.ssr = code |
| 27 | + return |
| 28 | + } |
| 29 | + |
| 30 | + const id = await hashId(filename) |
| 31 | + const { errors, descriptor } = parse(code, { filename, sourceMap: true }) |
| 32 | + if (errors.length) { |
| 33 | + store.errors = errors |
| 34 | + return |
| 35 | + } |
| 36 | + |
| 37 | + if ( |
| 38 | + (descriptor.script && descriptor.script.lang) || |
| 39 | + (descriptor.scriptSetup && descriptor.scriptSetup.lang) || |
| 40 | + descriptor.styles.some(s => s.lang) || |
| 41 | + (descriptor.template && descriptor.template.lang) |
| 42 | + ) { |
| 43 | + store.errors = [ |
| 44 | + 'lang="x" pre-processors are not supported in the in-browser playground.' |
| 45 | + ] |
| 46 | + return |
| 47 | + } |
| 48 | + |
| 49 | + const hasScoped = descriptor.styles.some(s => s.scoped) |
| 50 | + let clientCode = '' |
| 51 | + let ssrCode = '' |
| 52 | + |
| 53 | + const appendSharedCode = (code: string) => { |
| 54 | + clientCode += code |
| 55 | + ssrCode += code |
| 56 | + } |
| 57 | + |
| 58 | + const clientScriptResult = doCompileScript(descriptor, id, false) |
| 59 | + if (!clientScriptResult) { |
| 60 | + return |
| 61 | + } |
| 62 | + const [clientScript, bindings] = clientScriptResult |
| 63 | + clientCode += clientScript |
| 64 | + |
| 65 | + // script ssr only needs to be performed if using <script setup> where |
| 66 | + // the render fn is inlined. |
| 67 | + if (descriptor.scriptSetup) { |
| 68 | + const ssrScriptResult = doCompileScript(descriptor, id, true) |
| 69 | + if (!ssrScriptResult) { |
| 70 | + return |
| 71 | + } |
| 72 | + ssrCode += ssrScriptResult[0] |
| 73 | + } else { |
| 74 | + // when no <script setup> is used, the script result will be identical. |
| 75 | + ssrCode += clientScript |
| 76 | + } |
| 77 | + |
| 78 | + // template |
| 79 | + // only need dedicated compilation if not using <script setup> |
| 80 | + if (descriptor.template && !descriptor.scriptSetup) { |
| 81 | + const clientTemplateResult = doCompileTemplate( |
| 82 | + descriptor, |
| 83 | + id, |
| 84 | + bindings, |
| 85 | + false |
| 86 | + ) |
| 87 | + if (!clientTemplateResult) { |
| 88 | + return |
| 89 | + } |
| 90 | + clientCode += clientTemplateResult |
| 91 | + |
| 92 | + const ssrTemplateResult = doCompileTemplate(descriptor, id, bindings, true) |
| 93 | + if (!ssrTemplateResult) { |
| 94 | + return |
| 95 | + } |
| 96 | + ssrCode += ssrTemplateResult |
| 97 | + } |
| 98 | + |
| 99 | + if (hasScoped) { |
| 100 | + appendSharedCode( |
| 101 | + `\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(`data-v-${id}`)}` |
| 102 | + ) |
| 103 | + } |
| 104 | + |
| 105 | + if (clientCode || ssrCode) { |
| 106 | + appendSharedCode( |
| 107 | + `\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}` + |
| 108 | + `\nexport default ${COMP_IDENTIFIER}` |
| 109 | + ) |
| 110 | + compiled.js = clientCode.trimStart() |
| 111 | + compiled.ssr = ssrCode.trimStart() |
| 112 | + } |
| 113 | + |
| 114 | + // styles |
| 115 | + let css = '' |
| 116 | + for (const style of descriptor.styles) { |
| 117 | + if (style.module) { |
| 118 | + // TODO error |
| 119 | + continue |
| 120 | + } |
| 121 | + |
| 122 | + const styleResult = await compileStyleAsync({ |
| 123 | + source: style.content, |
| 124 | + filename, |
| 125 | + id, |
| 126 | + scoped: style.scoped, |
| 127 | + modules: !!style.module |
| 128 | + }) |
| 129 | + if (styleResult.errors.length) { |
| 130 | + // postcss uses pathToFileURL which isn't polyfilled in the browser |
| 131 | + // ignore these errors for now |
| 132 | + if (!styleResult.errors[0].message.includes('pathToFileURL')) { |
| 133 | + store.errors = styleResult.errors |
| 134 | + } |
| 135 | + // proceed even if css compile errors |
| 136 | + } else { |
| 137 | + css += styleResult.code + '\n' |
| 138 | + } |
| 139 | + } |
| 140 | + if (css) { |
| 141 | + compiled.css = css.trim() |
| 142 | + } else { |
| 143 | + compiled.css = '/* No <style> tags present */' |
| 144 | + } |
| 145 | + |
| 146 | + // clear errors |
| 147 | + store.errors = [] |
| 148 | +} |
| 149 | + |
| 150 | +function doCompileScript( |
| 151 | + descriptor: SFCDescriptor, |
| 152 | + id: string, |
| 153 | + ssr: boolean |
| 154 | +): [string, BindingMetadata | undefined] | undefined { |
| 155 | + if (descriptor.script || descriptor.scriptSetup) { |
| 156 | + try { |
| 157 | + const compiledScript = compileScript(descriptor, { |
| 158 | + id, |
| 159 | + refSugar: true, |
| 160 | + inlineTemplate: true, |
| 161 | + templateOptions: { |
| 162 | + ssr, |
| 163 | + ssrCssVars: descriptor.cssVars |
| 164 | + } |
| 165 | + }) |
| 166 | + let code = '' |
| 167 | + if (compiledScript.bindings) { |
| 168 | + code += `\n/* Analyzed bindings: ${JSON.stringify( |
| 169 | + compiledScript.bindings, |
| 170 | + null, |
| 171 | + 2 |
| 172 | + )} */` |
| 173 | + } |
| 174 | + code += `\n` + rewriteDefault(compiledScript.content, COMP_IDENTIFIER) |
| 175 | + return [code, compiledScript.bindings] |
| 176 | + } catch (e) { |
| 177 | + store.errors = [e] |
| 178 | + return |
| 179 | + } |
| 180 | + } else { |
| 181 | + return [`\nconst ${COMP_IDENTIFIER} = {}`, undefined] |
| 182 | + } |
| 183 | +} |
| 184 | + |
| 185 | +function doCompileTemplate( |
| 186 | + descriptor: SFCDescriptor, |
| 187 | + id: string, |
| 188 | + bindingMetadata: BindingMetadata | undefined, |
| 189 | + ssr: boolean |
| 190 | +) { |
| 191 | + const templateResult = compileTemplate({ |
| 192 | + source: descriptor.template!.content, |
| 193 | + filename: descriptor.filename, |
| 194 | + id, |
| 195 | + scoped: descriptor.styles.some(s => s.scoped), |
| 196 | + slotted: descriptor.slotted, |
| 197 | + ssr, |
| 198 | + ssrCssVars: descriptor.cssVars, |
| 199 | + isProd: false, |
| 200 | + compilerOptions: { |
| 201 | + bindingMetadata |
| 202 | + } |
| 203 | + }) |
| 204 | + if (templateResult.errors.length) { |
| 205 | + store.errors = templateResult.errors |
| 206 | + return |
| 207 | + } |
| 208 | + |
| 209 | + const fnName = ssr ? `ssrRender` : `render` |
| 210 | + |
| 211 | + return ( |
| 212 | + `\n${templateResult.code.replace( |
| 213 | + /\nexport (function|const) (render|ssrRender)/, |
| 214 | + `$1 ${fnName}` |
| 215 | + )}` + `\n${COMP_IDENTIFIER}.${fnName} = ${fnName}` |
| 216 | + ) |
| 217 | +} |
| 218 | + |
| 219 | +async function hashId(filename: string) { |
| 220 | + const msgUint8 = new TextEncoder().encode(filename) // encode as (utf-8) Uint8Array |
| 221 | + const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) // hash the message |
| 222 | + const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array |
| 223 | + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string |
| 224 | + return hashHex.slice(0, 8) |
| 225 | +} |
0 commit comments