Skip to content

Commit 4d19218

Browse files
committed
feat: 构建时将组件 SCSS 转换
1 parent 39f3c9f commit 4d19218

77 files changed

Lines changed: 742 additions & 586 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/skills/nutui-proportional-scaling/SKILL.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ description: >-
44
NutUI React proportional scaling on branch feat_resize: runtime --nut-scale-f /
55
--nut-scale-font / --nut-scale-icon from scale-f.ts (H5) and scale-f.taro.ts
66
(Taro), Sass helpers scale-px / scale-font-px / scale-icon-px and theme font
7-
tokens in variables.scss & theme-*.scss; profiles standard / large / elderly;
7+
tokens in variables.scss & theme-*.scss; npm run build / build:taro run
8+
scripts/px-to-scale-px-in-component-scss.cjs on component SCSS in memory; profiles standard / large / elderly;
89
commit-backed rules e.g. never scale 0px. Use when implementing 多尺寸适配,
910
等比适配, 大字版, 老年版, scale-px, viewport or native bridge scaling, or
1011
editing component SCSS for resize.
@@ -48,6 +49,11 @@ description: >-
4849

4950
**主题字号档**`theme-default.scss` / `theme-dark.scss`):`--nutui-font-size-*` 使用 `calc(Npx * var(--nut-scale-font, var(--nut-scale-f, 1)))`,与 **大字/老年** 档位对齐。
5051

52+
### 2.1 `npm run build` / `npm run build:taro` 时的 px → `scale-px`
53+
54+
-`package.json` 中顺序一致:先跑 **`scripts/replace-css-var.js`**,再 **`scripts/build.mjs`****`scripts/build-taro.mjs`**;上述脚本在读取 **`src/packages/**/\*.scss`(不含 demo)** 后,会经 **`scripts/px-to-scale-px-in-component-scss.cjs`****内存**里把声明值中的裸 **`Npx`** 转为 **`scale-px(Npx)`**(规则见 §3),**不写回\*\*仓库文件。
55+
- 源码里可继续手写 **`scale-px` / `scale-font-px` / `scale-icon-px`**;构建不会重复嵌套 `scale-px`
56+
5157
---
5258

5359
## 3. 提交里固化的规范(务必遵守)

scripts/build-taro.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import scss from 'postcss-scss'
77
import { copy } from 'fs-extra'
88
import { deleteAsync } from 'del'
99
import { fileURLToPath } from 'url'
10+
import { createRequire } from 'node:module'
1011
import { execSync } from 'child_process'
1112
import { access, mkdir, readFile, writeFile } from 'fs/promises'
1213
import { basename, dirname, extname, join, relative, resolve } from 'path'
@@ -18,6 +19,8 @@ import { generate } from './build-theme-typings.mjs'
1819

