Skip to content

Commit 425ce44

Browse files
committed
feat: add legacy builds documentation and improve regex compatibility for older iOS
close: #162
1 parent 9be9eb1 commit 425ce44

File tree

6 files changed

+324
-7
lines changed

6 files changed

+324
-7
lines changed

docs/.vitepress/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export default defineConfig({
7171
{ text: 'Math', link: '/guide/math' },
7272
{ text: 'Mermaid', link: '/guide/mermaid' },
7373
{ text: 'Tailwind', link: '/guide/tailwind' },
74+
{ text: 'Legacy builds & iOS regex compatibility', link: '/guide/legacy-builds' },
7475
{ text: 'Thanks', link: '/guide/thanks' },
7576
],
7677
},
@@ -124,6 +125,7 @@ export default defineConfig({
124125
{ text: 'Math', link: '/zh/guide/math' },
125126
{ text: 'Mermaid', link: '/zh/guide/mermaid' },
126127
{ text: 'Tailwind', link: '/zh/guide/tailwind' },
128+
{ text: 'Legacy 构建与 iOS 正则兼容', link: '/zh/guide/legacy-builds' },
127129
{ text: '致谢', link: '/zh/guide/thanks' },
128130
{
129131
text: '研究与调查',
@@ -163,6 +165,7 @@ export default defineConfig({
163165
{ text: 'Math', link: '/guide/math' },
164166
{ text: 'Mermaid', link: '/guide/mermaid' },
165167
{ text: 'Tailwind', link: '/guide/tailwind' },
168+
{ text: 'Legacy builds & iOS regex compatibility', link: '/guide/legacy-builds' },
166169
{ text: 'Thanks', link: '/guide/thanks' },
167170
],
168171
},

docs/LEGACY-BUILDS.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Legacy 构建与兼容性指南(Vite 与 Webpack)
2+
3+
目的
4+
- 当你遇到旧浏览器(例如低版本 iOS / Safari)导致页面白屏或运行时报错的兼容性问题时,应考虑两类方案:
5+
1. 在源码层修复(首选,必要时必须做的,比如 RegExp 语法不可能被 polyfill)
6+
2. 在构建/部署层使用 legacy 构建(为旧浏览器生成降级 bundle + polyfills)
7+
8+
重要提醒(核心)
9+
- 一些问题(例如使用正则 lookbehind `(?<!...)`)是 JS 引擎级语法问题:无法用 polyfill 在旧引擎中“添加”正则语法支持。换言之,legacy 构建能转换语法和注入 API polyfill,但不能把运行时未实现的正则语法自动转换为等价旧写法。遇到这类问题,应在源码层重写或在运行时做特性检测并 fallback(参见 `docs/IOS-regex-compat.md`)。
10+
11+
下面文档提供:
12+
- Vite 的 `@vitejs/plugin-legacy` 用法与 caveats
13+
- Webpack 下实现 modern/legacy 差异化构建的常见模式
14+
- polyfills 管理(`core-js` / `regenerator-runtime` / 浏览器目标)
15+
- 测试与 CI 建议
16+
17+
1) Vite(推荐用于 Vite 项目)
18+
19+
- 安装(项目根):
20+
21+
```bash
22+
pnpm add -D @vitejs/plugin-legacy
23+
```
24+
25+
- 最简单的配置(vite.config.ts)
26+
27+
```ts
28+
import legacy from '@vitejs/plugin-legacy'
29+
import { defineConfig } from 'vite'
30+
31+
export default defineConfig({
32+
plugins: [
33+
legacy({
34+
targets: ['defaults', 'iOS >= 11'],
35+
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
36+
// modern polyfilled bundle + legacy (nomodule) bundle -> differential serving
37+
renderLegacyChunks: true,
38+
})
39+
]
40+
})
41+
```
42+
43+
- 解释与建议:
44+
- `targets` 使用浏览器目标来决定哪些语法/transform 需要降级;根据你需要支持的最低版本设置(例如 `iOS >= 11`)。
45+
- `additionalLegacyPolyfills` 可以把某些运行时库插入 legacy bundle;但大多数 API polyfill(例如 `Promise` / `fetch`)应通过 `core-js` 或手动引入更精细地控制。
46+
- `renderLegacyChunks: true` 会生成兼容旧浏览器的 chunk(按需 polyfill 注入),并配合 `<script type="module">` / `<script nomodule>` 差异化加载。
47+
- plugin-legacy 使用 Babel 来转换语法,但**无法**把正则 lookbehind 之类的引擎级语法“改写”为等价旧写法:如果源码里有不支持的正则语法,仍可能在打包时或运行时报错(取决于是否把该正则放到运行时构造)。因此仍要在源码层修复正则语法。
48+
49+
- 使用建议流程:
50+
1. 尝试用 runtime feature detection(例如 `new RegExp('(?<!a)b')`)判断是否需要改写/延迟构造正则字面量。
51+
2. 为支持低版本平台保留 legacy 构建(plugin-legacy)以覆盖大多数语法 / API 差异。
52+
3. 在 QA/CI 上用旧 iOS Safari(或 BrowserStack)验证构建产物。查看是否有 `SyntaxError: Invalid regular expression` 类型错误(若有,说明源码中有不兼容的正则字面量仍需改写)。
53+
54+
2) Webpack(适用于基于 Webpack 的项目)
55+
56+
Webpack 没有像 Vite 那样开箱即用的单一插件来生成 differential bundles,但常见方案是:使用 Babel 对代码做两套构建(modern + legacy),并用 `type="module"` / `nomodule` 或不同 HTML 输出来差异化服务。
57+
58+
- 关键依赖
59+
60+
```bash
61+
pnpm add -D @babel/core babel-loader @babel/preset-env core-js regenerator-runtime webpack webpack-cli
62+
```
63+
64+
- 简单思路(多配置/多构建)
65+
- 配置 A(modern):目标现代浏览器(browserslist 可设置),输出 `module` bundle
66+
- 配置 B(legacy):目标旧浏览器,开启更激进的 `@babel/preset-env` 转换与 `useBuiltIns: 'usage'`,并注入 `core-js` polyfills
67+
68+
示例 `babel.config.js`(用于 legacy 构建)
69+
70+
```js
71+
module.exports = function (api) {
72+
const isLegacy = api.env('legacy')
73+
return {
74+
presets: [
75+
[
76+
'@babel/preset-env',
77+
{
78+
targets: isLegacy ? { ie: '11', ios: '10' } : { esmodules: true },
79+
useBuiltIns: isLegacy ? 'usage' : false,
80+
corejs: isLegacy ? { version: 3, proposals: false } : undefined,
81+
},
82+
],
83+
],
84+
}
85+
}
86+
```
87+
88+
然后在 package.json 中定义两个构建命令(分别以不同的 NODE_ENV/env 来调用 babel):
89+
90+
```json
91+
{
92+
"scripts": {
93+
"build:modern": "cross-env BABEL_ENV=modern webpack --config webpack.modern.config.js",
94+
"build:legacy": "cross-env BABEL_ENV=legacy webpack --config webpack.legacy.config.js",
95+
"build": "pnpm run build:modern && pnpm run build:legacy"
96+
}
97+
}
98+
```
99+
100+
- HTML 部署时使用 `type="module"` / `nomodule`
101+
102+
```html
103+
<script type="module" src="/assets/app.modern.js"></script>
104+
<script nomodule src="/assets/app.legacy.js"></script>
105+
```
106+
107+
- 注意事项:
108+
- 两套构建会增加构建复杂度与 CI 时间;但可以确保对旧浏览器的兼容性控制更粒度。
109+
- 同样强调:如果源码中包含引擎级不支持的正则字面量(例如 lookbehind),在构建阶段仍可能失败或在旧浏览器上报错 —— 这些必须在源码层修复或用运行时延迟构造。
110+
111+
3) polyfills 管理建议
112+
- 使用 `core-js`(v3)做标准 API polyfill,结合 Babel 的 `useBuiltIns: 'usage'` 自动注入,或手动在入口文件按需 `import 'core-js/stable'`
113+
- 对于 async/await:`regenerator-runtime/runtime`
114+
- 注意不要把不必要的 polyfill注入到 modern bundle;使用 differential serving(或 plugin-legacy)确保最小现代 bundle。
115+
116+
4) 正则(RegExp)与引擎级语法的特殊说明
117+
- 正则语法(例如 lookbehind)是 JS 引擎实现的语言特性,无法用 runtime polyfill 修复。解决办法:
118+
- 在源码中改写为兼容写法(例如把 `(?<!a)b` 改为 `(^|[^a])b` 或使用字符串检查回调),或
119+
- 把潜在会抛错的正则延迟为运行时构造(`new RegExp(...)`)并在运行时做 feature-detection(参考 `docs/IOS-regex-compat.md`),以避免模块加载解析时报错。
120+
121+
5) 测试与 CI / 预提交检查
122+
- 在 CI 中加入:
123+
- `pnpm run build`(确保 modern+legacy 构建在 CI 执行无异常)
124+
- 旧版浏览器 smoke tests(可选):用 BrowserStack / SauceLabs 在 iOS 16.2 / Safari 等上做速测,或在 Xcode Simulator 上跑关键页面。
125+
- 静态检测脚本:阻止新提交引入诸如 `(?<!` / `(?<=` 的字面量正则(示例脚本见下)。把脚本加入 `package.json` 并在 CI/预提交钩子里运行。
126+
127+
示例检查脚本(package.json -> scripts)
128+
129+
```json
130+
{
131+
"scripts": {
132+
"check:regex-advanced": "rg --hidden --glob '!node_modules' --glob '!dist' -n --glob '**/*.{js,ts,vue,jsx,tsx,mjs,cjs}' '\\(\\?<!|\\(\\?<=|\\(\\?<[^=]|\\\\p\\{' || true"
133+
}
134+
}
135+
```
136+
137+
6) 快速排错清单(当你看到旧设备白屏或控制台有错误时)
138+
- 打开控制台,查找 `SyntaxError: Invalid regular expression` 或语法错误;如果是 RegExp 报错,优先检查源码是否含 lookbehind 等高级特性。
139+
-`rg '\\(\\?<!|\\(\\?<=|\\\\p\\{'` 在源码中搜索(排除缓存文件与 node_modules)。
140+
- 若查明是正则语法导致:在源码中改写或延迟构造;不要仅依赖 legacy 构建来修复此类问题。
141+
- 若是其它语法/API:确保 legacy 构建或 Babel preset-env 的 targets 包含你需要支持的版本,并把 polyfill 策略设置为 `usage`(或按需引入)。
142+
143+
7) 总结要点(简短)
144+
- legacy 构建(Vite plugin-legacy / Webpack + Babel 两套构建)能解决大量语法与 API 的兼容问题,但不能修复 JS 引擎本身不支持的正则语法(如 lookbehind)。遇到这类问题,必须在源码层改写或使用运行时检测与 fallback。
145+
- 推荐流程:优先源码兼容 -> 在构建中添加 legacy 支持 -> 在 CI/QA 上验证旧设备的真实表现 -> 在项目中加入静态检查以防回归。
146+
147+
附录:参考命令
148+
149+
```bash
150+
# Vite 本地打包 (含 legacy)
151+
pnpm build
152+
153+
# Webpack 两套构建 (示例)
154+
pnpm run build:modern
155+
pnpm run build:legacy
156+
157+
# 静态检查(本地快速跑)
158+
pnpm dlx rg "\\(\\?<!|\\(\\?<=|\\\\p\\{" --glob "**/*.{js,ts,vue,jsx,tsx,mjs,cjs}" -n
159+
```

