Skip to content

Commit 2f4ec5b

Browse files
committed
fix: throw errors when parsing invalid sfcs
1 parent f0e307f commit 2f4ec5b

File tree

2 files changed

+280
-278
lines changed

2 files changed

+280
-278
lines changed

src/mkdist.ts

Lines changed: 1 addition & 278 deletions
Original file line numberDiff line numberDiff line change
@@ -1,278 +1 @@
1-
import type { SFCBlock, SFCTemplateBlock } from 'vue/compiler-sfc'
2-
import type { InputFile, Loader, LoaderContext, LoaderResult, OutputFile } from './types/mkdist'
3-
import process from 'node:process'
4-
import { transform } from 'esbuild'
5-
import { preTranspileScriptSetup, transpileVueTemplate } from './index'
6-
7-
interface DefineVueLoaderOptions {
8-
blockLoaders?: {
9-
[blockType: string]: VueBlockLoader | undefined
10-
}
11-
}
12-
13-
type VueBlockOutput = Pick<SFCBlock, 'type' | 'content' | 'attrs'>
14-
15-
interface VueBlockLoaderContext extends LoaderContext {
16-
requireTranspileTemplate: boolean
17-
rawInput: InputFile
18-
addOutput: (...files: OutputFile[]) => void
19-
}
20-
21-
interface VueBlockLoader {
22-
(
23-
block: SFCBlock,
24-
context: VueBlockLoaderContext,
25-
): Promise<VueBlockOutput | undefined>
26-
}
27-
28-
interface DefaultBlockLoaderOptions {
29-
type: 'script' | 'style' | 'template'
30-
defaultLang: string
31-
validExtensions?: string[]
32-
}
33-
34-
function defineVueLoader(options?: DefineVueLoaderOptions): Loader {
35-
const blockLoaders = options?.blockLoaders || {}
36-
37-
return async (input, context) => {
38-
if (input.extension !== '.vue') {
39-
return
40-
}
41-
42-
const { parse } = await import('vue/compiler-sfc')
43-
44-
let modified = false
45-
46-
const raw = await input.getContents()
47-
const sfc = parse(raw, {
48-
filename: input.srcPath,
49-
ignoreEmpty: true,
50-
})
51-
if (sfc.errors.length > 0) {
52-
for (const error of sfc.errors) {
53-
console.error(error)
54-
}
55-
return
56-
}
57-
58-
// we need to remove typescript from template block if the block is typescript
59-
const isTs = [sfc.descriptor.script, sfc.descriptor.scriptSetup].some(
60-
block => block?.lang === 'ts',
61-
)
62-
63-
const output: LoaderResult = []
64-
const addOutput = (...files: OutputFile[]) => output.push(...files)
65-
66-
const blocks: SFCBlock[] = [
67-
...sfc.descriptor.styles,
68-
...sfc.descriptor.customBlocks,
69-
].filter(item => !!item)
70-
71-
if (sfc.descriptor.template) {
72-
blocks.unshift(sfc.descriptor.template)
73-
}
74-
75-
if (sfc.descriptor.script) {
76-
blocks.unshift(sfc.descriptor.script)
77-
}
78-
if (sfc.descriptor.scriptSetup && input.srcPath) {
79-
blocks.unshift(
80-
isTs
81-
? await preTranspileScriptSetup(sfc.descriptor, input.srcPath)
82-
: sfc.descriptor.scriptSetup,
83-
)
84-
}
85-
86-
// generate dts
87-
const files = await context.loadFile({
88-
path: `${input.path}.js`,
89-
srcPath: `${input.srcPath}.js`,
90-
extension: '.js',
91-
getContents: () => 'export default {}',
92-
})
93-
addOutput(...files?.filter(f => f.declaration) || [])
94-
95-
const results = await Promise.all(
96-
blocks.map(async (data) => {
97-
const blockLoader = blockLoaders[data.type]
98-
const result = await blockLoader?.(data, {
99-
...context,
100-
rawInput: input,
101-
addOutput,
102-
requireTranspileTemplate: isTs,
103-
})
104-
if (result) {
105-
modified = true
106-
}
107-
return { block: result || data, offset: data.loc.start.offset }
108-
}),
109-
)
110-
111-
if (!modified) {
112-
addOutput({
113-
path: input.path,
114-
srcPath: input.srcPath,
115-
extension: '.vue',
116-
contents: raw,
117-
declaration: false,
118-
})
119-
return output
120-
}
121-
122-
const contents = results
123-
.sort((a, b) => a.offset - b.offset)
124-
.map(({ block }) => {
125-
const attrs = Object.entries(block.attrs)
126-
.map(([key, value]) => {
127-
if (!value) {
128-
return undefined
129-
}
130-
131-
return value === true ? key : `${key}="${value}"`
132-
})
133-
.filter(item => !!item)
134-
.join(' ')
135-
136-
const header = `<${`${block.type} ${attrs}`.trim()}>`
137-
const footer = `</${block.type}>`
138-
139-
return `${header}\n${cleanupBreakLine(block.content)}\n${footer}\n`
140-
})
141-
.filter(item => !!item)
142-
.join('\n')
143-
144-
// @ts-expect-error internal flag for testing
145-
if (context.options._verify || process.env.VERIFY_VUE_FILES) {
146-
// verify the output
147-
const { parse } = await import('vue/compiler-sfc')
148-
const { errors } = parse(contents, {
149-
filename: input.srcPath,
150-
ignoreEmpty: true,
151-
})
152-
if (errors.length > 0) {
153-
for (const error of errors) {
154-
console.error(error)
155-
}
156-
return
157-
}
158-
}
159-
160-
addOutput({
161-
path: input.path,
162-
srcPath: input.srcPath,
163-
extension: '.vue',
164-
contents,
165-
declaration: false,
166-
})
167-
168-
return output
169-
}
170-
}
171-
172-
function defineDefaultBlockLoader(options: DefaultBlockLoaderOptions): VueBlockLoader {
173-
return async (block, { loadFile, rawInput, addOutput }) => {
174-
if (options.type !== block.type) {
175-
return
176-
}
177-
178-
const lang = typeof block.attrs.lang === 'string' ? block.attrs.lang : options.defaultLang
179-
const extension = `.${lang}`
180-
181-
const files = await loadFile({
182-
getContents: () => block.content,
183-
path: `${rawInput.path}${extension}`,
184-
srcPath: `${rawInput.srcPath}${extension}`,
185-
extension,
186-
}) || []
187-
188-
const blockOutputFile = files.find(f =>
189-
f.extension === `.${options.defaultLang}` || options.validExtensions?.includes(f.extension as string),
190-
)
191-
if (!blockOutputFile) {
192-
return
193-
}
194-
addOutput(...files.filter(f => f !== blockOutputFile))
195-
196-
return {
197-
type: block.type,
198-
attrs: toOmit(block.attrs, ['lang', 'generic']),
199-
content: blockOutputFile.contents!,
200-
}
201-
}
202-
}
203-
204-
const templateLoader: VueBlockLoader = async (
205-
rawBlock,
206-
{ requireTranspileTemplate, loadFile, rawInput },
207-
) => {
208-
if (rawBlock.type !== 'template') {
209-
return
210-
}
211-
212-
if (!requireTranspileTemplate) {
213-
return
214-
}
215-
216-
const block = rawBlock as SFCTemplateBlock
217-
218-
const transformed = await transpileVueTemplate(
219-
// for lower version of @vue/compiler-sfc, `ast.source` is the whole .vue file
220-
block.content,
221-
block.ast!,
222-
block.loc.start.offset,
223-
async (code) => {
224-
const res = await loadFile({
225-
getContents: () => code,
226-
path: `${rawInput.path}.ts`,
227-
srcPath: `${rawInput.srcPath}.ts`,
228-
extension: '.ts',
229-
})
230-
231-
return res?.find(f => (['.js', '.mjs', '.cjs'] as Array<string | undefined>).includes(f.extension))?.contents || code
232-
},
233-
)
234-
235-
return {
236-
type: 'template',
237-
content: transformed,
238-
attrs: block.attrs,
239-
}
240-
}
241-
242-
const styleLoader = defineDefaultBlockLoader({
243-
defaultLang: 'css',
244-
type: 'style',
245-
})
246-
247-
const scriptLoader: VueBlockLoader = async (block, { options }) => {
248-
if (block.type !== 'script') {
249-
return
250-
}
251-
252-
const { code: result } = await transform(block.content, {
253-
...options.esbuild,
254-
loader: 'ts',
255-
tsconfigRaw: { compilerOptions: { target: 'ESNext', verbatimModuleSyntax: true } },
256-
})
257-
258-
return {
259-
type: block.type,
260-
attrs: toOmit(block.attrs, ['lang', 'generic']),
261-
content: result,
262-
}
263-
}
264-
265-
export const vueLoader = defineVueLoader({
266-
blockLoaders: {
267-
script: scriptLoader,
268-
template: templateLoader,
269-
style: styleLoader,
270-
},
271-
})
272-
273-
function cleanupBreakLine(str: string): string {
274-
return str.replaceAll(/(\n\n)\n+/g, '\n\n').replace(/^\s*\n|\n\s*$/g, '')
275-
}
276-
function toOmit<R extends Record<keyof object, unknown>, K extends keyof R>(record: R, toRemove: K[]): Omit<R, K> {
277-
return Object.fromEntries(Object.entries(record).filter(([key]) => !toRemove.includes(key as K))) as Omit<R, K>
278-
}
1+
export { vueLoader } from './utils/mkdist'

0 commit comments

Comments
 (0)