diff --git a/lib/compile-vue.js b/lib/compile-vue.js new file mode 100644 index 0000000..77f62d6 --- /dev/null +++ b/lib/compile-vue.js @@ -0,0 +1,128 @@ +/** + * {} + * @author yutent + * @date 2022/09/06 14:43:01 + */ + +import fs from 'iofs' +import scss from '@bytedo/sass' + +import { JS_EXP, STYLE_EXP, HTML_EXP } from './constants.js' + +const OPTIONS = { + indentType: 'space', + indentWidth: 2 +} + +/** + * 编译scss为css + * @param file 文件路径或scss代码 + * @param style 代码风格, expanded | compressed + */ +export function compileScss(file, style = 'expanded') { + try { + if (fs.isfile(file)) { + return scss.compile(file, { style, ...OPTIONS }).css + } else { + return scss.compileString(file, { style, ...OPTIONS }).css + } + } catch (err) { + console.error(err) + } +} + +/** + * 解析js + * 主要是处理js的依赖引用 + * @param code js代码 + */ +export function parseJs(code = '', currentPage = '') { + let fixedStyle = '\n\n' + + return ( + code + .replace(/import (.*?) from (["'])(.*?)\2/g, function (m, alias, q, name) { + if (name.startsWith('@/')) { + name = name.replace('@/', '/aseets/js/') + } + + if (!conf.imports[name]) { + if (name.startsWith('./')) { + name = name.replace('./', `/aseets/js/${currentPage}/`) + } else if (name.startsWith('/') && !name.startsWith('/aseets/js/')) { + name = name.replace(/^\//, '/aseets/js/') + } + + if (!name.endsWith('.js') && !name.endsWith('.vue')) { + if (name.includes('components')) { + name += '.vue' + } else { + name += '.js' + } + } + } + return `import ${alias} from '${name}'` + }) + .replace(/import (["'])(.*?)\1/g, function (m, q, name) { + if (name.endsWith('.css') || name.endsWith('.scss')) { + if (name.startsWith('@/')) { + name = name.replace('@/', '/aseets/css/') + } + let tmp = `style${Date.now()}` + fixedStyle += `document.adoptedStyleSheets.push(${tmp})\n` + + return `import ${tmp} from '${name}' assert { type: 'css' }` + } else { + if (name.startsWith('@/')) { + name = name.replace('@/', '/aseets/js/') + } + // console.log(name, conf.imports[name]) + if (!conf.imports[name]) { + if (!name.startsWith('/') && !name.startsWith('./')) { + name = '/' + name + } + + if (!name.endsWith('.js') && !name.endsWith('.vue')) { + name += '.js' + } + } + return `import '${name}'` + } + }) + fixedStyle + ) +} + +/** + * 将vue转为js + * @param file 文件路径 + * @return 返回转换后的js代码 + */ +export function compileVue(file) { + let code = (fs.cat(file) || '').toString() + + let js = code.match(JS_EXP) + let scss = code.matchAll(STYLE_EXP) + let html = code.match(HTML_EXP) + + let fixedStyle = '\n\n' + + // console.log(typeof scss) + scss = [...scss].flatMap(it => (it ? it[1] : '')) + js = js ? js[1] : '' + html = (html ? html[1] : '').replace(/`/g, '\\`').replace(/\$\{/g, '\\${') + + js = parseJs(js).replace( + 'export default {', + `${fixedStyle}export default {\n template: \`${html}\`,` + ) + + if (scss.length) { + js += ` + let stylesheet = new CSSStyleSheet() + stylesheet.replaceSync(\`${compileScss(scss.join('\n'))}\`) + document.adoptedStyleSheets.push(stylesheet) + ` + } + + return js +} diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..8b350f0 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,13 @@ +/** + * {一些常量} + * @author yutent + * @date 2022/09/06 11:54:56 + */ + +export const JS_EXP = /]*?>([\w\W]*?)<\/script>/ +export const STYLE_EXP = /]*?>([\w\W]*?)<\/style>/g +export const HTML_EXP = /]*?>([\w\W]*?)<\/template>/ + +export const COMMON_HEADERS = { + 'Cache-Control': 'no-store' +} diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..8f62b3a --- /dev/null +++ b/lib/index.js @@ -0,0 +1,199 @@ +/** + * {vue live开发环境} + * @author yutent + * @date 2022/08/31 18:00:37 + */ + +import http from 'http' +import fs from 'iofs' +import { join, resolve } from 'path' +import { parse } from 'url' +import conf from './live.config.js.js' + +import { compileScss, parseJs, compileVue } from './compile-vue.js' + +import MIME_TYPES from './mime-tpyes.js' +import { COMMON_HEADERS } from './constants.js' + +const decode = decodeURIComponent +const root = resolve('./src/') + +/* ------------------------------- */ + +let pagesDir = resolve(conf.pages) +let pages = fs.ls(pagesDir).map(it => { + let tmp = it.slice(pagesDir.length + 1) + '.html' + return `
  • ${tmp}
  • ` +}) + +let currentPage = '' + +http + .createServer(function (req, res) { + let pathname = parse(req.url.slice(1)).pathname || 'index.html' + + // console.log(req.url, pathname, parse(req.url.slice(1))) + let isIndex = pathname === 'index.html' + + for (let k in COMMON_HEADERS) { + res.setHeader(k, COMMON_HEADERS[k]) + } + + let tmp = pathname.split('.') + let ext = tmp.pop() + let page = tmp.join('.') + + if (isIndex) { + res.setHeader('content-type', MIME_TYPES.html) + res.writeHead(200, 'OK') + res.end('
      ' + pages.join('') + '
    ') + } else { + res.setHeader('accept-ranges', 'bytes') + + let code = '' + + switch (ext) { + case 'html': + { + let entry = fs.cat(join(pagesDir, page, 'main.js')).toString() + let fixedStyle = '\n\n' + + currentPage = page + + res.setHeader('content-type', MIME_TYPES.html) + + entry = entry + .replace(/import (.*?) from (["'])(.*?)\2/g, function (m, alias, q, name) { + if (name.startsWith('@/')) { + name = name.replace('@/', '/aseets/js/') + } + + if (!conf.imports[name]) { + if (name.startsWith('./')) { + name = name.replace('./', `/aseets/js/${currentPage}/`) + } else if (name.startsWith('/') && !name.startsWith('/aseets/js/')) { + name = name.replace(/^\//, '/aseets/js/') + } + + if (!name.endsWith('.js') && !name.endsWith('.vue')) { + name += '.js' + } + } + return `import ${alias} from '${name}'` + }) + .replace(/import (["'])(.*?)\1/g, function (m, q, name) { + if (name.endsWith('.css') || name.endsWith('.scss')) { + if (name.startsWith('@/')) { + name = name.replace('@/', '/aseets/css/') + } + let tmp = `style${Date.now()}` + fixedStyle += `document.adoptedStyleSheets.push(${tmp})\n` + + return `import ${tmp} from '${name}' assert { type: 'css' }` + } else { + if (name.startsWith('@/')) { + name = name.replace('@/', '/aseets/js/') + } + // console.log(name, conf.imports[name]) + if (!conf.imports[name]) { + if (!name.startsWith('/') && !name.startsWith('./')) { + name = '/' + name + } + + if (!name.endsWith('.js') && !name.endsWith('.vue')) { + name += '.js' + } + } + return `import '${name}'` + } + }) + + entry += fixedStyle + + code = fs.cat('./index.html').toString() + code = code + .replace( + '', + "" + ) + .replace('{{importmap}}', JSON.stringify({ imports: conf.imports })) + .replace( + '', + `` + ) + } + + break + + case 'vue': + { + pathname = pathname.replace(/^aseets\/js\//, '') + let file + + if (pathname.startsWith(currentPage)) { + file = join(conf.pages, pathname) + } else { + file = join(root, pathname) + } + + if (!fs.isfile(file)) { + file = file.replace(/\.vue$/, '/index.vue') + } + + // console.log('>>>>', file) + code = compileVue(file) + + res.setHeader('content-type', MIME_TYPES.js) + } + break + + case 'scss': + case 'css': + { + let file = join(root, pathname.replace(/^aseets\/css\//, '')) + code = compileScss(file) + res.setHeader('content-type', MIME_TYPES.css) + } + break + + case 'js': + { + pathname = pathname.replace(/^aseets\/js\//, '') + let file + if (pathname.startsWith(currentPage)) { + file = join(conf.pages, pathname) + } else { + file = join(root, pathname) + } + if (fs.isfile(file)) { + code = fs.cat(file) + } else { + file = file.replace(/\.js$/, '/index.js') + code = fs.cat(file) + } + console.log(req.url, '>>>>', file) + code = parseJs(code + '') + res.setHeader('content-type', MIME_TYPES.js) + } + + break + + default: + res.setHeader('content-type', MIME_TYPES[ext] || MIME_TYPES.other) + break + } + + res.setHeader('content-length', Buffer.byteLength(code)) + res.writeHead(200, 'OK') + res.end(code + '') + } + }) + .listen(conf.port) + .on('error', err => { + console.log(`${conf.port}端口被占用~~~`) + conf.port++ + createServer() + }) + .on('listening', _ => { + // console.log('启动成功, 请访问', 'http://127.0.0.1:' + conf.port) + }) diff --git a/lib/mime-tpyes.js b/lib/mime-tpyes.js new file mode 100644 index 0000000..8ff86aa --- /dev/null +++ b/lib/mime-tpyes.js @@ -0,0 +1,34 @@ +// +const MIME_TYPES = { + html: 'text/html;charset=utf-8', + txt: 'text/plain;charset=utf-8', + css: 'text/css;charset=utf-8', + xml: 'text/xml;charset=utf-8', + gif: 'image/gif', + jpg: 'image/jpeg', + webp: 'image/webp', + tiff: 'image/tiff', + png: 'image/png', + svg: 'image/svg+xml', + ico: 'image/x-icon', + bmp: 'image/x-ms-bmp', + js: 'application/javascript;charset=utf-8', + json: 'application/json;charset=utf-8', + mp3: 'audio/mpeg', + ogg: 'audio/ogg', + m4a: 'audio/x-m4a', + mp4: 'video/mp4', + webm: 'video/webm', + ttf: 'font/font-ttf', + woff: 'font/font-woff', + woff2: 'font/font-woff2', + other: 'application/octet-stream' +} + +MIME_TYPES.vue = MIME_TYPES.js +MIME_TYPES.scss = MIME_TYPES.css +MIME_TYPES.htm = MIME_TYPES.html +MIME_TYPES.jpeg = MIME_TYPES.jpg +MIME_TYPES.tif = MIME_TYPES.tiff + +export default MIME_TYPES