feat(esm): 按目录加载脚本、打包与热更新
把一个目录当脚本仓库:按路径寻址、esbuild 打包、内容变了自动重编。
- Loader 扫目录建索引,Load(name) 给出打好包的源码和版本号
- bundler esbuild 的封装。ESM 格式而不是 IIFE——IIFE 会附带一整套
CommonJS interop helper,每建一个 VM 都要重跑一遍
- plugin 把扩展的 TS 模块变成可以 import 的虚拟模块,磁盘上没有文件
- typings 把这些虚拟模块的类型按 node_modules 布局落盘,编辑器才认识
esbuild 原生实现了 Node 的模块解析,所以脚本能直接 import node_modules
里的第三方库。写出来的类型文件是 index.ts 而不是 index.d.ts:扩展给的是
真正的模块源码,里面可能带实现,声明文件里不允许有实现。
node_modules 不参与热更新的版本计算——依赖包是装出来的,改动总伴随显式的
安装动作,而真实的 npm 包动辄上千个文件,每次取脚本 stat 一遍太贵。
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
export function money(n: number): string { return "¥" + n.toFixed(2) }
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "@fsdpf/util", "version": "1.0.0", "main": "index.ts", "types": "index.ts" }
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export function upper(s) { return String(s).toUpperCase() }
|
||||
export default { upper }
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "tinylib", "version": "1.0.0", "main": "index.js" }
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "fsdpf-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fsdpf/util": "^1.0.0",
|
||||
"tinylib": "^1.0.0"
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
export default class AsyncController {
|
||||
Sync(): string { return "同步结果" }
|
||||
|
||||
async Async(): Promise<string> { return "异步结果" }
|
||||
|
||||
async AwaitSomething(): Promise<string> {
|
||||
const v = await Promise.resolve("await 的结果")
|
||||
return v
|
||||
}
|
||||
|
||||
async Fails(): Promise<string> { throw new Error("异步失败") }
|
||||
|
||||
// 永远不会 resolve —— goja 没有事件循环,没人能让它完成
|
||||
Never(): Promise<string> { return new Promise(function () {}) }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default class PkgImportController {
|
||||
constructor(pkgName) { this.pkgName = pkgName; this.count = 0 }
|
||||
Init() { return "PkgImport:" + this.pkgName }
|
||||
Handle() { return ++this.count }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// 只有命名导出,没有 default——应该整个模块当导出对象用
|
||||
export function Options(): string { return "opts" }
|
||||
export function Version(): string { return "v1" }
|
||||
@@ -0,0 +1,16 @@
|
||||
import ResExecuter from "./ResExecuter"
|
||||
import { nextId, tag } from "./shared"
|
||||
|
||||
export default class ResCreateController {
|
||||
private ex: ResExecuter
|
||||
private count = 0
|
||||
|
||||
constructor(private resName: string, private mode: string) {
|
||||
this.ex = new ResExecuter()
|
||||
}
|
||||
|
||||
Init(): string { return "ResCreate:" + this.resName + "/" + this.mode }
|
||||
Store(cfg: string): string { this.count++; return this.ex.Store(cfg) + "#" + this.count }
|
||||
Seq(): string { return tag("create:" + nextId()) }
|
||||
Boom(): void { throw new Error("创建失败") }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { nextId, tag } from "./shared"
|
||||
export default class ResDeleteController {
|
||||
Seq(): string { return tag("delete:" + nextId()) }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// 被同目录 controller import 的公共类
|
||||
export default class ResExecuter {
|
||||
Store(cfg: string): string { return "stored:" + cfg }
|
||||
Destroy(id: string): string { return "destroyed:" + id }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { upper } from "tinylib"
|
||||
import { money } from "@fsdpf/util"
|
||||
import { bracket } from "@lib/format"
|
||||
import ResExecuter from "./ResExecuter"
|
||||
|
||||
export default class ResSchemaController {
|
||||
private ex = new ResExecuter()
|
||||
|
||||
Describe(name: string, price: number): string {
|
||||
return upper(name) + " " + money(price) + " " + this.ex.Store("s")
|
||||
}
|
||||
|
||||
// 走 tsconfig 里配的 @lib/* 路径别名
|
||||
Tag(s: string): string {
|
||||
return bracket(s)
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// 公共纯函数 + 一个模块级状态(用来验证状态不跨 controller 共享)
|
||||
let seq = 0
|
||||
export function nextId(): number { return ++seq }
|
||||
export function tag(s: string): string { return "[" + s + "]" }
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// 往共享的 store 里写购物车
|
||||
export default class CartController {
|
||||
private ops = 0
|
||||
|
||||
Add(sku: string, qty: number): number {
|
||||
this.ops++
|
||||
const items = store.GetOr("items", {}) as Record<string, number>
|
||||
items[sku] = (items[sku] || 0) + qty
|
||||
store.Set("items", items)
|
||||
return store.Incr("total", qty)
|
||||
}
|
||||
|
||||
// 自己的调用计数,跟另一个 controller 无关
|
||||
Ops(): number { return this.ops }
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// 从共享的 store 里读购物车下单
|
||||
export default class OrderController {
|
||||
private ops = 0
|
||||
|
||||
Checkout(): string {
|
||||
this.ops++
|
||||
const items = store.GetOr("items", {}) as Record<string, number>
|
||||
const skus = Object.keys(items).sort()
|
||||
return skus.join(",") + "|" + store.Get("total")
|
||||
}
|
||||
|
||||
Ops(): number { return this.ops }
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
// 放在 src/lib 下的公共模块,controller 通过 tsconfig 的 @lib/* 别名引用
|
||||
export function bracket(s: string): string {
|
||||
return "[" + s + "]"
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@lib/*": ["./src/lib/*"]
|
||||
},
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export function only() { return "app2 专属依赖" }
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "only2", "main": "index.js" }
|
||||
@@ -0,0 +1,5 @@
|
||||
import { only } from "only2"
|
||||
|
||||
export default class ExtraController {
|
||||
Who(): string { return "app2 独有:" + only() }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// 跟 app/src 下同名,用来验证「后面的目录覆盖前面的」
|
||||
export default class ResCreateController {
|
||||
Init(): string { return "来自 app2 的覆盖实现" }
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "compilerOptions": { "target": "ES2017", "strict": true } }
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// 用 import 拿到 store 的类型,而不是直接用全局
|
||||
import store, { Store } from "@jscriptx/store"
|
||||
|
||||
export default class TypedController {
|
||||
Put(k: string, v: string): void {
|
||||
store.Set(k, v)
|
||||
}
|
||||
|
||||
Take(k: string): any {
|
||||
return store.Get(k)
|
||||
}
|
||||
|
||||
// 类型也导出得到
|
||||
Describe(s: Store = store): number {
|
||||
return s.Len()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user