Compare commits

..

82 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
yutent 3c239fe721 首页分屏 2023-06-13 17:45:12 +08:00
yutent f08361a986 Merge pull request #2 from bytedo/http2
增加一个没什么用的gzip特性
2023-06-13 16:21:27 +08:00
yutent 6580ab6837 增加一个没什么用的gzip特性 2023-06-13 16:21:24 +08:00
yutent f150cbe0ff Merge pull request #1 from bytedo/http2
https -> http2
2023-06-13 15:32:20 +08:00
yutent aba20ae873 https -> http2 2023-06-13 15:32:19 +08:00
yutent 9966e3ac9a 修复vue文件中引入样式文件报错的bug 2023-06-08 16:03:05 +08:00
yutent 6af38aad87 修复--no-clean 2023-05-31 15:49:09 +08:00
yutent 239ebe6cf3 兼容辣鸡windows打包 2023-05-30 17:06:53 +08:00
yutent a10ebac085 1.1.7 2023-05-30 15:24:51 +08:00
yutent 24a450ffe8 修复后缀为mjs的依赖引入 2023-05-30 10:19:41 +08:00
yutent 73ff200b87 优化编译参数 2023-05-29 15:45:21 +08:00
yutent 45ac8e8b3d 计时改为进程退出时 2023-05-29 15:24:20 +08:00
yutent 28838bb848 格式化 2023-05-26 15:15:59 +08:00
yutent 3385c4947a 修复fite的环境判断 2023-05-23 18:32:24 +08:00
yutent 388465842c 修改process.env.PWD为process.cwd() 2023-05-22 19:29:02 +08:00
yutent e6af6842a5 优化css/scss处理;修复dist目录不清除的bug 2023-05-22 18:10:24 +08:00
yutent ba63cd14ba 临时取消特殊处理assets目录 2023-05-22 17:25:36 +08:00
yutent f09e6199ac 调整isCustomElement的设定 2023-05-22 15:06:33 +08:00
yutent 877ba7c8bf 开发模式, 可替换vue和vue-router为开发版 2023-05-19 10:40:45 +08:00
yutent 1a56f37879 兼容vue文件不写script标签的场景 2023-05-18 18:02:15 +08:00
yutent 7d9e691232 1.1.1 2023-05-18 16:34:13 +08:00
yutent ad12433785 兼容export xx from yy 语法 2023-05-18 15:50:29 +08:00
yutent 27afea8649 修复使用独立的样式文件时热更新失效的bug; 增加legacy模式的支持 2023-05-18 12:04:24 +08:00
yutent 7ed5f81da2 增加legacy模式, 兼容不支持import assertions的浏览器 2023-05-17 19:15:51 +08:00
yutent 5cd075c46e 优化dev输出 2023-05-16 16:38:03 +08:00
yutent 8c26342cd5 优化开发提示 2023-05-16 15:09:13 +08:00
yutent 1265d0e94f 优化注入提示 2023-05-16 14:29:34 +08:00
yutent 97a10f3235 增加scss注入的支持 2023-05-16 14:17:40 +08:00
yutent 927aaa5bc9 修复打包 2023-05-15 15:17:55 +08:00
yutent 5d9f768f1e 优化模板解析 2023-05-15 10:54:00 +08:00
yutent 99a5fac97d 修复文件目录名和页面名字相同时逻辑处理错误的bug 2023-05-12 18:47:06 +08:00
yutent 472aa4d116 修复入口文件样式引用不生效的bug 2023-05-12 11:05:03 +08:00
yutent 0b398f4d67 修复入口文件有样式引用时报错的bug 2023-05-11 18:17:28 +08:00
yutent 6a6a8e31c6 0.9.0 2023-05-11 12:25:36 +08:00
yutent 0af6b36027 代码中不再注入环境变量; 模板文件支持条件判断 2023-05-11 12:25:07 +08:00
yutent 3e6ecc5e5f 修复单文件组件中同时引入多个样式文件时重名的bug; 优化vue文件的解析 2023-05-11 11:37:50 +08:00
yutent b08b5c9172 生产模式增加--no-clean配置; 生产模式调整public目录的权重为最低 2023-05-10 13:22:02 +00:00
yutent f28212f933 修复scoped解析 2023-05-04 14:58:03 +08:00
yutent d1e1084ec1 非代码文件增加Last-Modified头返回 2023-04-28 19:02:23 +08:00
yutent d5dcd44fec 重构style[scoped]的实现 2023-04-27 17:13:58 +08:00
yutent 574dd7e693 优化style[scoped]的解析 2023-04-27 15:46:28 +08:00
yutent dc1fea1bbb 增加容错处理,避免代码出现语法错误时构建工具崩溃退出的问题 2023-04-26 17:16:43 +08:00
yutent a0f06b0171 修复样式配置scoped时, 误伤keyframes的bug 2023-04-26 15:10:16 +08:00
yutent d80fd97601 兼容国产辣鸡浏览器 2023-04-25 11:12:27 +08:00
yutent 5b3a976fd3 修复因端口占用导致的文件监听重复的bug 2023-04-24 09:33:33 +08:00
yutent e5a76f21ab 移除调试代码 2023-04-21 10:59:12 +08:00
yutent 5674634471 配置文件名更换为fite.config.js; 支持页面为二级目录 2023-04-20 18:56:01 +08:00
yutent 9da8158ea9 编译时过滤无效文件 2023-03-28 15:41:01 +08:00
yutent 22fa35c77d 增加打包配置 2023-03-28 12:06:12 +08:00
yutent d135f8c266 update readme 2023-03-28 11:43:29 +08:00
14 changed files with 1142 additions and 466 deletions