docs/guide/legacy-builds.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Legacy build & compatibility guide (Vite / Webpack)
2+
3+
Purpose
4+
- This document explains how to use legacy builds to improve compatibility with older browsers (for example older iOS / Safari) and when you should instead fix issues in source code.
5+
6+
Key reminder
7+
- Some problems (notably RegExp engine-level syntax like lookbehind `(?<!...)`) cannot be fixed by polyfills. Legacy builds transform syntax and inject API polyfills but cannot add RegExp engine features to an older JS runtime. If you encounter such issues, you must either rewrite the code or delay construction/runtime-detect and fallback. See `ios-regex-compat.md` for details.
8+
9+
What this page covers
10+
- Vite: `@vitejs/plugin-legacy` usage and caveats
11+
- Webpack: common two-build (modern + legacy) approaches
12+
- Managing polyfills (`core-js`, `regenerator-runtime`) and targets
13+
- Testing, CI and quick debug checklist
14+
15+
Vite (recommended if your project uses Vite)
16+
17+
Install:
18+
19+
```bash
20+
pnpm add -D @vitejs/plugin-legacy
21+
```
22+
23+
Example `vite.config.ts`:
24+
25+
```ts
26+
import legacy from '@vitejs/plugin-legacy'
27+
import { defineConfig } from 'vite'
28+
29+
export default defineConfig({
30+
plugins: [
31+
legacy({
32+
targets: ['defaults', 'iOS >= 11'],
33+
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
34+
renderLegacyChunks: true,
35+
})
36+
]
37+
})
38+
```
39+
40+
Notes
41+
- `targets` controls which transforms/polyfills are applied. Set to your minimum supported browsers.
42+
- `additionalLegacyPolyfills` injects runtime libraries into the legacy bundle.
43+
- `renderLegacyChunks` enables differential chunks served via `type="module"` / `nomodule`.
44+
- Important: plugin-legacy cannot rewrite engine-level RegExp syntax (lookbehind). Keep incompatible regex out of literal module scope or rewrite them.
45+
46+
Webpack
47+
48+
Webpack does not provide a single plugin equivalent to Vite's plugin-legacy. Common approach: build two bundles with Babel (modern + legacy) and serve them with `type="module"` / `nomodule`.
49+
50+
Key deps:
51+
52+
```bash
53+
pnpm add -D @babel/core babel-loader @babel/preset-env core-js regenerator-runtime webpack webpack-cli
54+
```
55+
56+
Example approach
57+
- Create a `babel.config.js` that supports `BABEL_ENV=legacy` to enable `useBuiltIns: 'usage'` and a legacy `targets` set. Run two webpack builds: one modern, one legacy. Serve modern bundle with `<script type="module">` and legacy with `<script nomodule>`.
58+
59+
Polyfills
60+
- Use `core-js` for standard API polyfills (v3). For generators/async: `regenerator-runtime/runtime`.
61+
- Prefer differential serving so modern bundles stay small.
62+
63+
RegExp and engine-level syntax
64+
- RegExp features such as lookbehind are implemented by the JS engine. Polyfills cannot add them. Either
65+
- rewrite the regex to a compatible form, or
66+
- delay regex construction (use `new RegExp(...)`) and use runtime feature-detection + fallback.
67+
68+
Testing & CI
69+
- Add `pnpm run build` (modern + legacy) in CI and run smoke tests on older browsers (BrowserStack / SauceLabs or Xcode simulator).
70+
- Add a static check to block new lookbehind/unicode-regex features (see examples below).
71+
72+
Quick local checks
73+
74+
```bash
75+
# Static scan for advanced regex in source
76+
pnpm dlx rg "\\(\\?<!|\\(\\?<=|\\\\p\\{" --glob "**/*.{js,ts,vue,jsx,tsx,mjs,cjs}" -n
77+
```
78+
79+
Summary
80+
- Legacy builds are useful for many syntax & API gaps, but they are not a replacement for fixing engine-level RegExp syntax in source. Prefer: source fix -> legacy bundle -> CI + old-device smoke tests.

