@workfly/vite-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +21 -0
- package/package.json +32 -0
- package/src/index.js +203 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @workfly/vite-plugin 类型声明(实现为纯 ESM JS)。
|
|
2
|
+
import type { Plugin } from 'vite'
|
|
3
|
+
|
|
4
|
+
export interface WorkflyPluginOptions {
|
|
5
|
+
/** manifest.json 路径,相对 vite root,默认 'manifest.json'。 */
|
|
6
|
+
manifest?: string
|
|
7
|
+
/** 输出 zip 名,默认 `${目录名}.wfapp.zip`(放 vite root,不进 dist)。 */
|
|
8
|
+
outName?: string
|
|
9
|
+
/** build 结束后是否自动 pack 成 .wfapp.zip,默认 true。 */
|
|
10
|
+
pack?: boolean
|
|
11
|
+
/** build:dist/index.html 内联 <script> 的 CSP 校验级别,默认 'warn'。 */
|
|
12
|
+
cspCheck?: 'error' | 'warn' | 'off'
|
|
13
|
+
/** dev:是否按 manifest.permissions 门控 wf 代理调用,默认 true。 */
|
|
14
|
+
enforcePermissions?: boolean
|
|
15
|
+
/** dev:调试面板开关,默认 true。 */
|
|
16
|
+
devPanel?: boolean
|
|
17
|
+
/** dev:模拟启动 query(对应客户端 getLaunchOptionsSync)。 */
|
|
18
|
+
query?: Record<string, string>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function workfly(options?: WorkflyPluginOptions): Plugin
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@workfly/vite-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WorkFly 小程序的 Vite 插件(框架无关):dev 注入 wf SDK + 网络/RSA 代理 + 权限门控;build 后校验 manifest 并打包成带 WFAPP1 魔数的 .wfapp.zip。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"default": "./src/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"index.d.ts"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@workfly/miniapp-kit": "^0.1.0"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"vite": ">=5"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"workfly",
|
|
24
|
+
"miniapp",
|
|
25
|
+
"vite-plugin",
|
|
26
|
+
"wfapp"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT"
|
|
32
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// @workfly/vite-plugin —— WorkFly 小程序的 Vite 插件(框架无关)。
|
|
2
|
+
// build:强制 base/modulePreload 基线 → 校验 manifest → 扫 CSP → 打包成 .wfapp.zip(魔数)。
|
|
3
|
+
// dev(见 configureServer/transformIndexHtml):注入 wf shim + 网络/RSA 代理 + 权限门控。
|
|
4
|
+
// 框架交给开发者自己的 @vitejs/plugin-react|vue;本插件只管 WorkFly 特有的注入与打包。
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { dirname, join, resolve } from 'node:path'
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
8
|
+
|
|
9
|
+
// 解析工具核心:主仓内走相对路径,发布后走依赖包名 @workfly/miniapp-kit。
|
|
10
|
+
// 惰性加载(不能用顶层 await:Vite/rolldown 预打包配置时不支持 TLA);specifier 用变量确保按运行时动态 import 处理。
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
let _kit
|
|
13
|
+
function loadKit() {
|
|
14
|
+
if (!_kit) {
|
|
15
|
+
const kitRepo = resolve(here, '../../miniapp-kit/src/index.js')
|
|
16
|
+
const spec = existsSync(kitRepo) ? pathToFileURL(kitRepo).href : '@workfly/miniapp-kit'
|
|
17
|
+
_kit = import(spec)
|
|
18
|
+
}
|
|
19
|
+
return _kit
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** dist/index.html 不应含违规内联 <script>(沙盒 CSP script-src 'self' 会拦截 → 白屏)。 */
|
|
23
|
+
function checkCsp(distDir, level, ctx) {
|
|
24
|
+
const idx = join(distDir, 'index.html')
|
|
25
|
+
if (!existsSync(idx)) return
|
|
26
|
+
const html = readFileSync(idx, 'utf8')
|
|
27
|
+
const inline = [...html.matchAll(/<script\b(?![^>]*\bsrc=)[^>]*>/gi)].filter(
|
|
28
|
+
(t) => !/type=["']application\/json["']/i.test(t[0])
|
|
29
|
+
)
|
|
30
|
+
if (!inline.length) return
|
|
31
|
+
const msg =
|
|
32
|
+
`dist/index.html 含 ${inline.length} 个内联 <script>,会被沙盒 CSP(script-src 'self')拦截导致白屏。` +
|
|
33
|
+
`请确认 build.modulePreload.polyfill=false,且不要内联脚本。`
|
|
34
|
+
level === 'error' ? ctx.error(msg) : ctx.warn(msg)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 规整 permissions(兼容旧 string[] 与新 {id,reason}[])→ 能力 id 列表。 */
|
|
38
|
+
function normPerms(perms) {
|
|
39
|
+
return (perms ?? []).map((p) => (typeof p === 'string' ? p : p.id))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 读 connect 请求体为 JSON。 */
|
|
43
|
+
function readBody(req) {
|
|
44
|
+
return new Promise((res, rej) => {
|
|
45
|
+
let raw = ''
|
|
46
|
+
req.on('data', (c) => (raw += c))
|
|
47
|
+
req.on('end', () => {
|
|
48
|
+
try {
|
|
49
|
+
res(raw ? JSON.parse(raw) : {})
|
|
50
|
+
} catch (e) {
|
|
51
|
+
rej(e)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
req.on('error', rej)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sendJson(res, code, obj) {
|
|
59
|
+
res.writeHead(code, { 'content-type': 'application/json; charset=utf-8' })
|
|
60
|
+
res.end(JSON.stringify(obj))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {import('../index.js').WorkflyPluginOptions} [options]
|
|
65
|
+
* @returns {import('vite').Plugin}
|
|
66
|
+
*/
|
|
67
|
+
export default function workfly(options = {}) {
|
|
68
|
+
const {
|
|
69
|
+
manifest: manifestRel = 'manifest.json',
|
|
70
|
+
outName,
|
|
71
|
+
pack = true,
|
|
72
|
+
cspCheck = 'warn',
|
|
73
|
+
enforcePermissions = true,
|
|
74
|
+
query = {}
|
|
75
|
+
} = options
|
|
76
|
+
|
|
77
|
+
let root = process.cwd()
|
|
78
|
+
let outDir = 'dist'
|
|
79
|
+
let isBuild = false
|
|
80
|
+
let manifest = null
|
|
81
|
+
|
|
82
|
+
function loadManifest() {
|
|
83
|
+
const p = resolve(root, manifestRel)
|
|
84
|
+
if (!existsSync(p)) return null
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(p, 'utf8'))
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: '@workfly/vite-plugin',
|
|
94
|
+
|
|
95
|
+
// 仅 build 强制正确基线(merge,用户显式设的优先;dev 不动 base 以保 HMR):
|
|
96
|
+
// · base:'./' 资源相对路径,适配 workfly-app://<id>/ 解析
|
|
97
|
+
// · modulePreload 关 polyfill 避免 Vite 注入内联脚本被沙盒 CSP 拦截(头号坑)
|
|
98
|
+
config(_userConfig, env) {
|
|
99
|
+
isBuild = env.command === 'build'
|
|
100
|
+
if (!isBuild) return {}
|
|
101
|
+
return { base: './', build: { modulePreload: { polyfill: false } } }
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
configResolved(resolved) {
|
|
105
|
+
root = resolved.root
|
|
106
|
+
outDir = resolved.build.outDir
|
|
107
|
+
manifest = loadManifest()
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// dev:注入 wf shim + 启动配置;去掉沙盒 CSP(dev 不需要,且会挡 shim 与同源代理)。生产由 preload 注入 wf,故 build 不注入。
|
|
111
|
+
transformIndexHtml: {
|
|
112
|
+
order: 'pre',
|
|
113
|
+
handler(html) {
|
|
114
|
+
if (isBuild) return html
|
|
115
|
+
const m = manifest ?? {}
|
|
116
|
+
const config = {
|
|
117
|
+
id: m.id,
|
|
118
|
+
name: m.name,
|
|
119
|
+
version: m.version,
|
|
120
|
+
permissions: normPerms(m.permissions),
|
|
121
|
+
proxyBase: '',
|
|
122
|
+
query
|
|
123
|
+
}
|
|
124
|
+
const stripped = html.replace(
|
|
125
|
+
/<meta[^>]+http-equiv=["']Content-Security-Policy["'][^>]*>\s*/gi,
|
|
126
|
+
''
|
|
127
|
+
)
|
|
128
|
+
return {
|
|
129
|
+
html: stripped,
|
|
130
|
+
tags: [
|
|
131
|
+
{
|
|
132
|
+
tag: 'script',
|
|
133
|
+
children: `window.__WF_CONFIG__=${JSON.stringify(config)}`,
|
|
134
|
+
injectTo: 'head-prepend'
|
|
135
|
+
},
|
|
136
|
+
{ tag: 'script', attrs: { src: '/__wf/shim.js' }, injectTo: 'head-prepend' }
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// dev:本地代理 wf 网络/RSA(复刻主进程语义、绕 CORS),按 manifest 权限门控。
|
|
143
|
+
configureServer(server) {
|
|
144
|
+
server.middlewares.use('/__wf/shim.js', async (_req, res) => {
|
|
145
|
+
const { shimSource } = await loadKit()
|
|
146
|
+
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' })
|
|
147
|
+
res.end(shimSource())
|
|
148
|
+
})
|
|
149
|
+
server.middlewares.use('/__wf/request', async (req, res, next) => {
|
|
150
|
+
if (req.method !== 'POST') return next()
|
|
151
|
+
if (enforcePermissions && !new Set(normPerms(manifest?.permissions)).has('net.fetch'))
|
|
152
|
+
return sendJson(res, 403, { error: "permission 'net.fetch' not granted" })
|
|
153
|
+
try {
|
|
154
|
+
const { handleWfRequest } = await loadKit()
|
|
155
|
+
const { status, body } = await handleWfRequest(await readBody(req))
|
|
156
|
+
sendJson(res, status, body)
|
|
157
|
+
} catch (e) {
|
|
158
|
+
sendJson(res, 502, { error: String(e?.message ?? e) })
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
server.middlewares.use('/__wf/rsa', async (req, res, next) => {
|
|
162
|
+
if (req.method !== 'POST') return next()
|
|
163
|
+
try {
|
|
164
|
+
const { handleWfRsa } = await loadKit()
|
|
165
|
+
const { status, body } = await handleWfRsa(await readBody(req))
|
|
166
|
+
sendJson(res, status, body)
|
|
167
|
+
} catch (e) {
|
|
168
|
+
sendJson(res, 400, { error: String(e?.message ?? e) })
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// 产物写盘后:校验 manifest → 扫 CSP → 打包 .wfapp.zip。
|
|
174
|
+
// 用 writeBundle(产物已落盘)而非 closeBundle(Vite 8/rolldown 下 closeBundle 早于 dist 落盘)。
|
|
175
|
+
async writeBundle() {
|
|
176
|
+
if (!isBuild) return
|
|
177
|
+
const { packDir, readMagic, validateManifest } = await loadKit()
|
|
178
|
+
const { report, manifest } = validateManifest(root, { forPack: true })
|
|
179
|
+
for (const w of report.warnings) this.warn(w)
|
|
180
|
+
if (report.errors.length) {
|
|
181
|
+
this.error(`manifest 校验失败:\n ${report.errors.join('\n ')}`)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (cspCheck !== 'off') checkCsp(resolve(root, outDir), cspCheck, this)
|
|
186
|
+
|
|
187
|
+
if (pack === false) return
|
|
188
|
+
// SPA:源 index.html(vite 入口)与 public/ 已构建进 dist,排除避免重复入包
|
|
189
|
+
const r = packDir({
|
|
190
|
+
dir: root,
|
|
191
|
+
manifest,
|
|
192
|
+
outName,
|
|
193
|
+
exclude: { dirs: ['public'], paths: ['index.html'] }
|
|
194
|
+
})
|
|
195
|
+
writeFileSync(join(root, r.outName), r.bytes)
|
|
196
|
+
const ok = !!readMagic(r.bytes)
|
|
197
|
+
this.info(
|
|
198
|
+
`[workfly] ✓ ${r.outName} · ${r.fileCount} 文件 · ${(r.bytes.length / 1024).toFixed(1)} KB · 魔数${ok ? '已写入' : '缺失!'} (${manifest.id}@${manifest.version})`
|
|
199
|
+
)
|
|
200
|
+
if (!ok) this.error('魔数写入校验失败')
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|