1
.gitignore vendored
View File

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

10
.prettierrc.yaml Normal file
View File

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

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

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

View File

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

View File

@ -6,36 +6,43 @@
import fs from 'iofs' import fs from 'iofs'
import scss from '@bytedo/sass' import scss from '@bytedo/sass'
import { createHash } from 'crypto'
import Es from 'esbuild' import Es from 'esbuild'
import { join } from 'path'
import { compile } from '@vue/compiler-dom' import { compile } from '@vue/compiler-dom'
import { red, cyan, blue } from 'kolorist'
import { import {
JS_EXP, JS_EXP,
STYLE_EXP, STYLE_EXP,
HTML_EXP, HTML_EXP,
CSS_SHEET_EXP, CSS_SHEET_EXP,
HMR_SCRIPT, V_DEEP,
V_DEEP PERCENT_EXP,
SHEETS_DEF,
LEGACY_POLYFILL
} from './constants.js' } from './constants.js'
import { createHmrScript, md5, uuid, urlJoin } from './utils.js'
const OPTIONS = { const OPTIONS = {
indentType: 'space', style: 'compressed'
indentWidth: 2
} }
// 修正路径合并 避免在windows下被转义 function minify(code) {
function urlJoin(...args) { return Es.transformSync(code, { minify: true }).code.trim()
return join(...args).replace(/\\/g, '/')
} }
function md5(str = '') { // 处理css中的 :deep()
let sum = createHash('md5') function parseVDeep(curr, val, scoped) {
sum.update(str, 'utf8') let res = V_DEEP.exec(curr)
return sum.digest('hex').slice(0, 8) if (res) {
scoped && (val = val.replace(/\[data\-[^\]]+\]/g, ''))
return `${res[1] + res[2]} ${val}`
} else {
return `${curr} ${val}`
}
} }
// 处理style中的scoped属性
function scopeCss(css = '', hash) { function scopeCss(css = '', hash) {
return css.replace(CSS_SHEET_EXP, (m, selector) => { return css.replace(CSS_SHEET_EXP, (m, selector) => {
if (!selector.startsWith('@')) { if (!selector.startsWith('@')) {
@ -43,26 +50,39 @@ function scopeCss(css = '', hash) {
selector = selector selector = selector
.map(s => { .map(s => {
// 针对 @keyframe的处理
if (s === 'from' || s === 'to' || PERCENT_EXP.test(s)) {
return s
}
let tmp = s.split(' ') let tmp = s.split(' ')
let last = tmp.pop() let output = ''
if (last.includes(':')) { let last
let res = V_DEEP.exec(last) let scoped = false
if (res) {
last = tmp.pop() while ((last = tmp.pop())) {
last += `[data-${hash}] ` + res[1] if (scoped) {
} else {
if (last.startsWith(':')) { if (last.startsWith(':')) {
let _prev = tmp.pop() output = parseVDeep(last, output, true)
last = `${_prev}[data-${hash}] ` + last
} else { } 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}]:`) last = last.replace(':', `[data-${hash}]:`)
} output = `${last} ${output}`
} }
} else { } else {
last += `[data-${hash}]` scoped = true
output = `${last}[data-${hash}] ${output}`
} }
tmp.push(last) }
return tmp.join(' ') }
return output
}) })
.join(', ') .join(', ')
} }
@ -73,18 +93,16 @@ function scopeCss(css = '', hash) {
/** /**
* 编译scss为css * 编译scss为css
* @param file <String> 文件路径或scss代码 * @param file <String> 文件路径或scss代码
* @param mini <Boolean> 是否压缩 * @param inject <String> 要注入的scss代码
*/ */
export function compileScss(file, mini = true) { export function compileScss(file, inject = '') {
let style = mini ? 'compressed' : 'expanded'
try { try {
if (fs.isfile(file)) { if (fs.isfile(file)) {
return scss.compile(file, { style, ...OPTIONS }).css.trim() return scss.compile(file, OPTIONS).css.trim()
} else { } else {
return scss.compileString(file, { style, ...OPTIONS }).css.trim() return scss.compileString(inject + file, OPTIONS).css.trim()
} }
} catch (err) { } catch (err) {
console.log('compile scss: ', file)
console.error(err) console.error(err)
} }
} }
@ -97,23 +115,47 @@ export function compileScss(file, mini = true) {
export function parseJs( export function parseJs(
code = '', code = '',
imports, imports,
{ IS_MPA, currentPage, IS_ENTRY, DEPLOY_PATH } = {}, { IS_MPA, currentPage, IS_ENTRY, DEPLOY_PATH, LEGACY_MODE, define } = {},
isBuild filename,
linePatch = 1
) { ) {
let fixedStyle = '\n\n' let fixedStyle = ''
let ASSETS_DIR = '/@/' let ASSETS_DIR = '/@/'
let isBuild = process.env.NODE_ENV === 'production'
if (isBuild) { if (isBuild) {
ASSETS_DIR = '/assets/' // + (IS_MPA ? 'pages/' : '') ASSETS_DIR = '/assets/'
} }
code = Es.transformSync(code).code || ''
return ( try {
code code = Es.transformSync(code).code || ''
} catch (e) {
let err = e.errors.pop()
let lines = code.split('\n')
console.log('%s: %s', red('Uncaught SyntaxError'), red(err.text))
// 将上下文几行都打印出来
for (let i = err.location.line - 3; i <= err.location.line + 1; i++) {
console.log(
' @ line %d: %s',
i + linePatch,
err.location.line === i + 1 ? red(lines[i]) : lines[i]
)
}
console.log(
' @ %s:%d:%d',
blue(filename),
err.location.line + linePatch - 1,
err.location.column
)
}
code = code
.replace(/\r\n/g, '\n') .replace(/\r\n/g, '\n')
.replace(/process\.env\.NODE_ENV/g, `'${process.env.NODE_ENV}'`)
.replace( .replace(
/import ([\w\W]*?) from (["'])(.*?)\2/g, /(import|export) ([^'"]*?) from (["'])(.*?)\3/g,
function (m, alias, q, name) { function (m, t, alias, q, name) {
if (name.startsWith('@/')) { if (name.startsWith('@/')) {
name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR)) name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR))
} }
@ -134,7 +176,11 @@ export function parseJs(
name = name.replace(/^\//, urlJoin(DEPLOY_PATH, ASSETS_DIR)) name = name.replace(/^\//, urlJoin(DEPLOY_PATH, ASSETS_DIR))
} }
if (!name.endsWith('.js') && !name.endsWith('.vue')) { if (
!name.endsWith('.js') &&
!name.endsWith('.mjs') &&
!name.endsWith('.vue')
) {
if (name.includes('components')) { if (name.includes('components')) {
name += '.vue' name += '.vue'
} else { } else {
@ -145,7 +191,19 @@ export function parseJs(
if (isBuild) { if (isBuild) {
name = name.replace(/\.vue$/, '.js') name = name.replace(/\.vue$/, '.js')
} }
return `import ${alias} from '${name}'` 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) { .replace(/import\((['"])(.*?)\1\)/g, function (m, q, name) {
@ -155,25 +213,49 @@ export function parseJs(
if (name.startsWith('@/')) { if (name.startsWith('@/')) {
name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR)) name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR))
} }
if (name.endsWith('.json')) {
name += '.js'
}
return `import('${name}')` return `import('${name}')`
}) })
.replace(/import (["'])(.*?)\1/g, function (m, q, name) { .replace(/import (["'])(.*?)\1/g, function (m, q, name) {
if (name.endsWith('.css') || name.endsWith('.scss')) { if (name.endsWith('.css') || name.endsWith('.scss')) {
if (name.startsWith('@/')) { if (name.startsWith('@/')) {
name = name.replace('@/', '/') name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR))
} }
if (isBuild) { if (isBuild) {
name = name.replace(/\.scss/, '.css') name = name.replace(/\.scss/, '.css')
} }
let tmp = `style${Date.now()}`
fixedStyle += `document.adoptedStyleSheets.push(${tmp})\n`
// 修正那反人类的windows路径 // 修正那反人类的windows路径
return `import ${tmp} from '${name}' assert { type: 'css' }\n${tmp}.path = '${name.replace( let _name = name.replace(/\\/g, '/').replace('@/', '')
/\\/g, let tmp = `style_${uuid()}`
'/'
)}'` // 因为esm语法的变更, 原先的 import xx from xx assets {type: css} 变为了 with
// 而这个语法的变化, 构建工具无法做版本判断, 故, 统一降级到fetch()加载
if (LEGACY_MODE) {
fixedStyle +=
`{\n` +
` let stylesheet = document.createElement('style');\n` +
` stylesheet.setAttribute('name', '${_name}');\n` +
` stylesheet.textContent = ${tmp};\n` +
` document.head.appendChild(stylesheet);\n` +
`}\n`
return `let ${tmp};\n!(async function(){\n ${tmp} = await __fite_import('${name}', import.meta.url);\n})()`
} else {
// CSSStyleSheet.replaceSync 需要FF v101, Safari 16.4才支持
fixedStyle +=
`{\n` +
` let stylesheet = new CSSStyleSheet();\n` +
` stylesheet.path = '${_name}';\n` +
` stylesheet.replaceSync(${tmp} );\n` +
` __sheets__.push(stylesheet);\n` +
` document.adoptedStyleSheets = __sheets__;\n` +
`}\n`
return `const ${tmp} = await __fite_import('${name}', import.meta.url)`
}
} else { } else {
if (name.startsWith('@/')) { if (name.startsWith('@/')) {
name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR)) name = name.replace('@/', urlJoin(DEPLOY_PATH, ASSETS_DIR))
@ -195,15 +277,32 @@ export function parseJs(
name = name.replace(/^\//, urlJoin(DEPLOY_PATH, ASSETS_DIR)) name = name.replace(/^\//, urlJoin(DEPLOY_PATH, ASSETS_DIR))
} }
if (!name.endsWith('.js') && !name.endsWith('.vue')) { if (
!name.endsWith('.js') &&
!name.endsWith('.mjs') &&
!name.endsWith('.vue')
) {
name += '.js' name += '.js'
} }
} }
return `import '${name}'` return `import '${name}'`
} }
}) + fixedStyle })
)
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
} }
/** /**
@ -211,16 +310,19 @@ export function parseJs(
* @param file <String> 文件路径 * @param file <String> 文件路径
* @return <String> 返回转换后的js代码 * @return <String> 返回转换后的js代码
*/ */
export function compileVue(file, imports, options = {}, isBuild) { export async function compileVue(file, imports, options = {}) {
// 修正那反人类的windows路径
let filename = file.slice(options.SOURCE_DIR.length).replace(/\\/g, '/') let filename = file.slice(options.SOURCE_DIR.length).replace(/\\/g, '/')
let code = (fs.cat(file) || '').toString().replace(/\r\n/g, '\n') let code = (fs.cat(file) || '').toString().replace(/\r\n/g, '\n')
let CACHE = options.CACHE || {} let CACHE = options.CACHE || {}
let { isCustomElement } = options
let output = '', let output = '',
scoped = false scoped = false
let js = code.match(JS_EXP) let js = code.match(JS_EXP)
let scss = [...code.matchAll(STYLE_EXP)] let scss = [...code.matchAll(STYLE_EXP)]
let html = code.match(HTML_EXP) || ['', ''] let html = code.match(HTML_EXP) || ['', '']
let linePatch = code.slice(0, js?.index || 0).split('\n').length // js起始行数修正
let hash = md5(file) let hash = md5(file)
let scopeId = 'data-' + hash let scopeId = 'data-' + hash
@ -229,21 +331,27 @@ export function compileVue(file, imports, options = {}, isBuild) {
.map(it => { .map(it => {
let css let css
if (it.length > 2) { if (it.length > 2) {
css = compileScss(it[2]) css = compileScss(it[2], options.INJECT_SCSS)
if (it[1].includes('scoped')) { if (it[1].includes('scoped')) {
scoped = true scoped = true
css = scopeCss(css, hash) css = scopeCss(css, hash)
} }
} else { } else {
css = compileScss(it[1]) css = compileScss(it[1], options.INJECT_SCSS)
} }
return css return css
}) })
.join(' ') .join(' ')
js = js ? js[1] : '' for (let fn of options.plugin) {
scss = await fn('css', scss)
}
js = js ? js[1] : 'export default {}'
try {
html = compile(html[1], { html = compile(html[1], {
mode: 'module', mode: 'module',
prefixIdentifiers: true, prefixIdentifiers: true,
@ -251,10 +359,40 @@ export function compileVue(file, imports, options = {}, isBuild) {
cacheHandlers: true, cacheHandlers: true,
scopeId: scoped ? scopeId : void 0, scopeId: scoped ? scopeId : void 0,
sourceMap: false, sourceMap: false,
isCustomElement: tag => tag.startsWith('wc-') isCustomElement
}).code.replace('export function render', 'function render') }).code.replace('export function render', 'function render')
} catch (err) {
let lines = html[1].split('\n')
let line = lines[err.loc?.start.line - 1]
output += html + '\n\n' console.log('%s: %s', red('SyntaxError'), red(err.message))
console.log(
' @ %s%s%s',
line.slice(0, err.loc.start.column - 1),
red(err.loc.source),
line.slice(err.loc.end.column - 1)
)
console.log(
' @ (%s:%d:%d)\n',
file,
err.loc.start.line,
err.loc.start.column
)
html = `
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, "SyntaxError: ${err.message}"))
}
`
}
html = html
.replace(/import .* from "vue"/, str => {
output += str + '\n'
return ''
})
.trim()
if (CACHE[file]) { if (CACHE[file]) {
CACHE[file] = { CACHE[file] = {
@ -266,22 +404,37 @@ export function compileVue(file, imports, options = {}, isBuild) {
CACHE[file] = { changed: false, js, html } CACHE[file] = { changed: false, js, html }
} }
output += parseJs(js, imports, options, isBuild).replace( output += parseJs(js, imports, options, file, linePatch).replace(
'export default {', 'export default {',
'const __sfc__ = {\n render,\n' `\n${
options.LEGACY_MODE ? '' : SHEETS_DEF
}${html}\n\nconst __sfc__ = {\n render,\n`
) )
if (scss) { if (scss) {
CACHE[file].css = scss CACHE[file].css = scss
// 修正那反人类的windows路径 if (options.LEGACY_MODE) {
output += ` output += `
{
let stylesheet = document.createElement('style')
stylesheet.setAttribute('name', '${filename}')
stylesheet.textContent = \`${scss}\`
document.head.appendChild(stylesheet)
}
`
} else {
output += `
{
let stylesheet = new CSSStyleSheet() let stylesheet = new CSSStyleSheet()
stylesheet.path = '${filename}' stylesheet.path = '${filename}'
stylesheet.replaceSync(\`${scss}\`) stylesheet.replaceSync(\`${scss}\`)
document.adoptedStyleSheets.push(stylesheet) __sheets__.push(stylesheet)
}
document.adoptedStyleSheets = __sheets__
` `
} }
}
if (scoped) { if (scoped) {
output += `__sfc__.__scopeId = '${scopeId}'\n` output += `__sfc__.__scopeId = '${scopeId}'\n`
} }
@ -293,19 +446,15 @@ document.adoptedStyleSheets.push(stylesheet)
/** /**
* 解析模板html * 解析模板html
*/ */
export function parseHtml(html, { page, imports, entry }, isBuild = false) { export function parseHtml(html, { page, imports, entry, LEGACY_MODE }) {
return html return html
.replace(/\r\n/g, '\n') .replace(/\r\n/g, '\n')
.replace( .replace(
'</head>', '</head>',
` <script>window.process = {env: {NODE_ENV: '${ `${
isBuild ? 'production' : 'development' process.env.NODE_ENV === 'development'
}'}}</script>\n${ ? ` <script>${minify(createHmrScript(LEGACY_MODE))}</script>\n`
isBuild : ` <script>${minify(LEGACY_POLYFILL)}</script>\n`
? ''
: ` <script>${
Es.transformSync(HMR_SCRIPT, { minify: true }).code
}</script>`
}</head>` }</head>`
) )
.replace('{{title}}', page.title || '') .replace('{{title}}', page.title || '')
@ -316,4 +465,9 @@ export function parseHtml(html, { page, imports, entry }, isBuild = false) {
'<script src="main.js"></script>', '<script src="main.js"></script>',
`<script type="module">\n${entry}\n</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}'`)
} }

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

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

@ -1,13 +1,13 @@
import http from 'http' import http from 'node:http'
import https from 'https' import https from 'node:http2'
import fs from 'iofs' import fs from 'iofs'
import { join, resolve, dirname } from 'path' import { join, dirname } from 'node:path'
import { parse } from 'url' import { parse } from 'node:url'
import socket from './ws.js' import socket from './ws.js'
import chokidar from 'chokidar' import chokidar from 'chokidar'
import { red, cyan, blue } from 'kolorist' import { red } from 'kolorist'
import { friendlyErrors } from './utils.js' import { friendlyErrors, defaultCustomElement, gzip } from './utils.js'
import { compileScss, parseJs, compileVue, parseHtml } from './compile-vue.js' import { compileScss, parseJs, compileVue, parseHtml } from './compile-vue.js'
@ -15,9 +15,13 @@ import MIME_TYPES from './mime-tpyes.js'
import { COMMON_HEADERS } from './constants.js' import { COMMON_HEADERS } from './constants.js'
const noc = Buffer.from('') const noc = Buffer.from('')
const SERVER_OPTIONS = {} const SERVER_OPTIONS = { allowHTTP1: true }
const CACHE = {} //文件缓存, 用于hmr const CACHE = {} //文件缓存, 用于hmr
function readFile(file) {
return (file && fs.cat(file)?.toString()) || ''
}
export default async function createServer(root = '', conf = {}) { export default async function createServer(root = '', conf = {}) {
const SOURCE_DIR = join(root, 'src') const SOURCE_DIR = join(root, 'src')
const PUBLIC_DIR = join(root, 'public') const PUBLIC_DIR = join(root, 'public')
@ -26,6 +30,22 @@ export default async function createServer(root = '', conf = {}) {
const PORT = conf.devServer.port || 8080 const PORT = conf.devServer.port || 8080
const USE_HTTPS = conf.devServer.https const USE_HTTPS = conf.devServer.https
const DOMAIN = conf.devServer.domain || 'localhost' 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) { if (USE_HTTPS) {
Object.assign(SERVER_OPTIONS, conf.devServer.ssl) Object.assign(SERVER_OPTIONS, conf.devServer.ssl)
@ -37,7 +57,7 @@ export default async function createServer(root = '', conf = {}) {
} }
const server = (USE_HTTPS ? https : http) const server = (USE_HTTPS ? https : http)
.createServer(SERVER_OPTIONS) [USE_HTTPS ? 'createSecureServer' : 'createServer'](SERVER_OPTIONS)
.listen(PORT) .listen(PORT)
const ws = socket(server) const ws = socket(server)
@ -54,7 +74,7 @@ export default async function createServer(root = '', conf = {}) {
currentPage = '' currentPage = ''
server server
.on('request', function (req, res) { .on('request', async function (req, res) {
let prefix = DEPLOY_PATH ? DEPLOY_PATH.replace(/\/$/, '') : '' let prefix = DEPLOY_PATH ? DEPLOY_PATH.replace(/\/$/, '') : ''
let url = let url =
prefix && req.url.startsWith(prefix) prefix && req.url.startsWith(prefix)
@ -67,11 +87,20 @@ export default async function createServer(root = '', conf = {}) {
if (prefix && req.url === '/') { if (prefix && req.url === '/') {
res.setHeader('Location', DEPLOY_PATH) res.setHeader('Location', DEPLOY_PATH)
res.writeHead(302, 'Redirect') res.writeHead(302, USE_HTTPS ? void 0 : 'Redirect')
return res.end('') return res.end('')
} }
if (pathname) { 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)
}
} else {
pathname = pathname.split('/') pathname = pathname.split('/')
if (pathname[0].endsWith('.html')) { if (pathname[0].endsWith('.html')) {
@ -81,9 +110,14 @@ export default async function createServer(root = '', conf = {}) {
ext = tmp.pop() ext = tmp.pop()
pageName = tmp.join('.') 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 currentPage = pageName
pagesDir = dirname(conf.pages[pageName].entry) pagesDir = dirname(conf.pages[pageName]?.entry)
} else { } else {
if (currentPage) { if (currentPage) {
let tmp = pathname.at(-1).split('.') let tmp = pathname.at(-1).split('.')
@ -98,6 +132,7 @@ export default async function createServer(root = '', conf = {}) {
} }
} }
pathname = pathname.join('/') pathname = pathname.join('/')
}
} else { } else {
if (IS_MPA) { if (IS_MPA) {
isIndex = true isIndex = true
@ -115,8 +150,13 @@ export default async function createServer(root = '', conf = {}) {
if (isIndex) { if (isIndex) {
res.setHeader('content-type', MIME_TYPES.html) res.setHeader('content-type', MIME_TYPES.html)
res.writeHead(200, 'OK') res.writeHead(200, USE_HTTPS ? void 0 : 'OK')
res.end('<ul>' + indexPage + '</ul>') 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>'
)
} else { } else {
res.setHeader('accept-ranges', 'bytes') res.setHeader('accept-ranges', 'bytes')
@ -128,28 +168,47 @@ export default async function createServer(root = '', conf = {}) {
res.setHeader('content-type', MIME_TYPES.html) res.setHeader('content-type', MIME_TYPES.html)
let page = conf.pages[pageName] 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() let html = fs.cat(join(process.cwd(), 'index.html')).toString()
entry = parseJs(entry, conf.imports, { entry = parseJs(
entry,
conf.imports,
{
IS_MPA, IS_MPA,
currentPage, currentPage,
IS_ENTRY: true, IS_ENTRY: true,
DEPLOY_PATH DEPLOY_PATH,
}) LEGACY_MODE,
isCustomElement,
plugin,
define
},
page.entry
)
code = parseHtml(html, { page, imports: conf.imports, entry }) for (let fn of plugin) {
entry = await fn('js', entry)
}
code = parseHtml(html, {
page,
imports: conf.imports,
entry,
LEGACY_MODE
})
} }
break break
case 'vue': case 'vue':
{ {
let rpath = pathname.replace(/@\//, '') let rpath = pathname.replace('@/', '')
let file let file
if (IS_MPA) { if (IS_MPA) {
if (rpath.startsWith(currentPage)) { // 判断前后2个值相等, 避免出现目录名和页面名字相同时走错逻辑
if (rpath === pathname && rpath.startsWith(currentPage)) {
file = join(pagesDir, rpath.slice(currentPage.length)) file = join(pagesDir, rpath.slice(currentPage.length))
} else { } else {
file = join(SOURCE_DIR, rpath) file = join(SOURCE_DIR, rpath)
@ -159,19 +218,28 @@ export default async function createServer(root = '', conf = {}) {
} }
if (!fs.isfile(file)) { if (!fs.isfile(file)) {
friendlyErrors(pathname, ext) friendlyErrors(pathname, ext)
res.writeHead(404, 'Not Found') res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
res.end('') res.end('')
return return
} }
code = compileVue(file, conf.imports, { code = await compileVue(file, conf.imports, {
IS_MPA, IS_MPA,
currentPage, currentPage,
SOURCE_DIR, SOURCE_DIR,
CACHE, CACHE,
DEPLOY_PATH DEPLOY_PATH,
INJECT_SCSS,
LEGACY_MODE,
isCustomElement,
plugin,
define
}) })
for (let fn of plugin) {
code = await fn('js', code)
}
res.setHeader('content-type', MIME_TYPES.js) res.setHeader('content-type', MIME_TYPES.js)
} }
break break
@ -179,30 +247,47 @@ export default async function createServer(root = '', conf = {}) {
case 'scss': case 'scss':
case 'css': case 'css':
{ {
let file = join(SOURCE_DIR, pathname.replace(/@\//, '')) let file = join(SOURCE_DIR, pathname.replace('@/', ''))
if (!fs.isfile(file)) { if (!fs.isfile(file)) {
file = join(PUBLIC_DIR, pathname.replace(/@\//, '')) file = join(PUBLIC_DIR, pathname.replace('@/', ''))
if (!fs.isfile(file)) { if (!fs.isfile(file)) {
friendlyErrors(pathname, ext) friendlyErrors(pathname, ext)
res.setHeader('content-type', MIME_TYPES.html) res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
res.writeHead(404, 'Not Found')
res.end('') res.end('')
return 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) code = compileScss(file)
for (let fn of plugin) {
code = await fn('css', code)
}
res.setHeader('content-type', MIME_TYPES.css) res.setHeader('content-type', MIME_TYPES.css)
} }
break break
case 'js': case 'js':
case 'wasm':
{ {
let rpath = pathname.replace(/@\//, '') let rpath = pathname.replace('@/', '')
let file 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) { if (IS_MPA) {
if (rpath.startsWith(currentPage)) { // 判断前后2个值相等, 避免出现目录名和页面名字相同时走错逻辑
if (rpath === pathname && rpath.startsWith(currentPage)) {
file = join(pagesDir, rpath.slice(currentPage.length)) file = join(pagesDir, rpath.slice(currentPage.length))
} else { } else {
file = join(SOURCE_DIR, rpath) file = join(SOURCE_DIR, rpath)
@ -214,45 +299,92 @@ export default async function createServer(root = '', conf = {}) {
if (fs.isfile(file)) { if (fs.isfile(file)) {
code = fs.cat(file) code = fs.cat(file)
} else if (fs.isfile(join(PUBLIC_DIR, rpath))) { } 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 { } else {
friendlyErrors(rpath, ext) friendlyErrors(rpath, ext)
res.writeHead(404, 'Not Found') res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
res.end('') res.end('')
return 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, IS_MPA,
currentPage, currentPage,
DEPLOY_PATH DEPLOY_PATH,
}) LEGACY_MODE,
res.setHeader('content-type', MIME_TYPES.js) isCustomElement,
plugin,
define
},
file
)
for (let fn of plugin) {
code = await fn('js', code)
}
}
res.setHeader('content-type', MIME_TYPES[ext])
} }
break break
default: default:
res.setHeader('content-type', MIME_TYPES[ext] || MIME_TYPES.other) 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(join(PUBLIC_DIR, pathname))) { if (fs.isfile(pub_file)) {
code = fs.cat(join(PUBLIC_DIR, pathname)) code = fs.cat(pub_file)
} else if (fs.isfile(join(SOURCE_DIR, pathname))) { if (code) {
code = fs.cat(join(SOURCE_DIR, pathname)) 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()
)
}
} else { } else {
code = null code = null
} }
if (code === null) { if (code === null) {
friendlyErrors(pathname, ext) friendlyErrors(pathname, ext)
res.writeHead(404, 'Not Found') res.writeHead(404, USE_HTTPS ? void 0 : 'Not Found')
res.end('') res.end('')
return return
} }
break break
} }
res.setHeader('content-length', Buffer.byteLength(code || noc)) if (ENABLE_GZIP) {
res.writeHead(200, 'OK') code = gzip(code || noc)
res.end(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)
} }
}) })
@ -269,7 +401,7 @@ export default async function createServer(root = '', conf = {}) {
console.log( console.log(
' 本地: %s://%s:%d%s', ' 本地: %s://%s:%d%s',
USE_HTTPS ? 'https' : 'http', USE_HTTPS ? 'https' : 'http',
'127.0.0.1', USE_HTTPS ? 'localhost' : '127.0.0.1',
PORT, PORT,
DEPLOY_PATH DEPLOY_PATH
) )
@ -280,11 +412,9 @@ export default async function createServer(root = '', conf = {}) {
PORT, PORT,
DEPLOY_PATH DEPLOY_PATH
) )
})
chokidar chokidar
.watch([SOURCE_DIR, PUBLIC_DIR, join(root, './index.html')]) .watch([SOURCE_DIR, PUBLIC_DIR, join(root, './index.html')])
.on('all', (act, filePath) => { .on('all', async (act, filePath) => {
if (ready) { if (ready) {
let file = filePath.slice(SOURCE_DIR.length) let file = filePath.slice(SOURCE_DIR.length)
@ -295,7 +425,18 @@ export default async function createServer(root = '', conf = {}) {
case 'css': case 'css':
case 'scss': case 'scss':
{ {
let content = fs.cat(filePath).toString() 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({ ws.send({
action: 'render', action: 'render',
data: { path: file.replace(/\\/g, '/'), content } data: { path: file.replace(/\\/g, '/'), content }
@ -305,12 +446,17 @@ export default async function createServer(root = '', conf = {}) {
case 'vue': case 'vue':
{ {
let content = compileVue(filePath, conf.imports, { let content = await compileVue(filePath, conf.imports, {
IS_MPA, IS_MPA,
currentPage, currentPage,
SOURCE_DIR, SOURCE_DIR,
CACHE, CACHE,
DEPLOY_PATH DEPLOY_PATH,
INJECT_SCSS,
LEGACY_MODE,
isCustomElement,
plugin,
define
}) })
let tmp = CACHE[filePath] let tmp = CACHE[filePath]
if (tmp.changed) { if (tmp.changed) {
@ -318,7 +464,10 @@ export default async function createServer(root = '', conf = {}) {
} else { } else {
ws.send({ ws.send({
action: 'render', action: 'render',
data: { path: file.replace(/\\/g, '/'), content: tmp.css } data: {
path: file.replace(/\\/g, '/'),
content: tmp.css
}
}) })
} }
} }
@ -336,4 +485,5 @@ export default async function createServer(root = '', conf = {}) {
.on('ready', () => { .on('ready', () => {
ready = true ready = true
}) })
})
} }

View File

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

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

@ -1,4 +1,33 @@
/**
* {一些工具类函数}
* @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 { 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 = '') { export function friendlyErrors(pathname, ext = '') {
console.log(cyan(pathname), red(`not found!!!`)) console.log(cyan(pathname), red(`not found!!!`))
@ -7,3 +36,70 @@ export function friendlyErrors(pathname, ext = '') {
blue(`/index.${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,28 +6,40 @@
import { WebSocketServer } from 'ws' import { WebSocketServer } from 'ws'
class WebSocket { class WebSocket {
#ws = null // ws实例 #clients = new Map()
#queue = [] // 消息队列 #queue = [] // 消息队列
constructor(server) { constructor(server) {
if (server.listening) { if (server.listening) {
let conn = new WebSocketServer({ server, path: '/ws-vue-live' }) let conn = new WebSocketServer({ server, path: '/ws-fite-hmr' })
conn.on('connection', ws => { conn.on('connection', (client, req) => {
this.#ws = ws let params = new URLSearchParams(req.url.slice(req.url.indexOf('?')))
// ws.on('message', data => { let session = params.get('session')
// console.log(data + ''); 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) { while (this.#queue.length) {
let msg = this.#queue.shift() let msg = this.#queue.shift()
this.send(msg) this.send(msg)
} }
}
}) })
} }
} }
send(msg = {}) { send(msg = {}) {
if (this.#ws) { if (this.#clients.size) {
this.#ws.send(JSON.stringify(msg)) for (let [key, client] of this.#clients) {
client.send(JSON.stringify(msg))
}
} else { } else {
this.#queue.push(msg) this.#queue.push(msg)
} }

View File

@ -1,10 +1,13 @@
{ {
"name": "fite", "name": "fite",
"type": "module", "type": "module",
"version": "0.5.0", "version": "1.4.4",
"bin": { "bin": {
"fite": "index.js" "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": { "dependencies": {
"@bytedo/sass": "^1.54.8", "@bytedo/sass": "^1.54.8",
"@vue/compiler-dom": "^3.2.47", "@vue/compiler-dom": "^3.2.47",
@ -16,5 +19,6 @@
}, },
"engines": { "engines": {
"node": ">=16.6.0" "node": ">=16.6.0"
} },
"license": "MIT"
} }