fite/lib/compile-vue.js

437 lines
11 KiB
JavaScript
Raw Permalink Normal View History

2022-09-09 10:52:27 +08:00
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2022/09/06 14:43:01
*/
import fs from 'iofs'
import scss from '@bytedo/sass'
2023-05-22 15:06:33 +08:00
import { createHash, randomUUID } from 'node:crypto'
2023-02-01 10:51:33 +08:00
import Es from 'esbuild'
2023-05-22 15:06:33 +08:00
import { join } from 'node:path'
2023-03-01 00:30:07 +08:00
import { compile } from '@vue/compiler-dom'
import { red, cyan, blue } from 'kolorist'
2022-09-09 10:52:27 +08:00
function uuid() {
return randomUUID().slice(-8)
}
2023-02-01 10:51:33 +08:00
import {
JS_EXP,
STYLE_EXP,
HTML_EXP,
CSS_SHEET_EXP,
V_DEEP,
PERCENT_EXP,
2023-05-22 15:06:33 +08:00
SHEETS_DEF
2023-02-01 10:51:33 +08:00
} from './constants.js'
2023-05-22 15:06:33 +08:00
import {createHmrScript} from './utils.js'
2022-09-09 10:52:27 +08:00
const OPTIONS = {
2023-05-16 14:17:40 +08:00
style: 'compressed'
2022-09-09 10:52:27 +08:00
}
2023-02-21 18:34:48 +08:00
// 修正路径合并 避免在windows下被转义
function urlJoin(...args) {
return join(...args).replace(/\\/g, '/')
}
2022-10-18 16:02:29 +08:00
function md5(str = '') {
let sum = createHash('md5')
sum.update(str, 'utf8')
return sum.digest('hex').slice(0, 8)
}
2023-04-27 17:13:58 +08:00
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}`
}
}
2022-11-03 18:22:42 +08:00
function scopeCss(css = '', hash) {
return css.replace(CSS_SHEET_EXP, (m, selector) => {
if (!selector.startsWith('@')) {
2022-10-18 16:02:29 +08:00
selector = selector.split(',')
selector = selector
.map(s => {
// 针对 @keyframe的处理
if (s === 'from' || s === 'to' || PERCENT_EXP.test(s)) {
return s
}
2022-10-18 16:02:29 +08:00
let tmp = s.split(' ')
2023-04-27 17:13:58 +08:00
let output = ''
let last
let scoped = false
while ((last = tmp.pop())) {
if (scoped) {
if (last.startsWith(':')) {
output = parseVDeep(last, output, true)
2023-04-27 15:46:28 +08:00
} else {
2023-04-27 17:13:58 +08:00
output = `${last} ${output}`
2023-04-27 15:46:28 +08:00
}
2023-03-28 11:39:34 +08:00
} else {
2023-05-04 14:58:03 +08:00
if (last.includes(':')) {
if (last.startsWith(':')) {
output = parseVDeep(last, output)
} else {
scoped = true
last = last.replace(':', `[data-${hash}]:`)
output = `${last} ${output}`
}
2023-03-28 11:39:34 +08:00
} else {
2023-04-27 17:13:58 +08:00
scoped = true
output = `${last}[data-${hash}] ${output}`
2023-03-28 11:39:34 +08:00
}
}
2022-10-18 16:02:29 +08:00
}
2023-04-27 17:13:58 +08:00
return output
2022-10-18 16:02:29 +08:00
})
.join(', ')
}
return selector + '{'
})
2022-10-18 16:02:29 +08:00
}
2022-09-09 10:52:27 +08:00
/**
* 编译scss为css
* @param file <String> 文件路径或scss代码
2023-05-16 14:17:40 +08:00
* @param inject <String> 要注入的scss代码
2022-09-09 10:52:27 +08:00
*/
2023-05-16 14:17:40 +08:00
export function compileScss(file, inject = '') {
2022-09-09 10:52:27 +08:00
try {
if (fs.isfile(file)) {
2023-05-16 14:17:40 +08:00
return scss.compile(file, OPTIONS).css.trim()
2022-09-09 10:52:27 +08:00
} else {
2023-05-16 14:17:40 +08:00
return scss.compileString(inject + file, OPTIONS).css.trim()
2022-09-09 10:52:27 +08:00
}
} catch (err) {
console.error(err)
}
}
/**
* 解析js
* 主要是处理js的依赖引用
* @param code <String> js代码
*/
2023-01-13 11:40:53 +08:00
export function parseJs(
code = '',
imports,
{ IS_MPA, currentPage, IS_ENTRY, DEPLOY_PATH, LEGACY_MODE } = {},
filename
2023-01-13 11:40:53 +08:00
) {
let fixedStyle = ''
2023-03-17 17:56:05 +08:00
let ASSETS_DIR = '/@/'
let isBuild = process.env.NODE_ENV === 'production'
2023-03-02 16:55:52 +08:00
if (isBuild) {
ASSETS_DIR = '/assets/'
2023-03-02 16:55:52 +08:00
}
try {
code = Es.transformSync(code).code || ''
} catch (e) {
let err = e.errors.pop()
console.log('%s: %s', red('Uncaught SyntaxError'), err.text)
console.log(
' @ line %d: %s',
err.location.line,
cyan(err.location.lineText)
)
console.log(
' @ %s:%d:%d',
blue(filename),
err.location.line,
err.location.column
)
}
2022-09-09 10:52:27 +08:00
code = code
.replace(/\r\n/g, '\n')
.replace(/process\.env\.NODE_ENV/g, `'${process.env.NODE_ENV}'`)
.replace(
2023-05-18 15:50:29 +08:00
/(import|export) ([\w\W]*?) from (["'])(.*?)\3/g,
function (m, t, alias, q, name) {
if (name.startsWith('@/')) {
name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR))
}
2022-09-09 10:52:27 +08:00
if (!imports[name]) {
if (name.startsWith('./') || name.startsWith('../')) {
if (IS_ENTRY) {
if (IS_MPA) {
name = `pages/${currentPage}/` + name
2023-01-13 11:40:53 +08:00
}
name = urlJoin(DEPLOY_PATH, ASSETS_DIR, name)
2022-10-10 15:05:30 +08:00
}
} else if (
name.startsWith('/') &&
!name.startsWith('//') &&
!name.startsWith(urlJoin(DEPLOY_PATH, ASSETS_DIR))
) {
name = name.replace(/^\//, urlJoin(DEPLOY_PATH, ASSETS_DIR))
}
2022-09-09 10:52:27 +08:00
if (!name.endsWith('.js') && !name.endsWith('.vue')) {
if (name.includes('components')) {
name += '.vue'
} else {
name += '.js'
2022-09-09 10:52:27 +08:00
}
}
}
2022-10-11 19:31:04 +08:00
if (isBuild) {
name = name.replace(/\.vue$/, '.js')
}
2023-05-18 15:50:29 +08:00
return `import ${alias} from '${name}'${
t === 'export' ? `\nexport ${alias}` : ''
}`
}
)
.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))
}
return `import('${name}')`
})
.replace(/import (["'])(.*?)\1/g, function (m, q, name) {
if (name.endsWith('.css') || name.endsWith('.scss')) {
2023-03-02 16:55:52 +08:00
if (name.startsWith('@/')) {
2023-03-17 17:56:05 +08:00
name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR))
2023-03-02 16:55:52 +08:00
}
2023-01-31 19:17:38 +08:00
if (isBuild) {
name = name.replace(/\.scss/, '.css')
}
// 修正那反人类的windows路径
let _name = name.replace(/\\/g, '/').replace('@/', '')
let tmp = `style_${uuid()}`
if (LEGACY_MODE) {
fixedStyle += `${tmp}.then(r => {
let stylesheet = document.createElement('style')
stylesheet.setAttribute('name', '${_name}')
stylesheet.textContent = r
document.head.appendChild(stylesheet)
})
`
return `const ${tmp} = window.fetch('${name}').then(r => r.text())`
2022-09-09 10:52:27 +08:00
} else {
fixedStyle += `${tmp}.path = '${_name}'\n__sheets__.push(${tmp})\n`
2023-02-12 23:01:57 +08:00
return `import ${tmp} from '${name}' assert { type: 'css' }`
}
} 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)
2022-09-09 10:52:27 +08:00
}
} else if (
name.startsWith('/') &&
!name.startsWith('//') &&
!name.startsWith(urlJoin(DEPLOY_PATH, ASSETS_DIR))
) {
name = name.replace(/^\//, urlJoin(DEPLOY_PATH, ASSETS_DIR))
2022-09-09 10:52:27 +08:00
}
2023-02-12 23:01:57 +08:00
if (!name.endsWith('.js') && !name.endsWith('.vue')) {
name += '.js'
}
2022-09-09 10:52:27 +08:00
}
return `import '${name}'`
}
})
if (fixedStyle) {
code += '\n\n' + (LEGACY_MODE ? '' : SHEETS_DEF) + fixedStyle
if (IS_ENTRY && !LEGACY_MODE) {
code += '\ndocument.adoptedStyleSheets = __sheets__'
}
}
return code
2022-09-09 10:52:27 +08:00
}
/**
* 将vue转为js
* @param file <String> 文件路径
* @return <String> 返回转换后的js代码
*/
export function compileVue(file, imports, options = {}) {
// 修正那反人类的windows路径
2023-03-01 00:30:07 +08:00
let filename = file.slice(options.SOURCE_DIR.length).replace(/\\/g, '/')
2023-02-23 00:00:10 +08:00
let code = (fs.cat(file) || '').toString().replace(/\r\n/g, '\n')
2023-01-31 19:17:38 +08:00
let CACHE = options.CACHE || {}
2023-05-22 15:06:33 +08:00
let { isCustomElement } = options
let output = '',
scoped = false
2022-09-09 10:52:27 +08:00
let js = code.match(JS_EXP)
let scss = [...code.matchAll(STYLE_EXP)]
2023-03-02 16:55:52 +08:00
let html = code.match(HTML_EXP) || ['', '']
2022-09-09 10:52:27 +08:00
2022-10-18 16:02:29 +08:00
let hash = md5(file)
2023-03-01 00:30:07 +08:00
let scopeId = 'data-' + hash
2022-09-09 10:52:27 +08:00
scss = scss
.map(it => {
let css
if (it.length > 2) {
2023-05-16 14:17:40 +08:00
css = compileScss(it[2], options.INJECT_SCSS)
if (it[1].includes('scoped')) {
scoped = true
css = scopeCss(css, hash)
}
} else {
2023-05-16 14:17:40 +08:00
css = compileScss(it[1], options.INJECT_SCSS)
}
return css
})
.join(' ')
js = js ? js[1] : 'export default {}'
2023-01-31 19:17:38 +08:00
2023-05-15 10:54:00 +08:00
try {
html = compile(html[1], {
mode: 'module',
prefixIdentifiers: true,
hoistStatic: true,
cacheHandlers: true,
scopeId: scoped ? scopeId : void 0,
sourceMap: false,
2023-05-22 15:06:33 +08:00
isCustomElement
2023-05-15 10:54:00 +08:00
}).code.replace('export function render', 'function render')
} catch (err) {
let tmp = html[1].split('\n')
let line = tmp[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()
2022-09-09 10:52:27 +08:00
2023-01-31 19:17:38 +08:00
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).replace(
2022-09-09 10:52:27 +08:00
'export default {',
`\n${
options.LEGACY_MODE ? '' : SHEETS_DEF
}${html}\n\nconst __sfc__ = {\n render,\n`
2022-09-09 10:52:27 +08:00
)
if (scss) {
CACHE[file].css = scss
2022-10-18 16:02:29 +08:00
if (options.LEGACY_MODE) {
output += `
{
let stylesheet = document.createElement('style')
stylesheet.setAttribute('name', '${filename}')
stylesheet.textContent = \`${scss}\`
document.head.appendChild(stylesheet)
}
2023-03-01 00:30:07 +08:00
`
} else {
output += `
{
let stylesheet = new CSSStyleSheet()
stylesheet.path = '${filename}'
stylesheet.replaceSync(\`${scss}\`)
__sheets__.push(stylesheet)
}
document.adoptedStyleSheets = __sheets__
`
}
2023-03-01 00:30:07 +08:00
}
if (scoped) {
output += `__sfc__.__scopeId = '${scopeId}'\n`
2022-09-09 10:52:27 +08:00
}
2023-03-01 00:30:07 +08:00
output += `__sfc__.__file = '${filename}'\nexport default __sfc__`
2022-09-09 10:52:27 +08:00
return output
2022-09-09 10:52:27 +08:00
}
2022-10-11 19:31:04 +08:00
/**
* 解析模板html
*/
export function parseHtml(html, { page, imports, entry, LEGACY_MODE }) {
2022-10-11 19:31:04 +08:00
return html
2023-02-23 00:00:10 +08:00
.replace(/\r\n/g, '\n')
2022-10-11 19:31:04 +08:00
.replace(
'</head>',
`${
process.env.NODE_ENV === 'development'
? ` <script>${Es.transformSync(createHmrScript(LEGACY_MODE), {
minify: true
}).code.trim()}</script>\n`
: ''
2023-01-31 19:17:38 +08:00
}</head>`
2022-10-11 19:31:04 +08:00
)
.replace('{{title}}', page.title || '')
.replace('{{keywords}}', page.keywords || '')
.replace('{{description}}', page.description || '')
.replace('{{importmap}}', JSON.stringify({ imports }))
2023-01-13 11:40:53 +08:00
.replace(
'<script src="main.js"></script>',
`<script type="module">\n${entry}\n</script>`
)
.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}'`)
2022-10-11 19:31:04 +08:00
}