/** * {} * @author yutent * @date 2022/09/06 14:43:01 */ import fs from 'iofs' import scss from '@bytedo/sass' import Es from 'esbuild' import { compile } from '@vue/compiler-dom' import { red, cyan, blue } from 'kolorist' import { JS_EXP, STYLE_EXP, HTML_EXP, CSS_SHEET_EXP, V_DEEP, PERCENT_EXP, SHEETS_DEF, LEGACY_POLYFILL } from './constants.js' import { createHmrScript, md5, uuid, urlJoin } from './utils.js' const OPTIONS = { style: 'compressed' } function minify(code) { return Es.transformSync(code, { minify: true }).code.trim() } // 处理css中的 :deep() function parseVDeep(curr, val, scoped) { let res = V_DEEP.exec(curr) if (res) { scoped && (val = val.replace(/\[data\-[^\]]+\]/g, '')) return `${res[1] + res[2]} ${val}` } else { return `${curr} ${val}` } } // 处理style中的scoped属性 function scopeCss(css = '', hash) { return css.replace(CSS_SHEET_EXP, (m, selector) => { if (!selector.startsWith('@')) { selector = selector.split(',') selector = selector .map(s => { // 针对 @keyframe的处理 if (s === 'from' || s === 'to' || PERCENT_EXP.test(s)) { return s } let tmp = s.split(' ') let output = '' let last let scoped = false while ((last = tmp.pop())) { if (scoped) { if (last.startsWith(':')) { output = parseVDeep(last, output, true) } else { output = `${last} ${output}` } } else { if (last.includes(':')) { if (last.startsWith(':')) { output = parseVDeep(last, output) } else { scoped = true last = last.replace(':', `[data-${hash}]:`) output = `${last} ${output}` } } else { scoped = true output = `${last}[data-${hash}] ${output}` } } } return output }) .join(', ') } return selector + '{' }) } /** * 编译scss为css * @param file 文件路径或scss代码 * @param inject 要注入的scss代码 */ export function compileScss(file, inject = '') { try { if (fs.isfile(file)) { return scss.compile(file, OPTIONS).css.trim() } else { return scss.compileString(inject + file, OPTIONS).css.trim() } } catch (err) { console.error(err) } } /** * 解析js * 主要是处理js的依赖引用 * @param code js代码 */ export function parseJs( code = '', imports, { IS_MPA, currentPage, IS_ENTRY, DEPLOY_PATH, LEGACY_MODE, define } = {}, filename, linePatch = 1 ) { let fixedStyle = '' let ASSETS_DIR = '/@/' let isBuild = process.env.NODE_ENV === 'production' if (isBuild) { ASSETS_DIR = '/assets/' } try { code = Es.transformSync(code).code || '' } catch (e) { let err = e.errors.pop() let lines = code.split('\n') console.log('%s: %s', red('Uncaught SyntaxError'), red(err.text)) // 将上下文几行都打印出来 for (let i = err.location.line - 3; i <= err.location.line + 1; i++) { console.log( ' @ line %d: %s', i + linePatch, err.location.line === i + 1 ? red(lines[i]) : lines[i] ) } console.log( ' @ %s:%d:%d', blue(filename), err.location.line + linePatch - 1, err.location.column ) } code = code .replace(/\r\n/g, '\n') .replace(/process\.env\.NODE_ENV/g, `'${process.env.NODE_ENV}'`) .replace( /(import|export) ([^'"]*?) from (["'])(.*?)\3/g, function (m, t, alias, q, name) { if (name.startsWith('@/')) { name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR)) } if (!imports[name]) { if (name.startsWith('./') || name.startsWith('../')) { if (IS_ENTRY) { if (IS_MPA) { name = `pages/${currentPage}/` + name } name = urlJoin(DEPLOY_PATH, ASSETS_DIR, name) } } else if ( name.startsWith('/') && !name.startsWith('//') && !name.startsWith(urlJoin(DEPLOY_PATH, ASSETS_DIR)) ) { name = name.replace(/^\//, urlJoin(DEPLOY_PATH, ASSETS_DIR)) } if ( !name.endsWith('.js') && !name.endsWith('.mjs') && !name.endsWith('.vue') ) { if (name.includes('components')) { name += '.vue' } else { name += '.js' } } } if (isBuild) { name = name.replace(/\.vue$/, '.js') } if (alias.trim() === '*') { return `${t} ${alias} from '${name}'` } let _alias = alias let _import = '' if (alias.includes('* as')) { _alias = ' default ' + alias.replace('* as', '').trim() } _import = `import ${alias} from '${name}'` _import += t === 'export' ? `\nexport ${_alias}` : '' return _import } ) .replace(/import\((['"])(.*?)\1\)/g, function (m, q, name) { if (isBuild) { name = name.replace(/\.vue$/, '.js') } if (name.startsWith('@/')) { name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR)) } if (name.endsWith('.json')) { name += '.js' } return `import('${name}')` }) .replace(/import (["'])(.*?)\1/g, function (m, q, name) { if (name.endsWith('.css') || name.endsWith('.scss')) { if (name.startsWith('@/')) { name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR)) } if (isBuild) { name = name.replace(/\.scss/, '.css') } // 修正那反人类的windows路径 let _name = name.replace(/\\/g, '/').replace('@/', '') let tmp = `style_${uuid()}` // 因为esm语法的变更, 原先的 import xx from xx assets {type: css} 变为了 with // 而这个语法的变化, 构建工具无法做版本判断, 故, 统一降级到fetch()加载 if (LEGACY_MODE) { fixedStyle += `{\n` + ` let stylesheet = document.createElement('style');\n` + ` stylesheet.setAttribute('name', '${_name}');\n` + ` stylesheet.textContent = ${tmp};\n` + ` document.head.appendChild(stylesheet);\n` + `}\n` return `let ${tmp};\n!(async function(){\n ${tmp} = await __fite_import('${name}', import.meta.url);\n})()` } else { // CSSStyleSheet.replaceSync 需要FF v101, Safari 16.4才支持 fixedStyle += `{\n` + ` let stylesheet = new CSSStyleSheet();\n` + ` stylesheet.path = '${_name}';\n` + ` stylesheet.replaceSync(${tmp} );\n` + ` __sheets__.push(stylesheet);\n` + ` document.adoptedStyleSheets = __sheets__;\n` + `}\n` return `const ${tmp} = await __fite_import('${name}', import.meta.url)` } } else { if (name.startsWith('@/')) { name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR)) } // if (!imports[name]) { if (name.startsWith('./') || name.startsWith('../')) { if (IS_ENTRY) { if (IS_MPA) { name = `${currentPage}/` + name } name = urlJoin(DEPLOY_PATH, ASSETS_DIR, name) } } else if ( name.startsWith('/') && !name.startsWith('//') && !name.startsWith(urlJoin(DEPLOY_PATH, ASSETS_DIR)) ) { name = name.replace(/^\//, urlJoin(DEPLOY_PATH, ASSETS_DIR)) } if ( !name.endsWith('.js') && !name.endsWith('.mjs') && !name.endsWith('.vue') ) { name += '.js' } } return `import '${name}'` } }) for (let key in define) { code = code.replaceAll(key, define[key]) } if (fixedStyle) { code += '\n\n' + (IS_ENTRY ? SHEETS_DEF : '') + fixedStyle if (IS_ENTRY && !LEGACY_MODE) { code += '\ndocument.adoptedStyleSheets = __sheets__' } } return code } /** * 将vue转为js * @param file 文件路径 * @return 返回转换后的js代码 */ export async function compileVue(file, imports, options = {}) { // 修正那反人类的windows路径 let filename = file.slice(options.SOURCE_DIR.length).replace(/\\/g, '/') let code = (fs.cat(file) || '').toString().replace(/\r\n/g, '\n') let CACHE = options.CACHE || {} let { isCustomElement } = options let output = '', scoped = false let js = code.match(JS_EXP) let scss = [...code.matchAll(STYLE_EXP)] let html = code.match(HTML_EXP) || ['', ''] let linePatch = code.slice(0, js?.index || 0).split('\n').length // js起始行数修正 let hash = md5(file) let scopeId = 'data-' + hash scss = scss .map(it => { let css if (it.length > 2) { css = compileScss(it[2], options.INJECT_SCSS) if (it[1].includes('scoped')) { scoped = true css = scopeCss(css, hash) } } else { css = compileScss(it[1], options.INJECT_SCSS) } return css }) .join(' ') for (let fn of options.plugin) { scss = await fn('css', scss) } js = js ? js[1] : 'export default {}' try { html = compile(html[1], { mode: 'module', prefixIdentifiers: true, hoistStatic: true, cacheHandlers: true, scopeId: scoped ? scopeId : void 0, sourceMap: false, isCustomElement }).code.replace('export function render', 'function render') } catch (err) { let lines = html[1].split('\n') let line = lines[err.loc?.start.line - 1] console.log('%s: %s', red('SyntaxError'), red(err.message)) console.log( ' @ %s%s%s', line.slice(0, err.loc.start.column - 1), red(err.loc.source), line.slice(err.loc.end.column - 1) ) console.log( ' @ (%s:%d:%d)\n', file, err.loc.start.line, err.loc.start.column ) html = ` import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, "SyntaxError: ${err.message}")) } ` } html = html .replace(/import .* from "vue"/, str => { output += str + '\n' return '' }) .trim() if (CACHE[file]) { CACHE[file] = { changed: CACHE[file].js !== js || CACHE[file].html !== html, js, html } } else { CACHE[file] = { changed: false, js, html } } output += parseJs(js, imports, options, file, linePatch).replace( 'export default {', `\n${ options.LEGACY_MODE ? '' : SHEETS_DEF }${html}\n\nconst __sfc__ = {\n render,\n` ) if (scss) { CACHE[file].css = scss if (options.LEGACY_MODE) { output += ` { let stylesheet = document.createElement('style') stylesheet.setAttribute('name', '${filename}') stylesheet.textContent = \`${scss}\` document.head.appendChild(stylesheet) } ` } else { output += ` { let stylesheet = new CSSStyleSheet() stylesheet.path = '${filename}' stylesheet.replaceSync(\`${scss}\`) __sheets__.push(stylesheet) } document.adoptedStyleSheets = __sheets__ ` } } if (scoped) { output += `__sfc__.__scopeId = '${scopeId}'\n` } output += `__sfc__.__file = '${filename}'\nexport default __sfc__` return output } /** * 解析模板html */ export function parseHtml(html, { page, imports, entry, LEGACY_MODE }) { return html .replace(/\r\n/g, '\n') .replace( '', `${ process.env.NODE_ENV === 'development' ? ` \n` : ` \n` }` ) .replace('{{title}}', page.title || '') .replace('{{keywords}}', page.keywords || '') .replace('{{description}}', page.description || '') .replace('{{importmap}}', JSON.stringify({ imports })) .replace( '', `` ) .replace(/\{\{#if(.*?)\}\}([\w\W]*?)\{\{#\/if\}\}/g, (m, c, code) => { let res = Function('return ' + c)() return res ? code : '' }) .replace(/process\.env\.NODE_ENV/g, `'${process.env.NODE_ENV}'`) }