Compare commits

...

32 Commits

Author SHA1 Message Date
yutent 96b9554574 1.4.4:修复样式scoped的范围 2024-09-27 18:43:14 +08:00
yutent dd8ff91294 1.4.3: 修复LEGACY_MODE模式下的语法兼容问题 2024-09-13 18:36:44 +08:00
yutent fa84cc0c5d 修复开发模式 2024-08-01 13:55:40 +08:00
yutent 2b9599781e fixed 2024-07-31 14:15:29 +08:00
yutent 7167a8b467 增加define配置的支持 2024-07-25 16:30:30 +08:00
yutent c3dd7a843c 修复线程创建; 调整编译配置处理;增加插件的支持 2024-07-25 15:24:06 +08:00
yutent d84a59a5e5 1.3.4 2024-04-01 18:26:19 +08:00
yutent 66925dfbf2 适配es2024搞出来的ESM语法变化 2024-04-01 18:26:00 +08:00
yutent a15bd9ea95 esm增加对json文件的支持;优化legacy模式 2024-03-01 18:39:39 +08:00
yutent 100c372718 1.2.0 2023-11-07 18:38:32 +08:00
yutent bf68560ae5 Merge pull request '优化export; dev模式增加headers设置' (#9) from dev into master
Reviewed-on: #9
2023-11-07 18:35:42 +08:00
yutent 24aea78d3d 优化export; dev模式增加headers设置 2023-11-06 19:07:08 +08:00
yutent 6ca2d7aee0 Merge pull request #7 from bytedo/dev
优化HMR
2023-06-26 16:40:43 +08:00
yutent 0a05548c15 优化HMR 2023-06-26 16:39:45 +08:00
yutent ce34b01f68 1.1.12 2023-06-25 12:27:59 +08:00
yutent 00a421d728 Merge pull request #6 from bytedo/dev
bug修复
2023-06-25 12:27:38 +08:00
yutent da7649d362 修复单页应用的编译 2023-06-25 12:27:51 +08:00
yutent 0225e1e499 修复一处笔误; 修正js解析行号;修复js解析时文件名未传的bug 2023-06-25 11:59:20 +08:00
yutent da70bcc685 Merge pull request #5 from bytedo/dev
一小波优化
2023-06-22 21:21:22 +08:00
yutent 3404c87a71 增加容错判断, 避免vscode保存时格式化插件抽风 2023-06-21 19:06:55 +08:00
yutent 1b3425196e 优化js解析,更准确的把报错信息正确的输出 2023-06-21 17:10:32 +08:00
yutent d8a783a336 Merge pull request #4 from bytedo/dev
多线程编译自适应(4核及以上开启)
2023-06-20 10:07:23 +08:00
yutent f4a3ff6355 多线程编译自适应(4核及以上开启) 2023-06-20 09:53:01 +08:00
yutent 9e34077754 add MIT license 2023-06-16 17:25:05 +08:00
yutent 21df0b438b 1.1.11 2023-06-16 14:26:09 +08:00
yutent c414dbe549 Merge pull request #3 from bytedo/threads
Threads
2023-06-16 14:19:48 +08:00
yutent ea986f0a39 完成多线程编译 2023-06-16 14:19:32 +08:00
yutent cca39d9156 完成多线程编译, 修复静态文件复制路径 2023-06-16 12:11:43 +08:00
yutent 82c8686bfb once -> on 2023-06-15 19:51:21 +08:00
yutent 50a54a152c 优化线程操作 2023-06-15 19:43:21 +08:00
yutent ff5a225cc1 暂存6.15 2023-06-15 16:54:45 +08:00
yutent c675b73f25 多线程编译 2023-06-14 19:36:20 +08:00
11 changed files with 546 additions and 216 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -19,6 +19,7 @@ const IS_WINDOWS = process.platform === 'win32'
const CONFIG_FILE = normalize(join(WORK_SPACE, 'fite.config.js'))
const PROTOCOL = IS_WINDOWS ? 'file://' : ''
const NODE_VERSION = process.versions.node.split('.').map(n => +n)
const ABS_CONFIG_FILEPATH = PROTOCOL + CONFIG_FILE
let args = process.argv.slice(2)
let mode = args.shift() || 'prod'
@ -40,8 +41,9 @@ switch (mode) {
case 'dev':
process.env.NODE_ENV = 'development'
import(PROTOCOL + CONFIG_FILE)
import(ABS_CONFIG_FILEPATH)
.then(function (conf) {
conf.default.ABS_CONFIG_FILEPATH = ABS_CONFIG_FILEPATH
createServer(WORK_SPACE, conf.default)
})
.catch(err => {
@ -52,13 +54,14 @@ switch (mode) {
case 'build':
process.env.NODE_ENV = 'production'
import(PROTOCOL + CONFIG_FILE)
import(ABS_CONFIG_FILEPATH)
.then(function (conf) {
let dist = conf.buildDir || 'dist'
if (clean && fs.isdir(dist)) {
console.log('清除dist目录...')
fs.rm(dist)
}
conf.default.ABS_CONFIG_FILEPATH = ABS_CONFIG_FILEPATH
compile(WORK_SPACE, dist, conf.default, verbose)
})
.catch(err => {

View File

@ -18,7 +18,8 @@ import {
CSS_SHEET_EXP,
V_DEEP,
PERCENT_EXP,
SHEETS_DEF
SHEETS_DEF,
LEGACY_POLYFILL
} from './constants.js'
import { createHmrScript, md5, uuid, urlJoin } from './utils.js'
@ -26,6 +27,10 @@ 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)
@ -59,7 +64,7 @@ function scopeCss(css = '', hash) {
if (last.startsWith(':')) {
output = parseVDeep(last, output, true)
} else {
output = `${last} ${output}`
output = `${last}[data-${hash}] ${output}`
}
} else {
if (last.includes(':')) {
@ -110,8 +115,9 @@ export function compileScss(file, inject = '') {
export function parseJs(
code = '',
imports,
{ IS_MPA, currentPage, IS_ENTRY, DEPLOY_PATH, LEGACY_MODE } = {},
filename
{ IS_MPA, currentPage, IS_ENTRY, DEPLOY_PATH, LEGACY_MODE, define } = {},
filename,
linePatch = 1
) {
let fixedStyle = ''
let ASSETS_DIR = '/@/'
@ -125,16 +131,21 @@ export function parseJs(
code = Es.transformSync(code).code || ''
} catch (e) {
let err = e.errors.pop()
console.log('%s: %s', red('Uncaught SyntaxError'), err.text)
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',
err.location.line,
cyan(err.location.lineText)
i + linePatch,
err.location.line === i + 1 ? red(lines[i]) : lines[i]
)
}
console.log(
' @ %s:%d:%d',
blue(filename),
err.location.line,
err.location.line + linePatch - 1,
err.location.column
)
}
@ -143,7 +154,7 @@ export function parseJs(
.replace(/\r\n/g, '\n')
.replace(/process\.env\.NODE_ENV/g, `'${process.env.NODE_ENV}'`)
.replace(
/(import|export) ([\w\W]*?) from (["'])(.*?)\3/g,
/(import|export) ([^'"]*?) from (["'])(.*?)\3/g,
function (m, t, alias, q, name) {
if (name.startsWith('@/')) {
name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR))
@ -180,9 +191,19 @@ export function parseJs(
if (isBuild) {
name = name.replace(/\.vue$/, '.js')
}
return `import ${alias} from '${name}'${
t === 'export' ? `\nexport ${alias}` : ''
}`
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) {
@ -192,6 +213,9 @@ export function parseJs(
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) {
@ -207,19 +231,30 @@ export function parseJs(
let _name = name.replace(/\\/g, '/').replace('@/', '')
let tmp = `style_${uuid()}`
// 因为esm语法的变更, 原先的 import xx from xx assets {type: css} 变为了 with
// 而这个语法的变化, 构建工具无法做版本判断, 故, 统一降级到fetch()加载
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())`
} else {
fixedStyle += `${tmp}.path = '${_name}'\n__sheets__.push(${tmp})\n`
fixedStyle +=
`{\n` +
` let stylesheet = document.createElement('style');\n` +
` stylesheet.setAttribute('name', '${_name}');\n` +
` stylesheet.textContent = ${tmp};\n` +
` document.head.appendChild(stylesheet);\n` +
`}\n`
return `import ${tmp} from '${name}' assert { type: 'css' }`
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('@/')) {
@ -255,6 +290,10 @@ export function parseJs(
}
})
for (let key in define) {
code = code.replaceAll(key, define[key])
}
if (fixedStyle) {
code += '\n\n' + (IS_ENTRY ? SHEETS_DEF : '') + fixedStyle
@ -271,7 +310,7 @@ export function parseJs(
* @param file <String> 文件路径
* @return <String> 返回转换后的js代码
*/
export function compileVue(file, imports, options = {}) {
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')
@ -283,6 +322,7 @@ export function compileVue(file, imports, options = {}) {
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
@ -300,10 +340,15 @@ export function compileVue(file, imports, options = {}) {
} 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 {
@ -317,8 +362,8 @@ export function compileVue(file, imports, options = {}) {
isCustomElement
}).code.replace('export function render', 'function render')
} catch (err) {
let tmp = html[1].split('\n')
let line = tmp[err.loc.start.line - 1]
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(
@ -359,7 +404,7 @@ function render(_ctx, _cache) {
CACHE[file] = { changed: false, js, html }
}
output += parseJs(js, imports, options, file).replace(
output += parseJs(js, imports, options, file, linePatch).replace(
'export default {',
`\n${
options.LEGACY_MODE ? '' : SHEETS_DEF
@ -380,13 +425,13 @@ function render(_ctx, _cache) {
`
} else {
output += `
{
{
let stylesheet = new CSSStyleSheet()
stylesheet.path = '${filename}'
stylesheet.replaceSync(\`${scss}\`)
__sheets__.push(stylesheet)
}
document.adoptedStyleSheets = __sheets__
}
document.adoptedStyleSheets = __sheets__
`
}
}
@ -401,23 +446,15 @@ function render(_ctx, _cache) {
/**
* 解析模板html
*/
export function parseHtml(
html,
{ page, imports, entry, LEGACY_MODE, session }
) {
export function parseHtml(html, { page, imports, entry, LEGACY_MODE }) {
return html
.replace(/\r\n/g, '\n')
.replace(
'</head>',
`${
process.env.NODE_ENV === 'development'
? ` <script>${Es.transformSync(
createHmrScript(LEGACY_MODE, session),
{
minify: true
}
).code.trim()}</script>\n`
: ''
? ` <script>${minify(createHmrScript(LEGACY_MODE))}</script>\n`
: ` <script>${minify(LEGACY_POLYFILL)}</script>\n`
}</head>`
)
.replace('{{title}}', page.title || '')

113
lib/compile.js Normal file
View File

@ -0,0 +1,113 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2023/06/14 18:36:15
*/
import { join, dirname, parse, normalize } from 'node:path'
import fs from 'iofs'
import Es from 'esbuild'
import { compileScss, parseJs, compileVue, parseHtml } from './compile-vue.js'
const template = fs.cat(join(process.cwd(), 'index.html')).toString()
export async function compileFiles(
currentPage,
page,
files,
options,
{ verbose, imports, dist } = {}
) {
let pageDir = options.IS_MPA && currentPage ? `pages/${currentPage}` : ''
options.currentPage = currentPage
for (let [path, it] of files) {
// 入口文件, 特殊处理
if (page && path === page.entry) {
let entry = fs.cat(page.entry).toString()
entry = parseJs(entry, imports, { ...options, IS_ENTRY: true })
let code = parseHtml(template, {
page,
imports,
entry,
LEGACY_MODE: options.LEGACY_MODE
})
fs.echo(code, join(dist, `${currentPage}.html`))
continue
}
verbose && console.log(' 解析 %s ...', it.name)
switch (it.ext) {
case '.vue':
{
let code = await compileVue(path, imports, options)
await Es.transform(code, { minify: true }).then(async ({ code }) => {
for (let fn of options.plugin) {
code = await fn('js', code)
}
fs.echo(
code,
join(dist, 'assets/', pageDir, it.name.replace(/\.vue$/, '.js'))
)
})
}
break
case '.js':
{
let code = fs.cat(path)
code = parseJs(code + '', imports, options)
await Es.transform(code, { minify: true }).then(async ({ code }) => {
for (let fn of options.plugin) {
code = await fn('js', code)
}
fs.echo(code, join(dist, 'assets/', pageDir, it.name))
})
}
break
// es2024之后esm的语法的assets 变成了with, 对构建工具来说无法适配到具体的浏览器
// 故把json文件改成js文件
case '.json':
{
let code = fs.cat(path)
code = 'export default ' + JSON.stringify(JSON.parse(code + ''))
fs.echo(code, join(dist, 'assets/', pageDir, it.name + '.js'))
}
break
case '.scss':
case '.css':
{
let target = join(
dist,
'assets/',
pageDir,
it.name.replace(/\.scss$/, '.css')
)
if (it.ext === '.css') {
fs.cp(path, target)
} else {
let code = compileScss(path)
for (let fn of options.plugin) {
code = await fn('css', code)
}
fs.echo(code, target)
}
}
break
default:
fs.cp(path, join(dist, 'assets/', pageDir, it.name))
break
}
}
}

View File

@ -18,3 +18,33 @@ export const COMMON_HEADERS = {
export const SHEETS_DEF =
'const __sheets__ = [...document.adoptedStyleSheets];\n'
export const LEGACY_POLYFILL = `!(function(){
function join(p1, p2) {
let tmp1 = p1.split('/')
let tmp2 = p2.split('/')
if (tmp1.at(-1) === '') {
tmp1.pop()
}
while (tmp2.length) {
let tmp = tmp2.shift()
if (tmp === '.' || tmp === '') {
continue
} else if (tmp === '..') {
tmp1.pop()
} else {
tmp1.push(tmp)
}
}
return tmp1.join('/')
}
window.__fite_import = function(url,relPath){
let absPath = relPath.split('/').slice(0, -1).join('/')
let req
if(url.startsWith('./') || url.startsWith('../')) {
url = join(absPath, url)
}
return window.fetch(url).then(r => r.text())
}
})()`

View File

@ -7,7 +7,7 @@ import socket from './ws.js'
import chokidar from 'chokidar'
import { red } from 'kolorist'
import { friendlyErrors, isCustomElement, md5, gzip } from './utils.js'
import { friendlyErrors, defaultCustomElement, gzip } from './utils.js'
import { compileScss, parseJs, compileVue, parseHtml } from './compile-vue.js'
@ -33,6 +33,8 @@ export default async function createServer(root = '', conf = {}) {
const INJECT_SCSS = readFile(conf.inject?.scss)
const LEGACY_MODE = !!conf.legacy
const ENABLE_GZIP = !!conf.devServer.gzip
const { isCustomElement = defaultCustomElement } = conf.compileOptions || {}
const { plugin = [], define = {} } = conf
if (conf.imports['vue-dev']) {
conf.imports.vue = conf.imports['vue-dev']
@ -41,6 +43,10 @@ export default async function createServer(root = '', conf = {}) {
conf.imports['vue-router'] = conf.imports['vue-router-dev']
}
if (conf.devServer.headers) {
Object.assign(COMMON_HEADERS, conf.devServer.headers)
}
if (USE_HTTPS) {
Object.assign(SERVER_OPTIONS, conf.devServer.ssl)
@ -68,7 +74,7 @@ export default async function createServer(root = '', conf = {}) {
currentPage = ''
server
.on('request', function (req, res) {
.on('request', async function (req, res) {
let prefix = DEPLOY_PATH ? DEPLOY_PATH.replace(/\/$/, '') : ''
let url =
prefix && req.url.startsWith(prefix)
@ -165,20 +171,31 @@ export default async function createServer(root = '', conf = {}) {
let entry = fs.cat(page.entry)?.toString()
let html = fs.cat(join(process.cwd(), 'index.html')).toString()
entry = parseJs(entry, conf.imports, {
entry = parseJs(
entry,
conf.imports,
{
IS_MPA,
currentPage,
IS_ENTRY: true,
DEPLOY_PATH,
LEGACY_MODE
})
LEGACY_MODE,
isCustomElement,
plugin,
define
},
page.entry
)
for (let fn of plugin) {
entry = await fn('js', entry)
}
code = parseHtml(html, {
page,
imports: conf.imports,
entry,
LEGACY_MODE,
session: md5(page.entry)
LEGACY_MODE
})
}
@ -197,7 +214,6 @@ export default async function createServer(root = '', conf = {}) {
file = join(SOURCE_DIR, rpath)
}
} else {
ndex.html
file = join(SOURCE_DIR, rpath)
}
if (!fs.isfile(file)) {
@ -207,7 +223,7 @@ export default async function createServer(root = '', conf = {}) {
return
}
code = compileVue(file, conf.imports, {
code = await compileVue(file, conf.imports, {
IS_MPA,
currentPage,
SOURCE_DIR,
@ -215,9 +231,15 @@ export default async function createServer(root = '', conf = {}) {
DEPLOY_PATH,
INJECT_SCSS,
LEGACY_MODE,
isCustomElement: conf.isCustomElement || isCustomElement
isCustomElement,
plugin,
define
})
for (let fn of plugin) {
code = await fn('js', code)
}
res.setHeader('content-type', MIME_TYPES.js)
}
break
@ -243,14 +265,25 @@ export default async function createServer(root = '', conf = {}) {
return
}
code = compileScss(file)
for (let fn of plugin) {
code = await fn('css', code)
}
res.setHeader('content-type', MIME_TYPES.css)
}
break
case 'js':
case 'wasm':
{
let rpath = pathname.replace('@/', '')
let file
let isJson = false
let isWasm = rpath.endsWith('.wasm')
if (rpath.endsWith('json.js')) {
isJson = true
rpath = rpath.slice(0, -3)
}
if (IS_MPA) {
// 判断前后2个值相等, 避免出现目录名和页面名字相同时走错逻辑
@ -266,20 +299,43 @@ export default async function createServer(root = '', conf = {}) {
if (fs.isfile(file)) {
code = fs.cat(file)
} else if (fs.isfile(join(PUBLIC_DIR, rpath))) {
code = fs.cat(join(PUBLIC_DIR, rpath))
file = join(PUBLIC_DIR, rpath)
code = fs.cat(file)
} else {
friendlyErrors(rpath, ext)
res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
res.end('')
return
}
code = parseJs(code + '', conf.imports, {
if (isJson) {
try {
code =
'export default ' + JSON.stringify(JSON.parse(code + ''))
} catch (err) {
console.log('%s 语法错误: %s', rpath, red(err.message))
}
} else if (isWasm) {
//
} else {
code = parseJs(
code + '',
conf.imports,
{
IS_MPA,
currentPage,
DEPLOY_PATH,
LEGACY_MODE
})
res.setHeader('content-type', MIME_TYPES.js)
LEGACY_MODE,
isCustomElement,
plugin,
define
},
file
)
for (let fn of plugin) {
code = await fn('js', code)
}
}
res.setHeader('content-type', MIME_TYPES[ext])
}
break
@ -358,7 +414,7 @@ export default async function createServer(root = '', conf = {}) {
)
chokidar
.watch([SOURCE_DIR, PUBLIC_DIR, join(root, './index.html')])
.on('all', (act, filePath) => {
.on('all', async (act, filePath) => {
if (ready) {
let file = filePath.slice(SOURCE_DIR.length)
@ -378,6 +434,9 @@ export default async function createServer(root = '', conf = {}) {
} else {
content = fs.cat(filePath).toString()
}
for (let fn of plugin) {
content = await fn('css', content)
}
ws.send({
action: 'render',
data: { path: file.replace(/\\/g, '/'), content }
@ -387,7 +446,7 @@ export default async function createServer(root = '', conf = {}) {
case 'vue':
{
let content = compileVue(filePath, conf.imports, {
let content = await compileVue(filePath, conf.imports, {
IS_MPA,
currentPage,
SOURCE_DIR,
@ -395,7 +454,9 @@ export default async function createServer(root = '', conf = {}) {
DEPLOY_PATH,
INJECT_SCSS,
LEGACY_MODE,
isCustomElement: conf.isCustomElement || isCustomElement
isCustomElement,
plugin,
define
})
let tmp = CACHE[filePath]
if (tmp.changed) {

View File

@ -1,33 +1,79 @@
import { join, dirname, parse } from 'node:path'
import { join, dirname, parse, normalize } from 'node:path'
import { Worker, parentPort } from 'node:worker_threads'
import os from 'node:os'
import fs from 'iofs'
import Es from 'esbuild'
import { compileScss, parseJs, compileVue, parseHtml } from './compile-vue.js'
import { isCustomElement } from './utils.js'
import { compileFiles } from './compile.js'
import { defaultCustomElement } from './utils.js'
const IS_WIN = process.platform === 'win32'
const PREFIX = IS_WIN ? 'pages\\' : 'pages/'
// 4核(或4线程)以上的CPU, 才开启多线程编译。且线程开销太高, 开太多线程效率反而不高。
const CPU_CORES = os.cpus().length > 5 ? 6 : os.cpus().length
const THREADS_NUM = CPU_CORES > 3 ? CPU_CORES - 1 : 0
const __filename = normalize(import.meta.url.slice(IS_WIN ? 8 : 7))
const __dirname = dirname(__filename)
const WORKER_POOL = new Set() // 线程池
const JOBS_QUEUE = [] // 任务队列
function readFile(file) {
return (file && fs.cat(file)?.toString()) || ''
}
function doJob() {
while (JOBS_QUEUE.length && WORKER_POOL.size) {
let job = JOBS_QUEUE.shift()
let worker = WORKER_POOL.values().next().value
WORKER_POOL.delete(worker)
worker.once('message', _ => {
if (JOBS_QUEUE.length) {
WORKER_POOL.add(worker)
doJob()
} else {
worker.terminate()
}
})
worker.postMessage(job)
}
}
export default function compile(root = '', dist = '', conf = {}, verbose) {
//
const SOURCE_DIR = join(root, 'src')
const PUBLIC_DIR = join(root, 'public')
const DEPLOY_PATH = conf.base || '' // 部署目录, 默认是根目录部署
const IS_MPA = Object.keys(conf.pages).length > 1
const PAGES_PREFIX = Object.keys(conf.pages).map(it =>
const PAGES_KEYS = Object.keys(conf.pages)
const IS_MPA = PAGES_KEYS.length > 1
const PAGES_PREFIX = PAGES_KEYS.map(it =>
IS_WIN ? `${PREFIX + it}\\` : `${PREFIX + it}/`
)
const INJECT_SCSS = readFile(conf.inject?.scss)
const LEGACY_MODE = !!conf.legacy
const {
ABS_CONFIG_FILEPATH,
compileOptions = {},
define = {},
plugin = []
} = conf
const { isCustomElement = defaultCustomElement } = compileOptions
conf.inject = conf.inject || { scss: '' }
let timeStart = Date.now()
let template = fs.cat(join(process.cwd(), 'index.html')).toString()
let list = {}
let list = new Map()
let options = {
IS_MPA,
SOURCE_DIR,
DEPLOY_PATH,
INJECT_SCSS,
LEGACY_MODE,
ABS_CONFIG_FILEPATH,
define
}
fs.ls(SOURCE_DIR, true).forEach(path => {
if (fs.isdir(path)) {
@ -36,109 +82,43 @@ export default function compile(root = '', dist = '', conf = {}, verbose) {
let name = path.slice(SOURCE_DIR.length + 1)
let it = {
path,
name,
ext: parse(name).ext
}
if (it.ext !== '') {
if (it.ext) {
if (IS_MPA && it.name.startsWith(PREFIX)) {
if (PAGES_PREFIX.some(it => it.startsWith(it.name))) {
return (list[path] = it)
} else {
return
list.set(path, it)
}
}
if (it.path === conf.inject.scss) {
return
}
list[path] = it
if (path === conf.inject.scss) {
return
}
list.set(path, it)
}
})
let compileFiles = function (currentPage, page, files) {
let options = {
IS_MPA,
currentPage,
SOURCE_DIR,
DEPLOY_PATH,
INJECT_SCSS,
LEGACY_MODE,
isCustomElement: conf.isCustomElement || isCustomElement
}
// 创建线程池
if (THREADS_NUM > 0 && (IS_MPA || list.size > THREADS_NUM * 10)) {
// 页面数过少时, 线程数量不能比页面数多
let max = Math.min(THREADS_NUM, PAGES_KEYS.length)
for (let k in files) {
let it = files[k]
// 入口文件, 特殊处理
if (page && it.path === page.entry) {
let entry = fs.cat(page.entry).toString()
entry = parseJs(entry, conf.imports, { ...options, IS_ENTRY: true })
let code = parseHtml(template, {
page,
imports: conf.imports,
entry,
LEGACY_MODE
})
fs.echo(code, join(dist, `${currentPage}.html`))
continue
}
verbose && console.log(' 解析 %s ...', it.name)
let pageDir = IS_MPA && currentPage ? `pages/${currentPage}` : ''
switch (it.ext) {
case '.vue':
{
let code = compileVue(it.path, conf.imports, options)
Es.transform(code, { minify: true }).then(r => {
fs.echo(
r.code,
join(dist, 'assets/', pageDir, it.name.replace(/\.vue$/, '.js'))
)
})
}
break
case '.js':
{
let code = fs.cat(it.path)
code = parseJs(code + '', conf.imports, options)
Es.transform(code, { minify: true }).then(r => {
fs.echo(r.code, join(dist, 'assets/', pageDir, it.name))
})
}
break
case '.scss':
case '.css':
{
let target = join(
for (let i = 0; i < max; i++) {
WORKER_POOL.add(
new Worker(join(__dirname, './thread.js'), {
workerData: {
options,
verbose,
dist,
'assets/',
it.name.replace(/\.scss$/, '.css')
imports: conf.imports
}
})
)
if (it.ext === '.css') {
fs.cp(it.path, target)
}
} else {
let code = compileScss(it.path)
fs.echo(code, target)
}
}
break
default:
fs.cp(it.path, join(dist, it.name))
break
}
}
options.isCustomElement = isCustomElement
}
// 优先处理静态目录, 之后的源码目录中, 以便如果有产生相同的文件名, 则覆盖静态目录中的文件
@ -147,25 +127,21 @@ export default function compile(root = '', dist = '', conf = {}, verbose) {
fs.ls(PUBLIC_DIR, true).forEach(it => {
let ext = parse(it).ext
if (ext === '') {
return
}
if (fs.isfile(it)) {
if (ext && fs.isfile(it)) {
let name = it.slice(PUBLIC_DIR.length + 1)
verbose && console.log(' 复制 %s ...', name)
fs.cp(it, join(dist, name))
}
})
}
console.log('')
for (let currentPage in conf.pages) {
if (IS_MPA) {
for (let currentPage of PAGES_KEYS) {
let page = conf.pages[currentPage]
let dir = dirname(page.entry)
let files = list
if (IS_MPA) {
files = {}
let files = new Map()
let chunk = new Map()
fs.ls(dir, true).forEach(path => {
if (fs.isdir(path)) {
return
@ -178,17 +154,68 @@ export default function compile(root = '', dist = '', conf = {}, verbose) {
return
}
delete list[path]
files[path] = { name, path, ext }
list.delete(path)
files.set(path, { name, ext })
})
if (THREADS_NUM > 0) {
chunk.set(currentPage, { page, files })
JOBS_QUEUE.push(chunk)
doJob()
} else {
console.log(`正在生成 ${currentPage}.html ...`)
compileFiles(currentPage, page, files, options, {
verbose,
dist,
imports: conf.imports
})
}
console.log('正在生成 %s ...', `${currentPage}.html`)
compileFiles(currentPage, page, files)
}
if (IS_MPA) {
// 公共依赖
if (THREADS_NUM > 0) {
let chunk = new Map()
chunk.set('', { page: null, files: list })
JOBS_QUEUE.push(chunk)
doJob()
} else {
console.log('\n正在解析公共依赖 ...')
compileFiles('', null, list)
compileFiles('', null, list, options, {
verbose,
dist,
imports: conf.imports
})
}
} else {
// 每个线程处理的文件数
let chunkSize = Math.ceil(list.size / THREADS_NUM)
let currentPage = PAGES_KEYS[0]
let page = conf.pages[currentPage]
console.log(`正在生成 ${currentPage}.html ...`)
if (THREADS_NUM > 0 && list.size > THREADS_NUM * 10) {
list = [...list]
for (let i = 0; i < THREADS_NUM; i++) {
let start = i * chunkSize
let end = start + chunkSize
let chunk = new Map()
chunk.set(currentPage, { page, files: list.slice(start, end) })
JOBS_QUEUE.push(chunk)
doJob()
}
} else {
options.plugin = plugin
options.isCustomElement = isCustomElement
compileFiles(currentPage, page, list, options, {
verbose,
dist,
imports: conf.imports
})
}
}
process.on('exit', _ => {

40
lib/thread.js Normal file
View File

@ -0,0 +1,40 @@
/**
* 子线程
* @author yutent<yutent.io@gmail.com>
* @date 2023/06/14 16:15:39
*/
import { parentPort, workerData } from 'node:worker_threads'
import { compileFiles } from './compile.js'
import { defaultCustomElement } from './utils.js'
const { options, verbose, dist, imports } = workerData
const { ABS_CONFIG_FILEPATH } = options
const { compileOptions = {}, plugin = [] } = await import(
ABS_CONFIG_FILEPATH
).then(r => r.default)
const { isCustomElement = defaultCustomElement } = compileOptions
options.isCustomElement = isCustomElement
options.plugin = plugin
//
async function doJob(job) {
let [currentPage, { page, files }] = job.entries().next().value
options.IS_MPA &&
console.log(
currentPage
? `正在生成 ${currentPage}.html ...`
: '\n正在解析公共依赖 ...'
)
await compileFiles(currentPage, page, files, options, {
verbose,
dist,
imports
})
parentPort.postMessage(true)
}
parentPort.on('message', doJob)

View File

@ -7,8 +7,8 @@
import { createHash, randomUUID } from 'node:crypto'
import { join } from 'node:path'
import { gzipSync } from 'node:zlib'
import { Worker } from 'node:worker_threads'
import { red, cyan, blue } from 'kolorist'
import { LEGACY_POLYFILL } from './constants.js'
// 修正路径合并 避免在windows下被转义
export function urlJoin(...args) {
@ -40,7 +40,7 @@ export function friendlyErrors(pathname, ext = '') {
export function createHmrScript(legacy, session = '') {
return `
!(function vue_live_hmr(){
var ws = new WebSocket(\`ws\${location.protocol === 'https:' ? 's' : ''}://\${location.host}/ws-fite-hmr?session=${session}\`)
let ws = new WebSocket(\`ws\${location.protocol === 'https:' ? 's' : ''}://\${location.host}/ws-fite-hmr?session=\${btoa(location.pathname).replace(/[=\+\/]/g, '')}&lock=\${localStorage.getItem(location.pathname) || 0}\`)
ws.addEventListener('open', function (r) {
if(vue_live_hmr.closed){
@ -52,6 +52,9 @@ export function createHmrScript(legacy, session = '') {
ws.addEventListener('close', function(){
vue_live_hmr.closed = true
if (localStorage.getItem(location.pathname) === '1') {
return
}
setTimeout(vue_live_hmr, 2000)
})
@ -91,23 +94,12 @@ export function createHmrScript(legacy, session = '') {
break
}
})
${LEGACY_POLYFILL}
})()
`
}
export function isCustomElement(tag) {
// 默认的 web components 判断
export function defaultCustomElement(tag) {
return tag.startsWith('wc-')
}
export function startWorker(file) {
return new Promise((resolve, reject) => {
let worker = new Worker(file)
worker.on('message', resolve)
worker.on('error', reject)
worker.on('exit', code => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`))
}
})
})
}

View File

@ -15,7 +15,11 @@ class WebSocket {
conn.on('connection', (client, req) => {
let params = new URLSearchParams(req.url.slice(req.url.indexOf('?')))
let session = params.get('session')
let lock = +params.get('lock')
if (lock === 1) {
client.close()
} else {
this.#clients.set(session, client)
client.once('close', _ => {
@ -26,13 +30,14 @@ class WebSocket {
let msg = this.#queue.shift()
this.send(msg)
}
}
})
}
}
send(msg = {}) {
if (this.#clients.size) {
for (let [key, client] of this.#clients.entries()) {
for (let [key, client] of this.#clients) {
client.send(JSON.stringify(msg))
}
} else {

View File

@ -1,7 +1,7 @@
{
"name": "fite",
"type": "module",
"version": "1.1.10",
"version": "1.4.4",
"bin": {
"fite": "index.js"
},
@ -19,5 +19,6 @@
},
"engines": {
"node": ">=16.6.0"
}
},
"license": "MIT"
}