|
| 1 | +import type { SFCBlock } from '@vue/compiler-sfc' |
1 | 2 | import type { TransformOpts } from '.' |
2 | | -import { transform } from '@unovue/detypes' |
| 3 | +import { parse } from '@vue/compiler-sfc' |
| 4 | +import { transform } from 'esbuild' |
| 5 | +import MagicString from 'magic-string' |
| 6 | +import { format } from 'prettier' |
| 7 | +import { preTranspileScriptSetup, transpileVueTemplate } from 'vue-sfc-transformer' |
3 | 8 |
|
4 | 9 | export async function transformSFC(opts: TransformOpts) { |
5 | 10 | if (opts.config?.typescript) |
6 | 11 | return opts.raw |
7 | 12 |
|
8 | | - return await transformByDetype(opts.raw, opts.filename).then(res => res as string) |
| 13 | + return opts.filename.endsWith('.vue') |
| 14 | + ? await transformVueSFC(opts.raw, opts.filename) |
| 15 | + : await transformScript(opts.raw, opts.filename) |
9 | 16 | } |
10 | 17 |
|
11 | | -export async function transformByDetype(content: string, filename: string) { |
12 | | - return await transform(content, filename, { |
13 | | - removeTsComments: true, |
14 | | - prettierOptions: { |
15 | | - proseWrap: 'never', |
16 | | - }, |
| 18 | +export async function transformVueSFC(content: string, filename: string) { |
| 19 | + const { descriptor, errors } = parse(content, { |
| 20 | + filename, |
| 21 | + ignoreEmpty: true, |
17 | 22 | }) |
| 23 | + |
| 24 | + if (errors.length) { |
| 25 | + throw new Error(errors.map(error => typeof error === 'string' ? error : error.message).join('\n')) |
| 26 | + } |
| 27 | + |
| 28 | + const output = new MagicString(content) |
| 29 | + const isTypeScriptSFC = [descriptor.script, descriptor.scriptSetup] |
| 30 | + .some(block => block?.lang === 'ts' || block?.lang === 'tsx') |
| 31 | + |
| 32 | + if (isTypeScriptSFC && descriptor.template?.ast) { |
| 33 | + const template = await transpileVueTemplate( |
| 34 | + descriptor.template.content, |
| 35 | + descriptor.template.ast as Parameters<typeof transpileVueTemplate>[1], |
| 36 | + descriptor.template.loc.start.offset, |
| 37 | + async code => await stripTypeScript(code, 'ts'), |
| 38 | + ) |
| 39 | + replaceBlockContent(output, content, descriptor.template, template) |
| 40 | + } |
| 41 | + |
| 42 | + if (descriptor.script?.lang === 'ts' || descriptor.script?.lang === 'tsx') { |
| 43 | + const script = await stripTypeScript(descriptor.script.content, descriptor.script.lang) |
| 44 | + replaceBlockContent(output, content, descriptor.script, script, true) |
| 45 | + } |
| 46 | + |
| 47 | + if (descriptor.scriptSetup?.lang === 'ts' || descriptor.scriptSetup?.lang === 'tsx') { |
| 48 | + const preTranspiled = await preTranspileScriptSetup( |
| 49 | + descriptor as Parameters<typeof preTranspileScriptSetup>[0], |
| 50 | + filename, |
| 51 | + ) |
| 52 | + const scriptSetup = await stripTypeScript(preTranspiled.content, descriptor.scriptSetup.lang) |
| 53 | + replaceBlockContent(output, content, descriptor.scriptSetup, scriptSetup, true) |
| 54 | + } |
| 55 | + |
| 56 | + return await format(output.toString(), { |
| 57 | + parser: 'vue', |
| 58 | + proseWrap: 'never', |
| 59 | + }) |
| 60 | +} |
| 61 | + |
| 62 | +async function transformScript(content: string, filename: string) { |
| 63 | + const loader = getScriptLoader(filename) |
| 64 | + |
| 65 | + if (!loader) |
| 66 | + return content |
| 67 | + |
| 68 | + const output = await stripTypeScript(content, loader) |
| 69 | + |
| 70 | + return await format(output, { |
| 71 | + parser: loader === 'ts' || loader === 'tsx' ? 'typescript' : 'babel', |
| 72 | + proseWrap: 'never', |
| 73 | + }) |
| 74 | +} |
| 75 | + |
| 76 | +function getScriptLoader(filename: string) { |
| 77 | + if (filename.endsWith('.tsx')) |
| 78 | + return 'tsx' as const |
| 79 | + if (filename.endsWith('.jsx')) |
| 80 | + return 'jsx' as const |
| 81 | + if (['.js', '.cjs', '.mjs'].some(extension => filename.endsWith(extension))) |
| 82 | + return 'js' as const |
| 83 | + if (['.ts', '.cts', '.mts'].some(extension => filename.endsWith(extension))) |
| 84 | + return 'ts' as const |
| 85 | +} |
| 86 | + |
| 87 | +async function stripTypeScript(content: string, loader: 'js' | 'jsx' | 'ts' | 'tsx') { |
| 88 | + const result = await transform(content, { |
| 89 | + loader, |
| 90 | + target: 'esnext', |
| 91 | + legalComments: 'none', |
| 92 | + }) |
| 93 | + |
| 94 | + return result.code.trimEnd() |
| 95 | +} |
| 96 | + |
| 97 | +function replaceBlockContent( |
| 98 | + output: MagicString, |
| 99 | + source: string, |
| 100 | + block: SFCBlock, |
| 101 | + content: string, |
| 102 | + removeTypeScriptLang = false, |
| 103 | +) { |
| 104 | + output.overwrite(block.loc.start.offset, block.loc.end.offset, content) |
| 105 | + |
| 106 | + if (!removeTypeScriptLang) |
| 107 | + return |
| 108 | + |
| 109 | + const blockStart = source.lastIndexOf(`<${block.type}`, block.loc.start.offset) |
| 110 | + const openTag = source.slice(blockStart, block.loc.start.offset) |
| 111 | + const transformedOpenTag = openTag.replace(/\s+lang=(?:"tsx?"|'tsx?'|tsx?)(?=\s|>)/, '') |
| 112 | + |
| 113 | + output.overwrite(blockStart, block.loc.start.offset, transformedOpenTag) |
18 | 114 | } |
0 commit comments