用 100 行代码,用 koa 写一个支持 vue 和 react 的赛博脚手架 😼
如何有效的保证不挂科,当然是背知识点啦;😏 同理,如何去理解脚手架呢,我的理解是以最低限度的可用性,去搭建一个脚手架,让我们放弃那些反复的钩子,plugin,以及一些其他的配置。我会在一个中间件中,来完成最低限度的解析代码的工作,希望能帮助大家去理解脚手架,此项目参考于 mini-vite 和 vite-plugin-vue;
我们 vite 也有自己的垃圾佬
这个项目是我在写 miniVite 的时候产生的想法,所以用的是同一个库,认准npm run cyber,就好了;
这个是仓库地址,觉得有用的话给个赞吧
这里是目录结构,以下是和当前项目相关的,专注于这些文件就好
|-- koaMinivite,
|-- .gitignore,
|-- cyber.html,
|-- ...,
|-- src,
|-- cyber.ts,
|-- cyber,
| |-- a.ts,
| |-- App.vue,
| |-- help.js,
| |-- react.tsx,
| |-- vue.ts,
|-- ...,
|-- node,
|-- ...,
|-- optimizer,
| |-- index.ts,
| |-- preBundlePlugin.ts,
| |-- scanplugin.ts,
一、koa 搭建
首先来看我们的入口文件cyber.ts;这个文件是核心文件,我们来看看他做了什么;
import Koa from "koa";
import { optimize } from "./node/optimizer";
const app = new Koa();
app.use(async (ctx, next) => {
//中间件区域
return next();
});
app.listen(8954, async () => {
await optimize(root, "src/cyber/react.tsx");
await optimize(root, "src/cyber/vue.ts");
console.log("http://localhost:8954 ");
});
二、预构建
我们看到app.listen开启服务的时候为什么会在回调中处理一个 optimize 的函数呢;
我们对代码会进行一个预构建;详细的解释请移步 从 koa 到 mini-vite ;
我现在对其进行一个通俗的解释
这里是 react 包的 index.js
"use strict";
if (process.env.NODE_ENV === "production") {
module.exports = require("./cjs/react.production.min.js");
} else {
module.exports = require("./cjs/react.development.js");
}
那么你有没有发现一个问题;我们常使用的引入方法是`import React from "react";`
这是两种模块类型,一个是esm,一个是cjs;就像天堂的白鸽不会亲吻田野的乌鸦,es模块也不会引入cjs模块; 你拿前朝的🗡,怎么能斩本朝的官那我们要解决以下几个问题
- 怎么让各个包的模块格式都转化为 esm 模块
- node_modules 包里的文件这么多,难道我们要全部代理成静态资源服务器吗
- 如何让外部资源包和相对路径文件进行区分
esbuild 可以通过打包来解决这些问题,朋克风的解释: 不打灰怎么浇筑混凝土; 我们的目的是想要将所有要用的包,可以理解成后面砌砖需要多少水泥,都拖到工地上来;找个地方,把他们放哪里让我们好使用,
这个文件是 nodemodule 包中的.vite 文件,你可以去任何一个 vite 项目中去寻找他,我们这里改一下名字,就叫".m-vite";
这里是 optimizer 的代码
import path from "path";
import { build } from "esbuild";
import { scanPlugin } from "./scanplugin";
import { preBundlePlugin } from "./preBundlePlugin";
import { PRE_BUNDLE_DIR } from "../contants";
export async function optimize(root: string, other?: string) {
// 1. 确定入口
// 2. 从入口处扫描依赖
// 3. 预构建依赖;
const deps = new Set<string>();
const entry = path.resolve(root, other ? other : "src/client/main.tsx");
await build({
entryPoints: [entry],
bundle: true,
write: false,
plugins: [scanPlugin(deps)]
});
await build({
entryPoints: [...deps],
write: true,
bundle: true,
format: "esm",
splitting: true,
outdir: path.resolve(root, PRE_BUNDLE_DIR)//PRE_BUNDLE_DIR = ".m-vite",
plugins: [preBundlePlugin(deps)]
});
console.log("需要构建的依赖项", deps)
}
他经历了两次 build,第一次是扫描依赖不生成文件,第二次是预构建依赖构建依赖,输出至 node_modules/.m-vite,
至于插件的详解 请移步
我们这里经历了两次预构建
await optimize(root, "src/cyber/react.tsx");
await optimize(root, "src/cyber/vue.ts");
因为这个项目会同时执行 react 和 vue,所以需要预构建两次,当然你可以自己选择,只需要在入口处写上对应的文件名即可;
这个是我们预构建后的目录
打包后的他们支持 es 模块语法了,我们接下来就会去其中提取相应的依赖
三、中间件
transformHtml 渲染初始页面
开始我们中间件的第一步,让我们获取到了 req.url 后该做什么,当页面没有任何路径的时候,我们会获取到req.url ="/",我们就在初始路径下,给他返回一个 html 文件,让页面去加载他
app.use(async (ctx, next) => {
const { req, res } = ctx;
if (req.url === "/") {
const realPath = path.join(process.cwd(), "/cyber.html");
//transformHtml
if (await fs.pathExists(realPath)) {
let code = await fs.readFile(realPath, "utf-8");
res.setHeader("Content-Type", "text/html");
res.end(code);
}
}
});
我们这里去读取了我们根目录下的 cyber.html 文件,我们使用了fs-extra库;这个库的 api 基本和 fs 原生的一致,但又更好的性能,和更加丰富的 api,我们检测是否存在该路径,如果存在则使用utf-8的格式去读取,并设置text/html的请求头返回给我们的服务器;不出意外的话,你应该可以看到一个html页面了
这里是cyber.html文件的内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="react"></div>
<script type="module" src="src/cyber/react.tsx"></script>
<div id="vue"></div>
<script type="module" src="src/cyber/vue.ts"></script>
</body>
</html>
我们可以看到,他会引入一个 react.tsx 文件,可是我们的服务器还没又办法去处理他,别慌,我们来进行下一步中间件的书写
app.use(async (ctx, next) => {
const { req, res } = ctx;
if (req.url === "/") {
const realPath = path.join(process.cwd(), "/cyber.html");
//transformHtml
if (await fs.pathExists(realPath)) {
let code = await fs.readFile(realPath, "utf-8");
res.setHeader("Content-Type", "text/html");
res.end(code);
}
} else if (req.method === "GET" || /\.(?:j|t)sx?$|\.mjs$|\.vue$/.test(req.url as string)) {
//resolve
const realPath = path.join(process.cwd(), req.url + "");
//loader
if (await fs.pathExists(realPath)) {
let code = await fs.readFile(realPath, "utf-8");
let exname =
path.extname(req.url as string).slice(1) === "vue"
? "js"
: path.extname(req.url as string).slice(1);
let { code: transformedCode, map } = await esbuild.transform(code, {
target: "esnext",
format: "esm",
sourcemap: true,
loader: exname as "js" | "ts" | "jsx" | "tsx",
});
// transform
const ms = new MagicString(transformedCode);
await init;
const [imports, exports] = parse(transformedCode);
for (const importInfo of imports) {
const { n: importedId, s: start, e: end } = importInfo;
if (!importedId) continue;
if (/^[\w@][^:]/.test(importedId)) {
let realPath = path.join("/", "node_modules", ".m-vite", `${importedId}.js`);
realPath = realPath.replace(/\\/g, "/");
ms.overwrite(start, end, realPath);
code = ms.toString();
}
}
res.setHeader("Content-Type", "application/javascript");
res.statusCode = 200;
res.end(code);
}
}
return next();
});
我们来进行下一步的处理,这个是 vite 的 load 阶段,去读取相应的文件
resolve 和 loader 解析文件路径并读取他
if (req.method === "GET" || /\.(?:j|t)sx?$|\.mjs$|\.vue$/.test(req.url as string)) {
const realPath = path.join(process.cwd(), req.url + "");
}
这个阶段我们主要做的是筛选和拼接路径,我们只允许 js,ts,jsx,tsx,vue,mjs 文件传入,我们使用正则对其过滤 然后再拼接出他的真正路径出来;我们 vite 的 resolve 阶段 就是做的这么一项工作
既然知道地址了,我们就可以和上文中的一样,去读取他
if (await fs.pathExists(realPath)) {
let code = await fs.readFile(realPath, "utf-8");
let { code: transformedCode, map } = await esbuild.transform(code, {
target: "esnext",
format: "esm",
sourcemap: true,
loader: path.extname(req.url as string).slice(1) as "js" | "ts" | "jsx" | "tsx",
});
}
我们可以看到,我们读取他后,使用 esbuild 对其进行了转换,我们这一步是将 ts 文件或者 tsx 文件转换为 esm 模块;其中 loader 是我们目前文件的后缀,react的优势就是可以直接被 esbuild 解析,可以帮我们节省很多功夫,vue则需要花费大量的精力进行书写了
我们得到的transformedCode就是我们处理好的tsx文件了,这时候他是一个原生的 es6 模块;但问题随之而来
transform 转换
我们的浏览器可不认识
bare引入的包;这时候我们需要对其进行改写,上面的预构建的文件就有了用武之地;
const ms = new MagicString(transformedCode);
await init;
const [imports, exports] = parse(transformedCode);
for (const importInfo of imports) {
const { n: importedId, s: start, e: end } = importInfo;
if (!importedId) continue;
if (/^[\w@][^:]/.test(importedId)) {
let realPath = path.join("/", "node_modules", ".m-vite", `${importedId}.js`);
realPath = realPath.replace(/\\/g, "/");
// ms.overwrite(start, end, realPath);
code = ms.toString();
}
}
我们使用magic-string这个库import MagicString from "magic-string";,new一个ms;出来,他的功能是去修改我们的代码;这个库是vite官方使用的
然后使用 await init 创建引入和导出的分析环境;import { init, parse } from "es-module-lexer";这个工具库可以帮助我们分析一个模块的导入导出,并返回相应的ast;
{ n: 'react', t: 1, s: 43, e: 48, ss: 24, se: 49, d: -1, a: -1 }
{
n: 'react-dom/client',
t: 1,
s: 79,
e: 95,
ss: 51,
se: 96,
d: -1,
a: -1
}
这个是我们分析出的 imports 的值,我们只需要关注三点;n 和 s 和 e;n是包名 s是开始位置 e是结束位置;知道这几个值我们就能改写原来的代码;
let realPath = path.join("/", "node_modules", ".m-vite", `${importedId}.js`);
将代码改为我们想要的路径,然后返回即可;这是包的位置就指向的了.m-vite文件中了
import a from "./a.ts";
import React from "/node_modules/.m-vite/react.js";
import { createRoot } from "/node_modules/.m-vite/react-dom/client.js";
console.log(React, a);
const App = () => /* @__PURE__ */ React.createElement("div", null, "react");
const root = createRoot(document.getElementById("react"));
root.render(/* @__PURE__ */ React.createElement(App, null));
这个就是最终返回的文件;esbuild 原生支持react;vue与之相比又很反人类了;
res.setHeader("Content-Type", "application/javascript");
res.statusCode = 200;
res.end(code);
最后将其作为 js 文件返回,你的 react 就成了,但是我们是否还遗忘了一个东西,vue该怎么处理?
vue-loader
vue的处理是一把心酸,一把泪,为了处理vue;跑去读了vue-plugin的源码;
其中难点就在于 sfc 怎么处理;怎么讲单文件拆分成三块进行处理,当然,我只拆了两块,华生,你发现了盲点。整个项目没有处理css的东西,哈哈 😄;当然我们先学会处理 js,css 会好处理很多
先叠个甲,这个vue-plugin的风格也是垃圾佬风格 😏
第一步,我们需要拆分 sfc 单文件组件
该怎么拆分呢,从 load 开始读
vue-plugin/indexload(id, opt) {
//...
if (query.vue) {
if (query.src) {
return fs.readFileSync(filename, 'utf-8')
}
const descriptor = getDescriptor(filename, options.value)!
let block: SFCBlock | null | undefined
if (query.type === 'script') {
// handle <script> + <script setup> merge via compileScript()
block = resolveScript(
descriptor,
options.value,
ssr,
customElementFilter.value(filename),
)
} else if (query.type === 'template') {
block = descriptor.template!
} else if (query.type === 'style') {
block = descriptor.styles[query.index!]
} else if (query.index != null) {
block = descriptor.customBlocks[query.index]
}
if (block) {
return {
code: block.content,
map: block.map as any,
}
}
}
},
从getDescriptor我找到了以下两个位置
代码位置:packages/plugin-vue/src/utils/descriptorCache.ts
注意这个parse ;为了寻找complier的位置,我寻找到了这个位置
代码位置:packages/plugin-vue/src/compiler.ts
我们拿到了两个信息 一个是
vue/compiler-sfc 一个是 parse;
既然如此 我们就import {parse as vueParse,compileTemplate,compileScript} from "vue/compiler-sfc"引入 parse 对其进行解析
const vueAst = vueParse(code);
我们得到了 vue 的 ast 语法树,代码成功的被我们拆分了;以下的代码块都经过简化
{
descriptor: {
filename: 'anonymous.vue',
source: `...`
template: {
type: 'template',
content: `...`,
loc: [Object],
attrs: {},
ast: [Object],
map: [Object]
},
script: {
type: 'script',
content: `...`,
loc: [Object],
attrs: [Object],
lang: 'ts',
map: [Object]
},
scriptSetup: {
type: 'script',
content: `...`,
loc: [Object],
attrs: [Object],
setup: true,
lang: 'ts'
},
styles: [],
customBlocks: [],
cssVars: [],
slotted: false,
shouldForceReload: [Function: shouldForceReload]
},
errors: []
}
接下来就是转换代码了;我们接着 load 钩子开始读
从resolveScript我又找到了以下位置
我们引入compileScript对 script 标签进行处理;
let scriptRes = compileScript(vueAst.descriptor, { id: req.url });
得到结果
{
type: 'script',
content: 'import { defineComponent as _defineComponent } from 'vue'
import { ref } from "vue";
import { defineComponent } from "vue";
const __default__ = defineComponent({
name: "App",
});
export default /*@__PURE__*/_defineComponent({
...__default__,
setup(__props, { expose: __expose }) {
__expose();
let count = ref(0);
const add = () => {
count.value++;
};
const __returned__ = { get count() { return count }, set count(v) { count = v }, add }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
})',
loc: {
....
},
attrs: { setup: true, lang: 'ts' },
setup: true,
lang: 'ts',
bindings: {
defineComponent: 'setup-const',
ref: 'setup-const',
count: 'setup-let',
add: 'setup-const'
},
imports: [Object: null prototype] {
defineComponent: {
isType: false,
imported: 'defineComponent',
local: 'defineComponent',
source: 'vue',
isFromSetup: false,
isUsedInTemplate: false
},
ref: {
isType: false,
imported: 'ref',
local: 'ref',
source: 'vue',
isFromSetup: true,
isUsedInTemplate: false
}
},
map: []
,
names: [],
mappings: '...'
},
scriptAst: [
Node {
type: 'ImportDeclaration',
start: 2,
end: 40,
loc: [SourceLocation],
importKind: 'value',
specifiers: [Array],
source: [Node],
attributes: []
},
Node {
type: 'ExportDefaultDeclaration',
start: 44,
end: 222,
loc: [SourceLocation],
exportKind: 'value',
declaration: [Node]
}
],
scriptSetupAst: [
Node {
type: 'ImportDeclaration',
start: 2,
end: 28,
loc: [SourceLocation],
importKind: 'value',
specifiers: [Array],
source: [Node],
attributes: []
},
Node {
type: 'VariableDeclaration',
start: 30,
end: 49,
loc: [SourceLocation],
declarations: [Array],
kind: 'let'
},
Node {
type: 'VariableDeclaration',
start: 51,
end: 92,
loc: [SourceLocation],
declarations: [Array],
kind: 'const'
}
],
dep
如法炮制 找到compileTemplate函数,对模板代码进行处理
let temp = compileTemplate({ source: vueAst.descriptor.template?.content as string, filename: realPath, id: req.url, });
这里是template 的编译结果
{
code: 'import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"\n' +
'\n' +
'export function render(_ctx, _cache) {\n' +
' return (_openBlock(), _createElementBlock("div", null, [\n' +
' _createElementVNode("div", null, "hello Vue " + _toDisplayString(_ctx.count), 1 /* TEXT */),\n' +
' _createElementVNode("button", {\n' +
' onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.add && _ctx.add(...args)))\n' +
' }, "add")\n' +
' ]))\n' +
'}',
ast: {
type: 0,
source: '\r\n' +
' <div>\r\n' +
' <div>hello Vue {{ count }}</div>\r\n' +
' <button @click="add">add</button>\r\n' +
' </div>\r\n',
children: [ [Object] ],
helpers: Set(4) {
Symbol(toDisplayString),
Symbol(createElementVNode),
Symbol(openBlock),
Symbol(createElementBlock)
},
components: [],
directives: [],
hoists: [],
imports: [],
cached: [ [Object] ],
temps: 0,
codegenNode: {
type: 13,
tag: '"div"',
props: undefined,
children: [Array],
patchFlag: undefined,
dynamicProps: undefined,
directives: undefined,
isBlock: true,
disableTracking: false,
isComponent: false,
loc: [Object]
},
loc: {
start: [Object],
end: [Object],
source: '\r\n' +
' <div>\r\n' +
' <div>hello Vue {{ count }}</div>\r\n' +
' <button @click="add">add</button>\r\n' +
' </div>\r\n'
},
transformed: true,
filters: []
},
preamble: '',
source: '\r\n' +
' <div>\r\n' +
' <div>hello Vue {{ count }}</div>\r\n' +
' <button @click="add">add</button>\r\n' +
' </div>\r\n',
errors: [],
tips: [],
map: {
version: 3,
sources: [ 'D:\\code\\study\\vite\\minivite\\koaVite\\src\\cyber\\App.vue' ],
names: [ 'count' ],
mappings: ';;;wBACE,oBAGM;IAFJ,oBAAgC,aAA3B,YAAU,oBAAGA,UAAK;IACvB,oBAAkC;MAAxB,OAAK,0CAAE,6BAAG;OAAE,KAAG',
sourcesContent: [
'\r\n' +
' <div>\r\n' +
' <div>hello Vue {{ count }}</div>\r\n' +
' <button @click="add">add</button>\r\n' +
' </div>\r\n'
]
}
}
我们分割了其sfc的代码后,我们该怎么做呢;我们来看看一个vue3的项目,服务器返回的vue文件是什么样子
import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/views/index.vue");import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=eaf793ab";
import { ElButton } from "/node_modules/.vite/deps/element-plus.js?v=eaf793ab";
import { testApi } from "/src/api/index.js";
import { defineComponent } from "/node_modules/.vite/deps/vue.js?v=eaf793ab";
const __default__ = defineComponent({
name: ""
});
const _sfc_main = /* @__PURE__ */ _defineComponent({
...__default__,
setup(__props, { expose: __expose }) {
__expose();
const sendTEst = async () => {
let res = await testApi();
console.log("ok", res);
};
const __returned__ = { sendTEst, get ElButton() {
return ElButton;
} };
Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
return __returned__;
}
});
import { createTextVNode as _createTextVNode, withCtx as _withCtx, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=eaf793ab";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, [
_createVNode($setup["ElButton"], { onClick: $setup.sendTEst }, {
default: _withCtx(() => _cache[0] || (_cache[0] = [
_createTextVNode("hello world")
])),
_: 1
/* STABLE */
}),
_cache[1] || (_cache[1] = _createTextVNode(" 1111111 "))
]);
}
_sfc_main.__hmrId = "b301384e";
typeof __VUE_HMR_RUNTIME__ !== "undefined" && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main);
import.meta.hot.on("file-changed", ({ file }) => {
__VUE_HMR_RUNTIME__.CHANGED_FILE = file;
});
import.meta.hot.accept((mod) => {
if (!mod) return;
const { default: updated, _rerender_only } = mod;
if (_rerender_only) {
__VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render);
} else {
__VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated);
}
});
import _export_sfc from "/@id/__x00__plugin-vue:export-helper";
export default /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__file", "D:/code/work/project/vite-project/src/views/index.vue"]]);
export default (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key,val] of props) {
target[key] = val;
}
return target;
}
我在阅读源码的时候,发现vue3实际上是通过默认暴露一个vue对象,这个vue对象就是其组件;我从上面源码获取到的信息就是vue文件时暴露一个default,这个default会根据改写其render和__file属性;将其视为vue组件,我们做的第一步就是将_sfc_main的render替换为compileTemplate处理过后的render方法;要想将其合并,我们先将其拼接
let arr = [scriptRes.content, temp.code]
code = arr.join("\n");
最终我们得到混合的代码块;我们的vue-loader算是初步完成了,接下来我们去处理他的code
vue-plugin transform
因为我们主要处理的是导出模块,我们可以通过 const [imports, exports] = parse(transformedCode);对他的export进行改写
if (req.url?.endsWith(".vue")) {
for (const exportInfo of exports) {
const { n: exportId, s: start, e: end } = exportInfo;
if (!exportInfo) continue;
if (exportId === "default") ms.overwrite(start, end, "other");
// console.log(realPath)
code = ms.toString() + `let sfc_main =stdin_default.__vccOpts || stdin_default;sfc_main.render =render};\n sfc_main.__file = "${realPath}";\nexport default /* @__PURE__ */sfc_main`
}
}
我们可以看到,我将default进行了处理;这是为什么呢;因为他解析出来的代码默认就是一个default导出;我将其改写为非default导出,这个位置需要留给vue实例;然后我们对其进行改写,将stdin_default(这个是script解析的结果)的属性改写,并提供新的名字sfc_main;sfc_main.render =render将基本信息改写完我们将其导出 export default /* @__PURE__ */sfc_main;
这是App.vue的代码
<template>
<div>
<div>hello Vue {{ count }}</div>
<button @click="add">add</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "App",
});
</script>
<script setup lang="ts">
import { ref } from "vue";
let count = ref(0);
const add = () => {
count.value++;
};
</script>
我们来看看页面
🥜,你又发现了忙点,怎么响应式数据没有
我对其sfc_main的render参数进行打印
我发现其是有值的,排除法得到应该是我的render有问题;我在重新去检查compileTemplate的参数,并去查询其源码 vite-plugin-vue/packages/plugin-vue/src
/template.ts
我发现其bindingMetadata;在vue3的源码的阅读中,bind我们一般知道含有这个词的一般为传递参数;我刚好发现在script返回值的ts类型中刚好有这个值
export interface SFCScriptBlock extends SFCBlock {
type: 'script';
setup?: string | boolean;
bindings?: BindingMetadata$1;
imports?: Record<string, ImportBinding>;
scriptAst?: _babel_types.Statement[];
scriptSetupAst?: _babel_types.Statement[];
warnings?: string[];
/**
* Fully resolved dependency file paths (unix slashes) with imported types
* used in macros, used for HMR cache busting in @vitejs/plugin-vue and
* vue-loader.
*/
deps?: string[];
}
那我们把他加进去
let temp = compileTemplate({ source: vueAst.descriptor.template?.content as string, filename: realPath, id: req.url, slotted: vueAst.descriptor.slotted, compilerOptions: { bindingMetadata: scriptRes.bindings } });
我们的代码就能正常工作了,甚至可以使用vue的开发者工具;
这里是完整的代码
import Koa from "koa";
import { optimize } from "./node/optimizer";
import { init, parse } from "es-module-lexer";
import MagicString from "magic-string";
import fs from "fs-extra";
import esbuild from "esbuild"
import path from "path";
import {
parse as vueParse,
compileTemplate,
compileScript,
} from "vue/compiler-sfc"
const root = process.cwd();
const app = new Koa();
app.use(async (ctx, next) => {
const { req, res } = ctx;
if (req.url === "/") {
const realPath = path.join(process.cwd(), "/cyber.html");
//transformHtml
if (await fs.pathExists(realPath)) {
let code = await fs.readFile(realPath, "utf-8");
res.setHeader("Content-Type", "text/html");
res.end(code)
}
} else if ((req.method === "GET" || /\.(?:j|t)sx?$|\.mjs$|\.vue$/.test(req.url as string))) {
//resolve
const realPath = path.join(process.cwd(), req.url + "");
//loader
if (await fs.pathExists(realPath)) {
let code = await fs.readFile(realPath, "utf-8");
//vue-plugin loader
if (req.url?.endsWith(".vue")) {
const vueAst = vueParse(code);
let scriptRes = compileScript(vueAst.descriptor, { id: req.url });
let temp = compileTemplate({ source: vueAst.descriptor.template?.content as string, filename: realPath, id: req.url, slotted: vueAst.descriptor.slotted, compilerOptions: { bindingMetadata: scriptRes.bindings } });
let arr = [scriptRes.content, temp.code]
code = arr.join("\n");
}
let exname = path.extname(req.url as string).slice(1) === "vue" ? "js" :path.extname(req.url as string).slice(1)
let { code: transformedCode, map } = await esbuild.transform(code, {
target: "esnext",
format: "esm",
sourcemap: true,
loader: exname as "js" | "ts" | "jsx" | "tsx",
});
// transform
const ms = new MagicString(transformedCode);
await init;
const [imports, exports] = parse(transformedCode);
for (const importInfo of imports) {
const { n: importedId, s: start, e: end } = importInfo;
if (!importedId) continue;
if (/^[\w@][^:]/.test(importedId)) {
let realPath = path.join('/', "node_modules", ".m-vite", `${importedId}.js`);
realPath = realPath.replace(/\\/g, "/");
ms.overwrite(start, end, realPath);
code = ms.toString();
}
}
// vue-plugin transform
if (req.url?.endsWith(".vue")) {
for (const exportInfo of exports) {
const { n: exportId, s: start, e: end } = exportInfo;
if (!exportInfo) continue;
if (exportId === "default") ms.overwrite(start, end, "other");
// console.log(realPath)
code = ms.toString() + `let sfc_main =stdin_default.__vccOpts || stdin_default;sfc_main.render = function(...arg){console.log(arg);return render(...arg)};\n sfc_main.__file = "${realPath}";\nexport default /* @__PURE__ */sfc_main`
}
}
res.setHeader("Content-Type", "application/javascript");
res.statusCode = 200;
res.end(code)
}
}
return next()
})
app.listen(8954, async () => {
await optimize(root, "src/cyber/react.tsx")
await optimize(root, "src/cyber/vue.ts")
console.log("http://localhost:8954 ")
})
同时支持模块化
什么叫做赛博朋克?这就叫赛博朋克;垃圾佬的哲学观,又不是不能跑😎;
靓仔;都看到这里了,点个赞再走呗