1920
const __filename = fileURLToPath(import.meta.url)
2021
const __dirname = dirname(__filename)
22+
const require = createRequire(import.meta.url)
23+
const pxToScalePxInComponentScss = require('./px-to-scale-px-in-component-scss.cjs')
2124
const dist = 'release/taro/dist'
2225
const filePath = resolve(__dirname, '../package.json')
2326
const packageJson = JSON.parse(readFileSync(filePath, 'utf8'))
@@ -352,9 +355,10 @@ async function buildCSS(themeName = '') {
352355
join(__dirname, `../src/styles/variables${themeName ? `-${themeName}` : ''}.scss`),
353356
)
354357
for (const file of componentScssFiles) {
355-
const scssContent = await readFile(join(__dirname, '../', file), {
358+
let scssContent = await readFile(join(__dirname, '../', file), {
356359
encoding: 'utf8',
357360
})
361+
scssContent = pxToScalePxInComponentScss(scssContent)
358362
// countup 是特例
359363
const base = basename(file)
360364
const loadPath = join(
@@ -444,9 +448,10 @@ async function buildHarmonyCSS(themeName = '') {
444448
),
445449
)
446450
for (const file of componentScssFiles) {
447-
const scssContent = await readFile(join(__dirname, '../', file), {
451+
let scssContent = await readFile(join(__dirname, '../', file), {
448452
encoding: 'utf8',
449453
})
454+
scssContent = pxToScalePxInComponentScss(scssContent)
450455
// countup 是特例
451456
const base = basename(file)
452457
const loadPath = join(

scripts/build.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import scss from 'postcss-scss'
77
import { copy } from 'fs-extra'
88
import { deleteAsync } from 'del'
99
import { fileURLToPath } from 'url'
10+
import { createRequire } from 'node:module'
1011
import { execSync } from 'child_process'
1112
import { access, mkdir, readFile, writeFile } from 'fs/promises'
1213
import { basename, dirname, extname, join, relative, resolve } from 'path'
@@ -18,6 +19,8 @@ import { generate } from './build-theme-typings.mjs'
1819

1920
const __filename = fileURLToPath(import.meta.url)
2021
const __dirname = dirname(__filename)
22+
const require = createRequire(import.meta.url)
23+
const pxToScalePxInComponentScss = require('./px-to-scale-px-in-component-scss.cjs')
2124
const dist = 'release/h5/dist'
2225
const filePath = resolve(__dirname, '../package.json')
2326
const packageJson = JSON.parse(readFileSync(filePath, 'utf8'))
@@ -301,9 +304,10 @@ async function buildCSS(themeName = '') {
301304
join(__dirname, `../src/styles/variables${themeName ? `-${themeName}` : ''}.scss`),
302305
)
303306
for (const file of componentScssFiles) {
304-
const scssContent = await readFile(join(__dirname, '../', file), {
307+
let scssContent = await readFile(join(__dirname, '../', file), {
305308
encoding: 'utf8',
306309
})
310+
scssContent = pxToScalePxInComponentScss(scssContent)
307311
// countup 是特例
308312
const base = basename(file)
309313
const loadPath = join(

scripts/generate-css-for-rtl-comparison.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const variables = fs.readFileSync(
2121
path.join(__dirname, '../src/styles/variables.scss')
2222
)
2323

24+
const pxToScalePxInComponentScss = require('./px-to-scale-px-in-component-scss.cjs')
25+
2426
function postcssRemoveRtl() {
2527
return {
2628
postcssPlugin: 'postcss-remove-rtl',
@@ -50,6 +52,7 @@ components.forEach((component) => {
5052
)
5153
)
5254
.toString()
55+
content = pxToScalePxInComponentScss(content)
5356
let to = path.join(
5457
__dirname,
5558
`../src/packages/${componentName}/${componentName}.rtl.css`
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* 仅在发布构建链路中(内存里)把组件 SCSS 声明值里的裸 Npx 转为 scale-px(Npx),
3+
* 与 .cursor/skills/nutui-proportional-scaling/SKILL.md 约定一致;不修改磁盘上的源文件。
4+
*
5+
* 规则摘要:
6+
* - 仅处理 Declaration,不处理 @media 等 at-rule 参数(避免断点被 scale)。
7+
* - 跳过 font-size、font、以及自定义属性 --*。
8+
* - 整段保留 scale-font-px(...) / scale-icon-px(...);已写的 scale-px(...) 不嵌套。
9+
* - 数值 0 的 px → 字面量 0(不写 scale-px(0px))。
10+
* - 已是 calc(Npx * var(--nut-scale-f,...)) 的不再包 scale-px。
11+
* - 含 Sass 变量 $ 且含除法 / 的 calc(...) 整段先占位再替换 px(避免 calc($var / 2) 被 postcss-scss 拆坏)。
12+
*/
13+
const postcss = require('postcss')
14+
const postcssScss = require('postcss-scss')
15+
16+
const PROP_SKIP = new Set(['font-size', 'font'])
17+
18+
function matchingCloseParen(str, openParenIdx) {
19+
let depth = 1
20+
for (let i = openParenIdx + 1; i < str.length; i++) {
21+
const c = str[i]
22+
if (c === '(') depth++
23+
else if (c === ')') {
24+
depth--
25+
if (depth === 0) return i
26+
}
27+
}
28+
return -1
29+
}
30+
31+
/** 最左侧的「体内不含 calc(」的 calc 块(同一层里先处理最左) */
32+
function findInnermostCalcRange(str) {
33+
let best = null
34+
let scan = 0
35+
const lower = str.toLowerCase()
36+
while (true) {
37+
const idx = lower.indexOf('calc(', scan)
38+
if (idx === -1) break
39+
const openParen = idx + 4
40+
const close = matchingCloseParen(str, openParen)
41+
if (close < 0) {
42+
scan = idx + 5
43+
continue
44+
}
45+
const body = str.slice(openParen + 1, close)
46+
if (!body.toLowerCase().includes('calc(')) {
47+
if (!best || idx < best.start) {
48+
best = { start: idx, end: close + 1, body }
49+
}
50+
}
51+
scan = idx + 5
52+
}
53+
return best
54+
}
55+
56+
/** 体内同时有 $ 与 / 时整段保护(典型:calc(#{$var} / 2)) */
57+
function shouldProtectCalcBody(body) {
58+
return /\$/.test(body) && /\//.test(body)
59+
}
60+
61+
function protectCalcsForPxPass(value) {
62+
const saved = []
63+
let v = value
64+
while (true) {
65+
const m = findInnermostCalcRange(v)
66+
if (!m) break
67+
if (!shouldProtectCalcBody(m.body)) break
68+
saved.push(v.slice(m.start, m.end))
69+
v = `${v.slice(0, m.start)}__NUT_CALC_${saved.length - 1}__${v.slice(m.end)}`
70+
}
71+
return { value: v, saved }
72+
}
73+
74+
function restoreCalcs(value, saved) {
75+
let out = value
76+
for (let i = saved.length - 1; i >= 0; i--) {
77+
out = out.split(`__NUT_CALC_${i}__`).join(saved[i])
78+
}
79+
return out
80+
}
81+
82+
function wrapBarePxInSegment(seg) {
83+
return seg.replace(
84+
/(-?\d*\.?\d+)px\b(?!\s*\*\s*var\(\s*--nut-scale-f)/g,
85+
(full, numStr) => {
86+
const n = parseFloat(numStr)
87+
if (!Number.isFinite(n) || n === 0) return '0'
88+
return `scale-px(${numStr}px)`
89+
},
90+
)
91+
}
92+
93+
/** 在非 scale-px(...) 段内包裸 px */
94+
function transformScalePxChunks(chunk) {
95+
return chunk.split(/(\bscale-px\s*\([^)]*\))/g).map((part, i) => {
96+
if (i % 2 === 1) return part
97+
return wrapBarePxInSegment(part)
98+
}).join('')
99+
}
100+
101+
function transformDeclValue(value) {
102+
if (value == null || !/[\d.]+\s*px/i.test(value)) return value
103+
const { value: v1, saved } = protectCalcsForPxPass(value)
104+
let out = v1
105+
.split(/(\bscale-(?:font|icon)-px\s*\([^)]*\))/g)
106+
.map((outer, oi) => {
107+
if (oi % 2 === 1) return outer
108+
return transformScalePxChunks(outer)
109+
})
110+
.join('')
111+
return restoreCalcs(out, saved)
112+
}
113+
114+
function pxToScalePxInComponentScssPlugin() {
115+
return {
116+
postcssPlugin: 'nutui-px-to-scale-px-in-component-scss',
117+
Once(root) {
118+
root.walkDecls((decl) => {
119+
const prop = decl.prop.toLowerCase()
120+
if (PROP_SKIP.has(prop)) return
121+
if (decl.prop.startsWith('--')) return
122+
decl.value = transformDeclValue(decl.value)
123+
})
124+
},
125+
}
126+
}
127+
128+
function pxToScalePxInComponentScss(source) {
129+
const result = postcss([pxToScalePxInComponentScssPlugin()]).process(source, {
130+
from: undefined,
131+
syntax: postcssScss,
132+
})
133+
return result.css
134+
}
135+
136+
module.exports = pxToScalePxInComponentScss
137+
module.exports.pxToScalePxInComponentScss = pxToScalePxInComponentScss

scripts/replace-css-var.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const theme = fs.readFileSync(
2424
path.join(__dirname, '../src/styles/theme-default.scss'),
2525
).toString().replace('@import "./jd-font";', '').replace(`@import './jd-font';`, '')
2626

27+
const pxToScalePxInComponentScss = require('./px-to-scale-px-in-component-scss.cjs')
28+
2729
const exclude = ['icon']
2830
components.forEach((component) => {
2931
const componentName = component.name.toLowerCase()
@@ -37,6 +39,7 @@ components.forEach((component) => {
3739
),
3840
)
3941
.toString()
42+
content = pxToScalePxInComponentScss(content)
4043
let to = path.join(
4144
__dirname,
4245
`../src/packages/${componentName}/${componentName}.harmony.css`,
@@ -56,14 +59,18 @@ components.forEach((component) => {
5659
content = content.replace(m, '')
5760
const splitScssName = m.match(/\'\.\/([a-z]+)\.scss/)
5861
if (splitScssName && splitScssName.length == 2) {
59-
componentSplitScss.push(fs
60-
.readFileSync(
61-
path.join(
62-
__dirname,
63-
`../src/packages/${componentName}/${splitScssName[1]}.scss`,
64-
),
65-
)
66-
.toString())
62+
componentSplitScss.push(
63+
pxToScalePxInComponentScss(
64+
fs
65+
.readFileSync(
66+
path.join(
67+
__dirname,
68+
`../src/packages/${componentName}/${splitScssName[1]}.scss`,
69+
),
70+
)
71+
.toString(),
72+
),
73+
)
6774
}
6875

6976
}

src/packages/actionsheet/actionsheet.scss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
}
1010

1111
.nut-popup-title {
12-
border-bottom: scale-px(1px) solid $actionsheet-border-color;
12+
border-bottom: 1px solid $actionsheet-border-color;
1313
}
1414

1515
&-list {
@@ -23,7 +23,7 @@
2323
&-cancel,
2424
&-item {
2525
display: block;
26-
padding: scale-px(10px);
26+
padding: 10px;
2727
text-align: $actionsheet-item-text-align;
2828
line-height: $actionsheet-item-line-height;
2929
font-size: $font-size-base;
@@ -54,8 +54,8 @@
5454
}
5555

5656
&-cancel {
57-
margin-top: scale-px(5px);
58-
border-top: scale-px(1px) solid $actionsheet-border-color;
57+
margin-top: 5px;
58+
border-top: 1px solid $actionsheet-border-color;
5959
border-radius: $actionsheet-border-radius;
6060
}
6161

0 commit comments

Comments
 (0)