| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- import { parse } from '../src'
- import {
- ElementTypes,
- NodeTypes,
- baseCompile,
- createRoot,
- } from '@vue/compiler-core'
- import { SourceMapConsumer } from 'source-map-js'
- describe('compiler:sfc', () => {
- describe('source map', () => {
- test('style block', () => {
- // Padding determines how many blank lines will there be before the style block
- const padding = Math.round(Math.random() * 10)
- const src =
- `${'\n'.repeat(padding)}` +
- `<style>
- .css {
- color: red;
- }
- </style>
- <style module>
- .css-module {
- color: red;
- }
- </style>
- <style scoped>
- .css-scoped {
- color: red;
- }
- </style>
- <style scoped>
- .css-scoped-nested {
- color: red;
- .dummy {
- color: green;
- }
- font-weight: bold;
- }
- </style>`
- const {
- descriptor: { styles },
- } = parse(src)
- expect(styles[0].map).not.toBeUndefined()
- const consumer = new SourceMapConsumer(styles[0].map!)
- const lineOffset =
- src.slice(0, src.indexOf(`<style>`)).split('\n').length - 1
- consumer.eachMapping(mapping => {
- expect(mapping.generatedLine + lineOffset).toBe(mapping.originalLine)
- })
- expect(styles[1].map).not.toBeUndefined()
- const consumer1 = new SourceMapConsumer(styles[1].map!)
- const lineOffset1 =
- src.slice(0, src.indexOf(`<style module>`)).split('\n').length - 1
- consumer1.eachMapping(mapping => {
- expect(mapping.generatedLine + lineOffset1).toBe(mapping.originalLine)
- })
- expect(styles[2].map).not.toBeUndefined()
- const consumer2 = new SourceMapConsumer(styles[2].map!)
- const lineOffset2 =
- src.slice(0, src.indexOf(`<style scoped>`)).split('\n').length - 1
- consumer2.eachMapping(mapping => {
- expect(mapping.generatedLine + lineOffset2).toBe(mapping.originalLine)
- })
- })
- test('script block', () => {
- // Padding determines how many blank lines will there be before the style block
- const padding = Math.round(Math.random() * 10)
- const script = parse(
- `${'\n'.repeat(padding)}<script>\nconsole.log(1)\n }\n</script>\n`,
- ).descriptor.script
- expect(script!.map).not.toBeUndefined()
- const consumer = new SourceMapConsumer(script!.map!)
- consumer.eachMapping(mapping => {
- expect(mapping.originalLine! - mapping.generatedLine).toBe(padding)
- })
- })
- test('template block with lang + indent', () => {
- // Padding determines how many blank lines will there be before the style block
- const padding = Math.round(Math.random() * 10)
- const template = parse(
- `${'\n'.repeat(padding)}<template lang="pug">
- h1 foo
- div bar
- span baz
- </template>\n`,
- ).descriptor.template!
- expect(template.map).not.toBeUndefined()
- const consumer = new SourceMapConsumer(template.map!)
- consumer.eachMapping(mapping => {
- expect(mapping.originalLine! - mapping.generatedLine).toBe(padding)
- expect(mapping.originalColumn! - mapping.generatedColumn).toBe(2)
- })
- })
- test('custom block', () => {
- const padding = Math.round(Math.random() * 10)
- const custom = parse(
- `${'\n'.repeat(padding)}<i18n>\n{\n "greeting": "hello"\n}\n</i18n>\n`,
- ).descriptor.customBlocks[0]
- expect(custom!.map).not.toBeUndefined()
- const consumer = new SourceMapConsumer(custom!.map!)
- consumer.eachMapping(mapping => {
- expect(mapping.originalLine! - mapping.generatedLine).toBe(padding)
- })
- })
- })
- test('pad content', () => {
- const content = `
- <template>
- <div></div>
- </template>
- <script>
- export default {}
- </script>
- <style>
- h1 { color: red }
- </style>
- <i18n>
- { "greeting": "hello" }
- </i18n>
- `
- const padFalse = parse(content.trim(), { pad: false }).descriptor
- expect(padFalse.template!.content).toBe('\n<div></div>\n')
- expect(padFalse.script!.content).toBe('\nexport default {}\n')
- expect(padFalse.styles[0].content).toBe('\nh1 { color: red }\n')
- expect(padFalse.customBlocks[0].content).toBe('\n{ "greeting": "hello" }\n')
- const padTrue = parse(content.trim(), { pad: true }).descriptor
- expect(padTrue.script!.content).toBe(
- Array(3 + 1).join('//\n') + '\nexport default {}\n',
- )
- expect(padTrue.styles[0].content).toBe(
- Array(6 + 1).join('\n') + '\nh1 { color: red }\n',
- )
- expect(padTrue.customBlocks[0].content).toBe(
- Array(9 + 1).join('\n') + '\n{ "greeting": "hello" }\n',
- )
- const padLine = parse(content.trim(), { pad: 'line' }).descriptor
- expect(padLine.script!.content).toBe(
- Array(3 + 1).join('//\n') + '\nexport default {}\n',
- )
- expect(padLine.styles[0].content).toBe(
- Array(6 + 1).join('\n') + '\nh1 { color: red }\n',
- )
- expect(padLine.customBlocks[0].content).toBe(
- Array(9 + 1).join('\n') + '\n{ "greeting": "hello" }\n',
- )
- const padSpace = parse(content.trim(), { pad: 'space' }).descriptor
- expect(padSpace.script!.content).toBe(
- `<template>\n<div></div>\n</template>\n<script>`.replace(/./g, ' ') +
- '\nexport default {}\n',
- )
- expect(padSpace.styles[0].content).toBe(
- `<template>\n<div></div>\n</template>\n<script>\nexport default {}\n</script>\n<style>`.replace(
- /./g,
- ' ',
- ) + '\nh1 { color: red }\n',
- )
- expect(padSpace.customBlocks[0].content).toBe(
- `<template>\n<div></div>\n</template>\n<script>\nexport default {}\n</script>\n<style>\nh1 { color: red }\n</style>\n<i18n>`.replace(
- /./g,
- ' ',
- ) + '\n{ "greeting": "hello" }\n',
- )
- })
- test('should parse correct range for root level self closing tag', () => {
- const content = `\n <div/>\n`
- const { descriptor } = parse(`<template>${content}</template>`)
- expect(descriptor.template).toBeTruthy()
- expect(descriptor.template!.content).toBe(content)
- expect(descriptor.template!.loc).toMatchObject({
- start: { line: 1, column: 11, offset: 10 },
- end: {
- line: 3,
- column: 1,
- offset: 10 + content.length,
- },
- })
- })
- test('should parse correct range for blocks with no content (self closing)', () => {
- const { descriptor } = parse(`<template/>`)
- expect(descriptor.template).toBeTruthy()
- expect(descriptor.template!.content).toBeFalsy()
- expect(descriptor.template!.loc).toMatchObject({
- start: { line: 1, column: 12, offset: 11 },
- end: { line: 1, column: 12, offset: 11 },
- })
- })
- test('should parse correct range for blocks with no content (explicit)', () => {
- const { descriptor } = parse(`<template></template>`)
- expect(descriptor.template).toBeTruthy()
- expect(descriptor.template!.content).toBeFalsy()
- expect(descriptor.template!.loc).toMatchObject({
- start: { line: 1, column: 11, offset: 10 },
- end: { line: 1, column: 11, offset: 10 },
- })
- })
- test('should ignore other nodes with no content', () => {
- expect(parse(`<script/>`).descriptor.script).toBe(null)
- expect(parse(`<script> \n\t </script>`).descriptor.script).toBe(null)
- expect(parse(`<style/>`).descriptor.styles.length).toBe(0)
- expect(parse(`<style> \n\t </style>`).descriptor.styles.length).toBe(0)
- expect(parse(`<custom/>`).descriptor.customBlocks.length).toBe(0)
- expect(
- parse(`<custom> \n\t </custom>`).descriptor.customBlocks.length,
- ).toBe(0)
- })
- test('handle empty nodes with src attribute', () => {
- const { descriptor } = parse(`<script src="com"/>`)
- expect(descriptor.script).toBeTruthy()
- expect(descriptor.script!.content).toBeFalsy()
- expect(descriptor.script!.attrs['src']).toBe('com')
- })
- test('should not expose ast on template node if has src import', () => {
- const { descriptor } = parse(`<template src="./foo.html"/>`)
- expect(descriptor.template!.ast).toBeUndefined()
- })
- test('ignoreEmpty: false', () => {
- const { descriptor } = parse(
- `<script></script>\n<script setup>\n</script>`,
- {
- ignoreEmpty: false,
- },
- )
- expect(descriptor.script).toBeTruthy()
- expect(descriptor.script!.loc).toMatchObject({
- start: { line: 1, column: 9, offset: 8 },
- end: { line: 1, column: 9, offset: 8 },
- })
- expect(descriptor.scriptSetup).toBeTruthy()
- expect(descriptor.scriptSetup!.loc).toMatchObject({
- start: { line: 2, column: 15, offset: 32 },
- end: { line: 3, column: 1, offset: 33 },
- })
- })
- test('nested templates', () => {
- const content = `
- <template v-if="ok">ok</template>
- <div><div></div></div>
- `
- const { descriptor } = parse(`<template>${content}</template>`)
- expect(descriptor.template!.content).toBe(content)
- })
- test('treat empty lang attribute as the html', () => {
- const content = `<div><template v-if="ok">ok</template></div>`
- const { descriptor, errors } = parse(
- `<template lang="">${content}</template>`,
- )
- expect(descriptor.template!.content).toBe(content)
- expect(errors.length).toBe(0)
- })
- // #1120
- test('template with preprocessor lang should be treated as plain text', () => {
- const content = `p(v-if="1 < 2") test <div/>`
- const { descriptor, errors } = parse(
- `<template lang="pug">` + content + `</template>`,
- )
- expect(errors.length).toBe(0)
- expect(descriptor.template!.content).toBe(content)
- // should not attempt to parse the content
- expect(descriptor.template!.ast!.children.length).toBe(1)
- })
- //#2566
- test('div lang should not be treated as plain text', () => {
- const { errors } = parse(`
- <template lang="pug">
- <div lang="">
- <div></div>
- </div>
- </template>
- `)
- expect(errors.length).toBe(0)
- })
- test('slotted detection', async () => {
- expect(parse(`<template>hi</template>`).descriptor.slotted).toBe(false)
- expect(
- parse(`<template>hi</template><style>h1{color:red;}</style>`).descriptor
- .slotted,
- ).toBe(false)
- expect(
- parse(
- `<template>hi</template><style scoped>:slotted(h1){color:red;}</style>`,
- ).descriptor.slotted,
- ).toBe(true)
- expect(
- parse(
- `<template>hi</template><style scoped>::v-slotted(h1){color:red;}</style>`,
- ).descriptor.slotted,
- ).toBe(true)
- })
- test('error tolerance', () => {
- const { errors } = parse(`<template>`)
- expect(errors.length).toBe(1)
- })
- test('should parse as DOM by default', () => {
- const { errors } = parse(`<template><input></template>`)
- expect(errors.length).toBe(0)
- })
- test('custom compiler', () => {
- const { errors } = parse(`<template><input></template>`, {
- compiler: {
- parse: (_, options) => {
- options.onError!(new Error('foo') as any)
- return createRoot([])
- },
- compile: baseCompile,
- },
- })
- expect(errors.length).toBe(2)
- // error thrown by the custom parse
- expect(errors[0].message).toBe('foo')
- // error thrown based on the returned root
- expect(errors[1].message).toMatch('At least one')
- })
- test('treat custom blocks as raw text', () => {
- const { errors, descriptor } = parse(
- `<template><input></template><foo> <-& </foo>`,
- )
- expect(errors.length).toBe(0)
- expect(descriptor.customBlocks[0].content).toBe(` <-& `)
- })
- test('should accept parser options', () => {
- const { errors, descriptor } = parse(`<template><hello/></template>`, {
- templateParseOptions: {
- isCustomElement: t => t === 'hello',
- },
- })
- expect(errors.length).toBe(0)
- expect(descriptor.template!.ast!.children[0]).toMatchObject({
- type: NodeTypes.ELEMENT,
- tag: 'hello',
- tagType: ElementTypes.ELEMENT,
- })
- // test cache invalidation on different options
- const { descriptor: d2 } = parse(`<template><hello/></template>`, {
- templateParseOptions: {
- isCustomElement: t => t !== 'hello',
- },
- })
- expect(d2.template!.ast!.children[0]).toMatchObject({
- type: NodeTypes.ELEMENT,
- tag: 'hello',
- tagType: ElementTypes.COMPONENT,
- })
- })
- describe('vapor mode', () => {
- test('on empty script', () => {
- const { descriptor } = parse(`<script vapor></script>`)
- expect(descriptor.vapor).toBe(true)
- })
- test('on template', () => {
- const { descriptor } = parse(`<template vapor><div/></template>`)
- expect(descriptor.vapor).toBe(true)
- })
- })
- describe('warnings', () => {
- function assertWarning(errors: Error[], msg: string) {
- expect(errors.some(e => e.message.match(msg))).toBe(true)
- }
- test('should only allow single template element', () => {
- assertWarning(
- parse(`<template><div/></template><template><div/></template>`).errors,
- `Single file component can contain only one <template> element`,
- )
- })
- test('should only allow single script element', () => {
- assertWarning(
- parse(`<script>console.log(1)</script><script>console.log(1)</script>`)
- .errors,
- `Single file component can contain only one <script> element`,
- )
- })
- test('should only allow single script setup element', () => {
- assertWarning(
- parse(
- `<script setup>console.log(1)</script><script setup>console.log(1)</script>`,
- ).errors,
- `Single file component can contain only one <script setup> element`,
- )
- })
- test('should not warn script & script setup', () => {
- expect(
- parse(
- `<script setup>console.log(1)</script><script>console.log(1)</script>`,
- ).errors.length,
- ).toBe(0)
- })
- // # 6676
- test('should throw error if no <template> or <script> is present', () => {
- assertWarning(
- parse(`import { ref } from 'vue'`).errors,
- `At least one <template> or <script> is required in a single file component`,
- )
- })
- test('should throw error if template functional is given', () => {
- assertWarning(
- parse(`<template functional></template>`).errors,
- `<template functional> is no longer supported in Vue 3, since ` +
- `functional components no longer have significant performance ` +
- `difference from stateful ones. Just use a normal <template> ` +
- `instead.`,
- )
- })
- })
- })
|