docs/zh/guide/legacy-builds.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Legacy 构建与兼容性指南(Vite / Webpack)
2+
3+
目的
4+
- 本文档说明如何使用 legacy 构建以增强旧浏览器(例如较低版本 iOS / Safari)的兼容性,以及何时应该在源码层修复问题。
5+
6+
重要提醒
7+
- 部分问题(尤其是正则引擎级语法,如 lookbehind `(?<!...)`)无法通过 polyfill 修复。legacy 构建可以转换语法和注入 API polyfill,但不能向旧的 JS 运行时“增加”正则引擎特性。如果遇到这类问题,必须在源码层重写或延迟构造并运行时检测/回退。详见 `ios-regex-compat` 文档。
8+
9+
Vite(推荐)
10+
11+
安装:
12+
13+
```bash
14+
pnpm add -D @vitejs/plugin-legacy
15+
```
16+
17+
示例配置(`vite.config.ts`):
18+
19+
```ts
20+
import legacy from '@vitejs/plugin-legacy'
21+
import { defineConfig } from 'vite'
22+
23+
export default defineConfig({
24+
plugins: [
25+
legacy({
26+
targets: ['defaults', 'iOS >= 11'],
27+
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
28+
renderLegacyChunks: true,
29+
})
30+
]
31+
})
32+
```
33+
34+
注意:
35+
- `targets` 决定哪些语法/Polyfill 需要应用。根据需要支持的最低版本设置。
36+
- `additionalLegacyPolyfills` 可以把运行时库注入到 legacy bundle。
37+
- `renderLegacyChunks` 生成按需加载的 legacy chunk,并配合 `type="module"` / `nomodule` 差异化加载。
38+
- 但 plugin-legacy 无法改写正则 lookbehind 等引擎级语法;此类问题仍需在源码层处理。
39+
40+
Webpack
41+
42+
常见做法是用 Babel 做两套构建(modern + legacy),通过 `type="module"` / `nomodule` 差异化服务。示例依赖与 `babel.config.js` 请参考英文文档上半部分。
43+
44+
Polyfills
45+
- 使用 `core-js`(v3)和 Babel 的 `useBuiltIns: 'usage'`,并仅在 legacy 构建中注入需要的 polyfill。
46+
47+
正则语法特殊说明
48+
- 引擎级正则语法必须在源码层处理:改写或运行时延迟构造并做 feature-detect。
49+
50+
测试与 CI
51+
- 在 CI 中运行 modern + legacy 构建,并在旧设备/模拟器上做 smoke tests。可加入静态检查阻止新提交引入高级正则写法。
52+
53+
快速检查(本地)
54+
55+
```bash
56+
pnpm dlx rg "\\(\\?<!|\\(\\?<=|\\\\p\\{" --glob "**/*.{js,ts,vue,jsx,tsx,mjs,cjs}" -n
57+
```
58+
59+
总结
60+
- 优先在源码层修复不兼容的正则语法;使用 legacy 构建来覆盖语法与 API 差异,并在 CI/QA 中验证旧设备表现。

