@open-wxa/wxa-runner-core 0.8.2 → 0.10.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
@@ -13,6 +13,47 @@ var FileStore = class {
13
13
  this.etagPrefix = randomUUID();
14
14
  this.version = 0;
15
15
  this.nodes = /* @__PURE__ */ new Map([["/", { dir: true, data: Buffer.alloc(0), mtime: Date.now(), etag: "" }]]);
16
+ this.operations = {
17
+ readFile: ({ filePath, encoding }) => {
18
+ const data = this.read(filePath);
19
+ return encoding === void 0 ? { data: data.toString("base64"), binary: true } : { data: data.toString(encoding) };
20
+ },
21
+ writeFile: (args) => this.writeEncoded(args, false),
22
+ appendFile: (args) => this.writeEncoded(args, true),
23
+ copyFile: ({ srcPath, destPath }) => {
24
+ this.write(destPath, this.read(srcPath));
25
+ return {};
26
+ },
27
+ access: ({ path: path7 }) => {
28
+ this.existing(path7);
29
+ return {};
30
+ },
31
+ stat: ({ path: path7 }) => {
32
+ const node = this.existing(path7);
33
+ const stats = {
34
+ mode: node.dir ? 16877 : 33188,
35
+ size: node.data.length,
36
+ lastAccessedTime: Math.floor(node.mtime / 1e3),
37
+ lastModifiedTime: Math.floor(node.mtime / 1e3)
38
+ };
39
+ return { stats, ...stats };
40
+ },
41
+ getFileInfo: ({ filePath }) => ({ size: this.read(filePath).length }),
42
+ mkdir: ({ dirPath, recursive }) => {
43
+ this.mkdir(dirPath, !!recursive);
44
+ return {};
45
+ },
46
+ readdir: ({ dirPath }) => ({ files: this.readdir(dirPath) }),
47
+ unlink: ({ filePath }) => {
48
+ this.remove(filePath, { dir: false });
49
+ return {};
50
+ },
51
+ rmdir: ({ dirPath, recursive }) => {
52
+ this.assertNotRoot(dirPath);
53
+ this.remove(dirPath, { dir: true, recursive: !!recursive });
54
+ return {};
55
+ }
56
+ };
16
57
  this.roots = roots.map((root) => this.normalize(root));
17
58
  for (const root of this.roots) this.mkdir(root, true);
18
59
  }
@@ -73,66 +114,37 @@ var FileStore = class {
73
114
  }
74
115
  this.nodes.delete(target);
75
116
  }
117
+ /** 文件 API 经开发服务器调用的操作,参数和返回值的字段名与基础库一致 */
76
118
  execute(operation, args) {
77
- switch (operation) {
78
- case "readFile": {
79
- const data = this.read(args.filePath);
80
- return args.encoding === void 0 ? { data: data.toString("base64"), binary: true } : { data: data.toString(args.encoding) };
81
- }
82
- case "writeFile":
83
- case "appendFile": {
84
- const data = Buffer.from(args.data ?? "", args.binary ? "base64" : args.encoding || "utf8");
85
- this.write(args.filePath, data, operation === "appendFile");
86
- return {};
87
- }
88
- case "copyFile":
89
- this.write(args.destPath, this.read(args.srcPath));
90
- return {};
91
- case "access":
92
- if (!this.stat(args.path)) throw new Error(`no such file or directory ${args.path}`);
93
- return {};
94
- case "stat": {
95
- const node = this.stat(args.path);
96
- if (!node) throw new Error(`no such file or directory ${args.path}`);
97
- const stats = {
98
- mode: node.dir ? 16877 : 33188,
99
- size: node.data.length,
100
- lastAccessedTime: Math.floor(node.mtime / 1e3),
101
- lastModifiedTime: Math.floor(node.mtime / 1e3)
102
- };
103
- return { stats, ...stats };
104
- }
105
- case "getFileInfo":
106
- return { size: this.read(args.filePath).length };
107
- case "mkdir":
108
- this.mkdir(args.dirPath, !!args.recursive);
109
- return {};
110
- case "readdir":
111
- return { files: this.readdir(args.dirPath) };
112
- case "unlink":
113
- this.remove(args.filePath, { dir: false });
114
- return {};
115
- case "rmdir": {
116
- const target = this.normalize(args.dirPath);
117
- const prefix = target === "/" ? "/" : target + "/";
118
- if (this.roots.some((root) => root === target || root.startsWith(prefix))) {
119
- throw new Error(`operation not permitted ${args.dirPath}`);
120
- }
121
- this.remove(args.dirPath, { dir: true, recursive: !!args.recursive });
122
- return {};
123
- }
124
- default:
125
- throw new Error(`unsupported filesystem operation ${operation}`);
119
+ if (!Object.prototype.hasOwnProperty.call(this.operations, operation)) {
120
+ throw new Error(`unsupported filesystem operation ${operation}`);
121
+ }
122
+ return this.operations[operation](args);
123
+ }
124
+ writeEncoded(args, append) {
125
+ this.write(args.filePath, Buffer.from(args.data ?? "", args.binary ? "base64" : args.encoding || "utf8"), append);
126
+ return {};
127
+ }
128
+ existing(filePath) {
129
+ const node = this.stat(filePath);
130
+ if (!node) throw new Error(`no such file or directory ${filePath}`);
131
+ return node;
132
+ }
133
+ assertNotRoot(dirPath) {
134
+ const target = this.normalize(dirPath);
135
+ const prefix = target === "/" ? "/" : target + "/";
136
+ if (this.roots.some((root) => root === target || root.startsWith(prefix))) {
137
+ throw new Error(`operation not permitted ${dirPath}`);
126
138
  }
127
139
  }
128
140
  };
129
141
 
130
142
  // src/server/DevServer.ts
131
- import fs4 from "fs";
143
+ import fs6 from "fs";
132
144
  import http from "http";
133
145
  import net from "net";
134
- import os from "os";
135
- import path4 from "path";
146
+ import os3 from "os";
147
+ import path6 from "path";
136
148
  import express2 from "express";
137
149
  import { WebSocket as WebSocket2, WebSocketServer } from "ws";
138
150
  import { readRegionData, resolveBaseLib, usesIsolatedContext } from "@open-wxa/wxa-runner-extractor";
@@ -141,6 +153,164 @@ import { readRegionData, resolveBaseLib, usesIsolatedContext } from "@open-wxa/w
141
153
  import { compileWxml, compileWxss } from "@open-wxa/wxa-runner-compiler";
142
154
  import { transformSync } from "esbuild";
143
155
 
