@lark-apaas/fullstack-rspack-preset 1.0.56 → 1.0.57-alpha.1

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"));
@@ -75,6 +76,34 @@ function createRecommendRspackConfig(options) {
75
76
  }
76
77
  }
77
78
  catch { /* ignore */ }
79
+ // 本地 dev 链路兜底:将 {clientBasePath}/__runtime__ 反代到公网沙箱
80
+ // (由 MIAODA_DEV_PLATFORM_BASE 或 SANDBOX_PUBLIC_URL 提供 origin),
81
+ // 由 SDK 而非用户应用承担 cookie / x-larkgw-suda-webuser 注入。
82
+ const localDev = (0, local_dev_1.isLocalDev)();
83
+ const sandboxBase = (0, local_dev_1.resolveSandboxOrigin)();
84
+ const { cookie: outboundCookie, csrfToken: outboundCsrfToken } = (0, local_dev_1.composeSandboxOutboundAuth)();
85
+ // parseSudaWebUserEnv 容忍 dotenv@17 对 shell-quoted JSON 不 unescape `\"` 的边界。
86
+ let localDevWebUserHeader = '';
87
+ const parsedSudaWebUser = (0, local_dev_1.parseSudaWebUserEnv)(process.env.SUDA_WEBUSER);
88
+ if (parsedSudaWebUser) {
89
+ localDevWebUserHeader = encodeURIComponent(JSON.stringify(parsedSudaWebUser));
90
+ }
91
+ // 沙箱上 /__runtime__/* 的路由本身就期望带 /app/<appId> 前缀
92
+ // (跟生产网关 miaoda.feishu-boe.cn 的行为一致),所以直接透传完整路径,不做 pathRewrite。
93
+ const localDevRuntimeProxyEntries = localDev && sandboxBase
94
+ ? [{
95
+ context: [`${clientBasePath}/__runtime__`],
96
+ target: sandboxBase,
97
+ changeOrigin: true,
98
+ secure: true,
99
+ headers: {
100
+ ...(outboundCookie ? { cookie: outboundCookie } : {}),
101
+ 'accept-encoding': 'identity',
102
+ ...(localDevWebUserHeader ? { 'x-larkgw-suda-webuser': localDevWebUserHeader } : {}),
103
+ ...(outboundCsrfToken ? { 'x-suda-csrf-token': outboundCsrfToken } : {}),
104
+ },
105
+ }]
106
+ : [];
78
107
  return {
79
108
  mode: isDev ? 'development' : 'production',
80
109
  cache: false,
@@ -384,10 +413,36 @@ function createRecommendRspackConfig(options) {
384
413
  (0, dev_server_listener_1.listenHmrTiming)(server);
385
414
  },
386
415
  proxy: [
416
+ ...localDevRuntimeProxyEntries,
387
417
  {
418
+ // 本地 dev 没有 larkgw 经过这条 incoming 链路,server 端 NestJS 缺:
419
+ // 1. x-larkgw-suda-webuser header(身份)→ ViewController 拼出 window.appId=""
420
+ // 前端 auth-sdk 拼 URL 变 /app//api/... 全 404
421
+ // 2. x-suda-csrf-token header(csrf double-submit)→ csrf middleware 403
422
+ // 在 dev server 这层模拟 larkgw 注入这两样,让应用层 middleware 行为跟沙箱完全一致
423
+ // (只信 incoming header)。webuser 静态从 SUDA_WEBUSER env 取(lark-cli +env-pull
424
+ // 从沙箱 dump 到 .env.local);csrf token 在 onProxyReq 钩子里 per-request 从
425
+ // cookie 抠(浏览器 cookie 是反代 /__runtime__ 时沙箱 Set-Cookie 透传过来的)。
388
426
  context: [`${clientBasePath}/api`],
389
427
  target: `http://localhost:${serverPort}`,
390
428
  changeOrigin: true,
429
+ ...(localDev && localDevWebUserHeader
430
+ ? { headers: { 'x-larkgw-suda-webuser': localDevWebUserHeader } }
431
+ : {}),
432
+ ...(localDev
433
+ ? {
434
+ onProxyReq: (proxyReq, req) => {
435
+ if (proxyReq.getHeader('x-suda-csrf-token'))
436
+ return;
437
+ const cookie = req.headers.cookie;
438
+ if (!cookie)
439
+ return;
440
+ const m = /(?:^|;\s*)suda-csrf-token=([^;]+)/.exec(cookie);
441
+ if (m)
442
+ proxyReq.setHeader('x-suda-csrf-token', m[1]);
443
+ },
444
+ }
445
+ : {}),
391
446
  onError: sendBackendUnavailable502,
392
447
  },
393
448
  {
@@ -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",
3
+ "version": "1.0.57-alpha.1",
4
4
  "files": [
5
5
  "lib",
6
6
  "patches",