@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.js CHANGED
@@ -49,6 +49,47 @@ var FileStore = class {
49
49
  this.etagPrefix = (0, import_crypto.randomUUID)();
50
50
  this.version = 0;
51
51
  this.nodes = /* @__PURE__ */ new Map([["/", { dir: true, data: Buffer.alloc(0), mtime: Date.now(), etag: "" }]]);
52
+ this.operations = {
53
+ readFile: ({ filePath, encoding }) => {
54
+ const data = this.read(filePath);
55
+ return encoding === void 0 ? { data: data.toString("base64"), binary: true } : { data: data.toString(encoding) };
56
+ },
57
+ writeFile: (args) => this.writeEncoded(args, false),
58
+ appendFile: (args) => this.writeEncoded(args, true),
59
+ copyFile: ({ srcPath, destPath }) => {
60
+ this.write(destPath, this.read(srcPath));
61
+ return {};
62
+ },
63
+ access: ({ path: path7 }) => {
64
+ this.existing(path7);
65
+ return {};
66
+ },
67
+ stat: ({ path: path7 }) => {
68
+ const node = this.existing(path7);
69
+ const stats = {
70
+ mode: node.dir ? 16877 : 33188,
71
+ size: node.data.length,
72
+ lastAccessedTime: Math.floor(node.mtime / 1e3),
73
+ lastModifiedTime: Math.floor(node.mtime / 1e3)
74
+ };
75
+ return { stats, ...stats };
76
+ },
77
+ getFileInfo: ({ filePath }) => ({ size: this.read(filePath).length }),
78
+ mkdir: ({ dirPath, recursive }) => {
79
+ this.mkdir(dirPath, !!recursive);
80
+ return {};
81
+ },
82
+ readdir: ({ dirPath }) => ({ files: this.readdir(dirPath) }),
83
+ unlink: ({ filePath }) => {
84
+ this.remove(filePath, { dir: false });
85
+ return {};
86
+ },
87
+ rmdir: ({ dirPath, recursive }) => {
88
+ this.assertNotRoot(dirPath);
89
+ this.remove(dirPath, { dir: true, recursive: !!recursive });
90
+ return {};
91
+ }
92
+ };
52
93
  this.roots = roots.map((root) => this.normalize(root));
53
94
  for (const root of this.roots) this.mkdir(root, true);
54
95
  }
@@ -109,66 +150,37 @@ var FileStore = class {
109
150
  }
110
151
  this.nodes.delete(target);
111
152
  }
153
+ /** 文件 API 经开发服务器调用的操作,参数和返回值的字段名与基础库一致 */
112
154
  execute(operation, args) {
113
- switch (operation) {
114
- case "readFile": {
115
- const data = this.read(args.filePath);
116
- return args.encoding === void 0 ? { data: data.toString("base64"), binary: true } : { data: data.toString(args.encoding) };
117
- }
118
- case "writeFile":
119
- case "appendFile": {
120
- const data = Buffer.from(args.data ?? "", args.binary ? "base64" : args.encoding || "utf8");
121
- this.write(args.filePath, data, operation === "appendFile");
122
- return {};
123
- }
124
- case "copyFile":
125
- this.write(args.destPath, this.read(args.srcPath));
126
- return {};
127
- case "access":
128
- if (!this.stat(args.path)) throw new Error(`no such file or directory ${args.path}`);
129
- return {};
130
- case "stat": {
131
- const node = this.stat(args.path);
132
- if (!node) throw new Error(`no such file or directory ${args.path}`);
133
- const stats = {
134
- mode: node.dir ? 16877 : 33188,
135
- size: node.data.length,
136
- lastAccessedTime: Math.floor(node.mtime / 1e3),
137
- lastModifiedTime: Math.floor(node.mtime / 1e3)
138
- };
139
- return { stats, ...stats };
140
- }
141
- case "getFileInfo":
142
- return { size: this.read(args.filePath).length };
143
- case "mkdir":
144
- this.mkdir(args.dirPath, !!args.recursive);
145
- return {};
146
- case "readdir":
147
- return { files: this.readdir(args.dirPath) };
148
- case "unlink":
149
- this.remove(args.filePath, { dir: false });
150
- return {};
151
- case "rmdir": {
152
- const target = this.normalize(args.dirPath);
153
- const prefix = target === "/" ? "/" : target + "/";
154
- if (this.roots.some((root) => root === target || root.startsWith(prefix))) {
155
- throw new Error(`operation not permitted ${args.dirPath}`);
156
- }
157
- this.remove(args.dirPath, { dir: true, recursive: !!args.recursive });
158
- return {};
159
- }
160
- default:
161
- throw new Error(`unsupported filesystem operation ${operation}`);
155
+ if (!Object.prototype.hasOwnProperty.call(this.operations, operation)) {
156
+ throw new Error(`unsupported filesystem operation ${operation}`);
157
+ }
158
+ return this.operations[operation](args);
159
+ }
160
+ writeEncoded(args, append) {
161
+ this.write(args.filePath, Buffer.from(args.data ?? "", args.binary ? "base64" : args.encoding || "utf8"), append);
162
+ return {};
163
+ }
164
+ existing(filePath) {
165
+ const node = this.stat(filePath);
166
+ if (!node) throw new Error(`no such file or directory ${filePath}`);
167
+ return node;
168
+ }
169
+ assertNotRoot(dirPath) {
170
+ const target = this.normalize(dirPath);
171
+ const prefix = target === "/" ? "/" : target + "/";
172
+ if (this.roots.some((root) => root === target || root.startsWith(prefix))) {
173
+ throw new Error(`operation not permitted ${dirPath}`);
162
174
  }
163
175
  }
164
176
  };
165
177
 
166
178
  // src/server/DevServer.ts
167
- var import_fs4 = __toESM(require("fs"));
179
+ var import_fs6 = __toESM(require("fs"));
168
180
  var import_http = __toESM(require("http"));
169
181
  var import_net = __toESM(require("net"));
170
- var import_os = __toESM(require("os"));
171
- var import_path4 = __toESM(require("path"));
182
+ var import_os3 = __toESM(require("os"));
183
+ var import_path6 = __toESM(require("path"));
172
184
  var import_express2 = __toESM(require("express"));
173
185
  var import_ws2 = require("ws");
174
186
  var import_wxa_runner_extractor5 = require("@open-wxa/wxa-runner-extractor");
@@ -177,6 +189,164 @@ var import_wxa_runner_extractor5 = require("@open-wxa/wxa-runner-extractor");
177
189
  var import_wxa_runner_compiler = require("@open-wxa/wxa-runner-compiler");
178
190
  var import_esbuild = require("esbuild");
179
191
 
