@open-wxa/wxa-runner-core 0.8.2 → 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.js CHANGED
@@ -164,11 +164,11 @@ var FileStore = class {
164
164
  };
165
165
 
166
166
  // src/server/DevServer.ts
167
- var import_fs4 = __toESM(require("fs"));
167
+ var import_fs5 = __toESM(require("fs"));
168
168
  var import_http = __toESM(require("http"));
169
169
  var import_net = __toESM(require("net"));
170
- var import_os = __toESM(require("os"));
171
- var import_path4 = __toESM(require("path"));
170
+ var import_os2 = __toESM(require("os"));
171
+ var import_path5 = __toESM(require("path"));
172
172
  var import_express2 = __toESM(require("express"));
173
173
  var import_ws2 = require("ws");
174
174
  var import_wxa_runner_extractor5 = require("@open-wxa/wxa-runner-extractor");
@@ -233,6 +233,50 @@ function createWxConfig(input) {
233
233
  };
234
234
  }
235
235
 
236
+ // src/project/WxmlMirror.ts
237
+ var import_fs = __toESM(require("fs"));
238
+ var import_os = __toESM(require("os"));
239
+ var import_path = __toESM(require("path"));
240
+ var WxmlMirror = class {
241
+ constructor(transform) {
242
+ this.transform = transform;
243
+ this.dir = null;
244
+ /** 已写入的文件:源内容(判断要不要重新转换)与写入的内容 */
245
+ this.files = /* @__PURE__ */ new Map();
246
+ }
247
+ /** 镜像目录;还没同步过时为 null */
248
+ get directory() {
249
+ return this.dir;
250
+ }
251
+ /**
252
+ * 让镜像目录与项目里的 wxml / wxs 一致,返回镜像目录。
253
+ * 只重写源内容变化的文件;项目里已删除的文件同时从镜像目录删除,被引用时 wcc 照常报错
254
+ */
255
+ sync(project, relPaths) {
256
+ const dir = this.dir ??= import_fs.default.mkdtempSync(import_path.default.join(import_os.default.tmpdir(), "wxa-runner-wxml-"));
257
+ const current = new Set(relPaths);
258
+ for (const rel of [...this.files.keys()]) {
259
+ if (current.has(rel)) continue;
260
+ import_fs.default.rmSync(import_path.default.join(dir, rel), { force: true });
261
+ this.files.delete(rel);
262
+ }
263
+ for (const rel of relPaths) {
264
+ const source = project.readText(rel);
265
+ if (this.files.get(rel) === source) continue;
266
+ const target = import_path.default.join(dir, rel);
267
+ import_fs.default.mkdirSync(import_path.default.dirname(target), { recursive: true });
268
+ import_fs.default.writeFileSync(target, rel.endsWith(".wxml") ? this.transform(source, rel) : source);
269
+ this.files.set(rel, source);
270
+ }
271
+ return dir;
272
+ }
273
+ dispose() {
274
+ if (this.dir) import_fs.default.rmSync(this.dir, { recursive: true, force: true });
275
+ this.dir = null;
276
+ this.files.clear();
277
+ }
278
+ };
279
+
236
280
  // src/project/AppBuilder.ts