packages/markdown-parser/src/plugins/isMathLike.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ const TEX_SPECIFIC_RE = /\\(?:text|frac|left|right|times)/
3535
// ensuring a lone '+' isn't matched when it's part of a '++' sequence.
3636
// Use a RegExp constructed from a string to avoid issues escaping '/' in a
3737
// regex literal on some platforms/linters.
38+
39+
// Avoid lookbehind for older iOS: use a non-capturing prefix instead
3840
// eslint-disable-next-line prefer-regex-literals
39-
const OPS_RE = new RegExp('(?<!\\+)\\+(?!\\+)|[=\\-*/^<>]|\\\\times|\\\\pm|\\\\cdot|\\\\le|\\\\ge|\\\\neq')
41+
const OPS_RE = new RegExp('(?:^|[^+])\\+(?!\\+)|[=\\-*/^<>]|\\\\times|\\\\pm|\\\\cdot|\\\\le|\\\\ge|\\\\neq')
4042
// Hyphenated multi-word (like "Quasi-Streaming") should not be treated
4143
// as a math operator. But single-letter-variable hyphens (e.g. "x-y") are
4244
// still math; so only ignore hyphens between multi-letter words.

packages/markdown-parser/src/plugins/math.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,12 @@ const SINGLE_BACKSLASH_NEWLINE_RE = /(^|[^\\])\\\r?\n/g
9191
const ENDING_SINGLE_BACKSLASH_RE = /(^|[^\\])\\$/g
9292

9393
// Cache for dynamically built regexes depending on commands list
94-
const DEFAULT_MATH_RE = new RegExp(`${CONTROL_CHARS_CLASS}|(?<!\\\\|\\w)(${ESCAPED_KATEX_COMMANDS})\\b`, 'g')
94+
// Avoid lookbehind; capture possible prefix so replacements can preserve it.
95+
// Pattern groups:
96+
// 1 - control char (e.g. '\t')
97+
// 2 - optional prefix char (start or a non-word/non-backslash)
98+
// 3 - command name
99+
const DEFAULT_MATH_RE = new RegExp(`(${CONTROL_CHARS_CLASS})|(${ESCAPED_KATEX_COMMANDS})\\b`, 'g')
95100
const MATH_RE_CACHE = new Map<string, RegExp>()
96101
const BRACE_CMD_RE_CACHE = new Map<string, RegExp>()
97102

@@ -105,7 +110,9 @@ function getMathRegex(commands: ReadonlyArray<string> | undefined) {
105110
if (cached)
106111
return cached
107112
const commandPattern = `(?:${arr.map(c => c.replace(/[.*+?^${}()|[\\]\\"\]/g, '\\$&')).join('|')})`
108-
const re = new RegExp(`${CONTROL_CHARS_CLASS}|(?<!\\\\|\\w)(${commandPattern})\\b`, 'g')
113+
// Use non-lookbehind prefix but capture the prefix so replacement can
114+
// re-insert it. Groups: (control) | (prefix)(command)
115+
const re = new RegExp(`(${CONTROL_CHARS_CLASS})|(${commandPattern})\\b`, 'g')
109116
MATH_RE_CACHE.set(key, re)
110117
return re
111118
}
@@ -159,11 +166,17 @@ export function normalizeStandaloneBackslashT(s: string, opts?: MathOptions) {
159166
// Build or reuse regex: match control chars or unescaped command words.
160167
const re = getMathRegex(useDefault ? undefined : commands)
161168

162-
let out = s.replace(re, (m: string, cmd?: string) => {
163-
if (CONTROL_MAP[m] !== undefined)
164-
return `\\${CONTROL_MAP[m]}`
165-
if (cmd && commands.includes(cmd))
169+
// Replace callback receives groups: (match, controlChar, cmd)
170+
let out = s.replace(re, (m: string, control?: string, cmd?: string, offset?: number, str?: string) => {
171+
if (control !== undefined && CONTROL_MAP[control] !== undefined)
172+
return `\\${CONTROL_MAP[control]}`
173+
if (cmd && commands.includes(cmd)) {
174+
// Ensure we are not inside a word or escaped by a backslash
175+
const prev = (str && typeof offset === 'number') ? str[offset - 1] : undefined
176+
if (prev === '\\' || (prev && /\w/.test(prev)))
177+
return m
166178
return `\\${cmd}`
179+
}
167180
return m
168181
})
169182

0 commit comments

Comments
 (0)