156
+ // src/project/StyleMirror.ts
157
+ import fs from "fs";
158
+ import { createRequire } from "module";
159
+ import os from "os";
160
+ import path from "path";
161
+ import { pathToFileURL } from "url";
162
+ var PLUGIN_EXTENSIONS = {
163
+ less: [".less"],
164
+ sass: [".sass", ".scss"]
165
+ };
166
+ var IMPORT_WXSS = /(?:^|\s)?(?:@import)(?:\s*)?(["'])([^"']+.wxss)\1(?:\s*);/g;
167
+ function readStyleSetting(setting) {
168
+ const plugins = [];
169
+ let lessGlobalVars = {};
170
+ for (const item of Array.isArray(setting?.useCompilerPlugins) ? setting.useCompilerPlugins : []) {
171
+ const [name, options] = Array.isArray(item) ? item : [item, void 0];
172
+ if (name !== "less" && name !== "sass" || plugins.includes(name)) continue;
173
+ plugins.push(name);
174
+ if (name === "less" && options?.globalVars) lessGlobalVars = options.globalVars;
175
+ }
176
+ return {
177
+ plugins,
178
+ lessGlobalVars,
179
+ lessCommonFile: setting?.lessSetting?.commonUseFilePath || "app.less",
180
+ sassCommonFile: setting?.sassSetting?.sassCommonUseFilePath || "global.sass",
181
+ scssCommonFile: setting?.sassSetting?.scssCommonUseFilePath || "global.scss"
182
+ };
183
+ }
184
+ function ignoredStyleSources(project, setting) {
185
+ return Object.keys(PLUGIN_EXTENSIONS).filter((plugin) => !setting.plugins.includes(plugin)).map((plugin) => ({ plugin, files: project.listFiles(PLUGIN_EXTENSIONS[plugin]) })).filter(({ files }) => files.length > 0);
186
+ }
187
+ var StyleMirror = class {
188
+ constructor(setting) {
189
+ this.setting = setting;
190
+ this.dir = null;
191
+ /** 已写入的 .wxss 与写入的内容 */
192
+ this.files = /* @__PURE__ */ new Map();
193
+ this.compilers = /* @__PURE__ */ new Map();
194
+ }
195
+ /** 镜像目录;还没同步过时为 null */
196
+ get directory() {
197
+ return this.dir;
198
+ }
199
+ /**
200
+ * 编译全部预处理文件,与原有的 .wxss 一起写进镜像目录。
201
+ * @returns 镜像目录,以及其中全部 .wxss 的相对路径
202
+ */
203
+ async sync(project) {
204
+ const sources = this.pickSources(project);
205
+ const outputs = await Promise.all(
206
+ [...sources].map(async ([target, source]) => [target, await this.compile(project, source)])
207
+ );
208
+ const dir = this.dir ??= fs.mkdtempSync(path.join(os.tmpdir(), "wxa-runner-wxss-"));
209
+ for (const rel of [...this.files.keys()]) {
210
+ if (sources.has(rel)) continue;
211
+ fs.rmSync(path.join(dir, rel), { force: true });
212
+ this.files.delete(rel);
213
+ }
214
+ for (const [rel, css] of outputs) {
215
+ if (this.files.get(rel) === css) continue;
216
+ const target = path.join(dir, rel);
217
+ fs.mkdirSync(path.dirname(target), { recursive: true });
218
+ fs.writeFileSync(target, css);
219
+ this.files.set(rel, css);
220
+ }
221
+ return { cwd: dir, files: [...sources.keys()] };
222
+ }
223
+ dispose() {
224
+ if (this.dir) fs.rmSync(this.dir, { recursive: true, force: true });
225
+ this.dir = null;
226
+ this.files.clear();
227
+ }
228
+ /** 每个 .wxss 由哪个源文件生成:键为 .wxss 的相对路径 */
229
+ pickSources(project) {
230
+ const extensions = [".wxss", ...this.setting.plugins.flatMap((plugin) => PLUGIN_EXTENSIONS[plugin])];
231
+ const sources = /* @__PURE__ */ new Map();
232
+ for (const file of project.listFiles(extensions)) {
233
+ const ext = path.extname(file);
234
+ const target = file.slice(0, -ext.length) + ".wxss";
235
+ const current = sources.get(target);
236
+ if (!current || extensions.indexOf(ext) > extensions.indexOf(path.extname(current))) sources.set(target, file);
237
+ }
238
+ return sources;
239
+ }
240
+ async compile(project, file) {
241
+ const ext = path.extname(file);
242
+ if (ext === ".wxss") return project.readText(file);
243
+ const compiler = await this.loadCompiler(ext === ".less" ? "less" : "sass", project);
244
+ try {
245
+ return ext === ".less" ? await this.compileLess(compiler, project, file) : this.compileSass(compiler, project, file);
246
+ } catch (error) {
247
+ throw new Error(`${file} \u7F16\u8BD1\u5931\u8D25\uFF1A${describeError(project, error)}`);
248
+ }
249
+ }
250
+ async compileLess(less, project, file) {
251
+ const common = this.setting.lessCommonFile;
252
+ const prefix = Object.entries(this.setting.lessGlobalVars).map(([name, value]) => `@${name.replace(/^@/, "")}: ${value};`);
253
+ if (file !== common && project.exists(common)) prefix.push(`@import (optional, reference) '${toRoot(file)}/${common}';`);
254
+ let code = prefix.map((statement) => statement + " ").join("") + project.readText(file);
255
+ code = code.replace(IMPORT_WXSS, (statement, _quote, target) => statement.replace(target, `${target}?css`));
256
+ const { css } = await less.render(code, {
257
+ filename: path.join(project.root, file),
258
+ paths: [project.root]
259
+ });
260
+ return css.replace(/(@import\s*["'][^"']+\.wxss)\?css(["'])/g, "$1$2");
261
+ }
262
+ compileSass(sass, project, file) {
263
+ const indented = file.endsWith(".sass");
264
+ const preferred = indented ? this.setting.sassCommonFile : this.setting.scssCommonFile;
265
+ const fallback = indented ? this.setting.scssCommonFile : this.setting.sassCommonFile;
266
+ const common = project.exists(preferred) ? preferred : project.exists(fallback) ? fallback : null;
267
+ let code = project.readText(file);
268
+ if (common && file !== preferred && file !== fallback) {
269
+ code = `@use '${toRoot(file)}/${common}'${indented ? "\n" : "; "}${code}`;
270
+ }
271
+ const imported = /* @__PURE__ */ new Set();
272
+ code = code.replace(IMPORT_WXSS, (statement, _quote, target) => {
273
+ imported.add(target);
274
+ return statement.replace(target, `${target.slice(0, -".wxss".length)}.css`);
275
+ });
276
+ const css = typeof sass.compileString === "function" ? sass.compileString(code, { url: pathToFileURL(path.join(project.root, file)), syntax: indented ? "indented" : "scss", loadPaths: [project.root] }).css : sass.renderSync({ data: code, file: path.join(project.root, file), indentedSyntax: indented, includePaths: [project.root] }).css.toString();
277
+ return css.replace(
278
+ /(@import\s*["'])([^"']+)\.css(["'])/g,
279
+ (statement, head, target, tail) => imported.has(`${target}.wxss`) ? `${head}${target}.wxss${tail}` : statement
280
+ );
281
+ }
282
+ /** 用到时才加载:先从小程序根目录和项目目录向上查找 node_modules,找不到时用 wxa-runner 自带的 */
283
+ loadCompiler(name, project) {
284
+ let loading = this.compilers.get(name);
285
+ if (!loading) {
286
+ loading = importCompiler(name, project);
287
+ this.compilers.set(name, loading);
288
+ }
289
+ return loading;
290
+ }
291
+ };
292
+ async function importCompiler(name, project) {
293
+ let resolved = null;
294
+ try {
295
+ resolved = createRequire(path.join(project.root, "package.json")).resolve(name, { paths: [project.root, project.projectPath] });
296
+ } catch {
297
+ }
298
+ const module = resolved ? await import(pathToFileURL(resolved).href) : name === "less" ? await import("less") : await import("sass");
299
+ const entry = name === "less" ? "render" : "renderSync";
300
+ return typeof module[entry] === "function" ? module : module.default;
301
+ }
302
+ function toRoot(file) {
303
+ const dir = path.posix.dirname(file);
304
+ return dir === "." ? "." : dir.split("/").map(() => "..").join("/");
305
+ }
306
+ function describeError(project, error) {
307
+ if (typeof error?.line === "number" && error.filename) {
308
+ const file = path.relative(project.root, error.filename).split(path.sep).join("/");
309
+ return `${file}:${error.line}:${(error.column ?? 0) + 1} ${error.message}`;
310
+ }
311
+ return error?.message ?? String(error);
312
+ }
313
+
144
314
  // src/project/wxConfig.ts
145
315
  var DEFAULT_WINDOW = {
146
316
  navigationBarBackgroundColor: "#000000",
@@ -197,6 +367,50 @@ function createWxConfig(input) {
197
367
  };
198
368
  }
199
369
 
370
+ // src/project/WxmlMirror.ts
371
+ import fs2 from "fs";
372
+ import os2 from "os";
373
+ import path2 from "path";
374
+ var WxmlMirror = class {
375
+ constructor(transform) {
376
+ this.transform = transform;
377
+ this.dir = null;
378
+ /** 已写入的文件:源内容(判断要不要重新转换)与写入的内容 */
379
+ this.files = /* @__PURE__ */ new Map();
380
+ }
381
+ /** 镜像目录;还没同步过时为 null */
382
+ get directory() {
383
+ return this.dir;
384
+ }
385
+ /**
386
+ * 让镜像目录与项目里的 wxml / wxs 一致,返回镜像目录。
387
+ * 只重写源内容变化的文件;项目里已删除的文件同时从镜像目录删除,被引用时 wcc 照常报错
388
+ */
389
+ sync(project, relPaths) {
390
+ const dir = this.dir ??= fs2.mkdtempSync(path2.join(os2.tmpdir(), "wxa-runner-wxml-"));
391
+ const current = new Set(relPaths);
392
+ for (const rel of [...this.files.keys()]) {
393
+ if (current.has(rel)) continue;
394
+ fs2.rmSync(path2.join(dir, rel), { force: true });
395
+ this.files.delete(rel);
396
+ }
397
+ for (const rel of relPaths) {
398
+ const source = project.readText(rel);
399
+ if (this.files.get(rel) === source) continue;
400
+ const target = path2.join(dir, rel);
401
+ fs2.mkdirSync(path2.dirname(target), { recursive: true });
402
+ fs2.writeFileSync(target, rel.endsWith(".wxml") ? this.transform(source, rel) : source);
403
+ this.files.set(rel, source);
404
+ }
405
+ return dir;
406
+ }
407
+ dispose() {
408
+ if (this.dir) fs2.rmSync(this.dir, { recursive: true, force: true });
409
+ this.dir = null;
410
+ this.files.clear();
411
+ }
412
+ };
413
+
200
414
  // src/project/AppBuilder.ts
201
415
  var MODULE_PARAMS = [
202
416
  "require",
@@ -223,7 +437,7 @@ var MODULE_PARAMS = [
223
437
  ].join(", ");
224
438
  var ESM_SYNTAX = /(?:^|[;}])\s*(?:import\s*[{*'"]|import\s+[\w$]|export\s*[{*]|export\s+(?:default|const|let|var|function|class|async)\b)/m;
225
439
  var AppBuilder = class {
226
- constructor(project) {
440
+ constructor(project, options = {}) {
227
441
  this.project = project;
228
442
  this.version = 0;
229
443
  /** invalidate 标记过的变化次数;每次构建记下开始时覆盖到第几次 */
@@ -233,6 +447,18 @@ var AppBuilder = class {
233
447
  this.dirty = /* @__PURE__ */ new Set(["config", "wxml", "wxss", "js"]);
234
448
  this.wxmlCache = null;
235
449
  this.wxssCache = null;
450
+ /** 开启了 less / sass 编译插件时才有;插件设置变了就换一个 */
451
+ this.styleMirror = null;
452
+ this.wxmlMirror = options.wxmlTransform ? new WxmlMirror(options.wxmlTransform) : null;
453
+ }
454
+ /** wxmlTransform 的镜像目录;没有设置转换或还没编译过时为 null */
455
+ get wxmlMirrorDir() {
456
+ return this.wxmlMirror?.directory ?? null;
457
+ }
458
+ /** 删除 wxmlTransform 和样式预处理的镜像目录,开发服务器关闭时调用 */
459
+ dispose() {
460
+ this.wxmlMirror?.dispose();
461
+ this.styleMirror?.mirror.dispose();
236
462
  }
237
463
  /** 根据变化的文件标记需要重新构建的部分;返回 true 表示需要刷新 */
238
464
  invalidate(changedFiles = []) {
@@ -357,7 +583,7 @@ ${pageStyles}`,
357
583
  const json = unitJson.get(id);
358
584
  return { path: id, tags: Object.keys({ ...json.usingComponents, ...json.componentGenerics }) };
359
585
  });
360
- const cwd = this.project.root;
586
+ const cwd = this.wxmlMirror ? this.wxmlMirror.sync(this.project, files.map((file) => file.slice(2))) : this.project.root;
361
587
  const [full, cut] = await Promise.all([
362
588
  compileWxml({ cwd, files, components, cut: false }),
363
589
  compileWxml({ cwd, files, components, cut: true })
@@ -366,14 +592,26 @@ ${pageStyles}`,
366
592
  }
367
593
  /** 与开发者工具一致,项目里的 wxss 全部交给 wcsc:被 @import 的文件(如 uni-app 的 common/main.wxss)也要在列表里 */
368
594
  async compileStyles(units) {
369
- const pageFiles = units.filter((id) => this.project.exists(id + ".wxss")).map((id) => `./${id}.wxss`);
370
- const commonFiles = this.project.listFiles([".wxss"]).map((file) => `./${file}`).filter((file) => !pageFiles.includes(file));
371
- const { comm, pages } = await compileWxss({ cwd: this.project.root, pageFiles, commonFiles });
595
+ const mirror = this.currentStyleMirror();
596
+ const { cwd, files } = mirror ? await mirror.sync(this.project) : { cwd: this.project.root, files: this.project.listFiles([".wxss"]) };
597
+ const styles = new Set(files);
598
+ const pageFiles = units.filter((id) => styles.has(id + ".wxss")).map((id) => `./${id}.wxss`);
599
+ const commonFiles = files.map((file) => `./${file}`).filter((file) => !pageFiles.includes(file));
600
+ const { comm, pages } = await compileWxss({ cwd, pageFiles, commonFiles });
372
601
  return {
373
602
  comm: rewriteSafeAreaInsets(comm),
374
603
  pages: Object.fromEntries(Object.entries(pages).map(([file, css]) => [file, rewriteSafeAreaInsets(css)]))
375
604
  };
376
605
  }
606
+ currentStyleMirror() {
607
+ const setting = this.project.styleSetting;
608
+ const key = JSON.stringify(setting);
609
+ if (this.styleMirror?.setting !== key) {
610
+ this.styleMirror?.mirror.dispose();
611
+ this.styleMirror = setting.plugins.length > 0 ? { setting: key, mirror: new StyleMirror(setting) } : null;
612
+ }
613
+ return this.styleMirror?.mirror ?? null;
614
+ }
377
615
  /** $gwx 加上各页面、组件的 json / wxml 注册;逻辑层用裁剪版 $gwx,渲染层用完整版 */
378
616
  createTemplates(app, units, unitJson, gwx) {
379
617
  const lines = [gwx, `__wxAppCode__["app.json"] = ${toJs(app)};`];
@@ -418,28 +656,32 @@ function toJs(value) {
418
656
  function classifyFile(file) {
419
657
  if (file.endsWith(".json")) return "config";
420
658
  if (file.endsWith(".wxml") || file.endsWith(".wxs")) return "wxml";
421
- if (file.endsWith(".wxss")) return "wxss";
659
+ if (/\.(wxss|less|scss|sass|css)$/.test(file)) return "wxss";
422
660
  if (file.endsWith(".js")) return "js";
423
661
  return null;
424
662
  }
425
663
 
426
664
  // src/project/MiniProgram.ts
427
- import fs from "fs";
428
- import path from "path";
665
+ import fs3 from "fs";
666
+ import path3 from "path";
429
667
  var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".idea", ".vscode"]);
430
668
  var PROJECT_FILES = /* @__PURE__ */ new Set(["project.config.json", "project.private.config.json", "sitemap.json", "app.json", "wxa.setup.js"]);
431
669
  var MiniProgram = class {
432
670
  constructor(projectPath) {
433
671
  this.projectPath = projectPath;
434
- const projectConfig = readJsonFile(path.join(projectPath, "project.config.json")) || {};
435
- this.root = path.resolve(projectPath, projectConfig.miniprogramRoot || "");
672
+ const projectConfig = readJsonFile(path3.join(projectPath, "project.config.json")) || {};
673
+ this.root = path3.resolve(projectPath, projectConfig.miniprogramRoot || "");
436
674
  this.appid = projectConfig.appid || "touristappid";
437
- if (!fs.existsSync(path.join(this.root, "app.json"))) {
675
+ if (!fs3.existsSync(path3.join(this.root, "app.json"))) {
438
676
  throw new Error(`${this.root} \u4E0B\u6CA1\u6709 app.json\uFF0C\u4E0D\u662F\u6709\u6548\u7684\u5C0F\u7A0B\u5E8F\u76EE\u5F55`);
439
677
  }
440
678
  }
679
+ /** project.config.json 里与 less / sass 编译插件有关的设置;每次重新读取,开发中修改了也能生效 */
680
+ get styleSetting() {
681
+ return readStyleSetting(readJsonFile(path3.join(this.projectPath, "project.config.json"))?.setting);
682
+ }
441
683
  readAppJson() {
442
- const app = readJsonFile(path.join(this.root, "app.json"));
684
+ const app = readJsonFile(path3.join(this.root, "app.json"));
443
685
  if (!app || !Array.isArray(app.pages) || app.pages.length === 0) {
444
686
  throw new Error("app.json \u7F3A\u5C11 pages \u914D\u7F6E");
445
687
  }
@@ -451,14 +693,14 @@ var MiniProgram = class {
451
693
  /** Worker 代码所在目录,不带首尾的 /;没有配置 workers 时为 null */
452
694
  workersDir(app) {
453
695
  const dir = typeof app.workers === "string" ? app.workers : app.workers?.path;
454
- return dir ? path.posix.normalize(dir).replace(/^\/+|\/+$/g, "") : null;
696
+ return dir ? path3.posix.normalize(dir).replace(/^\/+|\/+$/g, "") : null;
455
697
  }
456
698
  /** 主包页面 + 分包页面(带 root 前缀) */
457
699
  allPages(app) {
458
700
  const pages = [...app.pages];
459
701
  for (const pkg of this.subPackages(app)) {
460
702
  for (const page of pkg.pages) {
461
- const full = path.posix.join(pkg.root, page);
703
+ const full = path3.posix.join(pkg.root, page);
462
704
  if (!pages.includes(full)) pages.push(full);
463
705
  }
464
706
  }
@@ -470,7 +712,7 @@ var MiniProgram = class {
470
712
  * 所以能解析到的引用都换成组件的绝对路径
471
713
  */
472
714
  readUnitJson(id, app) {
473
- const json = { ...readJsonFile(path.join(this.root, id + ".json")) };
715
+ const json = { ...readJsonFile(path3.join(this.root, id + ".json")) };
474
716
  const resolve = (fromId, ref) => {
475
717
  const resolved = this.resolveComponent(fromId, ref);
476
718
  return resolved ? "/" + resolved : null;
@@ -491,7 +733,7 @@ var MiniProgram = class {
491
733
  if (resolved || ref.startsWith("/") || /^plugin(-private)?:\/\//.test(ref)) {
492
734
  json.usingComponents[tag] = resolved ?? ref;
493
735
  } else {
494
- const relative = path.posix.relative(path.posix.dirname(id), ref);
736
+ const relative = path3.posix.relative(path3.posix.dirname(id), ref);
495
737
  json.usingComponents[tag] = relative.startsWith(".") ? relative : "./" + relative;
496
738
  }
497
739
  }
@@ -504,25 +746,25 @@ var MiniProgram = class {
504
746
  */
505
747
  resolveComponent(fromId, ref) {
506
748
  if (!ref || ref.startsWith("plugin://") || ref.startsWith("plugin-private://")) return null;
507
- const fromDir = path.posix.dirname(fromId);
749
+ const fromDir = path3.posix.dirname(fromId);
508
750
  const candidates = [];
509
751
  if (ref.startsWith("/")) {
510
752
  candidates.push(ref.slice(1));
511
753
  } else {
512
- candidates.push(path.posix.join(fromDir, ref));
754
+ candidates.push(path3.posix.join(fromDir, ref));
513
755
  if (!ref.startsWith(".")) {
514
756
  let dir = fromDir;
515
757
  for (; ; ) {
516
- candidates.push(path.posix.join(dir === "." ? "" : dir, "miniprogram_npm", ref));
758
+ candidates.push(path3.posix.join(dir === "." ? "" : dir, "miniprogram_npm", ref));
517
759
  if (dir === "." || dir === "") break;
518
- dir = path.posix.dirname(dir);
760
+ dir = path3.posix.dirname(dir);
519
761
  }
520
762
  }
521
763
  }
522
764
  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;
765
+ const normalized = path3.posix.normalize(candidate);
766
+ for (const id of [normalized, path3.posix.join(normalized, "index")]) {
767
+ if (fs3.existsSync(path3.join(this.root, id + ".json"))) return id;
526
768
  }
527
769
  }
528
770
  return null;
@@ -554,11 +796,11 @@ var MiniProgram = class {
554
796
  listFiles(extensions) {
555
797
  const result = [];
556
798
  const walk = (dir, rel) => {
557
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
799
+ for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
558
800
  if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
559
801
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
560
802
  if (entry.isDirectory()) {
561
- walk(path.join(dir, entry.name), childRel);
803
+ walk(path3.join(dir, entry.name), childRel);
562
804
  } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
563
805
  if (!rel && PROJECT_FILES.has(entry.name)) continue;
564
806
  result.push(childRel);
@@ -569,18 +811,18 @@ var MiniProgram = class {
569
811
  return result.sort();
570
812
  }
571
813
  exists(relPath) {
572
- return fs.existsSync(path.join(this.root, relPath));
814
+ return fs3.existsSync(path3.join(this.root, relPath));
573
815
  }
574
816
  readText(relPath) {
575
- return fs.readFileSync(path.join(this.root, relPath), "utf8");
817
+ return fs3.readFileSync(path3.join(this.root, relPath), "utf8");
576
818
  }
577
819
  isInIndependentSubPackage(id, app) {
578
820
  return this.subPackages(app).some((pkg) => pkg.independent && id.startsWith(pkg.root.replace(/\/?$/, "/")));
579
821
  }
580
822
  };
581
823
  function readJsonFile(file) {
582
- if (!fs.existsSync(file)) return null;
583
- const text = fs.readFileSync(file, "utf8");
824
+ if (!fs3.existsSync(file)) return null;
825
+ const text = fs3.readFileSync(file, "utf8");
584
826
  try {
585
827
  return JSON.parse(text);
586
828
  } catch (error) {
@@ -646,9 +888,9 @@ var WechatDevClient = class {
646
888
  *
647
889
  * @throws 没有可用的登录态、超时、连不上、HTTP 状态不是 2xx 时,消息是给用户看的原因
648
890
  */
649
- async post(path5, body, timeout = DEFAULT_TIMEOUT) {
891
+ async post(path7, body, timeout = DEFAULT_TIMEOUT) {
650
892
  const query = new URLSearchParams({ appid: this.appid, newticket: requireUsableTicket(await this.findTicket()), clientversion: CLIENT_VERSION });
651
- const response = await fetch(`${BASE_URL}/${path5}?${query}`, {
893
+ const response = await fetch(`${BASE_URL}/${path7}?${query}`, {
652
894
  method: "POST",
653
895
  headers: { "Content-Type": "application/json" },
654
896
  body: JSON.stringify(body),
@@ -660,8 +902,8 @@ var WechatDevClient = class {
660
902
  return response;
661
903
  }
662
904
  /** 同 post,返回解析后的 JSON */
663
- async postJson(path5, body) {
664
- return (await this.post(path5, body)).json();
905
+ async postJson(path7, body) {
906
+ return (await this.post(path7, body)).json();
665
907
  }
666
908
  };
667
909
 
@@ -857,12 +1099,12 @@ function webviewHtml(build2, pagePath, webviewId, options) {
857
1099
  return `<!DOCTYPE html>
858
1100
  <html>
859
1101
  <head>
860
- ${webviewHead(build2, pagePath, id)}
1102
+ ${webviewHead(build2, pagePath, id, options.scripts)}
861
1103
  ${enableHotUpdate(options.hot)}<script src="/__wxa__/page-frame.js?v=${build2.version}"></script>
862
1104
  ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</style>" : ""}
863
1105
  </head>
864
1106
  <body>
865
- <script>
1107
+ ${scriptTags(options.scripts, "body")}<script>
866
1108
  (function () {
867
1109
  var path = ${toJs(pagePath)};
868
1110
  if (typeof __wxAppCode__[path + '.wxss'] === 'function') __wxAppCode__[path + '.wxss']();
@@ -895,18 +1137,18 @@ self.console = __wxaConsole;
895
1137
  importScripts(${modules.map((url) => toJs(url)).join(", ")});
896
1138
  `;
897
1139
  }
898
- function pluginWebviewHtml(build2, plugin, innerPath, pagePath, webviewId) {
1140
+ function pluginWebviewHtml(build2, plugin, innerPath, pagePath, webviewId, options = {}) {
899
1141
  const id = Number(webviewId) || 0;
900
1142
  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
1143
  return `<!DOCTYPE html>
902
1144
  <html>
903
1145
  <head>
904
- ${webviewHead(build2, pagePath, id)}
1146
+ ${webviewHead(build2, pagePath, id, options.scripts)}
905
1147
  <script src="${pluginScriptUrl(plugin, "pageframe.js")}"></script>
906
1148
  ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</style>" : ""}
907
1149
  </head>
908
1150
  <body>
909
- ${page}
1151
+ ${scriptTags(options.scripts, "body")}${page}
910
1152
  <script>parent.__wxaHost.frameLoaded('webview', ${id});</script>
911
1153
  </body>
912
1154
  </html>`;
@@ -914,7 +1156,7 @@ ${page}
914
1156
  function pageWindow(build2, pagePath) {
915
1157
  return { ...build2.wxConfig.global?.window, ...build2.wxConfig.page?.[pagePath + ".html"]?.window };
916
1158
  }
917
- function webviewHead(build2, pagePath, id) {
1159
+ function webviewHead(build2, pagePath, id, scripts) {
918
1160
  const wxConfig = { ...build2.wxConfig, window: pageWindow(build2, pagePath) };
919
1161
  return `<meta charset="utf-8">
920
1162
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
@@ -929,7 +1171,12 @@ function webviewHead(build2, pagePath, id) {
929
1171
  <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
1172
  <script>var __wxConfig = ${toJs(wxConfig)};</script>
931
1173
  <script src="/__wxa__/runtime/frame.js" data-kind="webview" data-webview-id="${id}"></script>
932
- <script src="/__baselib__/WAWebview.js"></script>`;
1174
+ ${scriptTags(scripts, "head")}<script src="/__baselib__/WAWebview.js"></script>`;
1175
+ }
1176
+ function scriptTags(scripts, position) {
1177
+ return (scripts ?? []).filter((script) => (script.position ?? "body") === position).map((script) => script.src !== void 0 ? `<script src="${escapeHtml(script.src)}"></script>
1178
+ ` : `<script>${script.content.replace(/<\/script/gi, "<\\/script")}</script>
1179
+ `).join("");
933
1180
  }
934
1181
  function escapeHtml(text) {
935
1182
  return text.replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
@@ -1018,28 +1265,9 @@ var DROPPED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "con
1018
1265
  async function handleProxy(req, res) {
1019
1266
  const target = fromServerPath(PROXY_PREFIX, req.originalUrl);
1020
1267
  if (!target) {
1021
- res.status(400).set("x-wxa-proxy-error", "1").send(`invalid url "${req.originalUrl}"`);
1268
+ sendProxyError(res, 400, `invalid url "${req.originalUrl}"`);
1022
1269
  return;
1023
1270
  }
1024
- let requestHeaders = {};
1025
- try {
1026
- requestHeaders = JSON.parse(req.get("x-wxa-headers") || "{}");
1027
- } catch {
1028
- }
1029
- const headers = {};
1030
- for (const [key, value] of Object.entries(requestHeaders)) {
1031
- if (!DROPPED_REQUEST_HEADERS.has(key.toLowerCase()) && value !== void 0 && value !== null) {
1032
- headers[key] = String(value);
1033
- }
1034
- }
1035
- const browserContentType = req.get("content-type") || "";
1036
- if (browserContentType.startsWith("multipart/form-data")) {
1037
- for (const key of Object.keys(headers)) {
1038
- if (key.toLowerCase() === "content-type") delete headers[key];
1039
- }
1040
- headers["content-type"] = browserContentType;
1041
- }
1042
- const hasBody = !["GET", "HEAD"].includes(req.method) && Buffer.isBuffer(req.body) && req.body.length > 0;
1043
1271
  const controller = new AbortController();
1044
1272
  const cancel = () => controller.abort();
1045
1273
  res.once("close", cancel);
@@ -1048,48 +1276,77 @@ async function handleProxy(req, res) {
1048
1276
  try {
1049
1277
  upstream = await fetch(target, {
1050
1278
  method: req.method,
1051
- headers,
1052
- body: hasBody ? req.body : void 0,
1279
+ headers: forwardedRequestHeaders(req.get("x-wxa-headers"), req.get("content-type")),
1280
+ body: requestBody(req),
1053
1281
  redirect: "follow",
1054
1282
  signal: controller.signal
1055
1283
  });
1056
1284
  } catch (error) {
1057
1285
  if (res.destroyed || controller.signal.aborted) return;
1058
- const cause = error?.cause?.message;
1059
- res.status(502).set("x-wxa-proxy-error", "1").send(cause || error.message);
1060
- return;
1061
- }
1062
- const responseHeaders = {};
1063
- upstream.headers.forEach((value, key) => {
1064
- responseHeaders[key] = value;
1065
- });
1066
- const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
1067
- res.status(upstream.status);
1068
- upstream.headers.forEach((value, key) => {
1069
- if (!DROPPED_RESPONSE_HEADERS.has(key)) res.setHeader(key, value);
1070
- });
1071
- res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
1072
- res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
1073
- res.flushHeaders();
1074
- if (!upstream.body) {
1075
- res.end();
1286
+ sendProxyError(res, 502, fetchErrorMessage(error));
1076
1287
  return;
1077
1288
  }
1078
- await pipeline(Readable.fromWeb(upstream.body), res);
1289
+ await relayResponse(upstream, res);
1079
1290
  } catch (error) {
1080
1291
  if (!res.destroyed) res.destroy(error);
1081
1292
  } finally {
1082
1293
  res.off("close", cancel);
1083
1294
  }
1084
1295
  }
1296
+ function requestBody(req) {
1297
+ const hasBody = !["GET", "HEAD"].includes(req.method) && Buffer.isBuffer(req.body) && req.body.length > 0;
1298
+ return hasBody ? req.body : void 0;
1299
+ }
1300
+ function fetchErrorMessage(error) {
1301
+ return error?.cause?.message || error.message;
1302
+ }
1303
+ function sendProxyError(res, status, message) {
1304
+ res.status(status).set("x-wxa-proxy-error", "1").send(message);
1305
+ }
1306
+ function forwardedRequestHeaders(encoded, browserContentType = "") {
1307
+ const multipart = browserContentType.startsWith("multipart/form-data");
1308
+ const headers = {};
1309
+ for (const [key, value] of Object.entries(parseHeaders(encoded))) {
1310
+ const name = key.toLowerCase();
1311
+ if (DROPPED_REQUEST_HEADERS.has(name) || multipart && name === "content-type" || value === void 0 || value === null) continue;
1312
+ headers[key] = String(value);
1313
+ }
1314
+ if (multipart) headers["content-type"] = browserContentType;
1315
+ return headers;
1316
+ }
1317
+ function parseHeaders(encoded = "{}") {
1318
+ try {
1319
+ return JSON.parse(encoded);
1320
+ } catch {
1321
+ return {};
1322
+ }
1323
+ }
1324
+ async function relayResponse(upstream, res) {
1325
+ const responseHeaders = {};
1326
+ upstream.headers.forEach((value, key) => {
1327
+ responseHeaders[key] = value;
1328
+ if (!DROPPED_RESPONSE_HEADERS.has(key)) res.setHeader(key, value);
1329
+ });
1330
+ res.status(upstream.status);
1331
+ res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
1332
+ const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
1333
+ res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
1334
+ res.flushHeaders();
1335
+ if (!upstream.body) {
1336
+ res.end();
1337
+ return;
1338
+ }
1339
+ await pipeline(Readable.fromWeb(upstream.body), res);
1340
+ }
1085
1341
 
1086
1342
  // src/server/hmr.ts
1343
+ var STYLE_FILE = /\.(wxss|less|scss|sass|css)$/;
1087
1344
  function planHotUpdate(files, units) {
1088
1345
  const owners = /* @__PURE__ */ new Set([...units.pages, ...units.components]);
1089
1346
  const plan = { templates: [], scripts: [], styles: [] };
1090
1347
  for (const file of files) {
1091
- const id = file.replace(/\.(wxml|js|wxss)$/, "");
1092
- const target = file.endsWith(".wxss") ? plan.styles : !owners.has(id) ? null : file.endsWith(".wxml") ? plan.templates : file.endsWith(".js") ? plan.scripts : null;
1348
+ const id = file.replace(/\.(wxml|js|wxss|less|scss|sass|css)$/, "");
1349
+ const target = STYLE_FILE.test(file) ? plan.styles : !owners.has(id) ? null : file.endsWith(".wxml") ? plan.templates : file.endsWith(".js") ? plan.scripts : null;
1093
1350
  if (!target) return null;
1094
1351
  if (!target.includes(id)) target.push(id);
1095
1352
  }
@@ -1293,16 +1550,16 @@ function respond(action) {
1293
1550
  }
1294
1551
 
1295
1552
  // src/server/setup.ts
1296
- import fs2 from "fs";
1297
- import path2 from "path";
1553
+ import fs4 from "fs";
1554
+ import path4 from "path";
1298
1555
  import { build } from "esbuild";
1299
1556
  var SETUP_FILE_NAMES = ["wxa.setup.ts", "wxa.setup.js", "wxa.setup.mjs"];
1300
1557
  var SETUP_MODULE = "@open-wxa/wxa-runner-core/setup";
1301
1558
  var ENV_PREFIX = "WXA_";
1302
1559
  function findSetupFile(projectPath) {
1303
1560
  for (const name of SETUP_FILE_NAMES) {
1304
- const file = path2.join(projectPath, name);
1305
- if (fs2.existsSync(file)) return file;
1561
+ const file = path4.join(projectPath, name);
1562
+ if (fs4.existsSync(file)) return file;
1306
1563
  }
1307
1564
  return null;
1308
1565
  }
@@ -1314,7 +1571,7 @@ function collectSetupEnv(env) {
1314
1571
  async function bundleSetup(file) {
1315
1572
  const result = await build({
1316
1573
  entryPoints: [file],
1317
- absWorkingDir: path2.dirname(file),
1574
+ absWorkingDir: path4.dirname(file),
1318
1575
  bundle: true,
1319
1576
  format: "esm",
1320
1577
  platform: "browser",
@@ -1342,12 +1599,12 @@ function builtinSetupModule() {
1342
1599
 
1343
1600
  // src/server/watcher.ts
1344
1601
  import { createHash } from "crypto";
1345
- import fs3 from "fs";
1346
- import path3 from "path";
1602
+ import fs5 from "fs";
1603
+ import path5 from "path";
1347
1604
  import chokidar from "chokidar";
1348
1605
  var DEBOUNCE = 120;
1349
1606
  var IGNORED = /(^|[/\\])(node_modules|\.git)([/\\]|$)/;
1350
- var FINGERPRINTED = /\.(js|mjs|ts|json|wxml|wxss|wxs)$/;
1607
+ var FINGERPRINTED = /\.(js|mjs|ts|json|wxml|wxss|wxs|less|scss|sass|css)$/;
1351
1608
  function watchProject(root, paths, onChange) {
1352
1609
  let changed = /* @__PURE__ */ new Set();
1353
1610
  let structural = false;
@@ -1371,7 +1628,7 @@ function watchProject(root, paths, onChange) {
1371
1628
  const unchanged = updateFingerprint(fingerprints, file, event);
1372
1629
  if (!scanned || unchanged) return;
1373
1630
  if (event !== "change") structural = true;
1374
- changed.add(path3.relative(root, file).split(path3.sep).join("/"));
1631
+ changed.add(path5.relative(root, file).split(path5.sep).join("/"));
1375
1632
  if (timer) clearTimeout(timer);
1376
1633
  timer = setTimeout(flush, DEBOUNCE);
1377
1634
  });
@@ -1385,7 +1642,7 @@ function updateFingerprint(fingerprints, file, event) {
1385
1642
  if (event !== "add" && event !== "change" || !FINGERPRINTED.test(file)) return false;
1386
1643
  let next;
1387
1644
  try {
1388
- next = createHash("sha1").update(fs3.readFileSync(file)).digest("hex");
1645
+ next = createHash("sha1").update(fs5.readFileSync(file)).digest("hex");
1389
1646
  } catch {
1390
1647
  return false;
1391
1648
  }
@@ -1394,7 +1651,30 @@ function updateFingerprint(fingerprints, file, event) {
1394
1651
  return previous === next;
1395
1652
  }
1396
1653
 
1654
+ // src/server/extensions.ts
1655
+ function applyExtensions(base, extensions, context) {
1656
+ const webviewScripts = [...base.webviewScripts ?? []];
1657
+ const middlewares = [...base.middlewares ?? []];
1658
+ const transforms = base.wxmlTransform ? [base.wxmlTransform] : [];
1659
+ const entries = [];
1660
+ for (const extension of extensions) {
1661
+ let setup;
1662
+ try {
1663
+ setup = extension.setup(context);
1664
+ } catch (error) {
1665
+ throw new Error(`\u6269\u5C55 ${extension.name} \u521D\u59CB\u5316\u5931\u8D25\uFF1A${error.message}`);
1666
+ }
1667
+ webviewScripts.push(...setup.webviewScripts ?? []);
1668
+ middlewares.push(...setup.middlewares ?? []);
1669
+ if (setup.wxmlTransform) transforms.push(setup.wxmlTransform);
1670
+ if (setup.entry) entries.push({ name: extension.name, ...setup.entry });
1671
+ }
1672
+ const wxmlTransform = transforms.length === 0 ? void 0 : (code, file) => transforms.reduce((result, transform) => transform(result, file), code);
1673
+ return { webviewScripts, middlewares, wxmlTransform, entries };
1674
+ }
1675
+
1397
1676
  // src/server/DevServer.ts
1677
+ var RESERVED_PREFIXES = ["/__wxa__", "/__baselib__", "/__plugin__"];
1398
1678
  var DEFAULT_DEVICE = {
1399
1679
  model: "iPhone X",
1400
1680
  screenWidth: 375,
@@ -1416,7 +1696,17 @@ var DevServer = class {
1416
1696
  /** 运行时生成的临时文件(canvas 导出、拍照等),随服务器进程存在 */
1417
1697
  this.fileSessions = /* @__PURE__ */ new Map();
1418
1698
  this.logger = options.logger || console;
1419
- this.project = new MiniProgram(path4.resolve(options.projectPath));
1699
+ this.project = new MiniProgram(path6.resolve(options.projectPath));
1700
+ for (const { plugin, files } of ignoredStyleSources(this.project, this.project.styleSetting)) {
1701
+ this.logger.warn(
1702
+ `[wxa-runner] ${files.length} \u4E2A ${plugin} \u6837\u5F0F\u6587\u4EF6\u4E0D\u4F1A\u751F\u6548\uFF08\u5982 ${files[0]}\uFF09\uFF1Aproject.config.json \u7684 setting.useCompilerPlugins \u6CA1\u6709\u5F00\u542F ${plugin}\uFF0C\u5F00\u53D1\u8005\u5DE5\u5177\u540C\u6837\u4E0D\u7F16\u8BD1\u5B83\u4EEC`
1703
+ );
1704
+ }
1705
+ const extended = applyExtensions(options, options.extensions ?? [], { projectPath: this.project.projectPath, root: this.project.root });
1706
+ extended.middlewares.forEach(({ path: mountPath }) => assertMountPath(mountPath));
1707
+ this.webviewScripts = extended.webviewScripts;
1708
+ this.middlewares = extended.middlewares;
1709
+ this.extensionEntries = extended.entries;
1420
1710
  this.baseLib = resolveBaseLib({ version: options.baseLibVersion, cacheDir: options.baseLibCacheDir });
1421
1711
  if (!this.baseLib.compatible) {
1422
1712
  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 +1718,7 @@ var DevServer = class {
1428
1718
  this.ticketSource = this.devtoolsLogin ? developerTicketSource(this.project.projectPath, this.logger) : null;
1429
1719
  this.plugins = new PluginStore(this.ticketSource && new WechatDevClient(this.project.appid, this.ticketSource), this.logger);
1430
1720
  this.device = { ...DEFAULT_DEVICE, ...options.device };
1431
- this.builder = new AppBuilder(this.project);
1721
+ this.builder = new AppBuilder(this.project, { wxmlTransform: extended.wxmlTransform });
1432
1722
  this.setupFile = resolveSetupFile(options.setup, this.project.projectPath);
1433
1723
  this.setupEnv = collectSetupEnv(process.env);
1434
1724
  this.app = this.createApp();
@@ -1441,7 +1731,7 @@ var DevServer = class {
1441
1731
  get networkUrls() {
1442
1732
  if (!this.lanAccess) return [];
1443
1733
  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}`);
1734
+ return Object.values(os3.networkInterfaces()).flat().filter((net2) => net2 && net2.family === "IPv4" && !net2.internal).map((net2) => `http://${net2.address}:${port}`);
1445
1735
  }
1446
1736
  async start() {
1447
1737
  try {
@@ -1484,11 +1774,15 @@ ${error.message}`);
1484
1774
  await new Promise((resolve) => server.close(() => resolve()));
1485
1775
  }
1486
1776
  this.fileSessions.clear();
1777
+ this.builder.dispose();
1487
1778
  }
1488
1779
  createApp() {
1489
1780
  const app = express2();
1490
1781
  const root = this.project.root;
1491
1782
  const noStore = (res) => res.setHeader("Cache-Control", "no-store");
1783
+ for (const { path: mountPath, handler } of this.middlewares) {
1784
+ app.use(mountPath, (req, res, next) => handler(req, res, next));
1785
+ }
1492
1786
  app.get("/", (_req, res) => {
1493
1787
  noStore(res);
1494
1788
  res.type("html").send(hostHtml());
@@ -1543,7 +1837,8 @@ ${error.message}`);
1543
1837
  const pagePath = decodeURIComponent(req.params[0]);
1544
1838
  if (!build2.pages.includes(pagePath)) return next();
1545
1839
  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 }));
1840
+ const scripts = this.webviewScripts;
1841
+ 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
1842
  }));
1548
1843
  app.get("/__wxa__/plugin/:provider/:version/:file(appservice.js|pageframe.js)", this.withBuild((build2, req, res, next) => {
1549
1844
  const { provider, version, file } = req.params;
@@ -1554,7 +1849,7 @@ ${error.message}`);
1554
1849
  app.get(/^\/__plugin__\/([^/]+)(\/.+)$/, this.withBuild((build2, req, res, next) => {
1555
1850
  const file = build2.plugins.find((plugin) => plugin.provider === req.params[0])?.files.get(decodeURIComponent(req.params[1]));
1556
1851
  if (!file) return next();
1557
- res.type(path4.extname(req.params[1]) || "application/octet-stream").send(file);
1852
+ res.type(path6.extname(req.params[1]) || "application/octet-stream").send(file);
1558
1853
  }));
1559
1854
  app.get(/^\/__wxa__\/module\/(.+\.js)$/, this.withBuild((build2, req, res) => {
1560
1855
  const id = decodeURIComponent(req.params[0]);
@@ -1565,7 +1860,7 @@ ${error.message}`);
1565
1860
  res.type("js").send(this.builder.getModuleScript(id));
1566
1861
  }));
1567
1862
  app.get("/__wxa__/worker.js", this.withBuild((build2, _req, res) => {
1568
- const hasWorker = fs4.existsSync(path4.join(this.baseLib.dir, "WAWorker.js"));
1863
+ const hasWorker = fs6.existsSync(path6.join(this.baseLib.dir, "WAWorker.js"));
1569
1864
  res.type("js").send(workerScript(build2, { version: this.baseLib.version, hasWorker }));
1570
1865
  }));
1571
1866
  app.get("/__wxa__/service-bootstrap.js", this.withBuild((build2, _req, res) => {
@@ -1584,28 +1879,28 @@ ${error.message}`);
1584
1879
  res.type("js").send(build2.pageFrame);
1585
1880
  }));
1586
1881
  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")));
1882
+ const file = path6.join(this.runtimeDir(), "frame.js.map");
1883
+ if (!fs6.existsSync(file)) return next();
1884
+ res.type("json").send(ignoreConsoleHook(fs6.readFileSync(file, "utf8")));
1590
1885
  });
1591
1886
  app.use("/__wxa__/runtime", express2.static(this.runtimeDir(), {
1592
1887
  cacheControl: false,
1593
1888
  // Service Worker 默认只能接管自己所在目录下的页面,宿主页面在根路径
1594
1889
  setHeaders: (res, file) => {
1595
- if (path4.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1890
+ if (path6.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1596
1891
  }
1597
1892
  }));
1598
1893
  const sourceMaps = /* @__PURE__ */ new Map();
1599
1894
  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`));
1895
+ const file = path6.resolve(this.baseLib.dir, req.params[0]);
1896
+ if (!file.startsWith(this.baseLib.dir + path6.sep) || !fs6.existsSync(file)) return next();
1897
+ if (!sourceMaps.has(file)) sourceMaps.set(file, ignoreListSourceMap(fs6.readFileSync(file, "utf8"), `${path6.basename(file)}?original`));
1603
1898
  res.type("json").send(sourceMaps.get(file));
1604
1899
  });
1605
1900
  app.use("/__baselib__", express2.static(this.baseLib.dir, {
1606
1901
  maxAge: "1d",
1607
1902
  setHeaders: (res, file) => {
1608
- if (file.endsWith(".js")) res.setHeader("SourceMap", `${path4.basename(file)}.map`);
1903
+ if (file.endsWith(".js")) res.setHeader("SourceMap", `${path6.basename(file)}.map`);
1609
1904
  }
1610
1905
  }));
1611
1906
  app.use(PROXY_PREFIX, express2.raw({ type: () => true, limit: "100mb" }), handleProxy);
@@ -1665,7 +1960,7 @@ ${error.message}`);
1665
1960
  return;
1666
1961
  }
1667
1962
  const data = node.data;
1668
- res.type(path4.extname(req.params[0]));
1963
+ res.type(path6.extname(req.params[0]));
1669
1964
  res.setHeader("ETag", `"${node.etag}"`);
1670
1965
  res.setHeader("Accept-Ranges", "bytes");
1671
1966
  const ranges = req.range(data.length);
@@ -1735,11 +2030,11 @@ ${error.message}`);
1735
2030
  runtimeDir() {
1736
2031
  const candidates = [
1737
2032
  this.options.runtimeDir,
1738
- path4.join(__dirname, "runtime"),
1739
- path4.join(__dirname, "../runtime"),
1740
- path4.join(__dirname, "../../dist/runtime")
2033
+ path6.join(__dirname, "runtime"),
2034
+ path6.join(__dirname, "../runtime"),
2035
+ path6.join(__dirname, "../../dist/runtime")
1741
2036
  ].filter((dir) => !!dir);
1742
- const found = candidates.find((dir) => fs4.existsSync(path4.join(dir, "host.js")));
2037
+ const found = candidates.find((dir) => fs6.existsSync(path6.join(dir, "host.js")));
1743
2038
  if (!found) {
1744
2039
  throw new Error("\u627E\u4E0D\u5230\u6D4F\u89C8\u5668\u7AEF\u8FD0\u884C\u65F6\uFF08host.js\uFF09\uFF0C\u8BF7\u5148\u6267\u884C pnpm build");
1745
2040
  }
@@ -1752,7 +2047,10 @@ ${error.message}`);
1752
2047
  startWatching() {
1753
2048
  let lastBuildFailed = false;
1754
2049
  const watched = [this.project.root];
1755
- if (this.setupFile && path4.relative(this.project.root, this.setupFile).startsWith("..")) watched.push(this.setupFile);
2050
+ const outsideRoot = (file) => path6.relative(this.project.root, file).startsWith("..");
2051
+ if (this.setupFile && outsideRoot(this.setupFile)) watched.push(this.setupFile);
2052
+ const projectConfig = path6.join(this.project.projectPath, "project.config.json");
2053
+ if (outsideRoot(projectConfig) && fs6.existsSync(projectConfig)) watched.push(projectConfig);
1756
2054
  this.watcher = watchProject(this.project.root, watched, async ({ files, structural }) => {
1757
2055
  const reloadOnly = structural || lastBuildFailed;
1758
2056
  this.builder.invalidate(files);
@@ -1783,7 +2081,7 @@ var DEFAULT_PORT = 3e3;
1783
2081
  var LOCAL_HOST_OF_ANY_ADDRESS = /* @__PURE__ */ new Map([["0.0.0.0", "127.0.0.1"], ["::", "localhost"]]);
1784
2082
  async function listen(server, port, host, logger) {
1785
2083
  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);
2084
+ const taken = await takenOnLocalAddresses(candidate) || !await bind(server, candidate, host);
1787
2085
  if (!taken) {
1788
2086
  const actual = server.address().port;
1789
2087
  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 +2100,8 @@ function bind(server, port, host) {
1802
2100
  });
1803
2101
  });
1804
2102
  }
1805
- async function takenOnLoopback(port) {
1806
- for (const address of ["127.0.0.1", "::1"]) {
2103
+ async function takenOnLocalAddresses(port) {
2104
+ for (const address of ["127.0.0.1", "::1", "0.0.0.0", "::"]) {
1807
2105
  const probe = net.createServer();
1808
2106
  const free = await bind(probe, port, address).catch(() => true);
1809
2107
  if (!free) return true;
@@ -1811,11 +2109,21 @@ async function takenOnLoopback(port) {
1811
2109
  }
1812
2110
  return false;
1813
2111
  }
2112
+ function assertMountPath(mountPath) {
2113
+ const literal = typeof mountPath === "string" && /^(\/[\w.~-]+)+$/.test(mountPath);
2114
+ const lower = literal ? mountPath.toLowerCase() : "";
2115
+ const reserved = RESERVED_PREFIXES.some((prefix) => lower === prefix || lower.startsWith(prefix + "/"));
2116
+ if (!literal || reserved) {
2117
+ throw new Error(
2118
+ `\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`
2119
+ );
2120
+ }
2121
+ }
1814
2122
  function resolveSetupFile(option, projectPath) {
1815
2123
  if (option === false) return null;
1816
2124
  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}`);
2125
+ const file = path6.resolve(option);
2126
+ if (!fs6.existsSync(file)) throw new Error(`\u627E\u4E0D\u5230 setup \u6587\u4EF6\uFF1A${file}`);
1819
2127
  return file;
1820
2128
  }
1821
2129
  export {