237
281
  var MODULE_PARAMS = [
238
282
  "require",
@@ -259,7 +303,7 @@ var MODULE_PARAMS = [
259
303
  ].join(", ");
260
304
  var ESM_SYNTAX = /(?:^|[;}])\s*(?:import\s*[{*'"]|import\s+[\w$]|export\s*[{*]|export\s+(?:default|const|let|var|function|class|async)\b)/m;
261
305
  var AppBuilder = class {
262
- constructor(project) {
306
+ constructor(project, options = {}) {
263
307
  this.project = project;
264
308
  this.version = 0;
265
309
  /** invalidate 标记过的变化次数;每次构建记下开始时覆盖到第几次 */
@@ -269,6 +313,15 @@ var AppBuilder = class {
269
313
  this.dirty = /* @__PURE__ */ new Set(["config", "wxml", "wxss", "js"]);
270
314
  this.wxmlCache = null;
271
315
  this.wxssCache = null;
316
+ this.wxmlMirror = options.wxmlTransform ? new WxmlMirror(options.wxmlTransform) : null;
317
+ }
318
+ /** wxmlTransform 的镜像目录;没有设置转换或还没编译过时为 null */
319
+ get wxmlMirrorDir() {
320
+ return this.wxmlMirror?.directory ?? null;
321
+ }
322
+ /** 删除 wxmlTransform 的镜像目录,开发服务器关闭时调用 */
323
+ dispose() {
324
+ this.wxmlMirror?.dispose();
272
325
  }
273
326
  /** 根据变化的文件标记需要重新构建的部分;返回 true 表示需要刷新 */
274
327
  invalidate(changedFiles = []) {
@@ -393,7 +446,7 @@ ${pageStyles}`,
393
446
  const json = unitJson.get(id);
394
447
  return { path: id, tags: Object.keys({ ...json.usingComponents, ...json.componentGenerics }) };
395
448
  });
396
- const cwd = this.project.root;
449
+ const cwd = this.wxmlMirror ? this.wxmlMirror.sync(this.project, files.map((file) => file.slice(2))) : this.project.root;
397
450
  const [full, cut] = await Promise.all([
398
451
  (0, import_wxa_runner_compiler.compileWxml)({ cwd, files, components, cut: false }),
399
452
  (0, import_wxa_runner_compiler.compileWxml)({ cwd, files, components, cut: true })
@@ -460,22 +513,22 @@ function classifyFile(file) {
460
513
  }
461
514
 
462
515
  // src/project/MiniProgram.ts
463
- var import_fs = __toESM(require("fs"));
464
- var import_path = __toESM(require("path"));
516
+ var import_fs2 = __toESM(require("fs"));
517
+ var import_path2 = __toESM(require("path"));
465
518
  var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".idea", ".vscode"]);
466
519
  var PROJECT_FILES = /* @__PURE__ */ new Set(["project.config.json", "project.private.config.json", "sitemap.json", "app.json", "wxa.setup.js"]);
467
520
  var MiniProgram = class {
468
521
  constructor(projectPath) {
469
522
  this.projectPath = projectPath;
470
- const projectConfig = readJsonFile(import_path.default.join(projectPath, "project.config.json")) || {};
471
- this.root = import_path.default.resolve(projectPath, projectConfig.miniprogramRoot || "");
523
+ const projectConfig = readJsonFile(import_path2.default.join(projectPath, "project.config.json")) || {};
524
+ this.root = import_path2.default.resolve(projectPath, projectConfig.miniprogramRoot || "");
472
525
  this.appid = projectConfig.appid || "touristappid";
473
- if (!import_fs.default.existsSync(import_path.default.join(this.root, "app.json"))) {
526
+ if (!import_fs2.default.existsSync(import_path2.default.join(this.root, "app.json"))) {
474
527
  throw new Error(`${this.root} \u4E0B\u6CA1\u6709 app.json\uFF0C\u4E0D\u662F\u6709\u6548\u7684\u5C0F\u7A0B\u5E8F\u76EE\u5F55`);
475
528
  }
476
529
  }
477
530
  readAppJson() {
478
- const app = readJsonFile(import_path.default.join(this.root, "app.json"));
531
+ const app = readJsonFile(import_path2.default.join(this.root, "app.json"));
479
532
  if (!app || !Array.isArray(app.pages) || app.pages.length === 0) {
480
533
  throw new Error("app.json \u7F3A\u5C11 pages \u914D\u7F6E");
481
534
  }
@@ -487,14 +540,14 @@ var MiniProgram = class {
487
540
  /** Worker 代码所在目录,不带首尾的 /;没有配置 workers 时为 null */
488
541
  workersDir(app) {
489
542
  const dir = typeof app.workers === "string" ? app.workers : app.workers?.path;
490
- return dir ? import_path.default.posix.normalize(dir).replace(/^\/+|\/+$/g, "") : null;
543
+ return dir ? import_path2.default.posix.normalize(dir).replace(/^\/+|\/+$/g, "") : null;
491
544
  }
492
545
  /** 主包页面 + 分包页面(带 root 前缀) */
493
546
  allPages(app) {
494
547
  const pages = [...app.pages];
495
548
  for (const pkg of this.subPackages(app)) {
496
549
  for (const page of pkg.pages) {
497
- const full = import_path.default.posix.join(pkg.root, page);
550
+ const full = import_path2.default.posix.join(pkg.root, page);
498
551
  if (!pages.includes(full)) pages.push(full);
499
552
  }
500
553
  }
@@ -506,7 +559,7 @@ var MiniProgram = class {
506
559
  * 所以能解析到的引用都换成组件的绝对路径
507
560
  */
508
561
  readUnitJson(id, app) {
509
- const json = { ...readJsonFile(import_path.default.join(this.root, id + ".json")) };
562
+ const json = { ...readJsonFile(import_path2.default.join(this.root, id + ".json")) };
510
563
  const resolve = (fromId, ref) => {
511
564
  const resolved = this.resolveComponent(fromId, ref);
512
565
  return resolved ? "/" + resolved : null;
@@ -527,7 +580,7 @@ var MiniProgram = class {
527
580
  if (resolved || ref.startsWith("/") || /^plugin(-private)?:\/\//.test(ref)) {
528
581
  json.usingComponents[tag] = resolved ?? ref;
529
582
  } else {
530
- const relative = import_path.default.posix.relative(import_path.default.posix.dirname(id), ref);
583
+ const relative = import_path2.default.posix.relative(import_path2.default.posix.dirname(id), ref);
531
584
  json.usingComponents[tag] = relative.startsWith(".") ? relative : "./" + relative;
532
585
  }
533
586
  }
@@ -540,25 +593,25 @@ var MiniProgram = class {
540
593
  */
541
594
  resolveComponent(fromId, ref) {
542
595
  if (!ref || ref.startsWith("plugin://") || ref.startsWith("plugin-private://")) return null;
543
- const fromDir = import_path.default.posix.dirname(fromId);
596
+ const fromDir = import_path2.default.posix.dirname(fromId);
544
597
  const candidates = [];
545
598
  if (ref.startsWith("/")) {
546
599
  candidates.push(ref.slice(1));
547
600
  } else {
548
- candidates.push(import_path.default.posix.join(fromDir, ref));
601
+ candidates.push(import_path2.default.posix.join(fromDir, ref));
549
602
  if (!ref.startsWith(".")) {
550
603
  let dir = fromDir;
551
604
  for (; ; ) {
552
- candidates.push(import_path.default.posix.join(dir === "." ? "" : dir, "miniprogram_npm", ref));
605
+ candidates.push(import_path2.default.posix.join(dir === "." ? "" : dir, "miniprogram_npm", ref));
553
606
  if (dir === "." || dir === "") break;
554
- dir = import_path.default.posix.dirname(dir);
607
+ dir = import_path2.default.posix.dirname(dir);
555
608
  }
556
609
  }
557
610
  }
558
611
  for (const candidate of candidates) {
559
- const normalized = import_path.default.posix.normalize(candidate);
560
- for (const id of [normalized, import_path.default.posix.join(normalized, "index")]) {
561
- if (import_fs.default.existsSync(import_path.default.join(this.root, id + ".json"))) return id;
612
+ const normalized = import_path2.default.posix.normalize(candidate);
613
+ for (const id of [normalized, import_path2.default.posix.join(normalized, "index")]) {
614
+ if (import_fs2.default.existsSync(import_path2.default.join(this.root, id + ".json"))) return id;
562
615
  }
563
616
  }
564
617
  return null;
@@ -590,11 +643,11 @@ var MiniProgram = class {
590
643
  listFiles(extensions) {
591
644
  const result = [];
592
645
  const walk = (dir, rel) => {
593
- for (const entry of import_fs.default.readdirSync(dir, { withFileTypes: true })) {
646
+ for (const entry of import_fs2.default.readdirSync(dir, { withFileTypes: true })) {
594
647
  if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
595
648
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
596
649
  if (entry.isDirectory()) {
597
- walk(import_path.default.join(dir, entry.name), childRel);
650
+ walk(import_path2.default.join(dir, entry.name), childRel);
598
651
  } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
599
652
  if (!rel && PROJECT_FILES.has(entry.name)) continue;
600
653
  result.push(childRel);
@@ -605,18 +658,18 @@ var MiniProgram = class {
605
658
  return result.sort();
606
659
  }
607
660
  exists(relPath) {
608
- return import_fs.default.existsSync(import_path.default.join(this.root, relPath));
661
+ return import_fs2.default.existsSync(import_path2.default.join(this.root, relPath));
609
662
  }
610
663
  readText(relPath) {
611
- return import_fs.default.readFileSync(import_path.default.join(this.root, relPath), "utf8");
664
+ return import_fs2.default.readFileSync(import_path2.default.join(this.root, relPath), "utf8");
612
665
  }
613
666
  isInIndependentSubPackage(id, app) {
614
667
  return this.subPackages(app).some((pkg) => pkg.independent && id.startsWith(pkg.root.replace(/\/?$/, "/")));
615
668
  }
616
669
  };
617
670
  function readJsonFile(file) {
618
- if (!import_fs.default.existsSync(file)) return null;
619
- const text = import_fs.default.readFileSync(file, "utf8");
671
+ if (!import_fs2.default.existsSync(file)) return null;
672
+ const text = import_fs2.default.readFileSync(file, "utf8");
620
673
  try {
621
674
  return JSON.parse(text);
622
675
  } catch (error) {
@@ -682,9 +735,9 @@ var WechatDevClient = class {
682
735
  *
683
736
  * @throws 没有可用的登录态、超时、连不上、HTTP 状态不是 2xx 时,消息是给用户看的原因
684
737
  */
685
- async post(path5, body, timeout = DEFAULT_TIMEOUT) {
738
+ async post(path6, body, timeout = DEFAULT_TIMEOUT) {
686
739
  const query = new URLSearchParams({ appid: this.appid, newticket: requireUsableTicket(await this.findTicket()), clientversion: CLIENT_VERSION });
687
- const response = await fetch(`${BASE_URL}/${path5}?${query}`, {
740
+ const response = await fetch(`${BASE_URL}/${path6}?${query}`, {
688
741
  method: "POST",
689
742
  headers: { "Content-Type": "application/json" },
690
743
  body: JSON.stringify(body),
@@ -696,8 +749,8 @@ var WechatDevClient = class {
696
749
  return response;
697
750
  }
698
751
  /** 同 post,返回解析后的 JSON */
699
- async postJson(path5, body) {
700
- return (await this.post(path5, body)).json();
752
+ async postJson(path6, body) {
753
+ return (await this.post(path6, body)).json();
701
754
  }
702
755
  };
703
756
 
@@ -893,12 +946,12 @@ function webviewHtml(build2, pagePath, webviewId, options) {
893
946
  return `<!DOCTYPE html>
894
947
  <html>
895
948
  <head>
896
- ${webviewHead(build2, pagePath, id)}
949
+ ${webviewHead(build2, pagePath, id, options.scripts)}
897
950
  ${enableHotUpdate(options.hot)}<script src="/__wxa__/page-frame.js?v=${build2.version}"></script>
898
951
  ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</style>" : ""}
899
952
  </head>
900
953
  <body>
901
- <script>
954
+ ${scriptTags(options.scripts, "body")}<script>
902
955
  (function () {
903
956
  var path = ${toJs(pagePath)};
904
957
  if (typeof __wxAppCode__[path + '.wxss'] === 'function') __wxAppCode__[path + '.wxss']();
@@ -931,18 +984,18 @@ self.console = __wxaConsole;
931
984
  importScripts(${modules.map((url) => toJs(url)).join(", ")});
932
985
  `;
933
986
  }
934
- function pluginWebviewHtml(build2, plugin, innerPath, pagePath, webviewId) {
987
+ function pluginWebviewHtml(build2, plugin, innerPath, pagePath, webviewId, options = {}) {
935
988
  const id = Number(webviewId) || 0;
936
989
  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>`;
937
990
  return `<!DOCTYPE html>
938
991
  <html>
939
992
  <head>
940
- ${webviewHead(build2, pagePath, id)}
993
+ ${webviewHead(build2, pagePath, id, options.scripts)}
941
994
  <script src="${pluginScriptUrl(plugin, "pageframe.js")}"></script>
942
995
  ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</style>" : ""}
943
996
  </head>
944
997
  <body>
945
- ${page}
998
+ ${scriptTags(options.scripts, "body")}${page}
946
999
  <script>parent.__wxaHost.frameLoaded('webview', ${id});</script>
947
1000
  </body>
948
1001
  </html>`;
@@ -950,7 +1003,7 @@ ${page}
950
1003
  function pageWindow(build2, pagePath) {
951
1004
  return { ...build2.wxConfig.global?.window, ...build2.wxConfig.page?.[pagePath + ".html"]?.window };
952
1005
  }
953
- function webviewHead(build2, pagePath, id) {
1006
+ function webviewHead(build2, pagePath, id, scripts) {
954
1007
  const wxConfig = { ...build2.wxConfig, window: pageWindow(build2, pagePath) };
955
1008
  return `<meta charset="utf-8">
956
1009
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
@@ -965,7 +1018,12 @@ function webviewHead(build2, pagePath, id) {
965
1018
  <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>
966
1019
  <script>var __wxConfig = ${toJs(wxConfig)};</script>
967
1020
  <script src="/__wxa__/runtime/frame.js" data-kind="webview" data-webview-id="${id}"></script>
968
- <script src="/__baselib__/WAWebview.js"></script>`;
1021
+ ${scriptTags(scripts, "head")}<script src="/__baselib__/WAWebview.js"></script>`;
1022
+ }
1023
+ function scriptTags(scripts, position) {
1024
+ return (scripts ?? []).filter((script) => (script.position ?? "body") === position).map((script) => script.src !== void 0 ? `<script src="${escapeHtml(script.src)}"></script>
1025
+ ` : `<script>${script.content.replace(/<\/script/gi, "<\\/script")}</script>
1026
+ `).join("");
969
1027
  }
970
1028
  function escapeHtml(text) {
971
1029
  return text.replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
@@ -1329,16 +1387,16 @@ function respond(action) {
1329
1387
  }
1330
1388
 
1331
1389
  // src/server/setup.ts
1332
- var import_fs2 = __toESM(require("fs"));
1333
- var import_path2 = __toESM(require("path"));
1390
+ var import_fs3 = __toESM(require("fs"));
1391
+ var import_path3 = __toESM(require("path"));
1334
1392
  var import_esbuild2 = require("esbuild");
1335
1393
  var SETUP_FILE_NAMES = ["wxa.setup.ts", "wxa.setup.js", "wxa.setup.mjs"];
1336
1394
  var SETUP_MODULE = "@open-wxa/wxa-runner-core/setup";
1337
1395
  var ENV_PREFIX = "WXA_";
1338
1396
  function findSetupFile(projectPath) {
1339
1397
  for (const name of SETUP_FILE_NAMES) {
1340
- const file = import_path2.default.join(projectPath, name);
1341
- if (import_fs2.default.existsSync(file)) return file;
1398
+ const file = import_path3.default.join(projectPath, name);
1399
+ if (import_fs3.default.existsSync(file)) return file;
1342
1400
  }
1343
1401
  return null;
1344
1402
  }
@@ -1350,7 +1408,7 @@ function collectSetupEnv(env) {
1350
1408
  async function bundleSetup(file) {
1351
1409
  const result = await (0, import_esbuild2.build)({
1352
1410
  entryPoints: [file],
1353
- absWorkingDir: import_path2.default.dirname(file),
1411
+ absWorkingDir: import_path3.default.dirname(file),
1354
1412
  bundle: true,
1355
1413
  format: "esm",
1356
1414
  platform: "browser",
@@ -1378,8 +1436,8 @@ function builtinSetupModule() {
1378
1436
 
1379
1437
  // src/server/watcher.ts
1380
1438
  var import_crypto3 = require("crypto");
1381
- var import_fs3 = __toESM(require("fs"));
1382
- var import_path3 = __toESM(require("path"));
1439
+ var import_fs4 = __toESM(require("fs"));
1440
+ var import_path4 = __toESM(require("path"));
1383
1441
  var import_chokidar = __toESM(require("chokidar"));
1384
1442
  var DEBOUNCE = 120;
1385
1443
  var IGNORED = /(^|[/\\])(node_modules|\.git)([/\\]|$)/;
@@ -1407,7 +1465,7 @@ function watchProject(root, paths, onChange) {
1407
1465
  const unchanged = updateFingerprint(fingerprints, file, event);
1408
1466
  if (!scanned || unchanged) return;
1409
1467
  if (event !== "change") structural = true;
1410
- changed.add(import_path3.default.relative(root, file).split(import_path3.default.sep).join("/"));
1468
+ changed.add(import_path4.default.relative(root, file).split(import_path4.default.sep).join("/"));
1411
1469
  if (timer) clearTimeout(timer);
1412
1470
  timer = setTimeout(flush, DEBOUNCE);
1413
1471
  });
@@ -1421,7 +1479,7 @@ function updateFingerprint(fingerprints, file, event) {
1421
1479
  if (event !== "add" && event !== "change" || !FINGERPRINTED.test(file)) return false;
1422
1480
  let next;
1423
1481
  try {
1424
- next = (0, import_crypto3.createHash)("sha1").update(import_fs3.default.readFileSync(file)).digest("hex");
1482
+ next = (0, import_crypto3.createHash)("sha1").update(import_fs4.default.readFileSync(file)).digest("hex");
1425
1483
  } catch {
1426
1484
  return false;
1427
1485
  }
@@ -1430,7 +1488,30 @@ function updateFingerprint(fingerprints, file, event) {
1430
1488
  return previous === next;
1431
1489
  }
1432
1490
 
1491
+ // src/server/extensions.ts
1492
+ function applyExtensions(base, extensions, context) {
1493
+ const webviewScripts = [...base.webviewScripts ?? []];
1494
+ const middlewares = [...base.middlewares ?? []];
1495
+ const transforms = base.wxmlTransform ? [base.wxmlTransform] : [];
1496
+ const entries = [];
1497
+ for (const extension of extensions) {
1498
+ let setup;
1499
+ try {
1500
+ setup = extension.setup(context);
1501
+ } catch (error) {
1502
+ throw new Error(`\u6269\u5C55 ${extension.name} \u521D\u59CB\u5316\u5931\u8D25\uFF1A${error.message}`);
1503
+ }
1504
+ webviewScripts.push(...setup.webviewScripts ?? []);
1505
+ middlewares.push(...setup.middlewares ?? []);
1506
+ if (setup.wxmlTransform) transforms.push(setup.wxmlTransform);
1507
+ if (setup.entry) entries.push({ name: extension.name, ...setup.entry });
1508
+ }
1509
+ const wxmlTransform = transforms.length === 0 ? void 0 : (code, file) => transforms.reduce((result, transform) => transform(result, file), code);
1510
+ return { webviewScripts, middlewares, wxmlTransform, entries };
1511
+ }
1512
+
1433
1513
  // src/server/DevServer.ts
1514
+ var RESERVED_PREFIXES = ["/__wxa__", "/__baselib__", "/__plugin__"];
1434
1515
  var DEFAULT_DEVICE = {
1435
1516
  model: "iPhone X",
1436
1517
  screenWidth: 375,
@@ -1452,7 +1533,12 @@ var DevServer = class {
1452
1533
  /** 运行时生成的临时文件(canvas 导出、拍照等),随服务器进程存在 */
1453
1534
  this.fileSessions = /* @__PURE__ */ new Map();
1454
1535
  this.logger = options.logger || console;
1455
- this.project = new MiniProgram(import_path4.default.resolve(options.projectPath));
1536
+ this.project = new MiniProgram(import_path5.default.resolve(options.projectPath));
1537
+ const extended = applyExtensions(options, options.extensions ?? [], { projectPath: this.project.projectPath, root: this.project.root });
1538
+ extended.middlewares.forEach(({ path: mountPath }) => assertMountPath(mountPath));
1539
+ this.webviewScripts = extended.webviewScripts;
1540
+ this.middlewares = extended.middlewares;
1541
+ this.extensionEntries = extended.entries;
1456
1542
  this.baseLib = (0, import_wxa_runner_extractor5.resolveBaseLib)({ version: options.baseLibVersion, cacheDir: options.baseLibCacheDir });
1457
1543
  if (!this.baseLib.compatible) {
1458
1544
  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`);
@@ -1464,7 +1550,7 @@ var DevServer = class {
1464
1550
  this.ticketSource = this.devtoolsLogin ? developerTicketSource(this.project.projectPath, this.logger) : null;
1465
1551
  this.plugins = new PluginStore(this.ticketSource && new WechatDevClient(this.project.appid, this.ticketSource), this.logger);
1466
1552
  this.device = { ...DEFAULT_DEVICE, ...options.device };
1467
- this.builder = new AppBuilder(this.project);
1553
+ this.builder = new AppBuilder(this.project, { wxmlTransform: extended.wxmlTransform });
1468
1554
  this.setupFile = resolveSetupFile(options.setup, this.project.projectPath);
1469
1555
  this.setupEnv = collectSetupEnv(process.env);
1470
1556
  this.app = this.createApp();
@@ -1477,7 +1563,7 @@ var DevServer = class {
1477
1563
  get networkUrls() {
1478
1564
  if (!this.lanAccess) return [];
1479
1565
  const { port } = new URL(this.address);
1480
- return Object.values(import_os.default.networkInterfaces()).flat().filter((net2) => net2 && net2.family === "IPv4" && !net2.internal).map((net2) => `http://${net2.address}:${port}`);
1566
+ return Object.values(import_os2.default.networkInterfaces()).flat().filter((net2) => net2 && net2.family === "IPv4" && !net2.internal).map((net2) => `http://${net2.address}:${port}`);
1481
1567
  }
1482
1568
  async start() {
1483
1569
  try {
@@ -1520,11 +1606,15 @@ ${error.message}`);
1520
1606
  await new Promise((resolve) => server.close(() => resolve()));
1521
1607
  }
1522
1608
  this.fileSessions.clear();
1609
+ this.builder.dispose();
1523
1610
  }
1524
1611
  createApp() {
1525
1612
  const app = (0, import_express2.default)();
1526
1613
  const root = this.project.root;
1527
1614
  const noStore = (res) => res.setHeader("Cache-Control", "no-store");
1615
+ for (const { path: mountPath, handler } of this.middlewares) {
1616
+ app.use(mountPath, (req, res, next) => handler(req, res, next));
1617
+ }
1528
1618
  app.get("/", (_req, res) => {
1529
1619
  noStore(res);
1530
1620
  res.type("html").send(hostHtml());
@@ -1579,7 +1669,8 @@ ${error.message}`);
1579
1669
  const pagePath = decodeURIComponent(req.params[0]);
1580
1670
  if (!build2.pages.includes(pagePath)) return next();
1581
1671
  const pluginPage = findPluginPage(build2, pagePath);
1582
- 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 }));
1672
+ const scripts = this.webviewScripts;
1673
+ 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 }));
1583
1674
  }));
1584
1675
  app.get("/__wxa__/plugin/:provider/:version/:file(appservice.js|pageframe.js)", this.withBuild((build2, req, res, next) => {
1585
1676
  const { provider, version, file } = req.params;
@@ -1590,7 +1681,7 @@ ${error.message}`);
1590
1681
  app.get(/^\/__plugin__\/([^/]+)(\/.+)$/, this.withBuild((build2, req, res, next) => {
1591
1682
  const file = build2.plugins.find((plugin) => plugin.provider === req.params[0])?.files.get(decodeURIComponent(req.params[1]));
1592
1683
  if (!file) return next();
1593
- res.type(import_path4.default.extname(req.params[1]) || "application/octet-stream").send(file);
1684
+ res.type(import_path5.default.extname(req.params[1]) || "application/octet-stream").send(file);
1594
1685
  }));
1595
1686
  app.get(/^\/__wxa__\/module\/(.+\.js)$/, this.withBuild((build2, req, res) => {
1596
1687
  const id = decodeURIComponent(req.params[0]);
@@ -1601,7 +1692,7 @@ ${error.message}`);
1601
1692
  res.type("js").send(this.builder.getModuleScript(id));
1602
1693
  }));
1603
1694
  app.get("/__wxa__/worker.js", this.withBuild((build2, _req, res) => {
1604
- const hasWorker = import_fs4.default.existsSync(import_path4.default.join(this.baseLib.dir, "WAWorker.js"));
1695
+ const hasWorker = import_fs5.default.existsSync(import_path5.default.join(this.baseLib.dir, "WAWorker.js"));
1605
1696
  res.type("js").send(workerScript(build2, { version: this.baseLib.version, hasWorker }));
1606
1697
  }));
1607
1698
  app.get("/__wxa__/service-bootstrap.js", this.withBuild((build2, _req, res) => {
@@ -1620,28 +1711,28 @@ ${error.message}`);
1620
1711
  res.type("js").send(build2.pageFrame);
1621
1712
  }));
1622
1713
  app.get("/__wxa__/runtime/frame.js.map", (_req, res, next) => {
1623
- const file = import_path4.default.join(this.runtimeDir(), "frame.js.map");
1624
- if (!import_fs4.default.existsSync(file)) return next();
1625
- res.type("json").send(ignoreConsoleHook(import_fs4.default.readFileSync(file, "utf8")));
1714
+ const file = import_path5.default.join(this.runtimeDir(), "frame.js.map");
1715
+ if (!import_fs5.default.existsSync(file)) return next();
1716
+ res.type("json").send(ignoreConsoleHook(import_fs5.default.readFileSync(file, "utf8")));
1626
1717
  });
1627
1718
  app.use("/__wxa__/runtime", import_express2.default.static(this.runtimeDir(), {
1628
1719
  cacheControl: false,
1629
1720
  // Service Worker 默认只能接管自己所在目录下的页面,宿主页面在根路径
1630
1721
  setHeaders: (res, file) => {
1631
- if (import_path4.default.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1722
+ if (import_path5.default.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1632
1723
  }
1633
1724
  }));
1634
1725
  const sourceMaps = /* @__PURE__ */ new Map();
1635
1726
  app.get(/^\/__baselib__\/(.+\.js)\.map$/, (req, res, next) => {
1636
- const file = import_path4.default.resolve(this.baseLib.dir, req.params[0]);
1637
- if (!file.startsWith(this.baseLib.dir + import_path4.default.sep) || !import_fs4.default.existsSync(file)) return next();
1638
- if (!sourceMaps.has(file)) sourceMaps.set(file, ignoreListSourceMap(import_fs4.default.readFileSync(file, "utf8"), `${import_path4.default.basename(file)}?original`));
1727
+ const file = import_path5.default.resolve(this.baseLib.dir, req.params[0]);
1728
+ if (!file.startsWith(this.baseLib.dir + import_path5.default.sep) || !import_fs5.default.existsSync(file)) return next();
1729
+ if (!sourceMaps.has(file)) sourceMaps.set(file, ignoreListSourceMap(import_fs5.default.readFileSync(file, "utf8"), `${import_path5.default.basename(file)}?original`));
1639
1730
  res.type("json").send(sourceMaps.get(file));
1640
1731
  });
1641
1732
  app.use("/__baselib__", import_express2.default.static(this.baseLib.dir, {
1642
1733
  maxAge: "1d",
1643
1734
  setHeaders: (res, file) => {
1644
- if (file.endsWith(".js")) res.setHeader("SourceMap", `${import_path4.default.basename(file)}.map`);
1735
+ if (file.endsWith(".js")) res.setHeader("SourceMap", `${import_path5.default.basename(file)}.map`);
1645
1736
  }
1646
1737
  }));
1647
1738
  app.use(PROXY_PREFIX, import_express2.default.raw({ type: () => true, limit: "100mb" }), handleProxy);
@@ -1701,7 +1792,7 @@ ${error.message}`);
1701
1792
  return;
1702
1793
  }
1703
1794
  const data = node.data;
1704
- res.type(import_path4.default.extname(req.params[0]));
1795
+ res.type(import_path5.default.extname(req.params[0]));
1705
1796
  res.setHeader("ETag", `"${node.etag}"`);
1706
1797
  res.setHeader("Accept-Ranges", "bytes");
1707
1798
  const ranges = req.range(data.length);
@@ -1771,11 +1862,11 @@ ${error.message}`);
1771
1862
  runtimeDir() {
1772
1863
  const candidates = [
1773
1864
  this.options.runtimeDir,
1774
- import_path4.default.join(__dirname, "runtime"),
1775
- import_path4.default.join(__dirname, "../runtime"),
1776
- import_path4.default.join(__dirname, "../../dist/runtime")
1865
+ import_path5.default.join(__dirname, "runtime"),
1866
+ import_path5.default.join(__dirname, "../runtime"),
1867
+ import_path5.default.join(__dirname, "../../dist/runtime")
1777
1868
  ].filter((dir) => !!dir);
1778
- const found = candidates.find((dir) => import_fs4.default.existsSync(import_path4.default.join(dir, "host.js")));
1869
+ const found = candidates.find((dir) => import_fs5.default.existsSync(import_path5.default.join(dir, "host.js")));
1779
1870
  if (!found) {
1780
1871
  throw new Error("\u627E\u4E0D\u5230\u6D4F\u89C8\u5668\u7AEF\u8FD0\u884C\u65F6\uFF08host.js\uFF09\uFF0C\u8BF7\u5148\u6267\u884C pnpm build");
1781
1872
  }
@@ -1788,7 +1879,7 @@ ${error.message}`);
1788
1879
  startWatching() {
1789
1880
  let lastBuildFailed = false;
1790
1881
  const watched = [this.project.root];
1791
- if (this.setupFile && import_path4.default.relative(this.project.root, this.setupFile).startsWith("..")) watched.push(this.setupFile);
1882
+ if (this.setupFile && import_path5.default.relative(this.project.root, this.setupFile).startsWith("..")) watched.push(this.setupFile);
1792
1883
  this.watcher = watchProject(this.project.root, watched, async ({ files, structural }) => {
1793
1884
  const reloadOnly = structural || lastBuildFailed;
1794
1885
  this.builder.invalidate(files);
@@ -1819,7 +1910,7 @@ var DEFAULT_PORT = 3e3;
1819
1910
  var LOCAL_HOST_OF_ANY_ADDRESS = /* @__PURE__ */ new Map([["0.0.0.0", "127.0.0.1"], ["::", "localhost"]]);
1820
1911
  async function listen(server, port, host, logger) {
1821
1912
  for (let candidate = port ?? DEFAULT_PORT; ; candidate++) {
1822
- const taken = LOCAL_HOST_OF_ANY_ADDRESS.has(host) && await takenOnLoopback(candidate) || !await bind(server, candidate, host);
1913
+ const taken = await takenOnLocalAddresses(candidate) || !await bind(server, candidate, host);
1823
1914
  if (!taken) {
1824
1915
  const actual = server.address().port;
1825
1916
  if (port === void 0 && actual !== DEFAULT_PORT) logger.warn(`[wxa-runner] \u7AEF\u53E3 ${DEFAULT_PORT} \u5DF2\u88AB\u5360\u7528\uFF0C\u6539\u7528 ${actual}`);
@@ -1838,8 +1929,8 @@ function bind(server, port, host) {
1838
1929
  });
1839
1930
  });
1840
1931
  }
1841
- async function takenOnLoopback(port) {
1842
- for (const address of ["127.0.0.1", "::1"]) {
1932
+ async function takenOnLocalAddresses(port) {
1933
+ for (const address of ["127.0.0.1", "::1", "0.0.0.0", "::"]) {
1843
1934
  const probe = import_net.default.createServer();
1844
1935
  const free = await bind(probe, port, address).catch(() => true);
1845
1936
  if (!free) return true;
@@ -1847,11 +1938,21 @@ async function takenOnLoopback(port) {
1847
1938
  }
1848
1939
  return false;
1849
1940
  }
1941
+ function assertMountPath(mountPath) {
1942
+ const literal = typeof mountPath === "string" && /^(\/[\w.~-]+)+$/.test(mountPath);
1943
+ const lower = literal ? mountPath.toLowerCase() : "";
1944
+ const reserved = RESERVED_PREFIXES.some((prefix) => lower === prefix || lower.startsWith(prefix + "/"));
1945
+ if (!literal || reserved) {
1946
+ throw new Error(
1947
+ `\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`
1948
+ );
1949
+ }
1950
+ }
1850
1951
  function resolveSetupFile(option, projectPath) {
1851
1952
  if (option === false) return null;
1852
1953
  if (!option) return findSetupFile(projectPath);
1853
- const file = import_path4.default.resolve(option);
1854
- if (!import_fs4.default.existsSync(file)) throw new Error(`\u627E\u4E0D\u5230 setup \u6587\u4EF6\uFF1A${file}`);
1954
+ const file = import_path5.default.resolve(option);
1955
+ if (!import_fs5.default.existsSync(file)) throw new Error(`\u627E\u4E0D\u5230 setup \u6587\u4EF6\uFF1A${file}`);
1855
1956
  return file;
1856
1957
  }
1857
1958
  // Annotate the CommonJS export names for ESM import in node: