@open-wxa/wxa-runner-core 0.8.1 → 0.9.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/dist/index.mjs CHANGED
@@ -128,11 +128,11 @@ var FileStore = class {
128
128
  };
129
129
 
130
130
  // src/server/DevServer.ts
131
- import fs4 from "fs";
131
+ import fs5 from "fs";
132
132
  import http from "http";
133
133
  import net from "net";
134
- import os from "os";
135
- import path4 from "path";
134
+ import os2 from "os";
135
+ import path5 from "path";
136
136
  import express2 from "express";
137
137
  import { WebSocket as WebSocket2, WebSocketServer } from "ws";
138
138
  import { readRegionData, resolveBaseLib, usesIsolatedContext } from "@open-wxa/wxa-runner-extractor";
@@ -197,6 +197,50 @@ function createWxConfig(input) {
197
197
  };
198
198
  }
199
199
 
200
+ // src/project/WxmlMirror.ts
201
+ import fs from "fs";
202
+ import os from "os";
203
+ import path from "path";
204
+ var WxmlMirror = class {
205
+ constructor(transform) {
206
+ this.transform = transform;
207
+ this.dir = null;
208
+ /** 已写入的文件:源内容(判断要不要重新转换)与写入的内容 */
209
+ this.files = /* @__PURE__ */ new Map();
210
+ }
211
+ /** 镜像目录;还没同步过时为 null */
212
+ get directory() {
213
+ return this.dir;
214
+ }
215
+ /**
216
+ * 让镜像目录与项目里的 wxml / wxs 一致,返回镜像目录。
217
+ * 只重写源内容变化的文件;项目里已删除的文件同时从镜像目录删除,被引用时 wcc 照常报错
218
+ */
219
+ sync(project, relPaths) {
220
+ const dir = this.dir ??= fs.mkdtempSync(path.join(os.tmpdir(), "wxa-runner-wxml-"));
221
+ const current = new Set(relPaths);
222
+ for (const rel of [...this.files.keys()]) {
223
+ if (current.has(rel)) continue;
224
+ fs.rmSync(path.join(dir, rel), { force: true });
225
+ this.files.delete(rel);
226
+ }
227
+ for (const rel of relPaths) {
228
+ const source = project.readText(rel);
229
+ if (this.files.get(rel) === source) continue;
230
+ const target = path.join(dir, rel);
231
+ fs.mkdirSync(path.dirname(target), { recursive: true });
232
+ fs.writeFileSync(target, rel.endsWith(".wxml") ? this.transform(source, rel) : source);
233
+ this.files.set(rel, source);
234
+ }
235
+ return dir;
236
+ }
237
+ dispose() {
238
+ if (this.dir) fs.rmSync(this.dir, { recursive: true, force: true });
239
+ this.dir = null;
240
+ this.files.clear();
241
+ }
242
+ };
243
+
200
244
  // src/project/AppBuilder.ts
201
245
  var MODULE_PARAMS = [
202
246
  "require",
@@ -223,7 +267,7 @@ var MODULE_PARAMS = [
223
267
  ].join(", ");
224
268
  var ESM_SYNTAX = /(?:^|[;}])\s*(?:import\s*[{*'"]|import\s+[\w$]|export\s*[{*]|export\s+(?:default|const|let|var|function|class|async)\b)/m;
225
269
  var AppBuilder = class {
226
- constructor(project) {
270
+ constructor(project, options = {}) {
227
271
  this.project = project;
228
272
  this.version = 0;
229
273
  /** invalidate 标记过的变化次数;每次构建记下开始时覆盖到第几次 */
@@ -233,6 +277,15 @@ var AppBuilder = class {
233
277
  this.dirty = /* @__PURE__ */ new Set(["config", "wxml", "wxss", "js"]);
234
278
  this.wxmlCache = null;
235
279
  this.wxssCache = null;
280
+ this.wxmlMirror = options.wxmlTransform ? new WxmlMirror(options.wxmlTransform) : null;
281
+ }
282
+ /** wxmlTransform 的镜像目录;没有设置转换或还没编译过时为 null */
283
+ get wxmlMirrorDir() {
284
+ return this.wxmlMirror?.directory ?? null;
285
+ }
286
+ /** 删除 wxmlTransform 的镜像目录,开发服务器关闭时调用 */
287
+ dispose() {
288
+ this.wxmlMirror?.dispose();
236
289
  }
237
290
  /** 根据变化的文件标记需要重新构建的部分;返回 true 表示需要刷新 */
238
291
  invalidate(changedFiles = []) {
@@ -357,7 +410,7 @@ ${pageStyles}`,
357
410
  const json = unitJson.get(id);
358
411
  return { path: id, tags: Object.keys({ ...json.usingComponents, ...json.componentGenerics }) };
359
412
  });
360
- const cwd = this.project.root;
413
+ const cwd = this.wxmlMirror ? this.wxmlMirror.sync(this.project, files.map((file) => file.slice(2))) : this.project.root;
361
414
  const [full, cut] = await Promise.all([
362
415
  compileWxml({ cwd, files, components, cut: false }),
363
416
  compileWxml({ cwd, files, components, cut: true })
@@ -424,22 +477,22 @@ function classifyFile(file) {
424
477
  }
425
478
 
426
479
  // src/project/MiniProgram.ts
427
- import fs from "fs";
428
- import path from "path";
480
+ import fs2 from "fs";
481
+ import path2 from "path";
429
482
  var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".idea", ".vscode"]);
430
483
  var PROJECT_FILES = /* @__PURE__ */ new Set(["project.config.json", "project.private.config.json", "sitemap.json", "app.json", "wxa.setup.js"]);
431
484
  var MiniProgram = class {
432
485
  constructor(projectPath) {
433
486
  this.projectPath = projectPath;
434
- const projectConfig = readJsonFile(path.join(projectPath, "project.config.json")) || {};
435
- this.root = path.resolve(projectPath, projectConfig.miniprogramRoot || "");
487
+ const projectConfig = readJsonFile(path2.join(projectPath, "project.config.json")) || {};
488
+ this.root = path2.resolve(projectPath, projectConfig.miniprogramRoot || "");
436
489
  this.appid = projectConfig.appid || "touristappid";
437
- if (!fs.existsSync(path.join(this.root, "app.json"))) {
490
+ if (!fs2.existsSync(path2.join(this.root, "app.json"))) {
438
491
  throw new Error(`${this.root} \u4E0B\u6CA1\u6709 app.json\uFF0C\u4E0D\u662F\u6709\u6548\u7684\u5C0F\u7A0B\u5E8F\u76EE\u5F55`);
439
492
  }
440
493
  }
441
494
  readAppJson() {
442
- const app = readJsonFile(path.join(this.root, "app.json"));
495
+ const app = readJsonFile(path2.join(this.root, "app.json"));
443
496
  if (!app || !Array.isArray(app.pages) || app.pages.length === 0) {
444
497
  throw new Error("app.json \u7F3A\u5C11 pages \u914D\u7F6E");
445
498
  }
@@ -451,14 +504,14 @@ var MiniProgram = class {
451
504
  /** Worker 代码所在目录,不带首尾的 /;没有配置 workers 时为 null */
452
505
  workersDir(app) {
453
506
  const dir = typeof app.workers === "string" ? app.workers : app.workers?.path;
454
- return dir ? path.posix.normalize(dir).replace(/^\/+|\/+$/g, "") : null;
507
+ return dir ? path2.posix.normalize(dir).replace(/^\/+|\/+$/g, "") : null;
455
508
  }
456
509
  /** 主包页面 + 分包页面(带 root 前缀) */
457
510
  allPages(app) {
458
511
  const pages = [...app.pages];
459
512
  for (const pkg of this.subPackages(app)) {
460
513
  for (const page of pkg.pages) {
461
- const full = path.posix.join(pkg.root, page);
514
+ const full = path2.posix.join(pkg.root, page);
462
515
  if (!pages.includes(full)) pages.push(full);
463
516
  }
464
517
  }
@@ -470,7 +523,7 @@ var MiniProgram = class {
470
523
  * 所以能解析到的引用都换成组件的绝对路径
471
524
  */
472
525
  readUnitJson(id, app) {
473
- const json = { ...readJsonFile(path.join(this.root, id + ".json")) };
526
+ const json = { ...readJsonFile(path2.join(this.root, id + ".json")) };
474
527
  const resolve = (fromId, ref) => {
475
528
  const resolved = this.resolveComponent(fromId, ref);
476
529
  return resolved ? "/" + resolved : null;
@@ -491,7 +544,7 @@ var MiniProgram = class {
491
544
  if (resolved || ref.startsWith("/") || /^plugin(-private)?:\/\//.test(ref)) {
492
545
  json.usingComponents[tag] = resolved ?? ref;
493
546
  } else {
494
- const relative = path.posix.relative(path.posix.dirname(id), ref);
547
+ const relative = path2.posix.relative(path2.posix.dirname(id), ref);
495
548
  json.usingComponents[tag] = relative.startsWith(".") ? relative : "./" + relative;
496
549
  }
497
550
  }
@@ -504,25 +557,25 @@ var MiniProgram = class {
504
557
  */
505
558
  resolveComponent(fromId, ref) {
506
559
  if (!ref || ref.startsWith("plugin://") || ref.startsWith("plugin-private://")) return null;
507
- const fromDir = path.posix.dirname(fromId);
560
+ const fromDir = path2.posix.dirname(fromId);
508
561
  const candidates = [];
509
562
  if (ref.startsWith("/")) {
510
563
  candidates.push(ref.slice(1));
511
564
  } else {
512
- candidates.push(path.posix.join(fromDir, ref));
565
+ candidates.push(path2.posix.join(fromDir, ref));
513
566
  if (!ref.startsWith(".")) {
514
567
  let dir = fromDir;
515
568
  for (; ; ) {
516
- candidates.push(path.posix.join(dir === "." ? "" : dir, "miniprogram_npm", ref));
569
+ candidates.push(path2.posix.join(dir === "." ? "" : dir, "miniprogram_npm", ref));
517
570
  if (dir === "." || dir === "") break;
518
- dir = path.posix.dirname(dir);
571
+ dir = path2.posix.dirname(dir);
519
572
  }
520
573
  }
521
574
  }
522
575
  for (const candidate of candidates) {
523
- const normalized = path.posix.normalize(candidate);
524
- for (const id of [normalized, path.posix.join(normalized, "index")]) {
525
- if (fs.existsSync(path.join(this.root, id + ".json"))) return id;
576
+ const normalized = path2.posix.normalize(candidate);
577
+ for (const id of [normalized, path2.posix.join(normalized, "index")]) {
578
+ if (fs2.existsSync(path2.join(this.root, id + ".json"))) return id;
526
579
  }
527
580
  }
528
581
  return null;
@@ -554,11 +607,11 @@ var MiniProgram = class {
554
607
  listFiles(extensions) {
555
608
  const result = [];
556
609
  const walk = (dir, rel) => {
557
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
610
+ for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
558
611
  if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
559
612
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
560
613
  if (entry.isDirectory()) {
561
- walk(path.join(dir, entry.name), childRel);
614
+ walk(path2.join(dir, entry.name), childRel);
562
615
  } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
563
616
  if (!rel && PROJECT_FILES.has(entry.name)) continue;
564
617
  result.push(childRel);
@@ -569,18 +622,18 @@ var MiniProgram = class {
569
622
  return result.sort();
570
623
  }
571
624
  exists(relPath) {
572
- return fs.existsSync(path.join(this.root, relPath));
625
+ return fs2.existsSync(path2.join(this.root, relPath));
573
626
  }
574
627
  readText(relPath) {
575
- return fs.readFileSync(path.join(this.root, relPath), "utf8");
628
+ return fs2.readFileSync(path2.join(this.root, relPath), "utf8");
576
629
  }
577
630
  isInIndependentSubPackage(id, app) {
578
631
  return this.subPackages(app).some((pkg) => pkg.independent && id.startsWith(pkg.root.replace(/\/?$/, "/")));
579
632
  }
580
633
  };
581
634
  function readJsonFile(file) {
582
- if (!fs.existsSync(file)) return null;
583
- const text = fs.readFileSync(file, "utf8");
635
+ if (!fs2.existsSync(file)) return null;
636
+ const text = fs2.readFileSync(file, "utf8");
584
637
  try {
585
638
  return JSON.parse(text);
586
639
  } catch (error) {
@@ -646,9 +699,9 @@ var WechatDevClient = class {
646
699
  *
647
700
  * @throws 没有可用的登录态、超时、连不上、HTTP 状态不是 2xx 时,消息是给用户看的原因
648
701
  */
649
- async post(path5, body, timeout = DEFAULT_TIMEOUT) {
702
+ async post(path6, body, timeout = DEFAULT_TIMEOUT) {
650
703
  const query = new URLSearchParams({ appid: this.appid, newticket: requireUsableTicket(await this.findTicket()), clientversion: CLIENT_VERSION });
651
- const response = await fetch(`${BASE_URL}/${path5}?${query}`, {
704
+ const response = await fetch(`${BASE_URL}/${path6}?${query}`, {
652
705
  method: "POST",
653
706
  headers: { "Content-Type": "application/json" },
654
707
  body: JSON.stringify(body),
@@ -660,8 +713,8 @@ var WechatDevClient = class {
660
713
  return response;
661
714
  }
662
715
  /** 同 post,返回解析后的 JSON */
663
- async postJson(path5, body) {
664
- return (await this.post(path5, body)).json();
716
+ async postJson(path6, body) {
717
+ return (await this.post(path6, body)).json();
665
718
  }
666
719
  };
667
720
 
@@ -857,12 +910,12 @@ function webviewHtml(build2, pagePath, webviewId, options) {
857
910
  return `<!DOCTYPE html>
858
911
  <html>
859
912
  <head>
860
- ${webviewHead(build2, pagePath, id)}
913
+ ${webviewHead(build2, pagePath, id, options.scripts)}
861
914
  ${enableHotUpdate(options.hot)}<script src="/__wxa__/page-frame.js?v=${build2.version}"></script>
862
915
  ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</style>" : ""}
863
916
  </head>
864
917
  <body>
865
- <script>
918
+ ${scriptTags(options.scripts, "body")}<script>
866
919
  (function () {
867
920
  var path = ${toJs(pagePath)};
868
921
  if (typeof __wxAppCode__[path + '.wxss'] === 'function') __wxAppCode__[path + '.wxss']();
@@ -895,18 +948,18 @@ self.console = __wxaConsole;
895
948
  importScripts(${modules.map((url) => toJs(url)).join(", ")});
896
949
  `;
897
950
  }
898
- function pluginWebviewHtml(build2, plugin, innerPath, pagePath, webviewId) {
951
+ function pluginWebviewHtml(build2, plugin, innerPath, pagePath, webviewId, options = {}) {
899
952
  const id = Number(webviewId) || 0;
900
953
  const page = plugin.files.get(`/${innerPath}.html`)?.toString("utf8") ?? `<script>console.error(${JSON.stringify(`[wxa-runner] \u63D2\u4EF6 ${plugin.alias} \u91CC\u6CA1\u6709\u9875\u9762 ${innerPath}`)})</script>`;
901
954
  return `<!DOCTYPE html>
902
955
  <html>
903
956
  <head>
904
- ${webviewHead(build2, pagePath, id)}
957
+ ${webviewHead(build2, pagePath, id, options.scripts)}
905
958
  <script src="${pluginScriptUrl(plugin, "pageframe.js")}"></script>
906
959
  ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</style>" : ""}
907
960
  </head>
908
961
  <body>
909
- ${page}
962
+ ${scriptTags(options.scripts, "body")}${page}
910
963
  <script>parent.__wxaHost.frameLoaded('webview', ${id});</script>
911
964
  </body>
912
965
  </html>`;
@@ -914,7 +967,7 @@ ${page}
914
967
  function pageWindow(build2, pagePath) {
915
968
  return { ...build2.wxConfig.global?.window, ...build2.wxConfig.page?.[pagePath + ".html"]?.window };
916
969
  }
917
- function webviewHead(build2, pagePath, id) {
970
+ function webviewHead(build2, pagePath, id, scripts) {
918
971
  const wxConfig = { ...build2.wxConfig, window: pageWindow(build2, pagePath) };
919
972
  return `<meta charset="utf-8">
920
973
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
@@ -929,7 +982,12 @@ function webviewHead(build2, pagePath, id) {
929
982
  <style>.wxa-service-canvas{position:absolute!important;left:0!important;top:0!important;width:100%!important;height:100%!important;visibility:inherit!important;pointer-events:none}</style>
930
983
  <script>var __wxConfig = ${toJs(wxConfig)};</script>
931
984
  <script src="/__wxa__/runtime/frame.js" data-kind="webview" data-webview-id="${id}"></script>
932
- <script src="/__baselib__/WAWebview.js"></script>`;
985
+ ${scriptTags(scripts, "head")}<script src="/__baselib__/WAWebview.js"></script>`;
986
+ }
987
+ function scriptTags(scripts, position) {
988
+ return (scripts ?? []).filter((script) => (script.position ?? "body") === position).map((script) => script.src !== void 0 ? `<script src="${escapeHtml(script.src)}"></script>
989
+ ` : `<script>${script.content.replace(/<\/script/gi, "<\\/script")}</script>
990
+ `).join("");
933
991
  }
934
992
  function escapeHtml(text) {
935
993
  return text.replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
@@ -1293,16 +1351,16 @@ function respond(action) {
1293
1351
  }
1294
1352
 
1295
1353
  // src/server/setup.ts
1296
- import fs2 from "fs";
1297
- import path2 from "path";
1354
+ import fs3 from "fs";
1355
+ import path3 from "path";
1298
1356
  import { build } from "esbuild";
1299
1357
  var SETUP_FILE_NAMES = ["wxa.setup.ts", "wxa.setup.js", "wxa.setup.mjs"];
1300
1358
  var SETUP_MODULE = "@open-wxa/wxa-runner-core/setup";
1301
1359
  var ENV_PREFIX = "WXA_";
1302
1360
  function findSetupFile(projectPath) {
1303
1361
  for (const name of SETUP_FILE_NAMES) {
1304
- const file = path2.join(projectPath, name);
1305
- if (fs2.existsSync(file)) return file;
1362
+ const file = path3.join(projectPath, name);
1363
+ if (fs3.existsSync(file)) return file;
1306
1364
  }
1307
1365
  return null;
1308
1366
  }
@@ -1314,7 +1372,7 @@ function collectSetupEnv(env) {
1314
1372
  async function bundleSetup(file) {
1315
1373
  const result = await build({
1316
1374
  entryPoints: [file],
1317
- absWorkingDir: path2.dirname(file),
1375
+ absWorkingDir: path3.dirname(file),
1318
1376
  bundle: true,
1319
1377
  format: "esm",
1320
1378
  platform: "browser",
@@ -1342,8 +1400,8 @@ function builtinSetupModule() {
1342
1400
 
1343
1401
  // src/server/watcher.ts
1344
1402
  import { createHash } from "crypto";
1345
- import fs3 from "fs";
1346
- import path3 from "path";
1403
+ import fs4 from "fs";
1404
+ import path4 from "path";
1347
1405
  import chokidar from "chokidar";
1348
1406
  var DEBOUNCE = 120;
1349
1407
  var IGNORED = /(^|[/\\])(node_modules|\.git)([/\\]|$)/;
@@ -1371,7 +1429,7 @@ function watchProject(root, paths, onChange) {
1371
1429
  const unchanged = updateFingerprint(fingerprints, file, event);
1372
1430
  if (!scanned || unchanged) return;
1373
1431
  if (event !== "change") structural = true;
1374
- changed.add(path3.relative(root, file).split(path3.sep).join("/"));
1432
+ changed.add(path4.relative(root, file).split(path4.sep).join("/"));
1375
1433
  if (timer) clearTimeout(timer);
1376
1434
  timer = setTimeout(flush, DEBOUNCE);
1377
1435
  });
@@ -1385,7 +1443,7 @@ function updateFingerprint(fingerprints, file, event) {
1385
1443
  if (event !== "add" && event !== "change" || !FINGERPRINTED.test(file)) return false;
1386
1444
  let next;
1387
1445
  try {
1388
- next = createHash("sha1").update(fs3.readFileSync(file)).digest("hex");
1446
+ next = createHash("sha1").update(fs4.readFileSync(file)).digest("hex");
1389
1447
  } catch {
1390
1448
  return false;
1391
1449
  }
@@ -1394,7 +1452,30 @@ function updateFingerprint(fingerprints, file, event) {
1394
1452
  return previous === next;
1395
1453
  }
1396
1454
 
1455
+ // src/server/extensions.ts
1456
+ function applyExtensions(base, extensions, context) {
1457
+ const webviewScripts = [...base.webviewScripts ?? []];
1458
+ const middlewares = [...base.middlewares ?? []];
1459
+ const transforms = base.wxmlTransform ? [base.wxmlTransform] : [];
1460
+ const entries = [];
1461
+ for (const extension of extensions) {
1462
+ let setup;
1463
+ try {
1464
+ setup = extension.setup(context);
1465
+ } catch (error) {
1466
+ throw new Error(`\u6269\u5C55 ${extension.name} \u521D\u59CB\u5316\u5931\u8D25\uFF1A${error.message}`);
1467
+ }
1468
+ webviewScripts.push(...setup.webviewScripts ?? []);
1469
+ middlewares.push(...setup.middlewares ?? []);
1470
+ if (setup.wxmlTransform) transforms.push(setup.wxmlTransform);
1471
+ if (setup.entry) entries.push({ name: extension.name, ...setup.entry });
1472
+ }
1473
+ const wxmlTransform = transforms.length === 0 ? void 0 : (code, file) => transforms.reduce((result, transform) => transform(result, file), code);
1474
+ return { webviewScripts, middlewares, wxmlTransform, entries };
1475
+ }
1476
+
1397
1477
  // src/server/DevServer.ts
1478
+ var RESERVED_PREFIXES = ["/__wxa__", "/__baselib__", "/__plugin__"];
1398
1479
  var DEFAULT_DEVICE = {
1399
1480
  model: "iPhone X",
1400
1481
  screenWidth: 375,
@@ -1416,7 +1497,12 @@ var DevServer = class {
1416
1497
  /** 运行时生成的临时文件(canvas 导出、拍照等),随服务器进程存在 */
1417
1498
  this.fileSessions = /* @__PURE__ */ new Map();
1418
1499
  this.logger = options.logger || console;
1419
- this.project = new MiniProgram(path4.resolve(options.projectPath));
1500
+ this.project = new MiniProgram(path5.resolve(options.projectPath));
1501
+ const extended = applyExtensions(options, options.extensions ?? [], { projectPath: this.project.projectPath, root: this.project.root });
1502
+ extended.middlewares.forEach(({ path: mountPath }) => assertMountPath(mountPath));
1503
+ this.webviewScripts = extended.webviewScripts;
1504
+ this.middlewares = extended.middlewares;
1505
+ this.extensionEntries = extended.entries;
1420
1506
  this.baseLib = resolveBaseLib({ version: options.baseLibVersion, cacheDir: options.baseLibCacheDir });
1421
1507
  if (!this.baseLib.compatible) {
1422
1508
  this.logger.warn(`[wxa-runner] \u57FA\u7840\u5E93 ${this.baseLib.version} \u9AD8\u4E8E\u5DF2\u9A8C\u8BC1\u7684\u7248\u672C\u8303\u56F4\uFF0C\u53EF\u80FD\u65E0\u6CD5\u6B63\u5E38\u8FD0\u884C`);
@@ -1428,7 +1514,7 @@ var DevServer = class {
1428
1514
  this.ticketSource = this.devtoolsLogin ? developerTicketSource(this.project.projectPath, this.logger) : null;
1429
1515
  this.plugins = new PluginStore(this.ticketSource && new WechatDevClient(this.project.appid, this.ticketSource), this.logger);
1430
1516
  this.device = { ...DEFAULT_DEVICE, ...options.device };
1431
- this.builder = new AppBuilder(this.project);
1517
+ this.builder = new AppBuilder(this.project, { wxmlTransform: extended.wxmlTransform });
1432
1518
  this.setupFile = resolveSetupFile(options.setup, this.project.projectPath);
1433
1519
  this.setupEnv = collectSetupEnv(process.env);
1434
1520
  this.app = this.createApp();
@@ -1441,7 +1527,7 @@ var DevServer = class {
1441
1527
  get networkUrls() {
1442
1528
  if (!this.lanAccess) return [];
1443
1529
  const { port } = new URL(this.address);
1444
- return Object.values(os.networkInterfaces()).flat().filter((net2) => net2 && net2.family === "IPv4" && !net2.internal).map((net2) => `http://${net2.address}:${port}`);
1530
+ return Object.values(os2.networkInterfaces()).flat().filter((net2) => net2 && net2.family === "IPv4" && !net2.internal).map((net2) => `http://${net2.address}:${port}`);
1445
1531
  }
1446
1532
  async start() {
1447
1533
  try {
@@ -1484,11 +1570,15 @@ ${error.message}`);
1484
1570
  await new Promise((resolve) => server.close(() => resolve()));
1485
1571
  }
1486
1572
  this.fileSessions.clear();
1573
+ this.builder.dispose();
1487
1574
  }
1488
1575
  createApp() {
1489
1576
  const app = express2();
1490
1577
  const root = this.project.root;
1491
1578
  const noStore = (res) => res.setHeader("Cache-Control", "no-store");
1579
+ for (const { path: mountPath, handler } of this.middlewares) {
1580
+ app.use(mountPath, (req, res, next) => handler(req, res, next));
1581
+ }
1492
1582
  app.get("/", (_req, res) => {
1493
1583
  noStore(res);
1494
1584
  res.type("html").send(hostHtml());
@@ -1543,7 +1633,8 @@ ${error.message}`);
1543
1633
  const pagePath = decodeURIComponent(req.params[0]);
1544
1634
  if (!build2.pages.includes(pagePath)) return next();
1545
1635
  const pluginPage = findPluginPage(build2, pagePath);
1546
- res.type("html").send(pluginPage ? pluginWebviewHtml(build2, pluginPage.plugin, pluginPage.innerPath, pagePath, Number(req.query.id)) : webviewHtml(build2, pagePath, Number(req.query.id), { hot: this.hotEnabled }));
1636
+ const scripts = this.webviewScripts;
1637
+ res.type("html").send(pluginPage ? pluginWebviewHtml(build2, pluginPage.plugin, pluginPage.innerPath, pagePath, Number(req.query.id), { scripts }) : webviewHtml(build2, pagePath, Number(req.query.id), { hot: this.hotEnabled, scripts }));
1547
1638
  }));
1548
1639
  app.get("/__wxa__/plugin/:provider/:version/:file(appservice.js|pageframe.js)", this.withBuild((build2, req, res, next) => {
1549
1640
  const { provider, version, file } = req.params;
@@ -1554,7 +1645,7 @@ ${error.message}`);
1554
1645
  app.get(/^\/__plugin__\/([^/]+)(\/.+)$/, this.withBuild((build2, req, res, next) => {
1555
1646
  const file = build2.plugins.find((plugin) => plugin.provider === req.params[0])?.files.get(decodeURIComponent(req.params[1]));
1556
1647
  if (!file) return next();
1557
- res.type(path4.extname(req.params[1]) || "application/octet-stream").send(file);
1648
+ res.type(path5.extname(req.params[1]) || "application/octet-stream").send(file);
1558
1649
  }));
1559
1650
  app.get(/^\/__wxa__\/module\/(.+\.js)$/, this.withBuild((build2, req, res) => {
1560
1651
  const id = decodeURIComponent(req.params[0]);
@@ -1565,7 +1656,7 @@ ${error.message}`);
1565
1656
  res.type("js").send(this.builder.getModuleScript(id));
1566
1657
  }));
1567
1658
  app.get("/__wxa__/worker.js", this.withBuild((build2, _req, res) => {
1568
- const hasWorker = fs4.existsSync(path4.join(this.baseLib.dir, "WAWorker.js"));
1659
+ const hasWorker = fs5.existsSync(path5.join(this.baseLib.dir, "WAWorker.js"));
1569
1660
  res.type("js").send(workerScript(build2, { version: this.baseLib.version, hasWorker }));
1570
1661
  }));
1571
1662
  app.get("/__wxa__/service-bootstrap.js", this.withBuild((build2, _req, res) => {
@@ -1584,28 +1675,28 @@ ${error.message}`);
1584
1675
  res.type("js").send(build2.pageFrame);
1585
1676
  }));
1586
1677
  app.get("/__wxa__/runtime/frame.js.map", (_req, res, next) => {
1587
- const file = path4.join(this.runtimeDir(), "frame.js.map");
1588
- if (!fs4.existsSync(file)) return next();
1589
- res.type("json").send(ignoreConsoleHook(fs4.readFileSync(file, "utf8")));
1678
+ const file = path5.join(this.runtimeDir(), "frame.js.map");
1679
+ if (!fs5.existsSync(file)) return next();
1680
+ res.type("json").send(ignoreConsoleHook(fs5.readFileSync(file, "utf8")));
1590
1681
  });
1591
1682
  app.use("/__wxa__/runtime", express2.static(this.runtimeDir(), {
1592
1683
  cacheControl: false,
1593
1684
  // Service Worker 默认只能接管自己所在目录下的页面,宿主页面在根路径
1594
1685
  setHeaders: (res, file) => {
1595
- if (path4.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1686
+ if (path5.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1596
1687
  }
1597
1688
  }));
1598
1689
  const sourceMaps = /* @__PURE__ */ new Map();
1599
1690
  app.get(/^\/__baselib__\/(.+\.js)\.map$/, (req, res, next) => {
1600
- const file = path4.resolve(this.baseLib.dir, req.params[0]);
1601
- if (!file.startsWith(this.baseLib.dir + path4.sep) || !fs4.existsSync(file)) return next();
1602
- if (!sourceMaps.has(file)) sourceMaps.set(file, ignoreListSourceMap(fs4.readFileSync(file, "utf8"), `${path4.basename(file)}?original`));
1691
+ const file = path5.resolve(this.baseLib.dir, req.params[0]);
1692
+ if (!file.startsWith(this.baseLib.dir + path5.sep) || !fs5.existsSync(file)) return next();
1693
+ if (!sourceMaps.has(file)) sourceMaps.set(file, ignoreListSourceMap(fs5.readFileSync(file, "utf8"), `${path5.basename(file)}?original`));
1603
1694
  res.type("json").send(sourceMaps.get(file));
1604
1695
  });
1605
1696
  app.use("/__baselib__", express2.static(this.baseLib.dir, {
1606
1697
  maxAge: "1d",
1607
1698
  setHeaders: (res, file) => {
1608
- if (file.endsWith(".js")) res.setHeader("SourceMap", `${path4.basename(file)}.map`);
1699
+ if (file.endsWith(".js")) res.setHeader("SourceMap", `${path5.basename(file)}.map`);
1609
1700
  }
1610
1701
  }));
1611
1702
  app.use(PROXY_PREFIX, express2.raw({ type: () => true, limit: "100mb" }), handleProxy);
@@ -1665,7 +1756,7 @@ ${error.message}`);
1665
1756
  return;
1666
1757
  }
1667
1758
  const data = node.data;
1668
- res.type(path4.extname(req.params[0]));
1759
+ res.type(path5.extname(req.params[0]));
1669
1760
  res.setHeader("ETag", `"${node.etag}"`);
1670
1761
  res.setHeader("Accept-Ranges", "bytes");
1671
1762
  const ranges = req.range(data.length);
@@ -1735,11 +1826,11 @@ ${error.message}`);
1735
1826
  runtimeDir() {
1736
1827
  const candidates = [
1737
1828
  this.options.runtimeDir,
1738
- path4.join(__dirname, "runtime"),
1739
- path4.join(__dirname, "../runtime"),
1740
- path4.join(__dirname, "../../dist/runtime")
1829
+ path5.join(__dirname, "runtime"),
1830
+ path5.join(__dirname, "../runtime"),
1831
+ path5.join(__dirname, "../../dist/runtime")
1741
1832
  ].filter((dir) => !!dir);
1742
- const found = candidates.find((dir) => fs4.existsSync(path4.join(dir, "host.js")));
1833
+ const found = candidates.find((dir) => fs5.existsSync(path5.join(dir, "host.js")));
1743
1834
  if (!found) {
1744
1835
  throw new Error("\u627E\u4E0D\u5230\u6D4F\u89C8\u5668\u7AEF\u8FD0\u884C\u65F6\uFF08host.js\uFF09\uFF0C\u8BF7\u5148\u6267\u884C pnpm build");
1745
1836
  }
@@ -1752,7 +1843,7 @@ ${error.message}`);
1752
1843
  startWatching() {
1753
1844
  let lastBuildFailed = false;
1754
1845
  const watched = [this.project.root];
1755
- if (this.setupFile && path4.relative(this.project.root, this.setupFile).startsWith("..")) watched.push(this.setupFile);
1846
+ if (this.setupFile && path5.relative(this.project.root, this.setupFile).startsWith("..")) watched.push(this.setupFile);
1756
1847
  this.watcher = watchProject(this.project.root, watched, async ({ files, structural }) => {
1757
1848
  const reloadOnly = structural || lastBuildFailed;
1758
1849
  this.builder.invalidate(files);
@@ -1783,7 +1874,7 @@ var DEFAULT_PORT = 3e3;
1783
1874
  var LOCAL_HOST_OF_ANY_ADDRESS = /* @__PURE__ */ new Map([["0.0.0.0", "127.0.0.1"], ["::", "localhost"]]);
1784
1875
  async function listen(server, port, host, logger) {
1785
1876
  for (let candidate = port ?? DEFAULT_PORT; ; candidate++) {
1786
- const taken = LOCAL_HOST_OF_ANY_ADDRESS.has(host) && await takenOnLoopback(candidate) || !await bind(server, candidate, host);
1877
+ const taken = await takenOnLocalAddresses(candidate) || !await bind(server, candidate, host);
1787
1878
  if (!taken) {
1788
1879
  const actual = server.address().port;
1789
1880
  if (port === void 0 && actual !== DEFAULT_PORT) logger.warn(`[wxa-runner] \u7AEF\u53E3 ${DEFAULT_PORT} \u5DF2\u88AB\u5360\u7528\uFF0C\u6539\u7528 ${actual}`);
@@ -1802,8 +1893,8 @@ function bind(server, port, host) {
1802
1893
  });
1803
1894
  });
1804
1895
  }
1805
- async function takenOnLoopback(port) {
1806
- for (const address of ["127.0.0.1", "::1"]) {
1896
+ async function takenOnLocalAddresses(port) {
1897
+ for (const address of ["127.0.0.1", "::1", "0.0.0.0", "::"]) {
1807
1898
  const probe = net.createServer();
1808
1899
  const free = await bind(probe, port, address).catch(() => true);
1809
1900
  if (!free) return true;
@@ -1811,11 +1902,21 @@ async function takenOnLoopback(port) {
1811
1902
  }
1812
1903
  return false;
1813
1904
  }
1905
+ function assertMountPath(mountPath) {
1906
+ const literal = typeof mountPath === "string" && /^(\/[\w.~-]+)+$/.test(mountPath);
1907
+ const lower = literal ? mountPath.toLowerCase() : "";
1908
+ const reserved = RESERVED_PREFIXES.some((prefix) => lower === prefix || lower.startsWith(prefix + "/"));
1909
+ if (!literal || reserved) {
1910
+ throw new Error(
1911
+ `\u4E2D\u95F4\u4EF6\u8DEF\u5F84 ${String(mountPath)} \u4E0D\u53EF\u7528\uFF1A\u8981\u4EE5 / \u5F00\u5934\uFF0C\u53EA\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u548C _ . ~ -\uFF08\u4E0D\u652F\u6301\u901A\u914D\u7B26\u548C\u53C2\u6570\uFF09\uFF0C\u4E0D\u80FD\u662F /\uFF0C\u4E5F\u4E0D\u80FD\u7528 wxa-runner \u4FDD\u7559\u7684\u524D\u7F00 ${RESERVED_PREFIXES.join("\u3001")}\uFF08\u4E0D\u533A\u5206\u5927\u5C0F\u5199\uFF09`
1912
+ );
1913
+ }
1914
+ }
1814
1915
  function resolveSetupFile(option, projectPath) {
1815
1916
  if (option === false) return null;
1816
1917
  if (!option) return findSetupFile(projectPath);
1817
- const file = path4.resolve(option);
1818
- if (!fs4.existsSync(file)) throw new Error(`\u627E\u4E0D\u5230 setup \u6587\u4EF6\uFF1A${file}`);
1918
+ const file = path5.resolve(option);
1919
+ if (!fs5.existsSync(file)) throw new Error(`\u627E\u4E0D\u5230 setup \u6587\u4EF6\uFF1A${file}`);
1819
1920
  return file;
1820
1921
  }
1821
1922
  export {