192
+ // src/project/StyleMirror.ts
193
+ var import_fs = __toESM(require("fs"));
194
+ var import_module = require("module");
195
+ var import_os = __toESM(require("os"));
196
+ var import_path = __toESM(require("path"));
197
+ var import_url = require("url");
198
+ var PLUGIN_EXTENSIONS = {
199
+ less: [".less"],
200
+ sass: [".sass", ".scss"]
201
+ };
202
+ var IMPORT_WXSS = /(?:^|\s)?(?:@import)(?:\s*)?(["'])([^"']+.wxss)\1(?:\s*);/g;
203
+ function readStyleSetting(setting) {
204
+ const plugins = [];
205
+ let lessGlobalVars = {};
206
+ for (const item of Array.isArray(setting?.useCompilerPlugins) ? setting.useCompilerPlugins : []) {
207
+ const [name, options] = Array.isArray(item) ? item : [item, void 0];
208
+ if (name !== "less" && name !== "sass" || plugins.includes(name)) continue;
209
+ plugins.push(name);
210
+ if (name === "less" && options?.globalVars) lessGlobalVars = options.globalVars;
211
+ }
212
+ return {
213
+ plugins,
214
+ lessGlobalVars,
215
+ lessCommonFile: setting?.lessSetting?.commonUseFilePath || "app.less",
216
+ sassCommonFile: setting?.sassSetting?.sassCommonUseFilePath || "global.sass",
217
+ scssCommonFile: setting?.sassSetting?.scssCommonUseFilePath || "global.scss"
218
+ };
219
+ }
220
+ function ignoredStyleSources(project, setting) {
221
+ 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);
222
+ }
223
+ var StyleMirror = class {
224
+ constructor(setting) {
225
+ this.setting = setting;
226
+ this.dir = null;
227
+ /** 已写入的 .wxss 与写入的内容 */
228
+ this.files = /* @__PURE__ */ new Map();
229
+ this.compilers = /* @__PURE__ */ new Map();
230
+ }
231
+ /** 镜像目录;还没同步过时为 null */
232
+ get directory() {
233
+ return this.dir;
234
+ }
235
+ /**
236
+ * 编译全部预处理文件,与原有的 .wxss 一起写进镜像目录。
237
+ * @returns 镜像目录,以及其中全部 .wxss 的相对路径
238
+ */
239
+ async sync(project) {
240
+ const sources = this.pickSources(project);
241
+ const outputs = await Promise.all(
242
+ [...sources].map(async ([target, source]) => [target, await this.compile(project, source)])
243
+ );
244
+ const dir = this.dir ??= import_fs.default.mkdtempSync(import_path.default.join(import_os.default.tmpdir(), "wxa-runner-wxss-"));
245
+ for (const rel of [...this.files.keys()]) {
246
+ if (sources.has(rel)) continue;
247
+ import_fs.default.rmSync(import_path.default.join(dir, rel), { force: true });
248
+ this.files.delete(rel);
249
+ }
250
+ for (const [rel, css] of outputs) {
251
+ if (this.files.get(rel) === css) continue;
252
+ const target = import_path.default.join(dir, rel);
253
+ import_fs.default.mkdirSync(import_path.default.dirname(target), { recursive: true });
254
+ import_fs.default.writeFileSync(target, css);
255
+ this.files.set(rel, css);
256
+ }
257
+ return { cwd: dir, files: [...sources.keys()] };
258
+ }
259
+ dispose() {
260
+ if (this.dir) import_fs.default.rmSync(this.dir, { recursive: true, force: true });
261
+ this.dir = null;
262
+ this.files.clear();
263
+ }
264
+ /** 每个 .wxss 由哪个源文件生成:键为 .wxss 的相对路径 */
265
+ pickSources(project) {
266
+ const extensions = [".wxss", ...this.setting.plugins.flatMap((plugin) => PLUGIN_EXTENSIONS[plugin])];
267
+ const sources = /* @__PURE__ */ new Map();
268
+ for (const file of project.listFiles(extensions)) {
269
+ const ext = import_path.default.extname(file);
270
+ const target = file.slice(0, -ext.length) + ".wxss";
271
+ const current = sources.get(target);
272
+ if (!current || extensions.indexOf(ext) > extensions.indexOf(import_path.default.extname(current))) sources.set(target, file);
273
+ }
274
+ return sources;
275
+ }
276
+ async compile(project, file) {
277
+ const ext = import_path.default.extname(file);
278
+ if (ext === ".wxss") return project.readText(file);
279
+ const compiler = await this.loadCompiler(ext === ".less" ? "less" : "sass", project);
280
+ try {
281
+ return ext === ".less" ? await this.compileLess(compiler, project, file) : this.compileSass(compiler, project, file);
282
+ } catch (error) {
283
+ throw new Error(`${file} \u7F16\u8BD1\u5931\u8D25\uFF1A${describeError(project, error)}`);
284
+ }
285
+ }
286
+ async compileLess(less, project, file) {
287
+ const common = this.setting.lessCommonFile;
288
+ const prefix = Object.entries(this.setting.lessGlobalVars).map(([name, value]) => `@${name.replace(/^@/, "")}: ${value};`);
289
+ if (file !== common && project.exists(common)) prefix.push(`@import (optional, reference) '${toRoot(file)}/${common}';`);
290
+ let code = prefix.map((statement) => statement + " ").join("") + project.readText(file);
291
+ code = code.replace(IMPORT_WXSS, (statement, _quote, target) => statement.replace(target, `${target}?css`));
292
+ const { css } = await less.render(code, {
293
+ filename: import_path.default.join(project.root, file),
294
+ paths: [project.root]
295
+ });
296
+ return css.replace(/(@import\s*["'][^"']+\.wxss)\?css(["'])/g, "$1$2");
297
+ }
298
+ compileSass(sass, project, file) {
299
+ const indented = file.endsWith(".sass");
300
+ const preferred = indented ? this.setting.sassCommonFile : this.setting.scssCommonFile;
301
+ const fallback = indented ? this.setting.scssCommonFile : this.setting.sassCommonFile;
302
+ const common = project.exists(preferred) ? preferred : project.exists(fallback) ? fallback : null;
303
+ let code = project.readText(file);
304
+ if (common && file !== preferred && file !== fallback) {
305
+ code = `@use '${toRoot(file)}/${common}'${indented ? "\n" : "; "}${code}`;
306
+ }
307
+ const imported = /* @__PURE__ */ new Set();
308
+ code = code.replace(IMPORT_WXSS, (statement, _quote, target) => {
309
+ imported.add(target);
310
+ return statement.replace(target, `${target.slice(0, -".wxss".length)}.css`);
311
+ });
312
+ const css = typeof sass.compileString === "function" ? sass.compileString(code, { url: (0, import_url.pathToFileURL)(import_path.default.join(project.root, file)), syntax: indented ? "indented" : "scss", loadPaths: [project.root] }).css : sass.renderSync({ data: code, file: import_path.default.join(project.root, file), indentedSyntax: indented, includePaths: [project.root] }).css.toString();
313
+ return css.replace(
314
+ /(@import\s*["'])([^"']+)\.css(["'])/g,
315
+ (statement, head, target, tail) => imported.has(`${target}.wxss`) ? `${head}${target}.wxss${tail}` : statement
316
+ );
317
+ }
318
+ /** 用到时才加载:先从小程序根目录和项目目录向上查找 node_modules,找不到时用 wxa-runner 自带的 */
319
+ loadCompiler(name, project) {
320
+ let loading = this.compilers.get(name);
321
+ if (!loading) {
322
+ loading = importCompiler(name, project);
323
+ this.compilers.set(name, loading);
324
+ }
325
+ return loading;
326
+ }
327
+ };
328
+ async function importCompiler(name, project) {
329
+ let resolved = null;
330
+ try {
331
+ resolved = (0, import_module.createRequire)(import_path.default.join(project.root, "package.json")).resolve(name, { paths: [project.root, project.projectPath] });
332
+ } catch {
333
+ }
334
+ const module2 = resolved ? await import((0, import_url.pathToFileURL)(resolved).href) : name === "less" ? await import("less") : await import("sass");
335
+ const entry = name === "less" ? "render" : "renderSync";
336
+ return typeof module2[entry] === "function" ? module2 : module2.default;
337
+ }
338
+ function toRoot(file) {
339
+ const dir = import_path.default.posix.dirname(file);
340
+ return dir === "." ? "." : dir.split("/").map(() => "..").join("/");
341
+ }
342
+ function describeError(project, error) {
343
+ if (typeof error?.line === "number" && error.filename) {
344
+ const file = import_path.default.relative(project.root, error.filename).split(import_path.default.sep).join("/");
345
+ return `${file}:${error.line}:${(error.column ?? 0) + 1} ${error.message}`;
346
+ }
347
+ return error?.message ?? String(error);
348
+ }
349
+
180
350
  // src/project/wxConfig.ts
181
351
  var DEFAULT_WINDOW = {
182
352
  navigationBarBackgroundColor: "#000000",
@@ -233,6 +403,50 @@ function createWxConfig(input) {
233
403
  };
234
404
  }
235
405
 
406
+ // src/project/WxmlMirror.ts
407
+ var import_fs2 = __toESM(require("fs"));
408
+ var import_os2 = __toESM(require("os"));
409
+ var import_path2 = __toESM(require("path"));
410
+ var WxmlMirror = class {
411
+ constructor(transform) {
412
+ this.transform = transform;
413
+ this.dir = null;
414
+ /** 已写入的文件:源内容(判断要不要重新转换)与写入的内容 */
415
+ this.files = /* @__PURE__ */ new Map();
416
+ }
417
+ /** 镜像目录;还没同步过时为 null */
418
+ get directory() {
419
+ return this.dir;
420
+ }
421
+ /**
422
+ * 让镜像目录与项目里的 wxml / wxs 一致,返回镜像目录。
423
+ * 只重写源内容变化的文件;项目里已删除的文件同时从镜像目录删除,被引用时 wcc 照常报错
424
+ */
425
+ sync(project, relPaths) {
426
+ const dir = this.dir ??= import_fs2.default.mkdtempSync(import_path2.default.join(import_os2.default.tmpdir(), "wxa-runner-wxml-"));
427
+ const current = new Set(relPaths);
428
+ for (const rel of [...this.files.keys()]) {
429
+ if (current.has(rel)) continue;
430
+ import_fs2.default.rmSync(import_path2.default.join(dir, rel), { force: true });
431
+ this.files.delete(rel);
432
+ }
433
+ for (const rel of relPaths) {
434
+ const source = project.readText(rel);
435
+ if (this.files.get(rel) === source) continue;
436
+ const target = import_path2.default.join(dir, rel);
437
+ import_fs2.default.mkdirSync(import_path2.default.dirname(target), { recursive: true });
438
+ import_fs2.default.writeFileSync(target, rel.endsWith(".wxml") ? this.transform(source, rel) : source);
439
+ this.files.set(rel, source);
440
+ }
441
+ return dir;
442
+ }
443
+ dispose() {
444
+ if (this.dir) import_fs2.default.rmSync(this.dir, { recursive: true, force: true });
445
+ this.dir = null;
446
+ this.files.clear();
447
+ }
448
+ };
449
+
236
450
  // src/project/AppBuilder.ts
237
451
  var MODULE_PARAMS = [
238
452
  "require",
@@ -259,7 +473,7 @@ var MODULE_PARAMS = [
259
473
  ].join(", ");
260
474
  var ESM_SYNTAX = /(?:^|[;}])\s*(?:import\s*[{*'"]|import\s+[\w$]|export\s*[{*]|export\s+(?:default|const|let|var|function|class|async)\b)/m;
261
475
  var AppBuilder = class {
262
- constructor(project) {
476
+ constructor(project, options = {}) {
263
477
  this.project = project;
264
478
  this.version = 0;
265
479
  /** invalidate 标记过的变化次数;每次构建记下开始时覆盖到第几次 */
@@ -269,6 +483,18 @@ var AppBuilder = class {
269
483
  this.dirty = /* @__PURE__ */ new Set(["config", "wxml", "wxss", "js"]);
270
484
  this.wxmlCache = null;
271
485
  this.wxssCache = null;
486
+ /** 开启了 less / sass 编译插件时才有;插件设置变了就换一个 */
487
+ this.styleMirror = null;
488
+ this.wxmlMirror = options.wxmlTransform ? new WxmlMirror(options.wxmlTransform) : null;
489
+ }
490
+ /** wxmlTransform 的镜像目录;没有设置转换或还没编译过时为 null */
491
+ get wxmlMirrorDir() {
492
+ return this.wxmlMirror?.directory ?? null;
493
+ }
494
+ /** 删除 wxmlTransform 和样式预处理的镜像目录,开发服务器关闭时调用 */
495
+ dispose() {
496
+ this.wxmlMirror?.dispose();
497
+ this.styleMirror?.mirror.dispose();
272
498
  }
273
499
  /** 根据变化的文件标记需要重新构建的部分;返回 true 表示需要刷新 */
274
500
  invalidate(changedFiles = []) {
@@ -393,7 +619,7 @@ ${pageStyles}`,
393
619
  const json = unitJson.get(id);
394
620
  return { path: id, tags: Object.keys({ ...json.usingComponents, ...json.componentGenerics }) };
395
621
  });
396
- const cwd = this.project.root;
622
+ const cwd = this.wxmlMirror ? this.wxmlMirror.sync(this.project, files.map((file) => file.slice(2))) : this.project.root;
397
623
  const [full, cut] = await Promise.all([
398
624
  (0, import_wxa_runner_compiler.compileWxml)({ cwd, files, components, cut: false }),
399
625
  (0, import_wxa_runner_compiler.compileWxml)({ cwd, files, components, cut: true })
@@ -402,14 +628,26 @@ ${pageStyles}`,
402
628
  }
403
629
  /** 与开发者工具一致,项目里的 wxss 全部交给 wcsc:被 @import 的文件(如 uni-app 的 common/main.wxss)也要在列表里 */
404
630
  async compileStyles(units) {
405
- const pageFiles = units.filter((id) => this.project.exists(id + ".wxss")).map((id) => `./${id}.wxss`);
406
- const commonFiles = this.project.listFiles([".wxss"]).map((file) => `./${file}`).filter((file) => !pageFiles.includes(file));
407
- const { comm, pages } = await (0, import_wxa_runner_compiler.compileWxss)({ cwd: this.project.root, pageFiles, commonFiles });
631
+ const mirror = this.currentStyleMirror();
632
+ const { cwd, files } = mirror ? await mirror.sync(this.project) : { cwd: this.project.root, files: this.project.listFiles([".wxss"]) };
633
+ const styles = new Set(files);
634
+ const pageFiles = units.filter((id) => styles.has(id + ".wxss")).map((id) => `./${id}.wxss`);
635
+ const commonFiles = files.map((file) => `./${file}`).filter((file) => !pageFiles.includes(file));
636
+ const { comm, pages } = await (0, import_wxa_runner_compiler.compileWxss)({ cwd, pageFiles, commonFiles });
408
637
  return {
409
638
  comm: rewriteSafeAreaInsets(comm),
410
639
  pages: Object.fromEntries(Object.entries(pages).map(([file, css]) => [file, rewriteSafeAreaInsets(css)]))
411
640
  };
412
641
  }
642
+ currentStyleMirror() {
643
+ const setting = this.project.styleSetting;
644
+ const key = JSON.stringify(setting);
645
+ if (this.styleMirror?.setting !== key) {
646
+ this.styleMirror?.mirror.dispose();
647
+ this.styleMirror = setting.plugins.length > 0 ? { setting: key, mirror: new StyleMirror(setting) } : null;
648
+ }
649
+ return this.styleMirror?.mirror ?? null;
650
+ }
413
651
  /** $gwx 加上各页面、组件的 json / wxml 注册;逻辑层用裁剪版 $gwx,渲染层用完整版 */
414
652
  createTemplates(app, units, unitJson, gwx) {
415
653
  const lines = [gwx, `__wxAppCode__["app.json"] = ${toJs(app)};`];
@@ -454,28 +692,32 @@ function toJs(value) {
454
692
  function classifyFile(file) {
455
693
  if (file.endsWith(".json")) return "config";
456
694
  if (file.endsWith(".wxml") || file.endsWith(".wxs")) return "wxml";
457
- if (file.endsWith(".wxss")) return "wxss";
695
+ if (/\.(wxss|less|scss|sass|css)$/.test(file)) return "wxss";
458
696
  if (file.endsWith(".js")) return "js";
459
697
  return null;
460
698
  }
461
699
 
462
700
  // src/project/MiniProgram.ts
463
- var import_fs = __toESM(require("fs"));
464
- var import_path = __toESM(require("path"));
701
+ var import_fs3 = __toESM(require("fs"));
702
+ var import_path3 = __toESM(require("path"));
465
703
  var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".idea", ".vscode"]);
466
704
  var PROJECT_FILES = /* @__PURE__ */ new Set(["project.config.json", "project.private.config.json", "sitemap.json", "app.json", "wxa.setup.js"]);
467
705
  var MiniProgram = class {
468
706
  constructor(projectPath) {
469
707
  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 || "");
708
+ const projectConfig = readJsonFile(import_path3.default.join(projectPath, "project.config.json")) || {};
709
+ this.root = import_path3.default.resolve(projectPath, projectConfig.miniprogramRoot || "");
472
710
  this.appid = projectConfig.appid || "touristappid";
473
- if (!import_fs.default.existsSync(import_path.default.join(this.root, "app.json"))) {
711
+ if (!import_fs3.default.existsSync(import_path3.default.join(this.root, "app.json"))) {
474
712
  throw new Error(`${this.root} \u4E0B\u6CA1\u6709 app.json\uFF0C\u4E0D\u662F\u6709\u6548\u7684\u5C0F\u7A0B\u5E8F\u76EE\u5F55`);
475
713
  }
476
714
  }
715
+ /** project.config.json 里与 less / sass 编译插件有关的设置;每次重新读取,开发中修改了也能生效 */
716
+ get styleSetting() {
717
+ return readStyleSetting(readJsonFile(import_path3.default.join(this.projectPath, "project.config.json"))?.setting);
718
+ }
477
719
  readAppJson() {
478
- const app = readJsonFile(import_path.default.join(this.root, "app.json"));
720
+ const app = readJsonFile(import_path3.default.join(this.root, "app.json"));
479
721
  if (!app || !Array.isArray(app.pages) || app.pages.length === 0) {
480
722
  throw new Error("app.json \u7F3A\u5C11 pages \u914D\u7F6E");
481
723
  }
@@ -487,14 +729,14 @@ var MiniProgram = class {
487
729
  /** Worker 代码所在目录,不带首尾的 /;没有配置 workers 时为 null */
488
730
  workersDir(app) {
489
731
  const dir = typeof app.workers === "string" ? app.workers : app.workers?.path;
490
- return dir ? import_path.default.posix.normalize(dir).replace(/^\/+|\/+$/g, "") : null;
732
+ return dir ? import_path3.default.posix.normalize(dir).replace(/^\/+|\/+$/g, "") : null;
491
733
  }
492
734
  /** 主包页面 + 分包页面(带 root 前缀) */
493
735
  allPages(app) {
494
736
  const pages = [...app.pages];
495
737
  for (const pkg of this.subPackages(app)) {
496
738
  for (const page of pkg.pages) {
497
- const full = import_path.default.posix.join(pkg.root, page);
739
+ const full = import_path3.default.posix.join(pkg.root, page);
498
740
  if (!pages.includes(full)) pages.push(full);
499
741
  }
500
742
  }
@@ -506,7 +748,7 @@ var MiniProgram = class {
506
748
  * 所以能解析到的引用都换成组件的绝对路径
507
749
  */
508
750
  readUnitJson(id, app) {
509
- const json = { ...readJsonFile(import_path.default.join(this.root, id + ".json")) };
751
+ const json = { ...readJsonFile(import_path3.default.join(this.root, id + ".json")) };
510
752
  const resolve = (fromId, ref) => {
511
753
  const resolved = this.resolveComponent(fromId, ref);
512
754
  return resolved ? "/" + resolved : null;
@@ -527,7 +769,7 @@ var MiniProgram = class {
527
769
  if (resolved || ref.startsWith("/") || /^plugin(-private)?:\/\//.test(ref)) {
528
770
  json.usingComponents[tag] = resolved ?? ref;
529
771
  } else {
530
- const relative = import_path.default.posix.relative(import_path.default.posix.dirname(id), ref);
772
+ const relative = import_path3.default.posix.relative(import_path3.default.posix.dirname(id), ref);
531
773
  json.usingComponents[tag] = relative.startsWith(".") ? relative : "./" + relative;
532
774
  }
533
775
  }
@@ -540,25 +782,25 @@ var MiniProgram = class {
540
782
  */
541
783
  resolveComponent(fromId, ref) {
542
784
  if (!ref || ref.startsWith("plugin://") || ref.startsWith("plugin-private://")) return null;
543
- const fromDir = import_path.default.posix.dirname(fromId);
785
+ const fromDir = import_path3.default.posix.dirname(fromId);
544
786
  const candidates = [];
545
787
  if (ref.startsWith("/")) {
546
788
  candidates.push(ref.slice(1));
547
789
  } else {
548
- candidates.push(import_path.default.posix.join(fromDir, ref));
790
+ candidates.push(import_path3.default.posix.join(fromDir, ref));
549
791
  if (!ref.startsWith(".")) {
550
792
  let dir = fromDir;
551
793
  for (; ; ) {
552
- candidates.push(import_path.default.posix.join(dir === "." ? "" : dir, "miniprogram_npm", ref));
794
+ candidates.push(import_path3.default.posix.join(dir === "." ? "" : dir, "miniprogram_npm", ref));
553
795
  if (dir === "." || dir === "") break;
554
- dir = import_path.default.posix.dirname(dir);
796
+ dir = import_path3.default.posix.dirname(dir);
555
797
  }
556
798
  }
557
799
  }
558
800
  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;
801
+ const normalized = import_path3.default.posix.normalize(candidate);
802
+ for (const id of [normalized, import_path3.default.posix.join(normalized, "index")]) {
803
+ if (import_fs3.default.existsSync(import_path3.default.join(this.root, id + ".json"))) return id;
562
804
  }
563
805
  }
564
806
  return null;
@@ -590,11 +832,11 @@ var MiniProgram = class {
590
832
  listFiles(extensions) {
591
833
  const result = [];
592
834
  const walk = (dir, rel) => {
593
- for (const entry of import_fs.default.readdirSync(dir, { withFileTypes: true })) {
835
+ for (const entry of import_fs3.default.readdirSync(dir, { withFileTypes: true })) {
594
836
  if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
595
837
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
596
838
  if (entry.isDirectory()) {
597
- walk(import_path.default.join(dir, entry.name), childRel);
839
+ walk(import_path3.default.join(dir, entry.name), childRel);
598
840
  } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
599
841
  if (!rel && PROJECT_FILES.has(entry.name)) continue;
600
842
  result.push(childRel);
@@ -605,18 +847,18 @@ var MiniProgram = class {
605
847
  return result.sort();
606
848
  }
607
849
  exists(relPath) {
608
- return import_fs.default.existsSync(import_path.default.join(this.root, relPath));
850
+ return import_fs3.default.existsSync(import_path3.default.join(this.root, relPath));
609
851
  }
610
852
  readText(relPath) {
611
- return import_fs.default.readFileSync(import_path.default.join(this.root, relPath), "utf8");
853
+ return import_fs3.default.readFileSync(import_path3.default.join(this.root, relPath), "utf8");
612
854
  }
613
855
  isInIndependentSubPackage(id, app) {
614
856
  return this.subPackages(app).some((pkg) => pkg.independent && id.startsWith(pkg.root.replace(/\/?$/, "/")));
615
857
  }
616
858
  };
617
859
  function readJsonFile(file) {
618
- if (!import_fs.default.existsSync(file)) return null;
619
- const text = import_fs.default.readFileSync(file, "utf8");
860
+ if (!import_fs3.default.existsSync(file)) return null;
861
+ const text = import_fs3.default.readFileSync(file, "utf8");
620
862
  try {
621
863
  return JSON.parse(text);
622
864
  } catch (error) {
@@ -682,9 +924,9 @@ var WechatDevClient = class {
682
924
  *
683
925
  * @throws 没有可用的登录态、超时、连不上、HTTP 状态不是 2xx 时,消息是给用户看的原因
684
926
  */
685
- async post(path5, body, timeout = DEFAULT_TIMEOUT) {
927
+ async post(path7, body, timeout = DEFAULT_TIMEOUT) {
686
928
  const query = new URLSearchParams({ appid: this.appid, newticket: requireUsableTicket(await this.findTicket()), clientversion: CLIENT_VERSION });
687
- const response = await fetch(`${BASE_URL}/${path5}?${query}`, {
929
+ const response = await fetch(`${BASE_URL}/${path7}?${query}`, {
688
930
  method: "POST",
689
931
  headers: { "Content-Type": "application/json" },
690
932
  body: JSON.stringify(body),
@@ -696,8 +938,8 @@ var WechatDevClient = class {
696
938
  return response;
697
939
  }
698
940
  /** 同 post,返回解析后的 JSON */
699
- async postJson(path5, body) {
700
- return (await this.post(path5, body)).json();
941
+ async postJson(path7, body) {
942
+ return (await this.post(path7, body)).json();
701
943
  }
702
944
  };
703
945
 
@@ -893,12 +1135,12 @@ function webviewHtml(build2, pagePath, webviewId, options) {
893
1135
  return `<!DOCTYPE html>
894
1136
  <html>
895
1137
  <head>
896
- ${webviewHead(build2, pagePath, id)}
1138
+ ${webviewHead(build2, pagePath, id, options.scripts)}
897
1139
  ${enableHotUpdate(options.hot)}<script src="/__wxa__/page-frame.js?v=${build2.version}"></script>
898
1140
  ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</style>" : ""}
899
1141
  </head>
900
1142
  <body>
901
- <script>
1143
+ ${scriptTags(options.scripts, "body")}<script>
902
1144
  (function () {
903
1145
  var path = ${toJs(pagePath)};
904
1146
  if (typeof __wxAppCode__[path + '.wxss'] === 'function') __wxAppCode__[path + '.wxss']();
@@ -931,18 +1173,18 @@ self.console = __wxaConsole;
931
1173
  importScripts(${modules.map((url) => toJs(url)).join(", ")});
932
1174
  `;
933
1175
  }
934
- function pluginWebviewHtml(build2, plugin, innerPath, pagePath, webviewId) {
1176
+ function pluginWebviewHtml(build2, plugin, innerPath, pagePath, webviewId, options = {}) {
935
1177
  const id = Number(webviewId) || 0;
936
1178
  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
1179
  return `<!DOCTYPE html>
938
1180
  <html>
939
1181
  <head>
940
- ${webviewHead(build2, pagePath, id)}
1182
+ ${webviewHead(build2, pagePath, id, options.scripts)}
941
1183
  <script src="${pluginScriptUrl(plugin, "pageframe.js")}"></script>
942
1184
  ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</style>" : ""}
943
1185
  </head>
944
1186
  <body>
945
- ${page}
1187
+ ${scriptTags(options.scripts, "body")}${page}
946
1188
  <script>parent.__wxaHost.frameLoaded('webview', ${id});</script>
947
1189
  </body>
948
1190
  </html>`;
@@ -950,7 +1192,7 @@ ${page}
950
1192
  function pageWindow(build2, pagePath) {
951
1193
  return { ...build2.wxConfig.global?.window, ...build2.wxConfig.page?.[pagePath + ".html"]?.window };
952
1194
  }
953
- function webviewHead(build2, pagePath, id) {
1195
+ function webviewHead(build2, pagePath, id, scripts) {
954
1196
  const wxConfig = { ...build2.wxConfig, window: pageWindow(build2, pagePath) };
955
1197
  return `<meta charset="utf-8">
956
1198
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
@@ -965,7 +1207,12 @@ function webviewHead(build2, pagePath, id) {
965
1207
  <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
1208
  <script>var __wxConfig = ${toJs(wxConfig)};</script>
967
1209
  <script src="/__wxa__/runtime/frame.js" data-kind="webview" data-webview-id="${id}"></script>
968
- <script src="/__baselib__/WAWebview.js"></script>`;
1210
+ ${scriptTags(scripts, "head")}<script src="/__baselib__/WAWebview.js"></script>`;
1211
+ }
1212
+ function scriptTags(scripts, position) {
1213
+ return (scripts ?? []).filter((script) => (script.position ?? "body") === position).map((script) => script.src !== void 0 ? `<script src="${escapeHtml(script.src)}"></script>
1214
+ ` : `<script>${script.content.replace(/<\/script/gi, "<\\/script")}</script>
1215
+ `).join("");
969
1216
  }
970
1217
  function escapeHtml(text) {
971
1218
  return text.replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
@@ -1054,28 +1301,9 @@ var DROPPED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "con
1054
1301
  async function handleProxy(req, res) {
1055
1302
  const target = fromServerPath(PROXY_PREFIX, req.originalUrl);
1056
1303
  if (!target) {
1057
- res.status(400).set("x-wxa-proxy-error", "1").send(`invalid url "${req.originalUrl}"`);
1304
+ sendProxyError(res, 400, `invalid url "${req.originalUrl}"`);
1058
1305
  return;
1059
1306
  }
1060
- let requestHeaders = {};
1061
- try {
1062
- requestHeaders = JSON.parse(req.get("x-wxa-headers") || "{}");
1063
- } catch {
1064
- }
1065
- const headers = {};
1066
- for (const [key, value] of Object.entries(requestHeaders)) {
1067
- if (!DROPPED_REQUEST_HEADERS.has(key.toLowerCase()) && value !== void 0 && value !== null) {
1068
- headers[key] = String(value);
1069
- }
1070
- }
1071
- const browserContentType = req.get("content-type") || "";
1072
- if (browserContentType.startsWith("multipart/form-data")) {
1073
- for (const key of Object.keys(headers)) {
1074
- if (key.toLowerCase() === "content-type") delete headers[key];
1075
- }
1076
- headers["content-type"] = browserContentType;
1077
- }
1078
- const hasBody = !["GET", "HEAD"].includes(req.method) && Buffer.isBuffer(req.body) && req.body.length > 0;
1079
1307
  const controller = new AbortController();
1080
1308
  const cancel = () => controller.abort();
1081
1309
  res.once("close", cancel);
@@ -1084,48 +1312,77 @@ async function handleProxy(req, res) {
1084
1312
  try {
1085
1313
  upstream = await fetch(target, {
1086
1314
  method: req.method,
1087
- headers,
1088
- body: hasBody ? req.body : void 0,
1315
+ headers: forwardedRequestHeaders(req.get("x-wxa-headers"), req.get("content-type")),
1316
+ body: requestBody(req),
1089
1317
  redirect: "follow",
1090
1318
  signal: controller.signal
1091
1319
  });
1092
1320
  } catch (error) {
1093
1321
  if (res.destroyed || controller.signal.aborted) return;
1094
- const cause = error?.cause?.message;
1095
- res.status(502).set("x-wxa-proxy-error", "1").send(cause || error.message);
1096
- return;
1097
- }
1098
- const responseHeaders = {};
1099
- upstream.headers.forEach((value, key) => {
1100
- responseHeaders[key] = value;
1101
- });
1102
- const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
1103
- res.status(upstream.status);
1104
- upstream.headers.forEach((value, key) => {
1105
- if (!DROPPED_RESPONSE_HEADERS.has(key)) res.setHeader(key, value);
1106
- });
1107
- res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
1108
- res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
1109
- res.flushHeaders();
1110
- if (!upstream.body) {
1111
- res.end();
1322
+ sendProxyError(res, 502, fetchErrorMessage(error));
1112
1323
  return;
1113
1324
  }
1114
- await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(upstream.body), res);
1325
+ await relayResponse(upstream, res);
1115
1326
  } catch (error) {
1116
1327
  if (!res.destroyed) res.destroy(error);
1117
1328
  } finally {
1118
1329
  res.off("close", cancel);
1119
1330
  }
1120
1331
  }
1332
+ function requestBody(req) {
1333
+ const hasBody = !["GET", "HEAD"].includes(req.method) && Buffer.isBuffer(req.body) && req.body.length > 0;
1334
+ return hasBody ? req.body : void 0;
1335
+ }
1336
+ function fetchErrorMessage(error) {
1337
+ return error?.cause?.message || error.message;
1338
+ }
1339
+ function sendProxyError(res, status, message) {
1340
+ res.status(status).set("x-wxa-proxy-error", "1").send(message);
1341
+ }
1342
+ function forwardedRequestHeaders(encoded, browserContentType = "") {
1343
+ const multipart = browserContentType.startsWith("multipart/form-data");
1344
+ const headers = {};
1345
+ for (const [key, value] of Object.entries(parseHeaders(encoded))) {
1346
+ const name = key.toLowerCase();
1347
+ if (DROPPED_REQUEST_HEADERS.has(name) || multipart && name === "content-type" || value === void 0 || value === null) continue;
1348
+ headers[key] = String(value);
1349
+ }
1350
+ if (multipart) headers["content-type"] = browserContentType;
1351
+ return headers;
1352
+ }
1353
+ function parseHeaders(encoded = "{}") {
1354
+ try {
1355
+ return JSON.parse(encoded);
1356
+ } catch {
1357
+ return {};
1358
+ }
1359
+ }
1360
+ async function relayResponse(upstream, res) {
1361
+ const responseHeaders = {};
1362
+ upstream.headers.forEach((value, key) => {
1363
+ responseHeaders[key] = value;
1364
+ if (!DROPPED_RESPONSE_HEADERS.has(key)) res.setHeader(key, value);
1365
+ });
1366
+ res.status(upstream.status);
1367
+ res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
1368
+ const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
1369
+ res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
1370
+ res.flushHeaders();
1371
+ if (!upstream.body) {
1372
+ res.end();
1373
+ return;
1374
+ }
1375
+ await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(upstream.body), res);
1376
+ }
1121
1377
 
1122
1378
  // src/server/hmr.ts
1379
+ var STYLE_FILE = /\.(wxss|less|scss|sass|css)$/;
1123
1380
  function planHotUpdate(files, units) {
1124
1381
  const owners = /* @__PURE__ */ new Set([...units.pages, ...units.components]);
1125
1382
  const plan = { templates: [], scripts: [], styles: [] };
1126
1383
  for (const file of files) {
1127
- const id = file.replace(/\.(wxml|js|wxss)$/, "");
1128
- const target = file.endsWith(".wxss") ? plan.styles : !owners.has(id) ? null : file.endsWith(".wxml") ? plan.templates : file.endsWith(".js") ? plan.scripts : null;
1384
+ const id = file.replace(/\.(wxml|js|wxss|less|scss|sass|css)$/, "");
1385
+ const target = STYLE_FILE.test(file) ? plan.styles : !owners.has(id) ? null : file.endsWith(".wxml") ? plan.templates : file.endsWith(".js") ? plan.scripts : null;
1129
1386
  if (!target) return null;
1130
1387
  if (!target.includes(id)) target.push(id);
1131
1388
  }
@@ -1329,16 +1586,16 @@ function respond(action) {
1329
1586
  }
1330
1587
 
1331
1588
  // src/server/setup.ts
1332
- var import_fs2 = __toESM(require("fs"));
1333
- var import_path2 = __toESM(require("path"));
1589
+ var import_fs4 = __toESM(require("fs"));
1590
+ var import_path4 = __toESM(require("path"));
1334
1591
  var import_esbuild2 = require("esbuild");
1335
1592
  var SETUP_FILE_NAMES = ["wxa.setup.ts", "wxa.setup.js", "wxa.setup.mjs"];
1336
1593
  var SETUP_MODULE = "@open-wxa/wxa-runner-core/setup";
1337
1594
  var ENV_PREFIX = "WXA_";
1338
1595
  function findSetupFile(projectPath) {
1339
1596
  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;
1597
+ const file = import_path4.default.join(projectPath, name);
1598
+ if (import_fs4.default.existsSync(file)) return file;
1342
1599
  }
1343
1600
  return null;
1344
1601
  }
@@ -1350,7 +1607,7 @@ function collectSetupEnv(env) {
1350
1607
  async function bundleSetup(file) {
1351
1608
  const result = await (0, import_esbuild2.build)({
1352
1609
  entryPoints: [file],
1353
- absWorkingDir: import_path2.default.dirname(file),
1610
+ absWorkingDir: import_path4.default.dirname(file),
1354
1611
  bundle: true,
1355
1612
  format: "esm",
1356
1613
  platform: "browser",
@@ -1378,12 +1635,12 @@ function builtinSetupModule() {
1378
1635
 
1379
1636
  // src/server/watcher.ts
1380
1637
  var import_crypto3 = require("crypto");
1381
- var import_fs3 = __toESM(require("fs"));
1382
- var import_path3 = __toESM(require("path"));
1638
+ var import_fs5 = __toESM(require("fs"));
1639
+ var import_path5 = __toESM(require("path"));
1383
1640
  var import_chokidar = __toESM(require("chokidar"));
1384
1641
  var DEBOUNCE = 120;
1385
1642
  var IGNORED = /(^|[/\\])(node_modules|\.git)([/\\]|$)/;
1386
- var FINGERPRINTED = /\.(js|mjs|ts|json|wxml|wxss|wxs)$/;
1643
+ var FINGERPRINTED = /\.(js|mjs|ts|json|wxml|wxss|wxs|less|scss|sass|css)$/;
1387
1644
  function watchProject(root, paths, onChange) {
1388
1645
  let changed = /* @__PURE__ */ new Set();
1389
1646
  let structural = false;
@@ -1407,7 +1664,7 @@ function watchProject(root, paths, onChange) {
1407
1664
  const unchanged = updateFingerprint(fingerprints, file, event);
1408
1665
  if (!scanned || unchanged) return;
1409
1666
  if (event !== "change") structural = true;
1410
- changed.add(import_path3.default.relative(root, file).split(import_path3.default.sep).join("/"));
1667
+ changed.add(import_path5.default.relative(root, file).split(import_path5.default.sep).join("/"));
1411
1668
  if (timer) clearTimeout(timer);
1412
1669
  timer = setTimeout(flush, DEBOUNCE);
1413
1670
  });
@@ -1421,7 +1678,7 @@ function updateFingerprint(fingerprints, file, event) {
1421
1678
  if (event !== "add" && event !== "change" || !FINGERPRINTED.test(file)) return false;
1422
1679
  let next;
1423
1680
  try {
1424
- next = (0, import_crypto3.createHash)("sha1").update(import_fs3.default.readFileSync(file)).digest("hex");
1681
+ next = (0, import_crypto3.createHash)("sha1").update(import_fs5.default.readFileSync(file)).digest("hex");
1425
1682
  } catch {
1426
1683
  return false;
1427
1684
  }
@@ -1430,7 +1687,30 @@ function updateFingerprint(fingerprints, file, event) {
1430
1687
  return previous === next;
1431
1688
  }
1432
1689
 
1690
+ // src/server/extensions.ts
1691
+ function applyExtensions(base, extensions, context) {
1692
+ const webviewScripts = [...base.webviewScripts ?? []];
1693
+ const middlewares = [...base.middlewares ?? []];
1694
+ const transforms = base.wxmlTransform ? [base.wxmlTransform] : [];
1695
+ const entries = [];
1696
+ for (const extension of extensions) {
1697
+ let setup;
1698
+ try {
1699
+ setup = extension.setup(context);
1700
+ } catch (error) {
1701
+ throw new Error(`\u6269\u5C55 ${extension.name} \u521D\u59CB\u5316\u5931\u8D25\uFF1A${error.message}`);
1702
+ }
1703
+ webviewScripts.push(...setup.webviewScripts ?? []);
1704
+ middlewares.push(...setup.middlewares ?? []);
1705
+ if (setup.wxmlTransform) transforms.push(setup.wxmlTransform);
1706
+ if (setup.entry) entries.push({ name: extension.name, ...setup.entry });
1707
+ }
1708
+ const wxmlTransform = transforms.length === 0 ? void 0 : (code, file) => transforms.reduce((result, transform) => transform(result, file), code);
1709
+ return { webviewScripts, middlewares, wxmlTransform, entries };
1710
+ }
1711
+
1433
1712
  // src/server/DevServer.ts
1713
+ var RESERVED_PREFIXES = ["/__wxa__", "/__baselib__", "/__plugin__"];
1434
1714
  var DEFAULT_DEVICE = {
1435
1715
  model: "iPhone X",
1436
1716
  screenWidth: 375,
@@ -1452,7 +1732,17 @@ var DevServer = class {
1452
1732
  /** 运行时生成的临时文件(canvas 导出、拍照等),随服务器进程存在 */
1453
1733
  this.fileSessions = /* @__PURE__ */ new Map();
1454
1734
  this.logger = options.logger || console;
1455
- this.project = new MiniProgram(import_path4.default.resolve(options.projectPath));
1735
+ this.project = new MiniProgram(import_path6.default.resolve(options.projectPath));
1736
+ for (const { plugin, files } of ignoredStyleSources(this.project, this.project.styleSetting)) {
1737
+ this.logger.warn(
1738
+ `[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`
1739
+ );
1740
+ }
1741
+ const extended = applyExtensions(options, options.extensions ?? [], { projectPath: this.project.projectPath, root: this.project.root });
1742
+ extended.middlewares.forEach(({ path: mountPath }) => assertMountPath(mountPath));
1743
+ this.webviewScripts = extended.webviewScripts;
1744
+ this.middlewares = extended.middlewares;
1745
+ this.extensionEntries = extended.entries;
1456
1746
  this.baseLib = (0, import_wxa_runner_extractor5.resolveBaseLib)({ version: options.baseLibVersion, cacheDir: options.baseLibCacheDir });
1457
1747
  if (!this.baseLib.compatible) {
1458
1748
  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 +1754,7 @@ var DevServer = class {
1464
1754
  this.ticketSource = this.devtoolsLogin ? developerTicketSource(this.project.projectPath, this.logger) : null;
1465
1755
  this.plugins = new PluginStore(this.ticketSource && new WechatDevClient(this.project.appid, this.ticketSource), this.logger);
1466
1756
  this.device = { ...DEFAULT_DEVICE, ...options.device };
1467
- this.builder = new AppBuilder(this.project);
1757
+ this.builder = new AppBuilder(this.project, { wxmlTransform: extended.wxmlTransform });
1468
1758
  this.setupFile = resolveSetupFile(options.setup, this.project.projectPath);
1469
1759
  this.setupEnv = collectSetupEnv(process.env);
1470
1760
  this.app = this.createApp();
@@ -1477,7 +1767,7 @@ var DevServer = class {
1477
1767
  get networkUrls() {
1478
1768
  if (!this.lanAccess) return [];
1479
1769
  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}`);
1770
+ return Object.values(import_os3.default.networkInterfaces()).flat().filter((net2) => net2 && net2.family === "IPv4" && !net2.internal).map((net2) => `http://${net2.address}:${port}`);
1481
1771
  }
1482
1772
  async start() {
1483
1773
  try {
@@ -1520,11 +1810,15 @@ ${error.message}`);
1520
1810
  await new Promise((resolve) => server.close(() => resolve()));
1521
1811
  }
1522
1812
  this.fileSessions.clear();
1813
+ this.builder.dispose();
1523
1814
  }
1524
1815
  createApp() {
1525
1816
  const app = (0, import_express2.default)();
1526
1817
  const root = this.project.root;
1527
1818
  const noStore = (res) => res.setHeader("Cache-Control", "no-store");
1819
+ for (const { path: mountPath, handler } of this.middlewares) {
1820
+ app.use(mountPath, (req, res, next) => handler(req, res, next));
1821
+ }
1528
1822
  app.get("/", (_req, res) => {
1529
1823
  noStore(res);
1530
1824
  res.type("html").send(hostHtml());
@@ -1579,7 +1873,8 @@ ${error.message}`);
1579
1873
  const pagePath = decodeURIComponent(req.params[0]);
1580
1874
  if (!build2.pages.includes(pagePath)) return next();
1581
1875
  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 }));
1876
+ const scripts = this.webviewScripts;
1877
+ 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
1878
  }));
1584
1879
  app.get("/__wxa__/plugin/:provider/:version/:file(appservice.js|pageframe.js)", this.withBuild((build2, req, res, next) => {
1585
1880
  const { provider, version, file } = req.params;
@@ -1590,7 +1885,7 @@ ${error.message}`);
1590
1885
  app.get(/^\/__plugin__\/([^/]+)(\/.+)$/, this.withBuild((build2, req, res, next) => {
1591
1886
  const file = build2.plugins.find((plugin) => plugin.provider === req.params[0])?.files.get(decodeURIComponent(req.params[1]));
1592
1887
  if (!file) return next();
1593
- res.type(import_path4.default.extname(req.params[1]) || "application/octet-stream").send(file);
1888
+ res.type(import_path6.default.extname(req.params[1]) || "application/octet-stream").send(file);
1594
1889
  }));
1595
1890
  app.get(/^\/__wxa__\/module\/(.+\.js)$/, this.withBuild((build2, req, res) => {
1596
1891
  const id = decodeURIComponent(req.params[0]);
@@ -1601,7 +1896,7 @@ ${error.message}`);
1601
1896
  res.type("js").send(this.builder.getModuleScript(id));
1602
1897
  }));
1603
1898
  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"));
1899
+ const hasWorker = import_fs6.default.existsSync(import_path6.default.join(this.baseLib.dir, "WAWorker.js"));
1605
1900
  res.type("js").send(workerScript(build2, { version: this.baseLib.version, hasWorker }));
1606
1901
  }));
1607
1902
  app.get("/__wxa__/service-bootstrap.js", this.withBuild((build2, _req, res) => {
@@ -1620,28 +1915,28 @@ ${error.message}`);
1620
1915
  res.type("js").send(build2.pageFrame);
1621
1916
  }));
1622
1917
  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")));
1918
+ const file = import_path6.default.join(this.runtimeDir(), "frame.js.map");
1919
+ if (!import_fs6.default.existsSync(file)) return next();
1920
+ res.type("json").send(ignoreConsoleHook(import_fs6.default.readFileSync(file, "utf8")));
1626
1921
  });
1627
1922
  app.use("/__wxa__/runtime", import_express2.default.static(this.runtimeDir(), {
1628
1923
  cacheControl: false,
1629
1924
  // Service Worker 默认只能接管自己所在目录下的页面,宿主页面在根路径
1630
1925
  setHeaders: (res, file) => {
1631
- if (import_path4.default.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1926
+ if (import_path6.default.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1632
1927
  }
1633
1928
  }));
1634
1929
  const sourceMaps = /* @__PURE__ */ new Map();
1635
1930
  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`));
1931
+ const file = import_path6.default.resolve(this.baseLib.dir, req.params[0]);
1932
+ if (!file.startsWith(this.baseLib.dir + import_path6.default.sep) || !import_fs6.default.existsSync(file)) return next();
1933
+ if (!sourceMaps.has(file)) sourceMaps.set(file, ignoreListSourceMap(import_fs6.default.readFileSync(file, "utf8"), `${import_path6.default.basename(file)}?original`));
1639
1934
  res.type("json").send(sourceMaps.get(file));
1640
1935
  });
1641
1936
  app.use("/__baselib__", import_express2.default.static(this.baseLib.dir, {
1642
1937
  maxAge: "1d",
1643
1938
  setHeaders: (res, file) => {
1644
- if (file.endsWith(".js")) res.setHeader("SourceMap", `${import_path4.default.basename(file)}.map`);
1939
+ if (file.endsWith(".js")) res.setHeader("SourceMap", `${import_path6.default.basename(file)}.map`);
1645
1940
  }
1646
1941
  }));
1647
1942
  app.use(PROXY_PREFIX, import_express2.default.raw({ type: () => true, limit: "100mb" }), handleProxy);
@@ -1701,7 +1996,7 @@ ${error.message}`);
1701
1996
  return;
1702
1997
  }
1703
1998
  const data = node.data;
1704
- res.type(import_path4.default.extname(req.params[0]));
1999
+ res.type(import_path6.default.extname(req.params[0]));
1705
2000
  res.setHeader("ETag", `"${node.etag}"`);
1706
2001
  res.setHeader("Accept-Ranges", "bytes");
1707
2002
  const ranges = req.range(data.length);
@@ -1771,11 +2066,11 @@ ${error.message}`);
1771
2066
  runtimeDir() {
1772
2067
  const candidates = [
1773
2068
  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")
2069
+ import_path6.default.join(__dirname, "runtime"),
2070
+ import_path6.default.join(__dirname, "../runtime"),
2071
+ import_path6.default.join(__dirname, "../../dist/runtime")
1777
2072
  ].filter((dir) => !!dir);
1778
- const found = candidates.find((dir) => import_fs4.default.existsSync(import_path4.default.join(dir, "host.js")));
2073
+ const found = candidates.find((dir) => import_fs6.default.existsSync(import_path6.default.join(dir, "host.js")));
1779
2074
  if (!found) {
1780
2075
  throw new Error("\u627E\u4E0D\u5230\u6D4F\u89C8\u5668\u7AEF\u8FD0\u884C\u65F6\uFF08host.js\uFF09\uFF0C\u8BF7\u5148\u6267\u884C pnpm build");
1781
2076
  }
@@ -1788,7 +2083,10 @@ ${error.message}`);
1788
2083
  startWatching() {
1789
2084
  let lastBuildFailed = false;
1790
2085
  const watched = [this.project.root];
1791
- if (this.setupFile && import_path4.default.relative(this.project.root, this.setupFile).startsWith("..")) watched.push(this.setupFile);
2086
+ const outsideRoot = (file) => import_path6.default.relative(this.project.root, file).startsWith("..");
2087
+ if (this.setupFile && outsideRoot(this.setupFile)) watched.push(this.setupFile);
2088
+ const projectConfig = import_path6.default.join(this.project.projectPath, "project.config.json");
2089
+ if (outsideRoot(projectConfig) && import_fs6.default.existsSync(projectConfig)) watched.push(projectConfig);
1792
2090
  this.watcher = watchProject(this.project.root, watched, async ({ files, structural }) => {
1793
2091
  const reloadOnly = structural || lastBuildFailed;
1794
2092
  this.builder.invalidate(files);
@@ -1819,7 +2117,7 @@ var DEFAULT_PORT = 3e3;
1819
2117
  var LOCAL_HOST_OF_ANY_ADDRESS = /* @__PURE__ */ new Map([["0.0.0.0", "127.0.0.1"], ["::", "localhost"]]);
1820
2118
  async function listen(server, port, host, logger) {
1821
2119
  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);
2120
+ const taken = await takenOnLocalAddresses(candidate) || !await bind(server, candidate, host);
1823
2121
  if (!taken) {
1824
2122
  const actual = server.address().port;
1825
2123
  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 +2136,8 @@ function bind(server, port, host) {
1838
2136
  });
1839
2137
  });
1840
2138
  }
1841
- async function takenOnLoopback(port) {
1842
- for (const address of ["127.0.0.1", "::1"]) {
2139
+ async function takenOnLocalAddresses(port) {
2140
+ for (const address of ["127.0.0.1", "::1", "0.0.0.0", "::"]) {
1843
2141
  const probe = import_net.default.createServer();
1844
2142
  const free = await bind(probe, port, address).catch(() => true);
1845
2143
  if (!free) return true;
@@ -1847,11 +2145,21 @@ async function takenOnLoopback(port) {
1847
2145
  }
1848
2146
  return false;
1849
2147
  }
2148
+ function assertMountPath(mountPath) {
2149
+ const literal = typeof mountPath === "string" && /^(\/[\w.~-]+)+$/.test(mountPath);
2150
+ const lower = literal ? mountPath.toLowerCase() : "";
2151
+ const reserved = RESERVED_PREFIXES.some((prefix) => lower === prefix || lower.startsWith(prefix + "/"));
2152
+ if (!literal || reserved) {
2153
+ throw new Error(
2154
+ `\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`
2155
+ );
2156
+ }
2157
+ }
1850
2158
  function resolveSetupFile(option, projectPath) {
1851
2159
  if (option === false) return null;
1852
2160
  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}`);
2161
+ const file = import_path6.default.resolve(option);
2162
+ if (!import_fs6.default.existsSync(file)) throw new Error(`\u627E\u4E0D\u5230 setup \u6587\u4EF6\uFF1A${file}`);
1855
2163
  return file;
1856
2164
  }
1857
2165
  // Annotate the CommonJS export names for ESM import in node: