UNPKG

vue-code-transformer

Version:

Codemod tool for Vue projects, built using jscodeshift, postcss, and vue-eslint-parser

275 lines (258 loc) 10.4 kB
import CompilerDom from "@vue/compiler-dom"; import VueCompilerCorePkg from "@vue/compiler-core"; const { NodeTypes, TextModes } = VueCompilerCorePkg; function stringify(sfcDescriptor) {     const { template, script, scriptSetup, styles, customBlocks } = sfcDescriptor;     const NEWLINE_OFFSET = -2;     // add newline to custom blocks which does not have newline     for (const block of customBlocks) {         if (block.loc.source.slice(NEWLINE_OFFSET) !== '\n') {             block.loc.source = block.loc.source + '\n';         }     }     return (         [template, script, scriptSetup, ...styles, ...customBlocks]             .filter(block => block !== null)             .sort((a, b) => a.loc.start.offset - b.loc.start.offset)             .map(block => {                 const openTag = createOpenTag(block);                 const closeTag = createCloseTag(block);                 const startOfOpenTag = block.loc.start.offset - openTag.length;                 const endOfOpenTag = block.loc.start.offset;                 const startOfCloseTag = block.loc.end.offset;                 const endOfCloseTag = block.loc.end.offset + closeTag.length;                 return {                     ...block,                     openTag,                     closeTag,                     startOfOpenTag,                     endOfOpenTag,                     startOfCloseTag,                     endOfCloseTag                 };             })             // generate sfc source             .reduce((sfcCode, block, index, array) => {                 if (block.openTag === '') {                     return sfcCode + block.loc.source;                 } else {                     return sfcCode + block.openTag + block.content + block.closeTag;                 }             }, '')     ); } function createOpenTag(block) {     if (!block.attrs) {         return '';     }     let source = '<' + block.type;     source += Object.keys(block.attrs)         .sort()         .map(attrName => {             const value = block.attrs[attrName];             if (value === true) {                 return attrName;             } else {                 return `${attrName}="${value}"`;             }         })         .map(attr => ' ' + attr)         .join('');     return source + '>'; } function createCloseTag(block) {     if (!block.attrs) {         return '';     }     return `</${block.type}>\n`; } function parse(source, { filename = 'anonymous.vue', pad = false, compiler = CompilerDom } = {}) {     const errors = [];     const ast = getAst(compiler, source, errors);     const file = {         source,         filename     };     let descriptor = getDescriptor(ast, file, pad, errors);     descriptor = cleanIncorrectScriptUsage(descriptor, errors);     const result = {         descriptor,         errors     };     return result; } function getAst(compiler, source, errors) {     return compiler.parse(source, { // forces all tags (e.g. <div>, <custom_component>) to be treated as native HTML tags, not vue components         isNativeTag: () => true, // preserves all whitespaces isPreTag: () => true,         getTextMode: ({ tag, props }, parent) => { // all top level elements except <template> are parsed as raw text containers // <template lang="xxx"> where "xxx" is not "html" should be treated as raw text             if (                 (!parent && tag !== 'template') ||                 (tag === 'template' && props.some(prop => prop.name === 'lang' && prop.value && prop.value.content !== 'html'))             ) {                 return TextModes.RAWTEXT;             }             return TextModes.DATA;         },         onError: err => {             errors.push(err);         }     }); } function getDescriptor(ast, file, pad, errors) {     const { source, filename } = file;     const descriptor = {         source,         filename,         customBlocks: [],         template: null,         script: null,         scriptSetup: null,         styles: []     };     ast.children.forEach(node => {         if (node.type === NodeTypes.ELEMENT) {             if (!node.children.length && !hasSrc(node) && node.tag !== 'template') {                 return;             }             switch (node.tag) {                 case 'template': {                     if (!descriptor.template) {                         const templateBlock = (descriptor.template = createBlock(node, source, pad));                         templateBlock.ast = node;                     } else {                         errors.push(createDuplicateBlockError(node));                     }                     break;                 }                 case 'style': {                     const styleBlock = createBlock(node, source, pad);                     if (styleBlock.attrs.vars) {                         errors.push(new SyntaxError('<style vars> cannot be used.'));                     }                     descriptor.styles.push(styleBlock);                     break;                 }                 case 'script': {                     const scriptBlock = createBlock(node, source, pad);                     const isSetup = !!scriptBlock.attrs.setup;                     if (!isSetup && !descriptor.script) {                         descriptor.script = scriptBlock;                         break;                     }                     if (isSetup && !descriptor.scriptSetup) {                         descriptor.scriptSetup = scriptBlock;                         break;                     }                     errors.push(createDuplicateBlockError(node, isSetup));                     break;                 }                 default:                     descriptor.customBlocks.push(createBlock(node, source, pad));                     break;             }         } else if (node.type === NodeTypes.COMMENT) {             descriptor.customBlocks.push(node);         }     });     return descriptor; } function cleanIncorrectScriptUsage(descriptor, errors) {     if (descriptor.scriptSetup) {         if (descriptor.scriptSetup.src) {             errors.push(new SyntaxError('<script setup> cannot use the "src" attribute because its syntax will be ambiguous outside of the component.'));             descriptor.scriptSetup = null;         }         if (descriptor.script && descriptor.script.src) {             errors.push(                 new SyntaxError('<script> cannot use the "src" attribute when <script setup> is also present because they must be processed together.')             );             descriptor.script = null;         }     }     return descriptor; } function createDuplicateBlockError(node, isScriptSetup = false) {     let err;     if (isScriptSetup === true) {         err = new SyntaxError('Single file component can contain only one <' + node.tag + ' setup> element');     } else {         err = new SyntaxError('Single file component can contain only one <' + node.tag + '> element');     }     err.loc = node.loc;     return err; } function createBlock(node, source, pad) {     const type = node.tag;     let content = '';     let { start, end } = node.loc;     const childrenLength = node.children.length;     if (childrenLength > 0) {         start = node.children[0].loc.start;         end = node.children[childrenLength - 1].loc.end;         content = source.slice(start.offset, end.offset);     }     const loc = {         source: content,         start: start,         end: end     };     const attrs = {};     const block = {         type: type,         content: content,         loc: loc,         attrs: attrs     };     if (pad) {         block.content = padContent(source, block, pad) + block.content;     }     node.props.forEach(prop => {         if (prop.type === NodeTypes.ATTRIBUTE) {             attrs[prop.name] = prop.value ? prop.value.content || true : true;             if (prop.name === 'lang') {                 block.lang = prop.value && prop.value.content;             } else if (prop.name === 'src') {                 block.src = prop.value && prop.value.content;             } else if (type === 'script' && prop.name === 'setup') {                 block.setup = attrs.setup;             } else if (type === 'style') {                 if (prop.name === 'scoped') {                     block.scoped = true;                 } else if (prop.name === 'module') {                     block.module = attrs[prop.name];                 }             }         }     });     return block; } function padContent(content, block, pad) {     const splitRE = /\r?\n/g;     const replaceRE = /./g;     content = content.slice(0, block.loc.start.offset);     if (pad === 'space') {         content = content.replace(replaceRE, ' ');     } else {         const offset = content.split(splitRE).length;         const padChar = block.type === 'script' && !block.lang ? '//\n' : '\n';         content = Array(offset).join(padChar);     }     return content; } function hasSrc(node) {     return node.props.some(p => {         if (p.type !== NodeTypes.ATTRIBUTE) {             return false;         }         return p.name === 'src';     }); } export {     stringify,     parse };