@lark-apaas/fullstack-rspack-preset 1.0.56-alpha.9 → 1.0.57-alpha.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/lib/index.js CHANGED
@@ -41,10 +41,14 @@ const normalize_base_path_1 = require("./utils/normalize-base-path");
41
41
  * - CLIENT_DEV_PORT: 客户端开发端口 (默认: 8080)
42
42
  */
43
43
  function createFullstackRspackConfig(overrides = {}) {
44
- // 1. 自动加载 .env 文件(同步加载,确保环境变量立即可用)
44
+ // 优先级 shell > .env.local > .env,对齐 Vite/Next.js 约定。
45
+ // dotenv.config 默认 override:false(先到先得),先 .env.local 让它优先,
46
+ // 再 .env 兜底;shell env 已在 process.env 中,两次 config 都不会覆盖。
45
47
  try {
46
48
  // eslint-disable-next-line @typescript-eslint/no-var-requires
47
- require('dotenv').config();
49
+ const dotenv = require('dotenv');
50
+ dotenv.config({ path: '.env.local' });
51
+ dotenv.config({ path: '.env' });
48
52
  }
49
53
  catch (e) {
50
54
  // dotenv 是可选依赖,如果没有安装也不影响使用
package/lib/preset.js CHANGED
@@ -10,6 +10,7 @@ const core_1 = __importDefault(require("@rspack/core"));
10
10
  const devtool_kits_1 = require("@lark-apaas/devtool-kits");
11
11
  const plugin_react_refresh_1 = __importDefault(require("@rspack/plugin-react-refresh"));
12
12
  const dev_server_listener_1 = require("./utils/dev-server-listener");
13
+ const local_dev_1 = require("./utils/local-dev");
13
14
  const route_parser_plugin_1 = __importDefault(require("./rspack-plugins/route-parser-plugin"));
14
15
  const slardar_performance_monitor_plugin_1 = __importDefault(require("./rspack-plugins/slardar-performance-monitor-plugin"));
15
16
  const view_context_injection_plugin_1 = __importDefault(require("./rspack-plugins/view-context-injection-plugin"));
@@ -20,7 +21,6 @@ const css_legacy_plugin_1 = __importDefault(require("./rspack-plugins/css-legacy
20
21
  const polyfill_plugin_1 = __importDefault(require("./rspack-plugins/polyfill-plugin"));
21
22
  const static_assets_plugin_1 = __importDefault(require("./rspack-plugins/static-assets-plugin"));
22
23
  const source_map_upload_plugin_1 = __importDefault(require("./rspack-plugins/source-map-upload-plugin"));
23
- const capabilities_plugin_1 = require("./rspack-plugins/capabilities-plugin");
24
24
  const dev_server_snapdom_proxy_1 = require("./utils/dev-server-snapdom-proxy");
25
25
  function sendBackendUnavailable502(_err, _req, res) {
26
26
  if (res.headersSent)
@@ -75,6 +75,34 @@ function createRecommendRspackConfig(options) {
75
75
  }
76
76
  }
77
77
  catch { /* ignore */ }
78
+ // 本地 dev 链路兜底:将 {clientBasePath}/__runtime__ 反代到公网沙箱
79
+ // (由 MIAODA_DEV_PLATFORM_BASE 或 SANDBOX_PUBLIC_URL 提供 origin),
80
+ // 由 SDK 而非用户应用承担 cookie / x-larkgw-suda-webuser 注入。
81
+ const localDev = (0, local_dev_1.isLocalDev)();
82
+ const sandboxBase = (0, local_dev_1.resolveSandboxOrigin)();
83
+ const { cookie: outboundCookie, csrfToken: outboundCsrfToken } = (0, local_dev_1.composeSandboxOutboundAuth)();
84
+ // parseSudaWebUserEnv 容忍 dotenv@17 对 shell-quoted JSON 不 unescape `\"` 的边界。
85
+ let localDevWebUserHeader = '';
86
+ const parsedSudaWebUser = (0, local_dev_1.parseSudaWebUserEnv)(process.env.SUDA_WEBUSER);
87
+ if (parsedSudaWebUser) {
88
+ localDevWebUserHeader = encodeURIComponent(JSON.stringify(parsedSudaWebUser));
89
+ }
90
+ // 沙箱上 /__runtime__/* 的路由本身就期望带 /app/<appId> 前缀
91
+ // (跟生产网关 miaoda.feishu-boe.cn 的行为一致),所以直接透传完整路径,不做 pathRewrite。
92
+ const localDevRuntimeProxyEntries = localDev && sandboxBase
93
+ ? [{
94
+ context: [`${clientBasePath}/__runtime__`],
95
+ target: sandboxBase,
96
+ changeOrigin: true,
97
+ secure: true,
98
+ headers: {
99
+ ...(outboundCookie ? { cookie: outboundCookie } : {}),
100
+ 'accept-encoding': 'identity',
101
+ ...(localDevWebUserHeader ? { 'x-larkgw-suda-webuser': localDevWebUserHeader } : {}),
102
+ ...(outboundCsrfToken ? { 'x-suda-csrf-token': outboundCsrfToken } : {}),
103
+ },
104
+ }]
105
+ : [];
78
106
  return {
79
107
  mode: isDev ? 'development' : 'production',
80
108
  cache: false,
@@ -193,13 +221,6 @@ function createRecommendRspackConfig(options) {
193
221
  ],
194
222
  },
195
223
  plugins: [
196
- // virtual:capabilities —— client-toolkit 经 client-capability 强依赖的虚拟模块。
197
- // 即便业务工程没有 capability JSON 也必须把虚拟模块注册成空 map,否则下游
198
- // node_modules/@lark-apaas/client-capability/dist/index.js 的
199
- // `import 'virtual:capabilities'` 在 rspack 解析阶段直接 fail。
200
- // 必须放在 RuntimeInjectionPlugin 前面 —— RuntimeInjectionPlugin 注入的
201
- // client-toolkit/runtime 入口会间接 import 到 client-capability。
202
- new capabilities_plugin_1.CapabilitiesPlugin({ rootDir }),
203
224
  // 运行时注入插件 - 自动将 @lark-apaas/client-toolkit/runtime 注入到所有入口之前
204
225
  new runtime_injection_plugin_1.default(),
205
226
  // basename 运行时注入 - 改写入口文件 process.env.CLIENT_BASE_PATH 为运行时读 window.__BASENAME__
@@ -384,10 +405,36 @@ function createRecommendRspackConfig(options) {
384
405
  (0, dev_server_listener_1.listenHmrTiming)(server);
385
406
  },
386
407
  proxy: [
408
+ ...localDevRuntimeProxyEntries,
387
409
  {
410
+ // 本地 dev 没有 larkgw 经过这条 incoming 链路,server 端 NestJS 缺:
411
+ // 1. x-larkgw-suda-webuser header(身份)→ ViewController 拼出 window.appId=""
412
+ // 前端 auth-sdk 拼 URL 变 /app//api/... 全 404
413
+ // 2. x-suda-csrf-token header(csrf double-submit)→ csrf middleware 403
414
+ // 在 dev server 这层模拟 larkgw 注入这两样,让应用层 middleware 行为跟沙箱完全一致
415
+ // (只信 incoming header)。webuser 静态从 SUDA_WEBUSER env 取(lark-cli +env-pull
416
+ // 从沙箱 dump 到 .env.local);csrf token 在 onProxyReq 钩子里 per-request 从
417
+ // cookie 抠(浏览器 cookie 是反代 /__runtime__ 时沙箱 Set-Cookie 透传过来的)。
388
418
  context: [`${clientBasePath}/api`],
389
419
  target: `http://localhost:${serverPort}`,
390
420
  changeOrigin: true,
421
+ ...(localDev && localDevWebUserHeader
422
+ ? { headers: { 'x-larkgw-suda-webuser': localDevWebUserHeader } }
423
+ : {}),
424
+ ...(localDev
425
+ ? {
426
+ onProxyReq: (proxyReq, req) => {
427
+ if (proxyReq.getHeader('x-suda-csrf-token'))
428
+ return;
429
+ const cookie = req.headers.cookie;
430
+ if (!cookie)
431
+ return;
432
+ const m = /(?:^|;\s*)suda-csrf-token=([^;]+)/.exec(cookie);
433
+ if (m)
434
+ proxyReq.setHeader('x-suda-csrf-token', m[1]);
435
+ },
436
+ }
437
+ : {}),
391
438
  onError: sendBackendUnavailable502,
392
439
  },
393
440
  {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 本地开发总开关。由 dev-local.sh export `MIAODA_LOCAL_DEV=1`;
3
+ * 沙箱 dev / 生产都不设此 env。
4
+ */
5
+ export declare function isLocalDev(): boolean;
6
+ /**
7
+ * 反代沙箱时的认证组装。两条入口:
8
+ * - 新流(推荐):env `FORCE_AUTHN_PREVIEW_SESSION_ID` —— 后端下发的单值 session(实际是
9
+ * X-Force-Runtime-Session 的值)。SDK 自动拼 cookie,suda-csrf-token 用本地常量。
10
+ * - 老流(兼容):env `SANDBOX_COOKIE` —— 用户从浏览器手抠的完整 cookie 字符串(包含
11
+ * X-Force-Runtime-Session / suda-csrf-token / suda_web_did 等)。原样透传,suda-csrf-token
12
+ * 从其中正则提取作为出向 x-suda-csrf-token header。
13
+ *
14
+ * 两个都给的话 `SANDBOX_COOKIE` 优先("已显式拼好"信号更强)。两个都缺的话返回空字符串
15
+ * (local dev 跑不通,但不抛错——交给上层 `localDev && sandboxBase` 条件兜底)。
16
+ */
17
+ export declare function composeSandboxOutboundAuth(): {
18
+ cookie: string;
19
+ csrfToken: string;
20
+ };
21
+ /**
22
+ * 解析沙箱反代的 target origin。两条入口:
23
+ * - 新流(推荐):env `MIAODA_DEV_PLATFORM_BASE` —— 后端下发的完整 URL,**可能带路径前缀**
24
+ * (例如 `https://ai-tenant-<app>-<sandbox>.aiforce-boe-preview.bytedance.net/app/<app>`)。
25
+ * SDK 抽 `new URL(...).origin` 拿 host,扔掉路径——避免反代 target 双前缀。
26
+ * - 老流(兼容):env `SANDBOX_PUBLIC_URL`(用户从沙箱信息手抠 host)。
27
+ *
28
+ * 两个都给的话 `MIAODA_DEV_PLATFORM_BASE` 优先。两个都缺的话返回空字符串
29
+ * (local dev 跑不通,但不抛错——交给上层 `localDev && sandboxBase` 条件兜底)。
30
+ */
31
+ export declare function resolveSandboxOrigin(): string;
32
+ /**
33
+ * 解析 `process.env.SUDA_WEBUSER`,容忍沙箱下发的 shell-quoted JSON。
34
+ *
35
+ * 沙箱 env pull 下发到 .env.local 的 SUDA_WEBUSER 是双引号包裹 + 内部 `\"` 转义;
36
+ * dotenv@17 剥外层引号后**不还原**内部 `\"`,导致 `process.env.SUDA_WEBUSER` 是
37
+ * `{\"user_id\":\"...\"}` 这种带反斜杠的字符串,直接 JSON.parse 会挂在
38
+ * `Expected property name or '}' in JSON at position 1`。
39
+ *
40
+ * 容错:先直接 parse;失败再 unescape `\"` 后 parse;都失败返回 null。
41
+ */
42
+ export declare function parseSudaWebUserEnv<T = unknown>(raw: string | undefined): T | null;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isLocalDev = isLocalDev;
4
+ exports.composeSandboxOutboundAuth = composeSandboxOutboundAuth;
5
+ exports.resolveSandboxOrigin = resolveSandboxOrigin;
6
+ exports.parseSudaWebUserEnv = parseSudaWebUserEnv;
7
+ /**
8
+ * 本地开发总开关。由 dev-local.sh export `MIAODA_LOCAL_DEV=1`;
9
+ * 沙箱 dev / 生产都不设此 env。
10
+ */
11
+ function isLocalDev() {
12
+ if (process.env.NODE_ENV === 'production')
13
+ return false;
14
+ const flag = process.env.MIAODA_LOCAL_DEV;
15
+ return flag === '1' || flag === 'true';
16
+ }
17
+ /** 本地常量 csrf token —— sandbox 是 double-submit 模式(cookie===header 即放行),本地随便定一个一致值即可。 */
18
+ const LOCAL_CSRF_TOKEN = 'local-dev-csrf';
19
+ /**
20
+ * 反代沙箱时的认证组装。两条入口:
21
+ * - 新流(推荐):env `FORCE_AUTHN_PREVIEW_SESSION_ID` —— 后端下发的单值 session(实际是
22
+ * X-Force-Runtime-Session 的值)。SDK 自动拼 cookie,suda-csrf-token 用本地常量。
23
+ * - 老流(兼容):env `SANDBOX_COOKIE` —— 用户从浏览器手抠的完整 cookie 字符串(包含
24
+ * X-Force-Runtime-Session / suda-csrf-token / suda_web_did 等)。原样透传,suda-csrf-token
25
+ * 从其中正则提取作为出向 x-suda-csrf-token header。
26
+ *
27
+ * 两个都给的话 `SANDBOX_COOKIE` 优先("已显式拼好"信号更强)。两个都缺的话返回空字符串
28
+ * (local dev 跑不通,但不抛错——交给上层 `localDev && sandboxBase` 条件兜底)。
29
+ */
30
+ function composeSandboxOutboundAuth() {
31
+ const legacyCookie = process.env.SANDBOX_COOKIE;
32
+ if (legacyCookie) {
33
+ const m = legacyCookie.match(/(?:^|;\s*)suda-csrf-token=([^;]+)/i);
34
+ return { cookie: legacyCookie, csrfToken: m ? m[1] : '' };
35
+ }
36
+ const sessionId = process.env.FORCE_AUTHN_PREVIEW_SESSION_ID;
37
+ if (sessionId) {
38
+ return {
39
+ cookie: `X-Force-Runtime-Session=${sessionId}; suda-csrf-token=${LOCAL_CSRF_TOKEN}`,
40
+ csrfToken: LOCAL_CSRF_TOKEN,
41
+ };
42
+ }
43
+ return { cookie: '', csrfToken: '' };
44
+ }
45
+ /**
46
+ * 解析沙箱反代的 target origin。两条入口:
47
+ * - 新流(推荐):env `MIAODA_DEV_PLATFORM_BASE` —— 后端下发的完整 URL,**可能带路径前缀**
48
+ * (例如 `https://ai-tenant-<app>-<sandbox>.aiforce-boe-preview.bytedance.net/app/<app>`)。
49
+ * SDK 抽 `new URL(...).origin` 拿 host,扔掉路径——避免反代 target 双前缀。
50
+ * - 老流(兼容):env `SANDBOX_PUBLIC_URL`(用户从沙箱信息手抠 host)。
51
+ *
52
+ * 两个都给的话 `MIAODA_DEV_PLATFORM_BASE` 优先。两个都缺的话返回空字符串
53
+ * (local dev 跑不通,但不抛错——交给上层 `localDev && sandboxBase` 条件兜底)。
54
+ */
55
+ function resolveSandboxOrigin() {
56
+ const platformBase = process.env.MIAODA_DEV_PLATFORM_BASE;
57
+ if (platformBase) {
58
+ try {
59
+ return new URL(platformBase).origin;
60
+ }
61
+ catch {
62
+ // 非合法 URL → 回落到 legacy
63
+ }
64
+ }
65
+ return process.env.SANDBOX_PUBLIC_URL || '';
66
+ }
67
+ /**
68
+ * 解析 `process.env.SUDA_WEBUSER`,容忍沙箱下发的 shell-quoted JSON。
69
+ *
70
+ * 沙箱 env pull 下发到 .env.local 的 SUDA_WEBUSER 是双引号包裹 + 内部 `\"` 转义;
71
+ * dotenv@17 剥外层引号后**不还原**内部 `\"`,导致 `process.env.SUDA_WEBUSER` 是
72
+ * `{\"user_id\":\"...\"}` 这种带反斜杠的字符串,直接 JSON.parse 会挂在
73
+ * `Expected property name or '}' in JSON at position 1`。
74
+ *
75
+ * 容错:先直接 parse;失败再 unescape `\"` 后 parse;都失败返回 null。
76
+ */
77
+ function parseSudaWebUserEnv(raw) {
78
+ if (!raw)
79
+ return null;
80
+ try {
81
+ return JSON.parse(raw);
82
+ }
83
+ catch {
84
+ // fall through to unescape attempt
85
+ }
86
+ try {
87
+ return JSON.parse(raw.replace(/\\"/g, '"'));
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-rspack-preset",
3
- "version": "1.0.56-alpha.9",
3
+ "version": "1.0.57-alpha.0",
4
4
  "files": [
5
5
  "lib",
6
6
  "patches",
@@ -1,35 +0,0 @@
1
- /**
2
- * Capabilities Plugin for Rspack
3
- *
4
- * 解析 `virtual:capabilities` 虚拟模块——`@lark-apaas/client-capability` SDK
5
- * (经 client-toolkit 间接消费)把 `virtual:capabilities` 标 tsup external 保留
6
- * 在 dist 里,下游构建工具必须自己提供解析,否则 rspack build / dev 直接 fail:
7
- *
8
- * Module not found: Can't resolve 'virtual:capabilities' in
9
- * node_modules/@lark-apaas/client-capability/dist/index.js
10
- *
11
- * 实现策略(参考同目录 static-assets-plugin 的虚拟模块模式):
12
- * - 扫 <rootDir>/server/capabilities/*.json + <rootDir>/shared/capabilities/*.json
13
- * 生成 capability map(都不存在时空 map,SDK 在 dev 预览 / 沙箱也能正常加载)
14
- * - 写虚拟模块文件到 node_modules/.cache/capabilities-virtual/index.js
15
- * - 通过 `compiler.hooks.normalModuleFactory + beforeResolve` 拦截
16
- * `virtual:capabilities` import,把 resolveData.request 改写为虚拟模块文件路径
17
- *
18
- * 跟 vite 版本的语义保持一致:DEFAULT_DIRS 顺序、capability 合并规则(后 id 覆盖前 id)、
19
- * 没有 id 的 JSON 跳过 + warn、JSON 解析失败跳过 + warn。
20
- */
21
- import type { Compiler } from '@rspack/core';
22
- export interface CapabilitiesPluginOptions {
23
- /** 覆盖扫描目录(相对项目根)。不传按 DEFAULT_DIRS 依次扫描。 */
24
- dir?: string;
25
- /** 项目根目录,不传走 compiler.options.context || process.cwd() */
26
- rootDir?: string;
27
- }
28
- export declare class CapabilitiesPlugin {
29
- private options;
30
- constructor(options?: CapabilitiesPluginOptions);
31
- private listDirs;
32
- private loadMap;
33
- apply(compiler: Compiler): void;
34
- }
35
- export default CapabilitiesPlugin;
@@ -1,153 +0,0 @@
1
- "use strict";
2
- /**
3
- * Capabilities Plugin for Rspack
4
- *
5
- * 解析 `virtual:capabilities` 虚拟模块——`@lark-apaas/client-capability` SDK
6
- * (经 client-toolkit 间接消费)把 `virtual:capabilities` 标 tsup external 保留
7
- * 在 dist 里,下游构建工具必须自己提供解析,否则 rspack build / dev 直接 fail:
8
- *
9
- * Module not found: Can't resolve 'virtual:capabilities' in
10
- * node_modules/@lark-apaas/client-capability/dist/index.js
11
- *
12
- * 实现策略(参考同目录 static-assets-plugin 的虚拟模块模式):
13
- * - 扫 <rootDir>/server/capabilities/*.json + <rootDir>/shared/capabilities/*.json
14
- * 生成 capability map(都不存在时空 map,SDK 在 dev 预览 / 沙箱也能正常加载)
15
- * - 写虚拟模块文件到 node_modules/.cache/capabilities-virtual/index.js
16
- * - 通过 `compiler.hooks.normalModuleFactory + beforeResolve` 拦截
17
- * `virtual:capabilities` import,把 resolveData.request 改写为虚拟模块文件路径
18
- *
19
- * 跟 vite 版本的语义保持一致:DEFAULT_DIRS 顺序、capability 合并规则(后 id 覆盖前 id)、
20
- * 没有 id 的 JSON 跳过 + warn、JSON 解析失败跳过 + warn。
21
- */
22
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
- if (k2 === undefined) k2 = k;
24
- var desc = Object.getOwnPropertyDescriptor(m, k);
25
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
- desc = { enumerable: true, get: function() { return m[k]; } };
27
- }
28
- Object.defineProperty(o, k2, desc);
29
- }) : (function(o, m, k, k2) {
30
- if (k2 === undefined) k2 = k;
31
- o[k2] = m[k];
32
- }));
33
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
- Object.defineProperty(o, "default", { enumerable: true, value: v });
35
- }) : function(o, v) {
36
- o["default"] = v;
37
- });
38
- var __importStar = (this && this.__importStar) || (function () {
39
- var ownKeys = function(o) {
40
- ownKeys = Object.getOwnPropertyNames || function (o) {
41
- var ar = [];
42
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
- return ar;
44
- };
45
- return ownKeys(o);
46
- };
47
- return function (mod) {
48
- if (mod && mod.__esModule) return mod;
49
- var result = {};
50
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
- __setModuleDefault(result, mod);
52
- return result;
53
- };
54
- })();
55
- Object.defineProperty(exports, "__esModule", { value: true });
56
- exports.CapabilitiesPlugin = void 0;
57
- const fs = __importStar(require("node:fs"));
58
- const path = __importStar(require("node:path"));
59
- const VIRTUAL_ID = 'virtual:capabilities';
60
- const PLUGIN_NAME = 'MiaodaCapabilitiesPlugin';
61
- // fullstack 模板把 capability JSON 放在 `server/capabilities`。
62
- // 同时兼容 jsPage 风格 `shared/capabilities`。
63
- const DEFAULT_DIRS = ['server/capabilities', 'shared/capabilities'];
64
- class CapabilitiesPlugin {
65
- constructor(options = {}) {
66
- this.options = options;
67
- }
68
- listDirs(rootDir) {
69
- if (this.options.dir)
70
- return [path.resolve(rootDir, this.options.dir)];
71
- return DEFAULT_DIRS.map((d) => path.resolve(rootDir, d));
72
- }
73
- loadMap(rootDir) {
74
- const map = {};
75
- for (const abs of this.listDirs(rootDir)) {
76
- if (!fs.existsSync(abs))
77
- continue;
78
- for (const f of fs.readdirSync(abs)) {
79
- if (!f.endsWith('.json'))
80
- continue;
81
- try {
82
- const cfg = JSON.parse(fs.readFileSync(path.join(abs, f), 'utf-8'));
83
- if (cfg?.id)
84
- map[cfg.id] = cfg;
85
- }
86
- catch (e) {
87
- console.warn(`[miaoda-capabilities] skip invalid json: ${path.join(abs, f)}: ${e instanceof Error ? e.message : String(e)}`);
88
- }
89
- }
90
- }
91
- return map;
92
- }
93
- apply(compiler) {
94
- const rootDir = this.options.rootDir || compiler.options.context || process.cwd();
95
- // 临时虚拟模块目录(跟 static-assets-plugin 同款模式)
96
- const virtualModulesDir = path.join(rootDir, 'node_modules', '.cache', 'capabilities-virtual');
97
- if (!fs.existsSync(virtualModulesDir)) {
98
- fs.mkdirSync(virtualModulesDir, { recursive: true });
99
- }
100
- const virtualFile = path.join(virtualModulesDir, 'index.js');
101
- // 构建期写一次虚拟模块内容;dev 期 capability JSON 变更触发 watch 重写。
102
- // 虚拟文件内容输出 ESM `export default ...` —— 跟 vite 版本严格对齐,避免
103
- // 未来 client-capability 改用 `import * as caps` 等 NS 形态时,CJS/ESM
104
- // 互操作产生的差异(CJS 会嵌一层 default)。rspack 默认把 .js 当 module 类型
105
- // 解析,ESM 语法可直接消费。
106
- const writeVirtualFile = () => {
107
- const map = this.loadMap(rootDir);
108
- const content = `export default ${JSON.stringify(map)};\n`;
109
- // 仅当内容变化时写盘,避免无意义触发 watcher
110
- let existing = null;
111
- try {
112
- existing = fs.readFileSync(virtualFile, 'utf-8');
113
- }
114
- catch {
115
- existing = null;
116
- }
117
- if (existing !== content) {
118
- fs.writeFileSync(virtualFile, content);
119
- }
120
- };
121
- writeVirtualFile();
122
- // rootDir fallback 静默退化排查难,启动期检查一次实际有没有扫到 capability 目录
123
- const foundDirs = this.listDirs(rootDir).filter((d) => fs.existsSync(d));
124
- if (foundDirs.length === 0) {
125
- console.info(`[miaoda-capabilities] no capability dirs found under ${rootDir}, registering empty map. Scanned: ${this.listDirs(rootDir).join(', ')}`);
126
- }
127
- // 拦截 `virtual:capabilities` import 改写到虚拟文件
128
- compiler.hooks.normalModuleFactory.tap(PLUGIN_NAME, (factory) => {
129
- factory.hooks.beforeResolve.tap(PLUGIN_NAME, (resolveData) => {
130
- if (resolveData.request === VIRTUAL_ID) {
131
- resolveData.request = virtualFile;
132
- }
133
- });
134
- });
135
- // dev 期监听 capability JSON 变更:每次 compile 把 capability 目录加入
136
- // contextDependencies(rspack watcher 会观察其下文件创建 / 修改 / 删除)。
137
- // 不在 afterEnvironment 一次性快照 —— 那样 startup 时还没建的目录永远抓不到。
138
- // 每次 compile 重新 listDirs() 让新建目录也能被加入监听。
139
- compiler.hooks.afterCompile.tap(PLUGIN_NAME, (compilation) => {
140
- for (const dir of this.listDirs(rootDir)) {
141
- // contextDependencies 接受不存在的路径,rspack 会观察父目录的创建事件
142
- compilation.contextDependencies.add(dir);
143
- }
144
- compilation.fileDependencies.add(virtualFile);
145
- });
146
- // 每次 watchRun 前重写虚拟文件(覆盖 dev 期新增 / 修改 capability)
147
- compiler.hooks.watchRun.tap(PLUGIN_NAME, () => {
148
- writeVirtualFile();
149
- });
150
- }
151
- }
152
- exports.CapabilitiesPlugin = CapabilitiesPlugin;
153
- exports.default = CapabilitiesPlugin;