@lark-apaas/fullstack-rspack-preset 1.0.56-alpha.5 → 1.0.56-alpha.7

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/preset.js CHANGED
@@ -16,6 +16,7 @@ const slardar_performance_monitor_plugin_1 = __importDefault(require("./rspack-p
16
16
  const view_context_injection_plugin_1 = __importDefault(require("./rspack-plugins/view-context-injection-plugin"));
17
17
  const og_meta_injection_plugin_1 = __importDefault(require("./rspack-plugins/og-meta-injection-plugin"));
18
18
  const runtime_injection_plugin_1 = __importDefault(require("./rspack-plugins/runtime-injection-plugin"));
19
+ const basename_injection_plugin_1 = __importDefault(require("./rspack-plugins/basename-injection-plugin"));
19
20
  const css_legacy_plugin_1 = __importDefault(require("./rspack-plugins/css-legacy-plugin"));
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"));
@@ -80,12 +81,11 @@ function createRecommendRspackConfig(options) {
80
81
  const localDev = (0, local_dev_1.isLocalDev)();
81
82
  const sandboxBase = (0, local_dev_1.resolveSandboxOrigin)();
82
83
  const { cookie: outboundCookie, csrfToken: outboundCsrfToken } = (0, local_dev_1.composeSandboxOutboundAuth)();
84
+ // parseSudaWebUserEnv 容忍 dotenv@17 对 shell-quoted JSON 不 unescape `\"` 的边界。
83
85
  let localDevWebUserHeader = '';
84
- if (process.env.SUDA_WEBUSER) {
85
- try {
86
- localDevWebUserHeader = encodeURIComponent(JSON.stringify(JSON.parse(process.env.SUDA_WEBUSER)));
87
- }
88
- catch { /* ignore */ }
86
+ const parsedSudaWebUser = (0, local_dev_1.parseSudaWebUserEnv)(process.env.SUDA_WEBUSER);
87
+ if (parsedSudaWebUser) {
88
+ localDevWebUserHeader = encodeURIComponent(JSON.stringify(parsedSudaWebUser));
89
89
  }
90
90
  // 沙箱上 /__runtime__/* 的路由本身就期望带 /app/<appId> 前缀
91
91
  // (跟生产网关 miaoda.feishu-boe.cn 的行为一致),所以直接透传完整路径,不做 pathRewrite。
@@ -223,6 +223,10 @@ function createRecommendRspackConfig(options) {
223
223
  plugins: [
224
224
  // 运行时注入插件 - 自动将 @lark-apaas/client-toolkit/runtime 注入到所有入口之前
225
225
  new runtime_injection_plugin_1.default(),
226
+ // basename 运行时注入 - 改写入口文件 process.env.CLIENT_BASE_PATH 为运行时读 window.__BASENAME__
227
+ // 必须在 DefinePlugin 之前生效(plugin 内部 enforce:'pre')
228
+ // 匹配 client/src/ 根层级入口文件(index.tsx / main.tsx 等),不影响 SDK 内部代码
229
+ new basename_injection_plugin_1.default({ envBasePath: clientBasePath }),
226
230
  new core_1.default.BannerPlugin({
227
231
  banner: `window.__RELEASE_COMMIT_ID__ = '${releaseId}';`,
228
232
  raw: true,
@@ -0,0 +1,2 @@
1
+ declare function _exports(source: any): any;
2
+ export = _exports;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ /**
3
+ * Basename Injection Loader
4
+ *
5
+ * 仅由 BasenameInjectionPlugin 在入口文件上挂载,做字符串替换:
6
+ * process.env.CLIENT_BASE_PATH
7
+ * → ((typeof window !== "undefined" && window.__BASENAME__) || "<envBasePath>")
8
+ *
9
+ * loader 阶段天然早于 DefinePlugin 的表达式替换(rspack source-transform → parsing → DefinePlugin 流水线顺序)。
10
+ * 替换后的字符串里没有 process.env.X 形式,DefinePlugin 不会再二次替换。
11
+ */
12
+ module.exports = function basenameInjectionLoader(source) {
13
+ const options = (typeof this.getOptions === 'function' ? this.getOptions() : this.query) || {};
14
+ const envBasePath = options.envBasePath || '';
15
+ if (typeof source !== 'string' || !source.includes('process.env.CLIENT_BASE_PATH')) {
16
+ return source;
17
+ }
18
+ const fallback = JSON.stringify(envBasePath);
19
+ const runtimeExpr = '((typeof window !== "undefined" && window.__BASENAME__) || ' +
20
+ fallback +
21
+ ')';
22
+ return source.replace(/\bprocess\.env\.CLIENT_BASE_PATH\b/g, runtimeExpr);
23
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Basename Injection Plugin for Rspack
3
+ *
4
+ * 在 module rules 最前面注入一个 loader,仅匹配应用入口文件,
5
+ * 把入口里的 process.env.CLIENT_BASE_PATH
6
+ * 改写为运行时读 window.__BASENAME__(fallback 到构建时常量)。
7
+ *
8
+ * 让 BrowserRouter basename 运行时动态化,实现同一份 bundle 跑在不同 basename 下
9
+ * (默认域名 `/app/<appId>`、自定义域名 `/` 或 `/<alias>`)。
10
+ *
11
+ * 入口文件识别:在 entryOption hook(配置已完全解析合并)中从 entry 提取业务入口路径,
12
+ * 过滤掉 node_modules 包名(runtime 注入、polyfill 等非业务入口)和 Module Federation
13
+ * 远程模块,生成精确匹配正则。兜底到 client/src/index.{tsx,ts,jsx,js} 正则。
14
+ *
15
+ * 关键约束:
16
+ * - 只匹配入口文件,不影响 SDK 内部代码(如 client-toolkit-lite 仍读编译期常量)
17
+ * - loader 阶段天然早于 DefinePlugin 的表达式替换(rspack 内部 source-transform → parsing → DefinePlugin 流水线顺序)
18
+ * enforce:'pre' 是用来跟 babel-loader / swc-loader 等其他 loader 抢先,避免被它们先处理掉表达式
19
+ * - 兜底常量来自构建时 process.env.CLIENT_BASE_PATH,保证降级运行
20
+ */
21
+ import type { Compiler } from '@rspack/core';
22
+ export interface BasenameInjectionPluginOptions {
23
+ /** 编译时兜底 basename,通常等于 process.env.CLIENT_BASE_PATH */
24
+ envBasePath?: string;
25
+ }
26
+ export declare class BasenameInjectionPlugin {
27
+ private readonly envBasePath;
28
+ constructor(options?: BasenameInjectionPluginOptions);
29
+ apply(compiler: Compiler): void;
30
+ }
31
+ export default BasenameInjectionPlugin;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * Basename Injection Plugin for Rspack
4
+ *
5
+ * 在 module rules 最前面注入一个 loader,仅匹配应用入口文件,
6
+ * 把入口里的 process.env.CLIENT_BASE_PATH
7
+ * 改写为运行时读 window.__BASENAME__(fallback 到构建时常量)。
8
+ *
9
+ * 让 BrowserRouter basename 运行时动态化,实现同一份 bundle 跑在不同 basename 下
10
+ * (默认域名 `/app/<appId>`、自定义域名 `/` 或 `/<alias>`)。
11
+ *
12
+ * 入口文件识别:在 entryOption hook(配置已完全解析合并)中从 entry 提取业务入口路径,
13
+ * 过滤掉 node_modules 包名(runtime 注入、polyfill 等非业务入口)和 Module Federation
14
+ * 远程模块,生成精确匹配正则。兜底到 client/src/index.{tsx,ts,jsx,js} 正则。
15
+ *
16
+ * 关键约束:
17
+ * - 只匹配入口文件,不影响 SDK 内部代码(如 client-toolkit-lite 仍读编译期常量)
18
+ * - loader 阶段天然早于 DefinePlugin 的表达式替换(rspack 内部 source-transform → parsing → DefinePlugin 流水线顺序)
19
+ * enforce:'pre' 是用来跟 babel-loader / swc-loader 等其他 loader 抢先,避免被它们先处理掉表达式
20
+ * - 兜底常量来自构建时 process.env.CLIENT_BASE_PATH,保证降级运行
21
+ */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.BasenameInjectionPlugin = void 0;
27
+ const path_1 = __importDefault(require("path"));
28
+ const PLUGIN_NAME = 'BasenameInjectionPlugin';
29
+ const FALLBACK_ENTRY_REGEX = /[/\\]client[/\\]src[/\\]index\.(tsx?|jsx?)$/;
30
+ /**
31
+ * 判断 import 路径是否为本地业务文件(相对路径或绝对路径),
32
+ * 排除 node_modules 包名(runtime 注入、polyfill、HMR client 等)。
33
+ */
34
+ function isLocalFile(imp) {
35
+ return imp.startsWith('.') || imp.startsWith('/');
36
+ }
37
+ /**
38
+ * 从 entry 配置中提取所有本地业务入口文件的绝对路径。
39
+ *
40
+ * entry 结构(entryOption hook 阶段已规范化):
41
+ * { main: { import: ['@lark-apaas/client-toolkit/runtime', './client/src/index.tsx'] } }
42
+ *
43
+ * 过滤规则:
44
+ * - 跳过非本地路径(node_modules 包名,如 runtime 注入、polyfill、HMR client)
45
+ * - 跳过 node_modules 内的文件(Module Federation 远程模块等)
46
+ */
47
+ function resolveEntryFiles(entry, context) {
48
+ const files = [];
49
+ for (const descriptor of Object.values(entry)) {
50
+ const imports = descriptor.import;
51
+ if (!Array.isArray(imports))
52
+ continue;
53
+ for (const imp of imports) {
54
+ if (!isLocalFile(imp))
55
+ continue;
56
+ const resolved = path_1.default.resolve(context, imp);
57
+ if (resolved.includes('node_modules'))
58
+ continue;
59
+ files.push(resolved);
60
+ }
61
+ }
62
+ return files;
63
+ }
64
+ /**
65
+ * 构建入口文件匹配正则:精确匹配 resolved 路径,无路径时兜底默认正则。
66
+ * 路径分隔符统一用 [/\\] 匹配,避免跨平台问题(rspack 传给 test 的路径分隔符不确定)。
67
+ */
68
+ function buildEntryTest(entryFiles) {
69
+ if (entryFiles.length === 0)
70
+ return FALLBACK_ENTRY_REGEX;
71
+ const escaped = entryFiles.map(f => {
72
+ return path_1.default.normalize(f)
73
+ .split(path_1.default.sep) // 按平台分隔符拆段
74
+ .map(seg => seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) // 每段转义正则特殊字符
75
+ .join('[/\\\\]'); // 用 [/\\] 重新拼接
76
+ });
77
+ return new RegExp(`(${escaped.join('|')})$`);
78
+ }
79
+ class BasenameInjectionPlugin {
80
+ constructor(options = {}) {
81
+ this.envBasePath = options.envBasePath ?? process.env.CLIENT_BASE_PATH ?? '';
82
+ }
83
+ apply(compiler) {
84
+ const loaderPath = path_1.default.resolve(__dirname, './basename-injection-loader.js');
85
+ const envBasePath = this.envBasePath;
86
+ // entryOption hook:配置已完全解析合并(包括异步 entry 函数的返回值),
87
+ // 是读 entry 最安全的时机。参考 RuntimeInjectionPlugin 的做法。
88
+ compiler.hooks.entryOption.tap(PLUGIN_NAME, (context, entry) => {
89
+ // 动态 entry(函数形式)无法在此阶段解析,兜底到默认正则
90
+ const entryFiles = (typeof entry !== 'function')
91
+ ? resolveEntryFiles(entry, context)
92
+ : [];
93
+ const entryTest = buildEntryTest(entryFiles);
94
+ if (!compiler.options.module) {
95
+ compiler.options.module = { rules: [] };
96
+ }
97
+ const rules = compiler.options.module.rules || [];
98
+ compiler.options.module.rules = [
99
+ {
100
+ test: entryTest,
101
+ enforce: 'pre',
102
+ use: [
103
+ {
104
+ loader: loaderPath,
105
+ options: { envBasePath },
106
+ },
107
+ ],
108
+ },
109
+ ...rules,
110
+ ];
111
+ });
112
+ }
113
+ }
114
+ exports.BasenameInjectionPlugin = BasenameInjectionPlugin;
115
+ exports.default = BasenameInjectionPlugin;
@@ -68,19 +68,25 @@ class SourceMapUploadPlugin {
68
68
  if (!fs.existsSync(destinationDir)) {
69
69
  fs.mkdirSync(destinationDir, { recursive: true });
70
70
  }
71
- // The asset source may be a Buffer or a string.
72
- const source = sourceMap.source.source();
73
- const sourceBuffer = Buffer.isBuffer(source) ? source : Buffer.from(source, 'utf-8');
74
- fs.writeFileSync(destinationPath, sourceBuffer);
75
- console.log(`SourceMapUploadPlugin: Consolidated ${assetName} to ${destinationPath}`);
76
- // Delete the original sourcemap file.
71
+ // Move the file; fallback to copy+delete on cross-filesystem (EXDEV)
77
72
  try {
78
- fs.unlinkSync(originalPath);
79
- console.log(`SourceMapUploadPlugin: Deleted original sourcemap ${originalPath}`);
73
+ fs.renameSync(originalPath, destinationPath);
80
74
  }
81
- catch (deleteError) {
82
- console.warn(`SourceMapUploadPlugin: Failed to delete original sourcemap ${originalPath}.`, deleteError);
75
+ catch (renameError) {
76
+ if (renameError.code === 'EXDEV') {
77
+ fs.copyFileSync(originalPath, destinationPath);
78
+ try {
79
+ fs.unlinkSync(originalPath);
80
+ }
81
+ catch (deleteError) {
82
+ console.warn(`SourceMapUploadPlugin: Failed to delete original sourcemap ${originalPath}.`, deleteError);
83
+ }
84
+ }
85
+ else {
86
+ throw renameError;
87
+ }
83
88
  }
89
+ console.log(`SourceMapUploadPlugin: Consolidated ${assetName} to ${destinationPath}`);
84
90
  }
85
91
  catch (error) {
86
92
  const errorMessage = `SourceMapUploadPlugin: Failed to consolidate ${assetName}.`;
@@ -79,5 +79,6 @@ function getViewContextScriptContent() {
79
79
  if (appInfo.name) {
80
80
  window._appInfo = appInfo;
81
81
  }
82
+ window.__BASENAME__ = "{{basename}}";
82
83
  `;
83
84
  }
@@ -29,3 +29,14 @@ export declare function composeSandboxOutboundAuth(): {
29
29
  * (local dev 跑不通,但不抛错——交给上层 `localDev && sandboxBase` 条件兜底)。
30
30
  */
31
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;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isLocalDev = isLocalDev;
4
4
  exports.composeSandboxOutboundAuth = composeSandboxOutboundAuth;
5
5
  exports.resolveSandboxOrigin = resolveSandboxOrigin;
6
+ exports.parseSudaWebUserEnv = parseSudaWebUserEnv;
6
7
  /**
7
8
  * 本地开发总开关。由 dev-local.sh export `MIAODA_LOCAL_DEV=1`;
8
9
  * 沙箱 dev / 生产都不设此 env。
@@ -63,3 +64,29 @@ function resolveSandboxOrigin() {
63
64
  }
64
65
  return process.env.SANDBOX_PUBLIC_URL || '';
65
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.5",
3
+ "version": "1.0.56-alpha.7",
4
4
  "files": [
5
5
  "lib",
6
6
  "patches",