Compare commits

..

No commits in common. "master" and "0.4.0" have entirely different histories.

14 changed files with 446 additions and 1135 deletions

1
.gitignore vendored
View File

@ -1,6 +1,5 @@
.vscode
node_modules/
dist
*.sublime-project
*.sublime-workspace

View File

@ -1,10 +0,0 @@
jsxBracketSameLine: true
jsxSingleQuote: true
semi: false
singleQuote: true
printWidth: 80
useTabs: false
tabWidth: 2
trailingComma: none
bracketSpacing: true
arrowParens: avoid

21
LICENSE
View File

@ -1,21 +0,0 @@
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

@ -24,7 +24,7 @@
- 因为没有打包, 所以所有的文件引用都是按源代码的结构, 对于源码的保护比较弱(虽然打包也没约等于没保护, 因为前端没秘密)。
- 因为是用的是原生的`ESM`,所以引用的**依赖/文件**, 需要完整的路径, 可以省略后缀名, 但不能省略`index.js/index.vue`。
- 因为没有内置完整的样式处理,支持`scoped`、`:deep()`, 但不支持`:global()`
- 因为没有内置完整的样式处理, 所以`scoped特性`虽然支持, 但vue中的 `>>>、:deep、v-deep`等功能不可用
- `单文件组件`中的样式, 如果是用scss, 不支持引用其他文件, 也不支持设置共用定义文件。
- 样式预处理器, 只支持scss, 不支持less。

View File

