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-11 11:37:50 +08:00
|
|
|
import { createHash, randomUUID } from 'crypto'
|
2023-02-01 10:51:33 +08:00
|
|
|
import Es from 'esbuild'
|
2023-02-21 18:34:48 +08:00
|
|
|
import { join } from 'path'
|
2023-03-01 00:30:07 +08:00
|
|
|
import { compile } from '@vue/compiler-dom'
|
2023-04-26 17:16:43 +08:00
|
|
|
import { red, cyan, blue } from 'kolorist'
|
2022-09-09 10:52:27 +08:00
|
|
|
|
2023-05-11 11:37:50 +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,
|
2023-03-28 11:39:34 +08:00
|
|
|
HMR_SCRIPT,
|
2023-04-26 15:10:16 +08:00
|
|
|
V_DEEP,
|
|
|
|
PERCENT_EXP
|
2023-02-01 10:51:33 +08:00
|
|
|
} from './constants.js'
|
2022-09-09 10:52:27 +08:00
|
|
|
|
|
|
|
const OPTIONS = {
|
|
|
|
indentType: 'space',
|
|
|
|
indentWidth: 2
|
|
|
|
}
|
|
|
|
|
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) {
|
2023-02-20 00:41:58 +08:00
|
|
|
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 => {
|
2023-04-26 15:10:16 +08:00
|
|
|
// 针对 @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(', ')
|
2023-02-20 00:41:58 +08:00
|
|
|
}
|
|
|
|
return selector + '{'
|
|
|
|
})
|
2022-10-18 16:02:29 +08:00
|
|
|
}
|
|
|
|
|
2022-09-09 10:52:27 +08:00
|
|
|
/**
|
|
|
|
* 编译scss为css
|
|
|
|
* @param file <String> 文件路径或scss代码
|
2022-10-18 16:02:29 +08:00
|
|
|
* @param mini <Boolean> 是否压缩
|
2022-09-09 10:52:27 +08:00
|
|
|
*/
|
2022-10-18 16:02:29 +08:00
|
|
|
export function compileScss(file, mini = true) {
|
|
|
|
let style = mini ? 'compressed' : 'expanded'
|
2022-09-09 10:52:27 +08:00
|
|
|
try {
|
|
|
|
if (fs.isfile(file)) {
|
2022-12-29 18:02:25 +08:00
|
|
|
return scss.compile(file, { style, ...OPTIONS }).css.trim()
|
2022-09-09 10:52:27 +08:00
|
|
|
} else {
|
2022-12-29 18:02:25 +08:00
|
|
|
return scss.compileString(file, { style, ...OPTIONS }).css.trim()
|
2022-09-09 10:52:27 +08:00
|
|
|
}
|
|
|
|
} catch (err) {
|
2023-03-02 16:55:52 +08:00
|
|
|
console.log('compile scss: ', file)
|
2022-09-09 10:52:27 +08:00
|
|
|
console.error(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 解析js
|
|
|
|
* 主要是处理js的依赖引用
|
|
|
|
* @param code <String> js代码
|
|
|
|
*/
|
2023-01-13 11:40:53 +08:00
|
|
|
export function parseJs(
|
|
|
|
code = '',
|
|
|
|
imports,
|
2023-02-19 16:52:55 +08:00
|
|
|
{ IS_MPA, currentPage, IS_ENTRY, DEPLOY_PATH } = {},
|
2023-04-26 17:16:43 +08:00
|
|
|
isBuild,
|
|
|
|
filename
|
2023-01-13 11:40:53 +08:00
|
|
|
) {
|
2022-09-09 10:52:27 +08:00
|
|
|
let fixedStyle = '\n\n'
|
2023-03-17 17:56:05 +08:00
|
|
|
let ASSETS_DIR = '/@/'
|
2023-03-02 16:55:52 +08:00
|
|
|
|
|
|
|
if (isBuild) {
|
2023-03-17 17:56:05 +08:00
|
|
|
ASSETS_DIR = '/assets/' // + (IS_MPA ? 'pages/' : '')
|
2023-03-02 16:55:52 +08:00
|
|
|
}
|
2023-04-26 17:16:43 +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
|
|
|
|
|
|
|
return (
|
|
|
|
code
|
2023-02-23 00:00:10 +08:00
|
|
|
.replace(/\r\n/g, '\n')
|
2023-01-13 11:40:53 +08:00
|
|
|
.replace(
|
2023-02-17 16:41:25 +08:00
|
|
|
/import ([\w\W]*?) from (["'])(.*?)\2/g,
|
2023-01-13 11:40:53 +08:00
|
|
|
function (m, alias, q, name) {
|
|
|
|
if (name.startsWith('@/')) {
|
2023-03-17 17:56:05 +08:00
|
|
|
name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR))
|
2023-01-13 11:40:53 +08:00
|
|
|
}
|
2022-09-09 10:52:27 +08:00
|
|
|
|
2023-01-13 11:40:53 +08:00
|
|
|
if (!imports[name]) {
|
2023-02-12 23:01:57 +08:00
|
|
|
if (name.startsWith('./') || name.startsWith('../')) {
|
|
|
|
if (IS_ENTRY) {
|
|
|
|
if (IS_MPA) {
|
2023-03-17 17:56:05 +08:00
|
|
|
name = `pages/${currentPage}/` + name
|
2023-02-12 23:01:57 +08:00
|
|
|
}
|
2023-03-17 17:56:05 +08:00
|
|
|
name = urlJoin(DEPLOY_PATH, ASSETS_DIR, name)
|
2023-01-13 11:40:53 +08:00
|
|
|
}
|
|
|
|
} else if (
|
|
|
|
name.startsWith('/') &&
|
|
|
|
!name.startsWith('//') &&
|
2023-03-17 17:56:05 +08:00
|
|
|
!name.startsWith(urlJoin(DEPLOY_PATH, ASSETS_DIR))
|
2023-01-13 11:40:53 +08:00
|
|
|
) {
|
2023-03-17 17:56:05 +08:00
|
|
|
name = name.replace(/^\//, urlJoin(DEPLOY_PATH, ASSETS_DIR))
|
2022-10-10 15:05:30 +08:00
|
|
|
}
|
2022-09-09 10:52:27 +08:00
|
|
|
|
2023-01-13 11:40:53 +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
|
|
|
}
|
|
|
|
}
|
2023-01-13 11:40:53 +08:00
|
|
|
if (isBuild) {
|
|
|
|
name = name.replace(/\.vue$/, '.js')
|
|
|
|
}
|
|
|
|
return `import ${alias} from '${name}'`
|
2022-09-09 10:52:27 +08:00
|
|
|
}
|
2023-01-13 11:40:53 +08:00
|
|
|
)
|
2022-10-11 19:31:04 +08:00
|
|
|
.replace(/import\((['"])(.*?)\1\)/g, function (m, q, name) {
|
|
|
|
if (isBuild) {
|
|
|
|
name = name.replace(/\.vue$/, '.js')
|
|
|
|
}
|
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
|
|
|
}
|
2022-10-11 19:31:04 +08:00
|
|
|
return `import('${name}')`
|
|
|
|
})
|
2022-09-09 10:52:27 +08:00
|
|
|
.replace(/import (["'])(.*?)\1/g, function (m, q, name) {
|
|
|
|
if (name.endsWith('.css') || name.endsWith('.scss')) {
|
|
|
|
if (name.startsWith('@/')) {
|
2023-01-31 19:17:38 +08:00
|
|
|
name = name.replace('@/', '/')
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isBuild) {
|
|
|
|
name = name.replace(/\.scss/, '.css')
|
2022-09-09 10:52:27 +08:00
|
|
|
}
|
2023-05-11 11:37:50 +08:00
|
|
|
let tmp = `style_${uuid()}`
|
|
|
|
fixedStyle += `__sheets__.push(${tmp})\n`
|
2022-09-09 10:52:27 +08:00
|
|
|
|
2023-02-02 16:44:18 +08:00
|
|
|
// 修正那反人类的windows路径
|
2023-02-02 16:39:31 +08:00
|
|
|
return `import ${tmp} from '${name}' assert { type: 'css' }\n${tmp}.path = '${name.replace(
|
|
|
|
/\\/g,
|
2023-02-21 18:34:48 +08:00
|
|
|
'/'
|
2023-02-02 16:39:31 +08:00
|
|
|
)}'`
|
2022-09-09 10:52:27 +08:00
|
|
|
} else {
|
|
|
|
if (name.startsWith('@/')) {
|
2023-03-17 17:56:05 +08:00
|
|
|
name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR))
|
2022-09-09 10:52:27 +08:00
|
|
|
}
|
2022-10-11 19:31:04 +08:00
|
|
|
//
|
2022-10-10 15:05:30 +08:00
|
|
|
if (!imports[name]) {
|
2023-02-12 23:01:57 +08:00
|
|
|
if (name.startsWith('./') || name.startsWith('../')) {
|
|
|
|
if (IS_ENTRY) {
|
|
|
|
if (IS_MPA) {
|
|
|
|
name = `${currentPage}/` + name
|
|
|
|
}
|
2023-03-17 17:56:05 +08:00
|
|
|
name = urlJoin(DEPLOY_PATH, ASSETS_DIR, name)
|
2023-02-12 23:01:57 +08:00
|
|
|
}
|
|
|
|
} else if (
|
|
|
|
name.startsWith('/') &&
|
|
|
|
!name.startsWith('//') &&
|
2023-03-17 17:56:05 +08:00
|
|
|
!name.startsWith(urlJoin(DEPLOY_PATH, ASSETS_DIR))
|
2023-02-12 23:01:57 +08:00
|
|
|
) {
|
2023-03-17 17:56:05 +08:00
|
|
|
name = name.replace(/^\//, urlJoin(DEPLOY_PATH, ASSETS_DIR))
|
2023-02-12 23:01:57 +08:00
|
|
|
}
|
|
|
|
|
2022-09-09 10:52:27 +08:00
|
|
|
if (!name.endsWith('.js') && !name.endsWith('.vue')) {
|
|
|
|
name += '.js'
|
|
|
|
}
|
|
|
|
}
|
2023-02-12 23:01:57 +08:00
|
|
|
|
2022-09-09 10:52:27 +08:00
|
|
|
return `import '${name}'`
|
|
|
|
}
|
|
|
|
}) + fixedStyle
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 将vue转为js
|
|
|
|
* @param file <String> 文件路径
|
|
|
|
* @return <String> 返回转换后的js代码
|
|
|
|
*/
|
2022-10-11 19:31:04 +08:00
|
|
|
export function compileVue(file, imports, options = {}, isBuild) {
|
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-02-24 15:45:55 +08:00
|
|
|
let output = '',
|
|
|
|
scoped = false
|
2022-09-09 10:52:27 +08:00
|
|
|
|
|
|
|
let js = code.match(JS_EXP)
|
2023-02-24 15:45:55 +08:00
|
|
|
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
|
|
|
|
2023-02-24 15:45:55 +08:00
|
|
|
scss = scss
|
|
|
|
.map(it => {
|
|
|
|
let css
|
|
|
|
if (it.length > 2) {
|
|
|
|
css = compileScss(it[2])
|
|
|
|
|
|
|
|
if (it[1].includes('scoped')) {
|
|
|
|
scoped = true
|
|
|
|
css = scopeCss(css, hash)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
css = compileScss(it[1])
|
|
|
|
}
|
|
|
|
return css
|
|
|
|
})
|
|
|
|
.join(' ')
|
|
|
|
|
2022-09-09 10:52:27 +08:00
|
|
|
js = js ? js[1] : ''
|
2023-01-31 19:17:38 +08:00
|
|
|
|
2023-03-01 00:30:07 +08:00
|
|
|
html = compile(html[1], {
|
|
|
|
mode: 'module',
|
|
|
|
prefixIdentifiers: true,
|
|
|
|
hoistStatic: true,
|
|
|
|
cacheHandlers: true,
|
|
|
|
scopeId: scoped ? scopeId : void 0,
|
|
|
|
sourceMap: false,
|
|
|
|
isCustomElement: tag => tag.startsWith('wc-')
|
|
|
|
}).code.replace('export function render', 'function render')
|
2023-02-24 15:45:55 +08:00
|
|
|
|
2023-05-11 11:37:50 +08:00
|
|
|
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 }
|
|
|
|
}
|
|
|
|
|
2023-04-26 17:16:43 +08:00
|
|
|
output += parseJs(js, imports, options, isBuild, file).replace(
|
2022-09-09 10:52:27 +08:00
|
|
|
'export default {',
|
2023-05-11 11:37:50 +08:00
|
|
|
`\nconst __sheets__ = [...document.adoptedStyleSheets]\n${html}\n\nconst __sfc__ = {\n render,\n`
|
2022-09-09 10:52:27 +08:00
|
|
|
)
|
|
|
|
|
2023-02-24 15:45:55 +08:00
|
|
|
if (scss) {
|
|
|
|
CACHE[file].css = scss
|
2022-10-18 16:02:29 +08:00
|
|
|
|
2023-02-02 16:44:18 +08:00
|
|
|
// 修正那反人类的windows路径
|
2023-02-24 15:45:55 +08:00
|
|
|
output += `
|
2023-05-11 11:37:50 +08:00
|
|
|
{
|
|
|
|
let stylesheet = new CSSStyleSheet()
|
|
|
|
stylesheet.path = '${filename}'
|
|
|
|
stylesheet.replaceSync(\`${scss}\`)
|
|
|
|
__sheets__.push(stylesheet)
|
|
|
|
}
|
2023-04-25 11:12:27 +08:00
|
|
|
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
|
|
|
|
2023-02-24 15:45:55 +08:00
|
|
|
return output
|
2022-09-09 10:52:27 +08:00
|
|
|
}
|
2022-10-11 19:31:04 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 解析模板html
|
|
|
|
*/
|
2023-01-31 19:17:38 +08:00
|
|
|
export function parseHtml(html, { page, imports, entry }, isBuild = false) {
|
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>',
|
2023-01-31 19:17:38 +08:00
|
|
|
` <script>window.process = {env: {NODE_ENV: '${
|
|
|
|
isBuild ? 'production' : 'development'
|
|
|
|
}'}}</script>\n${
|
2023-02-01 10:51:33 +08:00
|
|
|
isBuild
|
|
|
|
? ''
|
|
|
|
: ` <script>${
|
|
|
|
Es.transformSync(HMR_SCRIPT, { minify: true }).code
|
|
|
|
}</script>`
|
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>`
|
|
|
|
)
|
2022-10-11 19:31:04 +08:00
|
|
|
}
|