@ -7,7 +7,7 @@
*/
import fs from 'iofs'
import { join, normalize } from 'node:path'
import { join, normalize } from 'path'
import { red, blue } from 'kolorist'
import createServer from './lib/dev.js'
@ -16,20 +16,16 @@ import compile from './lib/prod.js'
const WORK_SPACE = process.cwd()
const IS_WINDOWS = process.platform === 'win32'
const CONFIG_FILE = normalize(join(WORK_SPACE, 'fite.config.js'))
const CONFIG_FILE = normalize(join(WORK_SPACE, 'vue.live.js'))
const PROTOCOL = IS_WINDOWS ? 'file://' : ''
const NODE_VERSION = process.versions.node.split('.').map(n => +n)
const ABS_CONFIG_FILEPATH = PROTOCOL + CONFIG_FILE
const NODE_VERSION = +process.versions.node.split('.').slice(0, 2).join('.')
let args = process.argv.slice(2)
let mode = args.shift() || 'prod'
let clean = !args.includes('--no-clean') //
let verbose = args.includes('--verbose') //
if (NODE_VERSION[0] < 16 || (NODE_VERSION[0] === 16 && NODE_VERSION[1] < 6)) {
console.log(red('Error: 你当前的环境不满足 fite 构建工具的要求'))
if (NODE_VERSION < 16.6) {
console.log(red('Error: 你当前的环境不满足 Vue-live 构建工具的要求'))
console.log(
'fite 需要Node.js版本在 %s 以上, \n你当前的Node.js版本为: %s',
'Vue-live 需要Node.js版本在 %s 以上, \n你当前的Node.js版本为: %s',
blue('v16.6.0'),
red(process.version),
'\n\n'
@ -37,13 +33,10 @@ if (NODE_VERSION[0] < 16 || (NODE_VERSION[0] === 16 && NODE_VERSION[1] < 6)) {
process.exit()
}
switch (mode) {
switch (args[0]) {
case 'dev':
process.env.NODE_ENV = 'development'
import(ABS_CONFIG_FILEPATH)
import(PROTOCOL + CONFIG_FILE)
.then(function (conf) {
conf.default.ABS_CONFIG_FILEPATH = ABS_CONFIG_FILEPATH
createServer(WORK_SPACE, conf.default)
})
.catch(err => {
@ -52,17 +45,14 @@ switch (mode) {
break
case 'build':
process.env.NODE_ENV = 'production'
import(ABS_CONFIG_FILEPATH)
import(PROTOCOL + CONFIG_FILE)
.then(function (conf) {
let dist = conf.buildDir || 'dist'
if (clean && fs.isdir(dist)) {
console.log('清除dist目录...')
fs.rm(dist)
if (fs.isdir(dist)) {
fs.rm(dist, true)
}
conf.default.ABS_CONFIG_FILEPATH = ABS_CONFIG_FILEPATH
compile(WORK_SPACE, dist, conf.default, verbose)
fs.mkdir(dist)
compile(WORK_SPACE, dist, conf.default)
})
.catch(err => {
console.log(err)

View File

@ -6,43 +6,35 @@
import fs from 'iofs'
import scss from '@bytedo/sass'
import { createHash } from 'crypto'
import Es from 'esbuild'
import { join } from 'path'
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
HMR_SCRIPT
} from './constants.js'
import { createHmrScript, md5, uuid, urlJoin } from './utils.js'
const OPTIONS = {
style: 'compressed'
indentType: 'space',
indentWidth: 2
}
function minify(code) {
return Es.transformSync(code, { minify: true }).code.trim()
// 修正路径合并 避免在windows下被转义
function urlJoin(...args) {
return join(...args).replace(/\\/g, '/')
}
// 处理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}`
}
function md5(str = '') {
let sum = createHash('md5')
sum.update(str, 'utf8')
return sum.digest('hex').slice(0, 8)
}
// 处理style中的scoped属性
function scopeCss(css = '', hash) {
return css.replace(CSS_SHEET_EXP, (m, selector) => {
if (!selector.startsWith('@')) {
@ -50,39 +42,15 @@ function scopeCss(css = '', hash) {
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}[data-${hash}] ${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}`
}
}
let last = tmp.pop()
if (last.includes(':')) {
last = last.replace(':', `[data-${hash}]:`)
} else {
last += `[data-${hash}]`
}
return output
tmp.push(last)
return tmp.join(' ')
})
.join(', ')
}
@ -93,16 +61,18 @@ function scopeCss(css = '', hash) {
/**
* 编译scss为css
* @param file <String> 文件路径或scss代码
* @param inject <String> 要注入的scss代码
* @param mini <Boolean> 是否压缩
*/
export function compileScss(file, inject = '') {
export function compileScss(file, mini = true) {
let style = mini ? 'compressed' : 'expanded'
try {
if (fs.isfile(file)) {
return scss.compile(file, OPTIONS).css.trim()
return scss.compile(file, { style, ...OPTIONS }).css.trim()
} else {
return scss.compileString(inject + file, OPTIONS).css.trim()
return scss.compileString(file, { style, ...OPTIONS }).css.trim()
}
} catch (err) {
console.log('compile scss: ', file)
console.error(err)
}
}
@ -115,194 +85,113 @@ export function compileScss(file, inject = '') {
export function parseJs(
code = '',
imports,
{ IS_MPA, currentPage, IS_ENTRY, DEPLOY_PATH, LEGACY_MODE, define } = {},
filename,
linePatch = 1
{ IS_MPA, currentPage, IS_ENTRY, DEPLOY_PATH } = {},
isBuild
) {
let fixedStyle = ''
let fixedStyle = '\n\n'
let ASSETS_DIR = '/@/'
let isBuild = process.env.NODE_ENV === 'production'
if (isBuild) {
ASSETS_DIR = '/assets/'
ASSETS_DIR = '/assets/' // + (IS_MPA ? 'pages/' : '')
}
code = Es.transformSync(code).code || ''
try {
code = Es.transformSync(code).code || ''
} catch (e) {
let err = e.errors.pop()
let lines = code.split('\n')
return (
code
.replace(/\r\n/g, '\n')
.replace(
/import ([\w\W]*?) from (["'])(.*?)\2/g,
function (m, alias, q, name) {
if (name.startsWith('@/')) {
name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR))
}
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
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)
}
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))
}
} 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 (!name.endsWith('.js') && !name.endsWith('.vue')) {
if (name.includes('components')) {
name += '.vue'
} else {
name += '.js'
}
}
}
if (isBuild) {
name = name.replace(/\.vue$/, '.js')
}
return `import ${alias} from '${name}'`
}
)
.replace(/import\((['"])(.*?)\1\)/g, function (m, q, name) {
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))
}
return `import('${name}')`
})
.replace(/import (["'])(.*?)\1/g, function (m, q, name) {
if (name.endsWith('.css') || name.endsWith('.scss')) {
if (name.startsWith('@/')) {
name = name.replace('@/', '/')
}
if (isBuild) {
name = name.replace(/\.scss/, '.css')
}
// 修正那反人类的windows路径
let _name = name.replace(/\\/g, '/').replace('@/', '')
let tmp = `style_${uuid()}`
if (isBuild) {
name = name.replace(/\.scss/, '.css')
}
let tmp = `style${Date.now()}`
fixedStyle += `document.adoptedStyleSheets.push(${tmp})\n`
// 因为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})()`
// 修正那反人类的windows路径
return `import ${tmp} from '${name}' assert { type: 'css' }\n${tmp}.path = '${name.replace(
/\\/g,
'/'
)}'`
} 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
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)
}
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('.vue')) {
name += '.js'
}
} 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}'`
}
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
}) + fixedStyle
)
}
/**
@ -310,19 +199,16 @@ export function parseJs(
* @param file <String> 文件路径
* @return <String> 返回转换后的js代码
*/
export async function compileVue(file, imports, options = {}) {
// 修正那反人类的windows路径
export function compileVue(file, imports, options = {}, isBuild) {
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
@ -331,68 +217,32 @@ export async function compileVue(file, imports, options = {}) {
.map(it => {
let css
if (it.length > 2) {
css = compileScss(it[2], options.INJECT_SCSS)
css = compileScss(it[2])
if (it[1].includes('scoped')) {
scoped = true
css = scopeCss(css, hash)
}
} else {
css = compileScss(it[1], options.INJECT_SCSS)
css = compileScss(it[1])
}
return css
})
.join(' ')
for (let fn of options.plugin) {
scss = await fn('css', scss)
}
js = js ? js[1] : ''
js = js ? js[1] : 'export default {}'
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')
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()
output += html + '\n\n'
if (CACHE[file]) {
CACHE[file] = {
@ -404,36 +254,21 @@ function render(_ctx, _cache) {
CACHE[file] = { changed: false, js, html }
}
output += parseJs(js, imports, options, file, linePatch).replace(
output += parseJs(js, imports, options, isBuild).replace(
'export default {',
`\n${
options.LEGACY_MODE ? '' : SHEETS_DEF
}${html}\n\nconst __sfc__ = {\n render,\n`
'const __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)
}
// 修正那反人类的windows路径
output += `
let stylesheet = new CSSStyleSheet()
stylesheet.path = '${filename}'
stylesheet.replaceSync(\`${scss}\`)
document.adoptedStyleSheets.push(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`
@ -446,15 +281,19 @@ document.adoptedStyleSheets = __sheets__
/**
* 解析模板html
*/
export function parseHtml(html, { page, imports, entry, LEGACY_MODE }) {
export function parseHtml(html, { page, imports, entry }, isBuild = false) {
return html
.replace(/\r\n/g, '\n')
.replace(
'</head>',
`${
process.env.NODE_ENV === 'development'
? ` <script>${minify(createHmrScript(LEGACY_MODE))}</script>\n`
: ` <script>${minify(LEGACY_POLYFILL)}</script>\n`
` <script>window.process = {env: {NODE_ENV: '${
isBuild ? 'production' : 'development'
}'}}</script>\n${
isBuild
? ''
: ` <script>${
Es.transformSync(HMR_SCRIPT, { minify: true }).code
}</script>`
}</head>`
)
.replace('{{title}}', page.title || '')
@ -465,9 +304,4 @@ export function parseHtml(html, { page, imports, entry, LEGACY_MODE }) {
'<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}'`)
}

View File

@ -1,113 +0,0 @@
/**
* {}
* @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

@ -4,47 +4,56 @@
* @date 2022/09/06 11:54:56
*/
export const JS_EXP = /(?<=\n|^)<script[^>]*?>([\w\W]*?)<\/script>/
export const STYLE_EXP = /(?<=\n|^)<style([^>]*?)>([\w\W]*?)<\/style>/g
export const HTML_EXP = /(?<=\n|^)<template[^>]*?>([\w\W]*?)\n<\/template>/
export const V_DEEP = /:deep\(([^)]*?)\)(.*)/
export const JS_EXP = /<script[^>]*?>([\w\W]*?)<\/script>/
export const STYLE_EXP = /<style([^>]*?)>([\w\W]*?)<\/style>/g
export const HTML_EXP = /<template[^>]*?>([\w\W]*?)\n<\/template>/
export const CSS_SHEET_EXP = /([%@\w\.,#\-:>\+\~\|\(\)\[\]"'\=\s]+)\{/g
export const PERCENT_EXP = /^\d+%$/
export const CSS_SHEET_EXP = /([@\w\.,#\-:>\+\~\|\(\)\[\]"'\=\s]+)\{/g
export const COMMON_HEADERS = {
'Cache-Control': 'no-store'
}
export const SHEETS_DEF =
'const __sheets__ = [...document.adoptedStyleSheets];\n'
export const HMR_SCRIPT = `
!(function vue_live_hmr(){
var ws = new WebSocket(\`ws\${location.protocol === 'https:' ? 's' : ''}://\${location.host}/ws-vue-live\`)
export const LEGACY_POLYFILL = `!(function(){
function join(p1, p2) {
let tmp1 = p1.split('/')
let tmp2 = p2.split('/')
if (tmp1.at(-1) === '') {
tmp1.pop()
ws.addEventListener('open', function (r) {
if(vue_live_hmr.closed){
delete vue_live_hmr.closed
location.reload()
}
while (tmp2.length) {
let tmp = tmp2.shift()
if (tmp === '.' || tmp === '') {
continue
} else if (tmp === '..') {
tmp1.pop()
} else {
tmp1.push(tmp)
}
}
return tmp1.join('/')
}
console.log('vue-live hmr ready...')
})
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)
ws.addEventListener('close', function(){
vue_live_hmr.closed = true
setTimeout(vue_live_hmr, 2000)
})
ws.addEventListener('message', function (ev) {
var { action, data } = JSON.parse(ev.data)
switch (action) {
case 'reload':
location.reload()
break
case 'render':
{
let tmp = [...document.adoptedStyleSheets]
for (let i = -1, it; (it = tmp[++i]); ) {
if (it.path === data.path) {
let stylesheet = new CSSStyleSheet()
stylesheet.path = data.path
stylesheet.replaceSync(data.content)
document.adoptedStyleSheets[i] = stylesheet
break
}
}
}
break
}
return window.fetch(url).then(r => r.text())
}
})()`
})
})()
`

View File

@ -1,13 +1,13 @@
import http from 'node:http'
import https from 'node:http2'
import http from 'http'
import https from 'https'
import fs from 'iofs'
import { join, dirname } from 'node:path'
import { parse } from 'node:url'
import { join, resolve, dirname } from 'path'
import { parse } from 'url'
import socket from './ws.js'
import chokidar from 'chokidar'
import { red } from 'kolorist'
import { red, cyan, blue } from 'kolorist'
import { friendlyErrors, defaultCustomElement, gzip } from './utils.js'
import { friendlyErrors } from './utils.js'
import { compileScss, parseJs, compileVue, parseHtml } from './compile-vue.js'
@ -15,13 +15,9 @@ import MIME_TYPES from './mime-tpyes.js'
import { COMMON_HEADERS } from './constants.js'
const noc = Buffer.from('')
const SERVER_OPTIONS = { allowHTTP1: true }
const SERVER_OPTIONS = {}
const CACHE = {} //文件缓存, 用于hmr
function readFile(file) {
return (file && fs.cat(file)?.toString()) || ''
}
export default async function createServer(root = '', conf = {}) {
const SOURCE_DIR = join(root, 'src')
const PUBLIC_DIR = join(root, 'public')
@ -30,22 +26,6 @@ export default async function createServer(root = '', conf = {}) {
const PORT = conf.devServer.port || 8080
const USE_HTTPS = conf.devServer.https
const DOMAIN = conf.devServer.domain || 'localhost'
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']
}
if (conf.imports['vue-router-dev']) {
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)
@ -57,7 +37,7 @@ export default async function createServer(root = '', conf = {}) {
}
const server = (USE_HTTPS ? https : http)
[USE_HTTPS ? 'createSecureServer' : 'createServer'](SERVER_OPTIONS)
.createServer(SERVER_OPTIONS)
.listen(PORT)
const ws = socket(server)
@ -74,7 +54,7 @@ export default async function createServer(root = '', conf = {}) {
currentPage = ''
server
.on('request', async function (req, res) {
.on('request', function (req, res) {
let prefix = DEPLOY_PATH ? DEPLOY_PATH.replace(/\/$/, '') : ''
let url =
prefix && req.url.startsWith(prefix)
@ -87,52 +67,37 @@ export default async function createServer(root = '', conf = {}) {
if (prefix && req.url === '/') {
res.setHeader('Location', DEPLOY_PATH)
res.writeHead(302, USE_HTTPS ? void 0 : 'Redirect')
res.writeHead(302, 'Redirect')
return res.end('')
}
if (pathname) {
// 这种情况是, 页面是子目录的情况
if (pathname.includes('/') && pathname.endsWith('.html')) {
pageName = pathname.slice(0, -5)
if (conf.pages[pageName]) {
ext = 'html'
currentPage = pageName
pagesDir = dirname(conf.pages[pageName]?.entry)
}
pathname = pathname.split('/')
if (pathname[0].endsWith('.html')) {
pageName = pathname.shift()
let tmp = pageName.split('.')
ext = tmp.pop()
pageName = tmp.join('.')
currentPage = pageName
pagesDir = dirname(conf.pages[pageName].entry)
} else {
pathname = pathname.split('/')
if (pathname[0].endsWith('.html')) {
pageName = pathname.shift()
let tmp = pageName.split('.')
ext = tmp.pop()
pageName = tmp.join('.')
// 页面不存在时输出404, 避免进程崩溃退出
if (!conf.pages[pageName]) {
res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
return res.end(`Oops!!! 404 Not Found`)
}
currentPage = pageName
pagesDir = dirname(conf.pages[pageName]?.entry)
if (currentPage) {
let tmp = pathname.at(-1).split('.')
// 修正history路由时的访问
ext = tmp.length > 1 ? tmp.pop() : 'html'
pageName = currentPage
} else {
if (currentPage) {
let tmp = pathname.at(-1).split('.')
// 修正history路由时的访问
ext = tmp.length > 1 ? tmp.pop() : 'html'
pageName = currentPage
} else {
pageName = Object.keys(conf.pages).pop()
currentPage = pageName
pagesDir = dirname(conf.pages[pageName].entry)
ext = 'html'
}
pageName = Object.keys(conf.pages).pop()
currentPage = pageName
pagesDir = dirname(conf.pages[pageName].entry)
ext = 'html'
}
pathname = pathname.join('/')
}
pathname = pathname.join('/')
} else {
if (IS_MPA) {
isIndex = true
@ -150,13 +115,8 @@ export default async function createServer(root = '', conf = {}) {
if (isIndex) {
res.setHeader('content-type', MIME_TYPES.html)
res.writeHead(200, USE_HTTPS ? void 0 : 'OK')
res.end(
'<style>body{font:14px/1.5 Arial}ol{display:flex;flex-wrap:wrap;}li{width:30%;}a{color:teal}a:visited{color:orange;}</style>' +
'<div>注意: 你看到这个页面, 仅在开发时可见。<br>仅为了方便开发多页应用时访问自己想要修改的页面, 而不需要手动输入地址。</div><ol>' +
indexPage +
'</ol>'
)
res.writeHead(200, 'OK')
res.end('<ul>' + indexPage + '</ul>')
} else {
res.setHeader('accept-ranges', 'bytes')
@ -168,47 +128,28 @@ export default async function createServer(root = '', conf = {}) {
res.setHeader('content-type', MIME_TYPES.html)
let page = conf.pages[pageName]
let entry = fs.cat(page.entry)?.toString()
let entry = fs.cat(page.entry).toString()
let html = fs.cat(join(process.cwd(), 'index.html')).toString()
entry = parseJs(
entry,
conf.imports,
{
IS_MPA,
currentPage,
IS_ENTRY: true,
DEPLOY_PATH,
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
entry = parseJs(entry, conf.imports, {
IS_MPA,
currentPage,
IS_ENTRY: true,
DEPLOY_PATH
})
code = parseHtml(html, { page, imports: conf.imports, entry })
}
break
case 'vue':
{
let rpath = pathname.replace('@/', '')
let rpath = pathname.replace(/@\//, '')
let file
if (IS_MPA) {
// 判断前后2个值相等, 避免出现目录名和页面名字相同时走错逻辑
if (rpath === pathname && rpath.startsWith(currentPage)) {
if (rpath.startsWith(currentPage)) {
file = join(pagesDir, rpath.slice(currentPage.length))
} else {
file = join(SOURCE_DIR, rpath)
@ -218,28 +159,19 @@ export default async function createServer(root = '', conf = {}) {
}
if (!fs.isfile(file)) {
friendlyErrors(pathname, ext)
res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
res.writeHead(404, 'Not Found')
res.end('')
return
}
code = await compileVue(file, conf.imports, {
code = compileVue(file, conf.imports, {
IS_MPA,
currentPage,
SOURCE_DIR,
CACHE,
DEPLOY_PATH,
INJECT_SCSS,
LEGACY_MODE,
isCustomElement,
plugin,
define
DEPLOY_PATH
})
for (let fn of plugin) {
code = await fn('js', code)
}
res.setHeader('content-type', MIME_TYPES.js)
}
break
@ -247,47 +179,30 @@ export default async function createServer(root = '', conf = {}) {
case 'scss':
case 'css':
{
let file = join(SOURCE_DIR, pathname.replace('@/', ''))
let file = join(SOURCE_DIR, pathname.replace(/@\//, ''))
if (!fs.isfile(file)) {
file = join(PUBLIC_DIR, pathname.replace('@/', ''))
file = join(PUBLIC_DIR, pathname.replace(/@\//, ''))
if (!fs.isfile(file)) {
friendlyErrors(pathname, ext)
res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
res.setHeader('content-type', MIME_TYPES.html)
res.writeHead(404, 'Not Found')
res.end('')
return
}
}
if (file === conf.inject?.scss) {
console.log(red('设置为注入的样式文件不可被vue/js文件引用\n'))
res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
res.end('')
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 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个值相等, 避免出现目录名和页面名字相同时走错逻辑
if (rpath === pathname && rpath.startsWith(currentPage)) {
if (rpath.startsWith(currentPage)) {
file = join(pagesDir, rpath.slice(currentPage.length))
} else {
file = join(SOURCE_DIR, rpath)
@ -299,92 +214,45 @@ export default async function createServer(root = '', conf = {}) {
if (fs.isfile(file)) {
code = fs.cat(file)
} else if (fs.isfile(join(PUBLIC_DIR, rpath))) {
file = join(PUBLIC_DIR, rpath)
code = fs.cat(file)
code = fs.cat(join(PUBLIC_DIR, rpath))
} else {
friendlyErrors(rpath, ext)
res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
res.writeHead(404, 'Not Found')
res.end('')
return
}
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,
isCustomElement,
plugin,
define
},
file
)
for (let fn of plugin) {
code = await fn('js', code)
}
}
res.setHeader('content-type', MIME_TYPES[ext])
code = parseJs(code + '', conf.imports, {
IS_MPA,
currentPage,
DEPLOY_PATH
})
res.setHeader('content-type', MIME_TYPES.js)
}
break
default:
res.setHeader('content-type', MIME_TYPES[ext] || MIME_TYPES.other)
let pub_file = join(PUBLIC_DIR, pathname)
let source_file = join(SOURCE_DIR, pathname)
if (fs.isfile(pub_file)) {
code = fs.cat(pub_file)
if (code) {
let stat = fs.stat(pub_file)
res.setHeader(
'Last-Modified',
new Date(stat.mtime).toGMTString()
)
}
} else if (fs.isfile(source_file)) {
code = fs.cat(source_file)
if (code) {
let stat = fs.stat(source_file)
res.setHeader(
'Last-Modified',
new Date(stat.mtime).toGMTString()
)
}
if (fs.isfile(join(PUBLIC_DIR, pathname))) {
code = fs.cat(join(PUBLIC_DIR, pathname))
} else if (fs.isfile(join(SOURCE_DIR, pathname))) {
code = fs.cat(join(SOURCE_DIR, pathname))
} else {
code = null
}
if (code === null) {
friendlyErrors(pathname, ext)
res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
res.writeHead(404, 'Not Found')
res.end('')
return
}
break
}
if (ENABLE_GZIP) {
code = gzip(code || noc)
res.setHeader('Content-Encoding', 'gzip')
} else {
code = code || noc
}
res.setHeader('Content-Length', Buffer.byteLength(code))
res.writeHead(200, USE_HTTPS ? void 0 : 'OK')
res.end(code)
res.setHeader('content-length', Buffer.byteLength(code || noc))
res.writeHead(200, 'OK')
res.end(code || noc)
}
})
@ -401,7 +269,7 @@ export default async function createServer(root = '', conf = {}) {
console.log(
' 本地: %s://%s:%d%s',
USE_HTTPS ? 'https' : 'http',
USE_HTTPS ? 'localhost' : '127.0.0.1',
'127.0.0.1',
PORT,
DEPLOY_PATH
)
@ -412,78 +280,60 @@ export default async function createServer(root = '', conf = {}) {
PORT,
DEPLOY_PATH
)
chokidar
.watch([SOURCE_DIR, PUBLIC_DIR, join(root, './index.html')])
.on('all', async (act, filePath) => {
if (ready) {
let file = filePath.slice(SOURCE_DIR.length)
})
if (act === 'add' || act === 'change') {
let ext = file.slice(file.lastIndexOf('.') + 1)
chokidar
.watch([SOURCE_DIR, PUBLIC_DIR, join(root, './index.html')])
.on('all', (act, filePath) => {
if (ready) {
let file = filePath.slice(SOURCE_DIR.length)
switch (ext) {
case 'css':
case 'scss':
{
let content = ''
if (filePath === conf.inject?.scss) {
return
}
if (ext === 'scss') {
content = compileScss(filePath)
} 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 }
})
}
break
if (act === 'add' || act === 'change') {
let ext = file.slice(file.lastIndexOf('.') + 1)
case 'vue':
{
let content = await compileVue(filePath, conf.imports, {
IS_MPA,
currentPage,
SOURCE_DIR,
CACHE,
DEPLOY_PATH,
INJECT_SCSS,
LEGACY_MODE,
isCustomElement,
plugin,
define
})
let tmp = CACHE[filePath]
if (tmp.changed) {
ws.send({ action: 'reload' })
} else {
ws.send({
action: 'render',
data: {
path: file.replace(/\\/g, '/'),
content: tmp.css
}
})
}
}
break
default:
ws.send({ action: 'reload' })
break
switch (ext) {
case 'css':
case 'scss':
{
let content = fs.cat(filePath).toString()
ws.send({
action: 'render',
data: { path: file.replace(/\\/g, '/'), content }
})
}
} else if (act === 'unlink' || act === 'unlinkDir') {
break
case 'vue':
{
let content = compileVue(filePath, conf.imports, {
IS_MPA,
currentPage,
SOURCE_DIR,
CACHE,
DEPLOY_PATH
})
let tmp = CACHE[filePath]
if (tmp.changed) {
ws.send({ action: 'reload' })
} else {
ws.send({
action: 'render',
data: { path: file.replace(/\\/g, '/'), content: tmp.css }
})
}
}
break
default:
ws.send({ action: 'reload' })
}
break
}
})
.on('ready', () => {
ready = true
})
} else if (act === 'unlink' || act === 'unlinkDir') {
ws.send({ action: 'reload' })
}
}
})
.on('ready', () => {
ready = true
})
}

View File

@ -1,224 +1,149 @@
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 { join, resolve, dirname, parse } from 'path'
import Es from 'esbuild'
import { compileScss, parseJs, compileVue, parseHtml } from './compile-vue.js'
import { compileFiles } from './compile.js'
import { defaultCustomElement } from './utils.js'
const noc = Buffer.from('')
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) {
export default function compile(root = '', dist = '', conf = {}) {
//
const SOURCE_DIR = join(root, 'src')
const PUBLIC_DIR = join(root, 'public')
const DEPLOY_PATH = conf.base || '' // 部署目录, 默认是根目录部署
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: '' }
const IS_MPA = Object.keys(conf.pages).length > 1
let timeStart = Date.now()
let template = fs.cat(join(process.cwd(), 'index.html')).toString()
let list = new Map()
let options = {
IS_MPA,
SOURCE_DIR,
DEPLOY_PATH,
INJECT_SCSS,
LEGACY_MODE,
ABS_CONFIG_FILEPATH,
define
}
let template = fs.cat(join(process.env.PWD, 'index.html')).toString()
fs.ls(SOURCE_DIR, true).forEach(path => {
if (fs.isdir(path)) {
return
}
let list = fs
.ls(SOURCE_DIR, true)
.map(it => ({
name: it.slice(SOURCE_DIR.length + 1),
path: it,
ext: parse(it).ext
}))
.filter(it => fs.isfile(it.path))
let name = path.slice(SOURCE_DIR.length + 1)
let it = {
name,
ext: parse(name).ext
}
if (it.ext) {
if (IS_MPA && it.name.startsWith(PREFIX)) {
if (PAGES_PREFIX.some(it => it.startsWith(it.name))) {
list.set(path, it)
}
return
let compileFiles = function (currentPage, page, files) {
for (let it of files) {
// 入口文件, 特殊处理
if (page && it.path === page.entry) {
let entry = fs.cat(page.entry).toString()
entry = parseJs(
entry,
conf.imports,
{ IS_MPA, currentPage, IS_ENTRY: true, DEPLOY_PATH },
true
)
let code = parseHtml(
template,
{ page, imports: conf.imports, entry },
true
)
fs.echo(code, join(dist, `${currentPage}.html`))
continue
}
if (path === conf.inject.scss) {
return
}
list.set(path, it)
}
})
console.log(' 解析 %s ...', it.name)
// 创建线程池
if (THREADS_NUM > 0 && (IS_MPA || list.size > THREADS_NUM * 10)) {
// 页面数过少时, 线程数量不能比页面数多
let max = Math.min(THREADS_NUM, PAGES_KEYS.length)
let pageDir = IS_MPA && currentPage ? `pages/${currentPage}` : ''
for (let i = 0; i < max; i++) {
WORKER_POOL.add(
new Worker(join(__dirname, './thread.js'), {
workerData: {
options,
verbose,
dist,
imports: conf.imports
switch (it.ext) {
case '.vue':
{
let code = compileVue(
it.path,
conf.imports,
{ IS_MPA, currentPage, SOURCE_DIR, DEPLOY_PATH },
true
)
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,
{ IS_MPA, currentPage, DEPLOY_PATH },
true
)
Es.transform(code, { minify: true }).then(r => {
fs.echo(r.code, join(dist, 'assets/', pageDir, it.name))
})
}
break
case '.scss':
case '.css':
{
let code = compileScss(it.path)
if (!it.name.startsWith('assets')) {
it.name = 'assets/' + it.name
}
fs.echo(code, join(dist, it.name.replace(/\.scss$/, '.css')))
}
break
default:
fs.cp(it.path, join(dist, it.name))
break
}
}
} else {
options.isCustomElement = isCustomElement
}
// 优先处理静态目录, 之后的源码目录中, 以便如果有产生相同的文件名, 则覆盖静态目录中的文件
for (let currentPage in conf.pages) {
let page = conf.pages[currentPage]
let dir = dirname(page.entry)
let files = list
if (IS_MPA) {
files = []
fs.ls(dir, true).forEach(it => {
if (fs.isdir(it)) {
return
}
let idx = list.findIndex(_ => _.path === it)
list.splice(idx, 1)
files.push({
name: it.slice(dir.length + 1),
path: it,
ext: parse(it).ext
})
})
}
console.log('正在生成 %s ...', `${currentPage}.html`)
compileFiles(currentPage, page, files)
}
if (IS_MPA) {
console.log('\n正在解析公共依赖 ...')
compileFiles('', null, list)
}
//
if (fs.isdir(PUBLIC_DIR)) {
console.log('\n正在处理静态资源 ...')
fs.ls(PUBLIC_DIR, true).forEach(it => {
let ext = parse(it).ext
if (ext && fs.isfile(it)) {
if (fs.isfile(it)) {
let name = it.slice(PUBLIC_DIR.length + 1)
verbose && console.log(' 复制 %s ...', name)
console.log(' 复制 %s ...', name)
fs.cp(it, join(dist, name))
}
})
}
if (IS_MPA) {
for (let currentPage of PAGES_KEYS) {
let page = conf.pages[currentPage]
let dir = dirname(page.entry)
let files = new Map()
let chunk = new Map()
fs.ls(dir, true).forEach(path => {
if (fs.isdir(path)) {
return
}
let name = path.slice(dir.length + 1)
let ext = parse(name).ext
if (ext === '') {
return
}
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
})
}
}
// 公共依赖
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, 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', _ => {
console.log('\n页面处理完成, 耗时 %ss\n', (Date.now() - timeStart) / 1000)
})
console.log('\n页面处理完成, 耗时 %ss\n', (Date.now() - timeStart) / 1000)
}

View File

@ -1,40 +0,0 @@
/**
* 子线程
* @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

@ -1,33 +1,4 @@
/**
* {一些工具类函数}
* @author yutent<yutent.io@gmail.com>
* @date 2023/05/22 14:52:00
*/
import { createHash, randomUUID } from 'node:crypto'
import { join } from 'node:path'
import { gzipSync } from 'node:zlib'
import { red, cyan, blue } from 'kolorist'
import { LEGACY_POLYFILL } from './constants.js'
// 修正路径合并 避免在windows下被转义
export function urlJoin(...args) {
return join(...args).replace(/\\/g, '/')
}
export function uuid() {
return randomUUID().slice(-8)
}
export function md5(str = '') {
let sum = createHash('md5')
sum.update(str, 'utf8')
return sum.digest('hex').slice(0, 8)
}
export function gzip(val) {
return gzipSync(val)
}
export function friendlyErrors(pathname, ext = '') {
console.log(cyan(pathname), red(`not found!!!`))
@ -36,70 +7,3 @@ export function friendlyErrors(pathname, ext = '') {
blue(`/index.${ext}`)
)
}
export function createHmrScript(legacy, session = '') {
return `
!(function vue_live_hmr(){
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){
delete vue_live_hmr.closed
location.reload()
}
console.log('fite hmr ready...')
})
ws.addEventListener('close', function(){
vue_live_hmr.closed = true
if (localStorage.getItem(location.pathname) === '1') {
return
}
setTimeout(vue_live_hmr, 2000)
})
ws.addEventListener('message', function (ev) {
var { action, data } = JSON.parse(ev.data)
switch (action) {
case 'reload':
location.reload()
break
case 'render':
{
${
legacy
? `
let stylesheet = document.head.children.namedItem(data.path)
if (stylesheet) {
stylesheet.textContent = data.content
}
`
: `
let tmp = [...document.adoptedStyleSheets]
for (let i = -1, it; (it = tmp[++i]); ) {
if (it.path === data.path) {
let stylesheet = new CSSStyleSheet()
stylesheet.path = data.path
stylesheet.replaceSync(data.content)
tmp[i] = stylesheet
document.adoptedStyleSheets = tmp
break
}
}
`
}
}
break
}
})
${LEGACY_POLYFILL}
})()
`
}
// 默认的 web components 判断
export function defaultCustomElement(tag) {
return tag.startsWith('wc-')
}

View File

@ -6,40 +6,28 @@
import { WebSocketServer } from 'ws'
class WebSocket {
#clients = new Map()
#ws = null // ws实例
#queue = [] // 消息队列
constructor(server) {
if (server.listening) {
let conn = new WebSocketServer({ server, path: '/ws-fite-hmr' })
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', _ => {
this.#clients.delete(session)
})
while (this.#queue.length) {
let msg = this.#queue.shift()
this.send(msg)
}
let conn = new WebSocketServer({ server, path: '/ws-vue-live' })
conn.on('connection', ws => {
this.#ws = ws
// ws.on('message', data => {
// console.log(data + '');
// })
while (this.#queue.length) {
let msg = this.#queue.shift()
this.send(msg)
}
})
}
}
send(msg = {}) {
if (this.#clients.size) {
for (let [key, client] of this.#clients) {
client.send(JSON.stringify(msg))
}
if (this.#ws) {
this.#ws.send(JSON.stringify(msg))
} else {
this.#queue.push(msg)
}

View File

@ -1,13 +1,10 @@
{
"name": "fite",
"type": "module",
"version": "1.4.4",
"version": "0.4.0",
"bin": {
"fite": "index.js"
},
"scripts": {
"pack": "esbuild index.js --minify --bundle --format=esm --target=esnext --platform=node --external:@bytedo/sass --external:esbuild --external:iofs --external:@vue/compiler-dom --external:ws --external:chokidar --external:kolorist --outfile=dist/index.js"
},
"dependencies": {
"@bytedo/sass": "^1.54.8",
"@vue/compiler-dom": "^3.2.47",
@ -19,6 +16,5 @@
},
"engines": {
"node": ">=16.6.0"
},
"license": "MIT"
}
}