@double-coding/pixel-print
Advanced tools
| { | ||
| "id": "rn", | ||
| "name": "pure React Native / Expo", | ||
| "description": "6 大 RN 原生标签保留原名(View / Text / Image / Pressable / TextInput / ScrollView),全部从 react-native 导入。适合纯 RN / Expo 项目;想用 xtaro / taro / native-base 请选对应预设", | ||
| "helperTemplate": "rn.rpx.ts", | ||
| "referenceDoc": "rn.reference.md", | ||
| "adapter": { | ||
| "enabled": true, | ||
| "tagMap": { | ||
| "View": "View", | ||
| "Text": "Text", | ||
| "Image": "Image", | ||
| "Pressable": "Pressable", | ||
| "TextInput": "TextInput", | ||
| "ScrollView": "ScrollView" | ||
| }, | ||
| "importMap": { | ||
| "View": "react-native", | ||
| "Text": "react-native", | ||
| "Image": "react-native", | ||
| "Pressable": "react-native", | ||
| "TextInput": "react-native", | ||
| "ScrollView": "react-native" | ||
| }, | ||
| "propMap": {}, | ||
| "reactImport": "react" | ||
| } | ||
| } |
| # pure RN / Expo adapter 参考手册 | ||
| > **pure RN 预设不做标签替换**(tagMap 全部是 identity 映射,View→View / Text→Text / ...),所以**没有跨组件库差异**需要处理。本手册只列 RN 内核本身可能触发 QA warn 的边界情况。 | ||
| > | ||
| > 如果你在用 native-base / react-native-paper / gluestack-ui 等 RN 生态组件库,请**另建预设**(参考 xtaro.json / taro.json 的结构),而不是继续用本预设。 | ||
| ## 使用约定 | ||
| - 本手册**只影响 §5.5.3c 步骤**,但对 pure RN 来说 §5.5.3c 几乎是 no-op(propMap 空、无跨组件库映射) | ||
| - 主要作用:让 agent 在生成 pure RN 产物时**保持 SKILL 内核默认行为**,不做任何后处理 | ||
| - 如果发现 pure RN 项目里出现了跨组件库差异,说明选错了预设 | ||
| --- | ||
| ## 一、值域映射 | ||
| **无**。pure RN 保留原生标签,无值域映射需求。 | ||
| --- | ||
| ## 二、布尔/值取反 | ||
| **无**。RN 内核就是 RN 内核,不需要转成别人。 | ||
| --- | ||
| ## 三、事件签名转换 | ||
| **无**。RN 内核就是 RN 内核,不需要事件签名转换。 | ||
| - `TextInput.onChangeText(text: string) => void` 直接保留 | ||
| - `Pressable.onPress(event: GestureResponderEvent) => void` 直接保留 | ||
| - `ScrollView.onEndReached / onEndReachedThreshold` 直接保留 | ||
| --- | ||
| ## 四、结构变化 | ||
| **无**。RN 内核标签的 prop 结构本来就是 SKILL 内核描述的目标形态,不需要重塑。 | ||
| - `ScrollView.horizontal={true}` 直接保留(RN 就是这个 API) | ||
| - `ScrollView.contentContainerStyle` 直接保留 | ||
| --- | ||
| ## 五、无跨端支持 | ||
| **无**。pure RN 生态本身就是 iOS / Android(+ Expo 场景)双端,SKILL 生成产物默认走 RN 官方 API,没有"不支持"这一说。 | ||
| 不过有几个**平台差异**需要提醒(不影响 agent 生成,只在 §7 QA 段做 info 提示): | ||
| | API | 差异 | agent 处理 | | ||
| |---|---|---| | ||
| | `TextInput.keyboardType='visible-password'` | 仅 Android | 静默保留 + QA info(iOS 上等效退化为 default) | | ||
| | `TextInput.textContentType` | 仅 iOS | 静默保留 + QA info(Android 无影响) | | ||
| | `Pressable.onLongPress delayLongPress` | RN 0.63+ | 静默保留(默认 500ms) | | ||
| --- | ||
| ## 六、agent 快速参考(§5.5.3c 执行 checklist) | ||
| **pure RN 预设下,§5.5.3c 直接跳过**(referenceDoc 存在但内容全部 no-op)。 | ||
| agent 仍需要走一遍 §5.5.3c 流程验证 preset 完整性,但**不会命中任何改写规则**。§7 QA 段照常输出"参考手册命中:0 条(pure RN 预设)"。 | ||
| --- | ||
| ## 为什么这个预设仍然需要? | ||
| pure RN 预设的意义**不在于映射规则**,而在于: | ||
| 1. **让用户显式选择**:CLI init 时"选择预设 adapter"选项列表里有一个明确的"我不做替换"选项,而不是让用户困惑于"不选预设 = 什么行为?" | ||
| 2. **保留 helper 一致性**:预设自带 `rn.rpx.ts` 使用 pure RN 的 `Dimensions.get('window')`,与 xtaro / taro 预设的 helper 走同一套接口(都从 `@/utils/rpx` 导出 `rpx()`),SKILL 生成产物无需关心底层实现 | ||
| 3. **未来扩展的对照基线**:如果社区加 native-base / paper 等预设,可以对照 pure RN 预设看"哪些差异需要处理" |
| import { Dimensions } from 'react-native' | ||
| /** | ||
| * 响应式尺寸包装(pure RN / Expo 版)— 把设计稿数值按屏幕宽度线性缩放。 | ||
| * | ||
| * 由 pp-d2c-rn SKILL 生成的代码使用。SKILL 只对 layout / spacing / borderRadius / fontSize 类属性调用 rpx(), | ||
| * 对 opacity / flex / color / fontWeight 这类"非像素属性"保持原始数值。 | ||
| * | ||
| * 命名说明: rpx 沿用小程序 / uni-app 的 responsive px 惯例。默认基准 375(iPhone Mini 类竖屏宽度), | ||
| * 与 config.unit.figmaBase 保持一致。改基准请同时改 config, 不要只改本文件。 | ||
| * | ||
| * 无障碍备注: 本 helper **不**跟随系统字号(Dimensions 不感知 fontScale), | ||
| * 视觉一致优先。需要无障碍的团队可自行改本文件, SKILL 不会覆盖已存在的文件。 | ||
| * | ||
| * 屏宽取值: Dimensions.get('window').width 是当前可用布局宽(排除状态栏); | ||
| * 静态导入时机是模块 load 时,横屏切换 / 分屏调整时 rpx() 返回值不会跟着变。 | ||
| * 如需响应式切换,请改成 useWindowDimensions() hook 版本(需要在组件内调用)。 | ||
| */ | ||
| const DESIGN_BASE = 375 | ||
| const SCREEN_W = Dimensions.get('window').width || DESIGN_BASE | ||
| const SCALE = SCREEN_W / DESIGN_BASE | ||
| export function rpx(size: number): number { | ||
| return size * SCALE | ||
| } | ||
| export const screenWidth = SCREEN_W | ||
| export const scale = SCALE |
| { | ||
| "id": "taro", | ||
| "name": "Taro (@tarojs/components)", | ||
| "description": "6 大 RN 标签映射到 @tarojs/components;Pressable 归到 View(View 自身可点击);Image.source → src(taro Image 用 src);TextInput → Input;rpx helper 走 Taro.getSystemInfoSync from @tarojs/taro", | ||
| "helperTemplate": "taro.rpx.ts", | ||
| "referenceDoc": "taro.reference.md", | ||
| "adapter": { | ||
| "enabled": true, | ||
| "tagMap": { | ||
| "View": "View", | ||
| "Text": "Text", | ||
| "Image": "Image", | ||
| "Pressable": "View", | ||
| "TextInput": "Input", | ||
| "ScrollView": "ScrollView" | ||
| }, | ||
| "importMap": { | ||
| "View": "@tarojs/components", | ||
| "Text": "@tarojs/components", | ||
| "Image": "@tarojs/components", | ||
| "Input": "@tarojs/components", | ||
| "ScrollView": "@tarojs/components" | ||
| }, | ||
| "propMap": { | ||
| "Image": { | ||
| "source": "src" | ||
| }, | ||
| "TextInput": { | ||
| "maxLength": "maxlength", | ||
| "secureTextEntry": "password", | ||
| "autoFocus": "focus", | ||
| "onSubmitEditing": "onConfirm" | ||
| }, | ||
| "ScrollView": { | ||
| "onEndReached": "onScrollToLower" | ||
| } | ||
| }, | ||
| "reactImport": "react" | ||
| } | ||
| } |
| # taro adapter 参考手册 | ||
| > SKILL 在 §5.5.3c 步骤 Read 本文件。**声明式 propMap 已在 taro.json 处理的差异不在此重复**,本文只覆盖"prop 名机械改名之外"的复杂差异。 | ||
| > | ||
| > 覆盖对象:@tarojs/components 组件(View / Text / Image / Input / ScrollView + Pressable→View 归并)与 RN 内核标签的语义差异。 | ||
| > | ||
| > 数据来源:Taro 官方文档 https://taro-docs.jd.com/docs/components/viewContainer/view | ||
| ## 使用约定 | ||
| - 本手册**只影响 §5.5.3c 步骤**(propMap 声明式改名之后、import 重写之前),不改变主流程结构 | ||
| - 每一节都是 agent 的 checklist:命中场景 → 按"改写规则"改;找不到对应关系 → 按"丢弃策略"处理并写入 §7 QA 段 | ||
| - **绝不**在此手册里放"改名类"差异(否则与 taro.json 分工混乱) — 纯改名一律回 taro.json `propMap` | ||
| --- | ||
| ## 一、值域映射(prop 名一致或已改名,但取值域不同) | ||
| taro.json propMap 只能改 prop 名,不能改 prop 值。命中下表时,agent **在 §5.5.3c 时按 valueMap 改属性值**。 | ||
| ### 1.1 `Image.resizeMode` (RN) → `mode` (Taro) | ||
| **说明**:Taro Image 用 `mode` prop(与 xtaro 一致的 taro 语义),取值和 RN 的 resizeMode 不同,agent 必须同时改 prop 名和值。 | ||
| | RN 值 | Taro 值 | 备注 | | ||
| |---|---|---| | ||
| | `'contain'` | `'aspectFit'` | 保持纵横比,长边完全显示 | | ||
| | `'cover'` | `'aspectFill'` | 保持纵横比,填满容器 | | ||
| | `'stretch'` | `'scaleToFill'` | 不保持比例,完全拉伸(Taro 默认值) | | ||
| | `'center'` | `'center'` | 不缩放,只显示中间区域 | | ||
| | `'repeat'` | *无对应* | Taro 无此值 → 退化 `'scaleToFill'` + 写入 §7 QA warn | | ||
| **注**:taro.json propMap 已把 `resizeMode` 视作字面改名(值域也需要同步改),此处补齐值改写规则。 | ||
| ### 1.2 `TextInput.keyboardType` (RN) → `type` (Taro Input) | ||
| **说明**:Taro Input 用 `type` 而不是 `keyboardType`,取值域也不同。 | ||
| | RN keyboardType | Taro Input type | 备注 | | ||
| |---|---|---| | ||
| | `'default'` | `'text'` | 默认文本键盘 | | ||
| | `'numeric'` | `'number'` | 数字键盘 | | ||
| | `'email-address'` | `'text'` | Taro Input 无 email 类型,退化 text + QA warn | | ||
| | `'phone-pad'` | `'number'` | 退化 number | | ||
| | `'decimal-pad'` | `'digit'` | 带小数点数字 | | ||
| | `'number-pad'` | `'number'` | 同 numeric | | ||
| | `'ascii-capable'` / `'url'` / 其他 | `'text'` | 全部退化 text + QA warn | | ||
| ### 1.3 `TextInput.returnKeyType` (RN) → `confirmType` (Taro Input) | ||
| **说明**:值域**基本一致**,但 RN 有 Taro 没有的 `'default'` / `'previous'` 等,退化处理。 | ||
| | RN returnKeyType | Taro confirmType | 备注 | | ||
| |---|---|---| | ||
| | `'done'` | `'done'` | 一致 | | ||
| | `'search'` | `'search'` | 一致 | | ||
| | `'go'` | `'go'` | 一致 | | ||
| | `'next'` | `'next'` | 一致 | | ||
| | `'send'` | `'send'` | 一致 | | ||
| | `'default'` / `'previous'` / `'yahoo'` / `'google'` / `'route'` | `'done'` | 全部退化 + QA warn | | ||
| --- | ||
| ## 二、布尔/值取反 | ||
| RN 与 Taro 语义相反的属性,agent 在 §5.5.3c 时同时改 prop 名和值。 | ||
| ### 2.1 `TextInput.editable` (RN, 默认 true) → `disabled` (Taro Input, 默认 false) | ||
| **改写规则**: | ||
| ```tsx | ||
| // 改写前 | ||
| <TextInput editable={false} /> | ||
| <TextInput editable={true} /> | ||
| <TextInput editable={someVar} /> | ||
| // 改写后 | ||
| <Input disabled={true} /> | ||
| <Input disabled={false} /> | ||
| <Input disabled={!someVar} /> | ||
| ``` | ||
| **注**:变量引用场景要包一层 `!` 取反;字面 boolean 直接对换 true/false;省略 `editable` prop 无需处理(默认 editable=true → 默认 disabled=false,语义一致)。 | ||
| --- | ||
| ## 三、事件签名转换 | ||
| RN 事件 payload 与 Taro 不同,agent 生成时需**改回调函数体**,不只是改 prop 名。 | ||
| ### 3.1 `TextInput.onChangeText` (RN) → `onInput` (Taro Input) | ||
| **签名差异**: | ||
| - RN: `onChangeText: (text: string) => void` | ||
| - Taro: `onInput: (e: { detail: { value: string, cursor: number, keyCode: number } }) => void` | ||
| **改写规则**: | ||
| ```tsx | ||
| // 改写前 | ||
| <TextInput onChangeText={(text) => setValue(text)} /> | ||
| <TextInput onChangeText={handleTextChange} /> | ||
| // 改写后 | ||
| <Input onInput={(e) => setValue(e.detail.value)} /> | ||
| <Input onInput={(e) => handleTextChange(e.detail.value)} /> | ||
| ``` | ||
| **注**:命中此项时 agent **必须查回调函数体**,把原来接收 `text` 的地方改成 `e.detail.value`;若原回调是命名函数(如 `handleTextChange`)则包一层箭头函数适配。 | ||
| ### 3.2 `Pressable.onPress` (RN) → `onClick` (Taro View) | ||
| **签名差异**: | ||
| - RN: `onPress: (event: GestureResponderEvent) => void` | ||
| - Taro: `onClick: (event: ITouchEvent) => void` — event 结构不同,但绝大多数业务代码不用 event → 直接改 prop 名即可 | ||
| **改写规则**: | ||
| ```tsx | ||
| // 改写前 | ||
| <Pressable onPress={() => doSomething()} /> | ||
| <Pressable onPress={handleTap} /> | ||
| // 改写后(Pressable 已被 tagMap 映射到 View) | ||
| <View onClick={() => doSomething()} /> | ||
| <View onClick={handleTap} /> | ||
| ``` | ||
| **注**:若原回调**用了** `event.nativeEvent` / `event.locationX` 之类 RN 特有字段,则改写后需 QA warn 提示手工核对(极少见);仅 `() => doSomething()` 或不用 event 的 → 静默改。 | ||
| ### 3.3 `Pressable.onPressIn` / `onPressOut` (RN) → 无直接对应 | ||
| **说明**:Taro View 没有 pressIn/pressOut,只有 touchStart / touchEnd。 | ||
| **改写规则**: | ||
| ```tsx | ||
| // 改写前 | ||
| <Pressable onPressIn={onIn} onPressOut={onOut} /> | ||
| // 改写后 | ||
| <View onTouchStart={onIn} onTouchEnd={onOut} /> | ||
| ``` | ||
| **注**:Taro 不做 tap 手势细分(不像 RN Pressable 有 delayPressIn),行为可能有微差,QA warn 记录。 | ||
| ### 3.4 `ScrollView.onEndReached` 阈值单位差异 | ||
| **说明**:taro.json 已把 `onEndReached` 改名成 `onScrollToLower`,但**相关的 `onEndReachedThreshold` 单位也变了**。 | ||
| - RN `onEndReachedThreshold`: `0-1 浮点数`(离底部剩余占屏幕比例) | ||
| - Taro `lowerThreshold`: `像素数`(离底部剩余像素) | ||
| **改写规则**: | ||
| ```tsx | ||
| // 改写前 | ||
| <ScrollView onEndReached={loadMore} onEndReachedThreshold={0.1} /> | ||
| // 改写后 | ||
| <ScrollView onScrollToLower={loadMore} lowerThreshold={50} /> | ||
| // 注: 0.1 * 屏高(约 812) ≈ 81px,四舍五入取 50(Taro 默认值) | ||
| // 若原代码有具体屏高变量,可写 lowerThreshold={屏高变量 * 0.1};否则用 50 作 sensible default + QA warn | ||
| ``` | ||
| **注**:值需要业务侧手工核对,agent 直接给 sensible default(50)并写 §7 QA warn。 | ||
| --- | ||
| ## 四、结构变化(一个 prop 拆多个 / 需要包一层) | ||
| 改写涉及 JSX 结构,不是简单 prop 转换。agent 在 §5.5.3c 时按下述规则重塑 JSX。 | ||
| ### 4.1 `ScrollView.horizontal` (RN, boolean) → `scrollX + scrollY` (Taro, 双 boolean 二选一) | ||
| **说明**:Taro ScrollView 用两个独立 prop 表达方向,必须二选一,不允许同 true。 | ||
| **改写规则**: | ||
| ```tsx | ||
| // 改写前 A: horizontal=true → 横滚 | ||
| <ScrollView horizontal={true} /> | ||
| // 改写前 B: horizontal=false / 缺省 → 纵滚 | ||
| <ScrollView horizontal={false} /> | ||
| <ScrollView /> | ||
| // 改写后 A | ||
| <ScrollView scrollX={true} /> | ||
| // 改写后 B | ||
| <ScrollView scrollY={true} /> | ||
| ``` | ||
| **注**:agent 必须**同时删除 horizontal prop** + **加对应方向的 scrollX/scrollY**。原来省略 horizontal 的场景需**主动补** `scrollY={true}`(RN 默认纵滚,Taro 必须显式声明)。 | ||
| ### 4.2 `ScrollView.contentContainerStyle` (RN) → 需包一层 View (Taro) | ||
| **说明**:Taro ScrollView 只有 `style`,没有 `contentContainerStyle`。RN 里 style 作用于外层滚动容器、contentContainerStyle 作用于内部内容区,Taro 需要**手工把内容区包一层 View**。 | ||
| **改写规则**: | ||
| ```tsx | ||
| // 改写前 | ||
| <ScrollView style={styles.wrap} contentContainerStyle={styles.content}> | ||
| <Item /> | ||
| <Item /> | ||
| </ScrollView> | ||
| // 改写后 | ||
| <ScrollView style={styles.wrap} scrollY={true}> | ||
| <View style={styles.content}> | ||
| <Item /> | ||
| <Item /> | ||
| </View> | ||
| </ScrollView> | ||
| ``` | ||
| **注**:若原代码没写 contentContainerStyle,不需要包 View,静默改;若有 contentContainerStyle,agent 必须包一层 View 并把该 style 挂上去 + QA info 提示"新增了 View 内容容器"。 | ||
| --- | ||
| ## 五、无跨端支持(直接丢弃 + QA warn) | ||
| 以下属性 Taro rn 端不支持,agent 在 §5.5.3c 时**删除属性** + 写入 §7 QA warn 段(列出文件名 + 行号 + 属性名)。 | ||
| | 原属性(RN) | 原标签 | 丢弃原因 | | ||
| |---|---|---| | ||
| | `numberOfLines` | `Text` | Taro Text 只在 alipay 端支持;rn/h5/小程序端多行省略需在业务代码用 CSS `-webkit-line-clamp` 或 Text 手工截断 | | ||
| | `ellipsizeMode` | `Text` | 无对应 | | ||
| | `selectable` | `Text` | ✅ 一致,不丢(此项仅列为对照,agent 保留原属性) | | ||
| | `showsHorizontalScrollIndicator` | `ScrollView` | 无内置;Taro 靠 CSS `::-webkit-scrollbar` 隐藏,业务侧手工写 | | ||
| | `showsVerticalScrollIndicator` | `ScrollView` | 同上 | | ||
| | `pagingEnabled` | `ScrollView` | Taro 只 weapp 支持;rn/h5 端无 | | ||
| | `onMomentumScrollEnd` | `ScrollView` | Taro 只 weapp 支持 | | ||
| | `multiline` | `TextInput` | 无;若确需多行,改用 `Textarea`(不同标签,超出本预设覆盖范围,QA error) | | ||
| | `keyboardAppearance` | `TextInput` | 无对应 | | ||
| | `blurOnSubmit` | `TextInput` | 无对应 | | ||
| | `caretHidden` | `TextInput` | 无对应 | | ||
| **注**:`selectable` 列在这里只作对照(实际保留);其他项 agent 静默删 + §7 QA 段 warn。 | ||
| --- | ||
| ## 六、agent 快速参考(§5.5.3c 执行 checklist) | ||
| 按此顺序遍历每个 index.tsx 的 JSX: | ||
| 1. **值域映射**(§一):`resizeMode`(Image)/ `keyboardType`(TextInput)/ `returnKeyType`(TextInput) 命中 → 按 valueMap 改值 + 必要时改 prop 名 | ||
| 2. **布尔取反**(§二):`editable`(TextInput) 命中 → 改成 `disabled` + 值取反 | ||
| 3. **事件签名**(§三):`onChangeText` / `onPress` / `onPressIn` / `onPressOut` / `onEndReachedThreshold` 命中 → 改 prop 名 + 改回调函数体(§3.1 需读回调体) | ||
| 4. **结构变化**(§四):`ScrollView.horizontal` 命中 → 删原 prop + 加 scrollX/scrollY;`contentContainerStyle` 命中 → 包 View | ||
| 5. **丢弃属性**(§五):表内属性命中 → 删属性 + 写入 §7 QA warn 段 | ||
| **未在本手册出现的属性**一律**保留原样**;`style / key / ref / children / className` 永远不改。 |
| import Taro from '@tarojs/taro' | ||
| /** | ||
| * 响应式尺寸包装(Taro 版)— 把设计稿数值按屏幕宽度线性缩放。 | ||
| * | ||
| * 与 pure RN 版的区别: 用 @tarojs/taro 提供的 getSystemInfoSync 而不是 Dimensions.get('window'), | ||
| * 因为 taro 覆盖多端(小程序 / H5 / RN),不能依赖 react-native 的 Dimensions。 | ||
| * taro 提供了统一的 getSystemInfoSync 屏蔽端差异。 | ||
| * | ||
| * 由 pp-d2c-rn SKILL 生成的代码使用。SKILL 只对 layout / spacing / borderRadius / fontSize 类属性调用 rpx(), | ||
| * 对 opacity / flex / color / fontWeight 这类"非像素属性"保持原始数值。 | ||
| * | ||
| * 命名说明: rpx 沿用小程序 / uni-app 的 responsive px 惯例。默认基准 375(iPhone Mini 类竖屏宽度), | ||
| * 与 config.unit.figmaBase 保持一致。改基准请同时改 config, 不要只改本文件。 | ||
| * | ||
| * 无障碍备注: 本 helper **不**跟随系统字号(小程序端 taro 也无 fontScale 概念), | ||
| * 视觉一致优先。需要无障碍的团队可自行改本文件, SKILL 不会覆盖已存在的文件。 | ||
| * | ||
| * windowWidth vs screenWidth: 优先 windowWidth(可用布局宽,排除状态栏/胶囊按钮等), | ||
| * 拿不到时降级 screenWidth, 再兜底 375(SSR / 编译期读取时无 window 全局)。 | ||
| */ | ||
| const DESIGN_BASE = 375 | ||
| function resolveWidth(): number { | ||
| try { | ||
| const info = Taro.getSystemInfoSync() | ||
| return info.windowWidth || info.screenWidth || DESIGN_BASE | ||
| } catch { | ||
| return DESIGN_BASE | ||
| } | ||
| } | ||
| const SCREEN_W = resolveWidth() | ||
| const SCALE = SCREEN_W / DESIGN_BASE | ||
| export function rpx(size: number): number { | ||
| return size * SCALE | ||
| } | ||
| export const screenWidth = SCREEN_W | ||
| export const scale = SCALE |
+173
-47
@@ -32,2 +32,74 @@ #!/usr/bin/env node | ||
| // ─── .env 读写(极简,不引 dotenv) ───────────────────────────── | ||
| // 只处理 KEY=VALUE,支持 # 注释和引号;不做变量插值。与 figma.mjs::parseEnvFile 语义对齐 | ||
| const ENV_PATH = path.join(CWD, '.env') | ||
| function readEnvFile() { | ||
| if (!fs.existsSync(ENV_PATH)) return { lines: [], map: {} } | ||
| const text = fs.readFileSync(ENV_PATH, 'utf8') | ||
| const lines = text.split(/\r?\n/) | ||
| const map = {} | ||
| for (const raw of lines) { | ||
| const line = raw.trim() | ||
| if (!line || line.startsWith('#')) continue | ||
| const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/) | ||
| if (!m) continue | ||
| let val = m[2] | ||
| if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { | ||
| val = val.slice(1, -1) | ||
| } | ||
| map[m[1]] = val | ||
| } | ||
| return { lines, map } | ||
| } | ||
| // 追加或更新一个 key,保留其他行不动;value 含空格/引号时自动包双引号 | ||
| // 原则:.env 已存在就往里写,不覆盖整份;同名 key 就地替换(如果新旧值不同才写 .env.bak,且 bak 不覆盖已有的老 bak) | ||
| function upsertEnvVar(key, value) { | ||
| const exists = fs.existsSync(ENV_PATH) | ||
| const needsQuote = /[\s"'#]/.test(value) | ||
| const rendered = needsQuote ? `"${value.replace(/"/g, '\\"')}"` : value | ||
| const targetLine = `${key}=${rendered}` | ||
| if (!exists) { | ||
| fs.writeFileSync(ENV_PATH, targetLine + '\n') | ||
| return { action: 'create' } | ||
| } | ||
| const text = fs.readFileSync(ENV_PATH, 'utf8') | ||
| const lineRe = new RegExp(`^${key}\\s*=.*$`, 'm') | ||
| if (lineRe.test(text)) { | ||
| // 判断新旧值是否真变了,没变就完全不动 | ||
| const oldLine = text.match(lineRe)[0] | ||
| if (oldLine === targetLine) return { action: 'unchanged' } | ||
| // 变了才做原地替换;bak 只在不存在时才创建,避免连续跑 init 冲掉真正想恢复的老备份 | ||
| const bakPath = ENV_PATH + '.bak' | ||
| if (!fs.existsSync(bakPath)) fs.writeFileSync(bakPath, text) | ||
| const next = text.replace(lineRe, targetLine) | ||
| fs.writeFileSync(ENV_PATH, next) | ||
| return { action: 'replace', backup: fs.existsSync(bakPath) ? bakPath : null } | ||
| } | ||
| // 追加(确保前面有换行) | ||
| const next = text.endsWith('\n') || text.length === 0 ? text + targetLine + '\n' : text + '\n' + targetLine + '\n' | ||
| fs.writeFileSync(ENV_PATH, next) | ||
| return { action: 'append' } | ||
| } | ||
| // 项目根 .gitignore 保证有 .env 行(否则 token 会被 git 追踪) | ||
| function ensureGitignoreHasEnv() { | ||
| const gitignorePath = path.join(CWD, '.gitignore') | ||
| const line = '.env' | ||
| if (!fs.existsSync(gitignorePath)) { | ||
| fs.writeFileSync(gitignorePath, `# Local env (contains FIGMA_TOKEN — never commit)\n${line}\n`) | ||
| console.log(' → 创建 .gitignore 并加入 .env') | ||
| return | ||
| } | ||
| const text = fs.readFileSync(gitignorePath, 'utf8') | ||
| const hasLine = text.split(/\r?\n/).some(l => l.trim() === line || l.trim() === line + '/') | ||
| if (hasLine) return | ||
| const next = text.endsWith('\n') ? text + `\n# Local env (contains FIGMA_TOKEN — never commit)\n${line}\n` : text + `\n\n# Local env (contains FIGMA_TOKEN — never commit)\n${line}\n` | ||
| fs.writeFileSync(gitignorePath, next) | ||
| console.log(' → .gitignore 追加 .env 行') | ||
| } | ||
| function copyDir(srcDir, destDir, force = false) { | ||
@@ -96,8 +168,18 @@ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { | ||
| let idx = Math.max(0, choices.indexOf(defaultVal)) | ||
| let rendered = false | ||
| // 多行渲染:label 一行 + 每个选项一行。上一版单行平铺遇到长选项会终端硬 wrap, | ||
| // 只清 \r\x1b[K 清不到 wrap 出来的行 → 方向键切换时堆多行。改成回到起点用 \x1b[0J 清到底。 | ||
| const totalLines = choices.length + 1 | ||
| function render() { | ||
| // 清除已渲染的行 | ||
| process.stdout.write(`\r\x1b[K`) | ||
| const parts = choices.map((c, i) => i === idx ? `\x1b[36m● ${c}\x1b[0m` : ` ${c}`) | ||
| process.stdout.write(` ${label}: ${parts.join(' ')}\x1b[0K`) | ||
| if (rendered) { | ||
| // 光标上移 totalLines 行,再清到屏幕底部 | ||
| process.stdout.write(`\x1b[${totalLines}A\x1b[0J`) | ||
| } | ||
| process.stdout.write(` ${label}:\n`) | ||
| for (let i = 0; i < choices.length; i++) { | ||
| if (i === idx) process.stdout.write(` \x1b[36m● ${choices[i]}\x1b[0m\n`) | ||
| else process.stdout.write(` ${choices[i]}\n`) | ||
| } | ||
| rendered = true | ||
| } | ||
@@ -119,3 +201,5 @@ | ||
| process.stdin.pause() | ||
| process.stdout.write(`\r\x1b[K ${label}: \x1b[36m${choices[idx]}\x1b[0m\n`) | ||
| // 确认后擦掉多行菜单,换成单行 "label: 选中值" | ||
| process.stdout.write(`\x1b[${totalLines}A\x1b[0J`) | ||
| process.stdout.write(` ${label}: \x1b[36m${choices[idx]}\x1b[0m\n`) | ||
| resolve(choices[idx]) | ||
@@ -244,42 +328,62 @@ } | ||
| // ─── 【新增】adapter 引导 ───────────────────────── | ||
| // ─── 【新增】组件框架映射引导 ───────────────────────── | ||
| // adapter 把 RN 原生标签映射到 xtaro / taro / 其他框架 | ||
| // 已有完整 adapter 配置 → 直接沿用,不再询问 | ||
| // 没配置过 → 一层选择:不启用 / xtaro / taro / pure RN / 自定义 | ||
| const existingAdapter = existing.adapter || {} | ||
| const existingEnabledYn = existingAdapter.enabled === true ? 'Yes' : existingAdapter.enabled === false ? 'No' : null | ||
| const enableAdapterYn = await pickOrUse('[2.1/8] 是否启用 adapter 映射(把 RN 标签映射到 xtaro/taro 等)', | ||
| existingEnabledYn, ['No', 'Yes'], 'No') | ||
| const enableAdapter = enableAdapterYn === 'Yes' | ||
| if (!enableAdapter) { | ||
| adapterCfg = { enabled: false, tagMap: {}, importMap: {}, propMap: {}, reactImport: 'react' } | ||
| } else { | ||
| // 判断是否已存在完整 adapter 配置,已有则直接沿用 | ||
| const hasExistingMap = existingAdapter.tagMap && Object.keys(existingAdapter.tagMap).length > 0 | ||
| if (hasExistingMap) { | ||
| logUseExisting('[2.2/8] adapter 映射', `${Object.keys(existingAdapter.tagMap).length} 条(沿用)`) | ||
| adapterCfg = { | ||
| enabled: true, | ||
| tagMap: existingAdapter.tagMap || {}, | ||
| importMap: existingAdapter.importMap || {}, | ||
| propMap: existingAdapter.propMap || {}, | ||
| reactImport: existingAdapter.reactImport || 'react' | ||
| } | ||
| } else { | ||
| // 扫 templates/adapter-presets/ 目录,把每个 preset 的 name 列成选项,末尾追加"自定义"兜底 | ||
| const hasExistingMap = existingAdapter.tagMap && Object.keys(existingAdapter.tagMap).length > 0 | ||
| if (hasExistingMap) { | ||
| logUseExisting('[2.1/8] 组件框架映射', `${Object.keys(existingAdapter.tagMap).length} 条(沿用)`) | ||
| adapterCfg = { | ||
| enabled: true, | ||
| tagMap: existingAdapter.tagMap || {}, | ||
| importMap: existingAdapter.importMap || {}, | ||
| propMap: existingAdapter.propMap || {}, | ||
| reactImport: existingAdapter.reactImport || 'react' | ||
| } | ||
| // 沿用旧 tagMap/propMap,但 referenceDoc + _presetSource 需要以最新 preset 为准 | ||
| // 老 config 里可能根本没这两字段(v1.0.1 之前 install.js 漏写),必须现在补上, | ||
| // 否则 SKILL §5.5.3c 拿不到手册路径 → 复杂差异处理全部 no-op | ||
| if (existingAdapter.referenceDoc) adapterCfg.referenceDoc = existingAdapter.referenceDoc | ||
| if (existingAdapter._presetSource) adapterCfg._presetSource = existingAdapter._presetSource | ||
| // 兜底回填:按 tagMap 反查匹配的 preset(用 View→? 这条最能定位框架) | ||
| if (!adapterCfg.referenceDoc || !adapterCfg._presetSource) { | ||
| const presets = loadAdapterPresets() | ||
| const CUSTOM_LABEL = '自定义' | ||
| const choices = [...presets.map(p => p.name), CUSTOM_LABEL] | ||
| const defaultChoice = presets[0]?.name || CUSTOM_LABEL | ||
| const picked = await select('[2.2/8] 选择预设 adapter', choices, defaultChoice) | ||
| if (picked === CUSTOM_LABEL) { | ||
| adapterCfg = { enabled: true, tagMap: {}, importMap: {}, propMap: {}, reactImport: 'react' } | ||
| console.log(' → adapter.enabled=true,请后续在 pp-d2c.config.json 手动填 tagMap / importMap / propMap') | ||
| const viewTarget = adapterCfg.tagMap.View | ||
| const hit = viewTarget | ||
| ? presets.find(p => p.adapter && p.adapter.tagMap && p.adapter.tagMap.View === viewTarget) | ||
| : null | ||
| if (hit) { | ||
| if (!adapterCfg.referenceDoc && hit.referenceDoc) adapterCfg.referenceDoc = hit.referenceDoc | ||
| if (!adapterCfg._presetSource) adapterCfg._presetSource = PRESETS_DIR | ||
| console.log(` → 补齐 adapter.referenceDoc / _presetSource(反查匹配 ${hit.name} 预设)`) | ||
| } else { | ||
| const hit = presets.find(p => p.name === picked) | ||
| adapterCfg = { ...hit.adapter } | ||
| pickedPreset = hit | ||
| console.log(` → 已写入 ${hit.name} 预设(${hit.description || '见 templates/adapter-presets/README.md'})`) | ||
| console.log(' ⚠️ 未能反查到匹配的 preset,adapter.referenceDoc / _presetSource 保持缺失;SKILL §5.5.3c 复杂差异处理将 no-op') | ||
| } | ||
| } | ||
| } else { | ||
| // 扫 templates/adapter-presets/ 目录,把 preset.name 与"不启用"/"自定义"平铺在同一层选择 | ||
| const presets = loadAdapterPresets() | ||
| const OFF_LABEL = '不启用(保留 RN 原写法)' | ||
| const CUSTOM_LABEL = '自定义(后续手填 tagMap/importMap/propMap)' | ||
| const choices = [OFF_LABEL, ...presets.map(p => p.name), CUSTOM_LABEL] | ||
| // 默认值:优先取第一个 preset(通常是 xtaro),没预设时用 OFF | ||
| const defaultChoice = presets[0]?.name || OFF_LABEL | ||
| const picked = await select('[2.1/8] 选择组件框架映射', choices, defaultChoice) | ||
| if (picked === OFF_LABEL) { | ||
| adapterCfg = { enabled: false, tagMap: {}, importMap: {}, propMap: {}, reactImport: 'react' } | ||
| } else if (picked === CUSTOM_LABEL) { | ||
| adapterCfg = { enabled: true, tagMap: {}, importMap: {}, propMap: {}, reactImport: 'react' } | ||
| console.log(' → adapter.enabled=true,请后续在 pp-d2c.config.json 手动填 tagMap / importMap / propMap') | ||
| } else { | ||
| const hit = presets.find(p => p.name === picked) | ||
| adapterCfg = { ...hit.adapter } | ||
| // 把 preset 顶层的 referenceDoc + preset 目录绝对路径写入 adapter 段 | ||
| // SKILL §5.5.3c 靠这两字段拼参考手册路径;缺任一 → §5.5.3c 直接 no-op 跳过复杂差异处理 | ||
| if (hit.referenceDoc) adapterCfg.referenceDoc = hit.referenceDoc | ||
| adapterCfg._presetSource = PRESETS_DIR | ||
| pickedPreset = hit | ||
| console.log(` → 已写入 ${hit.name} 预设(${hit.description || '见 templates/adapter-presets/README.md'})`) | ||
| } | ||
| } | ||
@@ -295,3 +399,3 @@ | ||
| const enableRespYn = await pickOrUse( | ||
| '[2.3/8] 是否启用响应式 rpx() 包装(按屏宽线性缩放尺寸)', | ||
| '[2.2/8] 是否启用响应式 rpx() 包装(按屏宽线性缩放尺寸)', | ||
| existingRespYn, ['Yes', 'No'], 'Yes' | ||
@@ -302,7 +406,7 @@ ) | ||
| const helperImport = await inputOrUse( | ||
| '[2.4/8] rpx helper import 路径', | ||
| '[2.3/8] rpx helper import 路径', | ||
| existingResp.helperImport, '@/utils/rpx' | ||
| ) | ||
| const helperName = await inputOrUse( | ||
| '[2.5/8] rpx helper 导出函数名', | ||
| '[2.4/8] rpx helper 导出函数名', | ||
| existingResp.helperName, 'rpx' | ||
@@ -408,12 +512,34 @@ ) | ||
| // Figma Token 现在存 .env FIGMA_TOKEN,不再写入 pp-d2c.config.json | ||
| // 默认值优先级:process.env > 项目根 .env > 旧 config.figma.token(迁移场景) | ||
| const envCurrent = readEnvFile().map.FIGMA_TOKEN || '' | ||
| const legacyTokenInConfig = fig.token || '' | ||
| const defaultToken = process.env.FIGMA_TOKEN || envCurrent || legacyTokenInConfig | ||
| const figmaToken = await inputOrUse( | ||
| isRn ? '[单位2/2] Figma Personal Access Token(用于导出透明图片,回车跳过)' | ||
| : '[单位4/4] Figma Personal Access Token(用于导出透明图片,回车跳过)', | ||
| fig.token, '' | ||
| isRn ? '[单位2/2] Figma Personal Access Token(存到项目根 .env,回车跳过)' | ||
| : '[单位4/4] Figma Personal Access Token(存到项目根 .env,回车跳过)', | ||
| defaultToken, '' | ||
| ) | ||
| // 落盘 .env + 保证 .gitignore 屏蔽 .env | ||
| if (figmaToken) { | ||
| const r = upsertEnvVar('FIGMA_TOKEN', figmaToken) | ||
| if (r.action === 'create') console.log(' ✓ 已创建 .env 并写入 FIGMA_TOKEN=<hidden>') | ||
| else if (r.action === 'append') console.log(' ✓ 已向现有 .env 追加 FIGMA_TOKEN=<hidden>') | ||
| else if (r.action === 'unchanged') console.log(' ✓ .env 中 FIGMA_TOKEN 已是最新值,未改动') | ||
| else { | ||
| const bakHint = r.backup ? `(原值备份到 ${path.relative(CWD, r.backup)})` : '(存在 .env.bak,保留旧备份)' | ||
| console.log(` ✓ 已更新现有 .env 中的 FIGMA_TOKEN=<hidden> ${bakHint}`) | ||
| } | ||
| ensureGitignoreHasEnv() | ||
| if (legacyTokenInConfig) { | ||
| console.log(' → 检测到旧 pp-d2c.config.json 里的 figma.token,已迁移到 .env,config 中将移除 figma 段') | ||
| } | ||
| } else { | ||
| console.log(' ⚠️ 未填 FIGMA_TOKEN,后续切图会失败;可手动编辑项目根 .env 补上') | ||
| } | ||
| const config = { | ||
| version: '2.0.0', | ||
| project: { name: path.basename(CWD), framework, styleFormat }, | ||
| figma: { token: figmaToken }, | ||
| merge: { mode: mergeMode }, | ||
@@ -556,3 +682,3 @@ unit: framework === 'rn' | ||
| console.log('\n─────────────────────────────────────────────────────') | ||
| console.log(' ✓ v0.3 起完全走 Figma REST API,无需 MCP;确保 figma.token 已配置即可。') | ||
| console.log(' ✓ v0.3 起完全走 Figma REST API,无需 MCP;确保项目根 .env 里 FIGMA_TOKEN 已配置即可。') | ||
| console.log(' ✓ pp-d2c.config.json 已配置') | ||
@@ -559,0 +685,0 @@ console.log(' ✓ code-connect/mappings.json 已就绪') |
+1
-1
| { | ||
| "name": "@double-coding/pixel-print", | ||
| "version": "1.0.0", | ||
| "version": "1.0.2", | ||
| "description": "PixelPrint —— Figma D2C 工具,一键安装 Claude Code Skill,自动将 Figma 设计稿转换为前端代码(H5 / React Native / xtaro)", | ||
@@ -5,0 +5,0 @@ "bin": { |
+20
-18
@@ -59,3 +59,3 @@ # PixelPrint | ||
| [2.1/8] 启用 adapter(把 6 大 RN 标签映射到目标框架)? ● Yes No # rn 分支才问 | ||
| [2.2/8] 选择预设 adapter: ● 携程 xtaro 自定义 # rn 分支才问 | ||
| [2.2/8] 选择预设 adapter: ● pure RN xtaro taro 自定义 # rn 分支才问 | ||
| [2.3/8] rpx 响应式包装启用? ● Yes No # rn 分支才问 | ||
@@ -254,14 +254,16 @@ [3/8] 合并模式: ● component flat | ||
| ## RN / xtaro adapter(v0.3+) | ||
| ## RN 分支 adapter(v0.3+) | ||
| RN 分支的核心机制:**内核以 6 大 RN 原生标签描述一切,再通过 config 映射到具体框架标签**。这样一套 SKILL 同时覆盖 pure React Native / Expo / xtaro / taro / 组织内部 RN 组件库。 | ||
| **内置 xtaro 预设**(`templates/adapter-presets/xtaro.json`): | ||
| **内置 3 个预设**(`templates/adapter-presets/`): | ||
| ``` | ||
| View / Text / Image / Pressable / TextInput / ScrollView | ||
| ↓ ↓ ↓ | ||
| XView / XText / XImage / XView / XInput / XScrollView from '@ctrip/xtaro' | ||
| ``` | ||
| | 预设 | 目标 | 映射示意 | | ||
| |---|---|---| | ||
| | `rn` | pure React Native / Expo | 保留原名(identity),`from 'react-native'`;适合"我不做替换"场景 | | ||
| | `xtaro` | 携程 `@ctrip/xtaro` | `View→XView / Text→XText / Image→XImage / Pressable→XView / TextInput→XInput / ScrollView→XScrollView`,`from '@ctrip/xtaro'` | | ||
| | `taro` | Taro `@tarojs/components` | `View→View / Text→Text / Image→Image / Pressable→View / TextInput→Input / ScrollView→ScrollView`,`from '@tarojs/components'` | | ||
| 每个预设 3 件套:`<id>.json`(映射规则)+ `<id>.rpx.ts`(专属屏宽 helper)+ `<id>.reference.md`(超改名的复杂差异手册)。 | ||
| **adapter 分工**: | ||
@@ -271,8 +273,8 @@ | ||
| |---|---|---| | ||
| | prop 名不同、值和语义一样(如 `Image.source → src`) | `xtaro.json` `propMap` | §5.5.3b 声明式改名 | | ||
| | 值域映射(如 `resizeMode='contain' → mode='aspectFit'`) | `xtaro.reference.md` §一 | §5.5.3c 查手册 | | ||
| | 布尔取反(如 `editable → disabled` 取反) | `xtaro.reference.md` §二 | §5.5.3c | | ||
| | 事件签名转换(如 `onChangeText(text) → onInput(e.detail.value)`) | `xtaro.reference.md` §三 | §5.5.3c | | ||
| | 结构变化(如 `ScrollView.horizontal → scrollX + scrollY`) | `xtaro.reference.md` §四 | §5.5.3c | | ||
| | 无跨端支持,需删属性 + warn | `xtaro.reference.md` §五 | §5.5.3c | | ||
| | prop 名不同、值和语义一样(如 `Image.source → src`) | `<id>.json` `propMap` | §5.5.3b 声明式改名 | | ||
| | 值域映射(如 `resizeMode='contain' → mode='aspectFit'`) | `<id>.reference.md` §一 | §5.5.3c 查手册 | | ||
| | 布尔取反(如 `editable → disabled` 取反) | `<id>.reference.md` §二 | §5.5.3c | | ||
| | 事件签名转换(如 `onChangeText(text) → onInput(e.detail.value)`) | `<id>.reference.md` §三 | §5.5.3c | | ||
| | 结构变化(如 `ScrollView.horizontal → scrollX + scrollY`) | `<id>.reference.md` §四 | §5.5.3c | | ||
| | 无跨端支持,需删属性 + warn | `<id>.reference.md` §五 | §5.5.3c | | ||
@@ -294,6 +296,6 @@ **加自己的预设**:见 [`templates/adapter-presets/README.md`](./templates/adapter-presets/README.md)。 | ||
| │ │ ├── README.md ← 加预设的说明 | ||
| │ │ ├── xtaro.json ← 携程 xtaro 预设映射 | ||
| │ │ ├── xtaro.rpx.ts ← xtaro 版 rpx helper | ||
| │ │ └── xtaro.reference.md ← xtaro 复杂差异手册 | ||
| │ ├── rn-helpers/rpx.ts ← pure RN 版 rpx helper(默认兜底) | ||
| │ │ ├── xtaro.{json,rpx.ts,reference.md} ← 携程 xtaro 预设(3 件套) | ||
| │ │ ├── taro.{json,rpx.ts,reference.md} ← Taro (@tarojs/components) 预设 | ||
| │ │ └── rn.{json,rpx.ts,reference.md} ← pure React Native / Expo 预设 | ||
| │ ├── rn-helpers/rpx.ts ← 兜底 rpx helper(用户选"自定义"无预设时用) | ||
| │ └── skills/ | ||
@@ -300,0 +302,0 @@ │ ├── pp-d2c/SKILL.md ← 主 D2C 流程(h5 分支,~1200 行) |
@@ -7,8 +7,16 @@ # adapter-presets | ||
| | 文件 | 目标框架 | 说明 | | ||
| |-----|---------|------| | ||
| | `xtaro.json` + `xtaro.rpx.ts` + `xtaro.reference.md` | 携程 xtaro | 6 大 RN 标签映射到 `@ctrip/xtaro`;`Pressable → XView`(XView 自身可点击,不引 XClickableSimplified);`Image.source → src`(xtaro 走 taro 语义);自带 rpx helper 走 `xGetSystemInfoSync from @ctrip/xtaro`(xtaro H5 端 webpack 不解析 react-native Flow 语法);**xtaro.reference.md** 承载"prop 名机械改名之外"的复杂差异(值域映射 / 布尔取反 / 事件签名 / 结构变化 / 丢弃属性),SKILL 在 §5.5.3c 时读取应用 | | ||
| | 预设 id | 名称 | 目标框架 | 说明 | | ||
| |---|---|---|---| | ||
| | `xtaro` | 携程 xtaro | `@ctrip/xtaro` | 6 大 RN 标签映射到 xtaro 组件;`Pressable → XView`(XView 自身可点击);`Image.source → src`;rpx helper 走 `xGetSystemInfoSync from @ctrip/xtaro`(xtaro H5 端 webpack 不解析 react-native Flow 语法) | | ||
| | `taro` | Taro (@tarojs/components) | `@tarojs/components` | 6 大 RN 标签映射到 taro 组件;`Pressable → View`;`TextInput → Input`;`Image.source → src`;rpx helper 走 `Taro.getSystemInfoSync from @tarojs/taro`(taro 覆盖多端时统一屏蔽) | | ||
| | `rn` | pure React Native / Expo | `react-native` | 6 大 RN 原生标签保留原名(identity 映射);全部从 `react-native` 导入;rpx helper 走 `Dimensions.get('window').width`。适合纯 RN / Expo,不做跨组件库替换 | | ||
| CLI 里始终有个 `自定义` 兜底选项 — 选它写空 adapter,用户后续在 `pp-d2c.config.json` 手改 tagMap / importMap / propMap 即可,不必先建 preset 文件。 | ||
| 每个预设由 3 个文件组成: | ||
| - `<id>.json` — 映射规则(tagMap / importMap / propMap) | ||
| - `<id>.rpx.ts` — 该预设专属的 rpx helper(不同框架屏宽 API 不同) | ||
| - `<id>.reference.md` — 超出 propMap 声明式改名的复杂差异手册(SKILL §5.5.3c 读取) | ||
| CLI 里始终有一个 `自定义` 兜底选项 — 选它写空 adapter,用户后续在 `pp-d2c.config.json` 手改 tagMap / importMap / propMap 即可,不必先建 preset 文件。 | ||
| ## 加自己的预设 | ||
@@ -69,16 +77,16 @@ | ||
| ### 举例:接 `taro` 的预设 | ||
| ### 举例:接 `native-base` 的预设(假想,示意结构) | ||
| ```json | ||
| { | ||
| "id": "taro", | ||
| "name": "Taro", | ||
| "description": "映射到 @tarojs/components,复用 taro Image 语义(source → src)", | ||
| "id": "native-base", | ||
| "name": "NativeBase (@native-base)", | ||
| "description": "映射到 native-base 组件库,提供 Box / VStack / HStack / Image / Pressable / Input / ScrollView 等", | ||
| "adapter": { | ||
| "enabled": true, | ||
| "tagMap": { | ||
| "View": "View", | ||
| "View": "Box", | ||
| "Text": "Text", | ||
| "Image": "Image", | ||
| "Pressable": "View", | ||
| "Pressable": "Pressable", | ||
| "TextInput": "Input", | ||
@@ -88,10 +96,11 @@ "ScrollView": "ScrollView" | ||
| "importMap": { | ||
| "View": "@tarojs/components", | ||
| "Text": "@tarojs/components", | ||
| "Image": "@tarojs/components", | ||
| "Input": "@tarojs/components", | ||
| "ScrollView": "@tarojs/components" | ||
| "Box": "native-base", | ||
| "Text": "native-base", | ||
| "Image": "native-base", | ||
| "Pressable": "native-base", | ||
| "Input": "native-base", | ||
| "ScrollView": "native-base" | ||
| }, | ||
| "propMap": { | ||
| "Image": { "source": "src" } | ||
| "Image": { "source": "source" } | ||
| } | ||
@@ -102,2 +111,4 @@ } | ||
| > **想接的框架已有预设**?先看 `现有预设` 表,直接选就行。上面示例只演示"如何加一个新框架"。真实 taro / xtaro / pure RN 见目录里现成的 3 个预设。 | ||
| ## 加了预设之后的生效路径 | ||
@@ -104,0 +115,0 @@ |
@@ -26,3 +26,4 @@ { | ||
| "Image": { | ||
| "source": "src" | ||
| "source": "src", | ||
| "resizeMode": "mode" | ||
| }, | ||
@@ -33,3 +34,5 @@ "TextInput": { | ||
| "autoFocus": "focus", | ||
| "onSubmitEditing": "onConfirm" | ||
| "onSubmitEditing": "onConfirm", | ||
| "keyboardType": "type", | ||
| "returnKeyType": "confirmType" | ||
| }, | ||
@@ -36,0 +39,0 @@ "ScrollView": { |
@@ -17,3 +17,3 @@ # xtaro adapter 参考手册 | ||
| ## 一、值域映射(prop 名一致或已改名,但取值域不同) | ||
| ## 一、值域映射(prop 名已在 xtaro.json 改名,取值域不同) | ||
@@ -24,18 +24,31 @@ xtaro.json propMap 只能改 prop 名,不能改 prop 值。命中下表时,agent **在 §5.5.3c 时按 valueMap 改属性值**。 | ||
| **说明**:xtaro 已经通过 propMap 把 `resizeMode` 改名成 `mode`(见 xtaro.json),但**值域也变了**,agent 必须同步改属性值。 | ||
| **说明**:xtaro.json propMap 已把 `resizeMode` 改名成 `mode`,agent 在 §5.5.3c 时**只需改属性值**(prop 名已由 §5.5.3b 处理)。 | ||
| | RN 值 | xtaro 值 | 备注 | | ||
| xtaro `XImage.mode` 完整取值来自 `@ctrip/xtaro-types/types/component/XImage.d.ts` 的 `XImageProps.Mode`,共 15 个值(4 类缩放 + 9 类裁剪);其中类型注释明确 **`@rn 部分支持 scaleToFill, aspectFit, aspectFill, widthFix`**,其余值 rn 端会退化。 | ||
| | RN `resizeMode` 值 | xtaro `mode` 值 | rn 端行为 | | ||
| |---|---|---| | ||
| | `'contain'` | `'aspectFit'` | 保持纵横比,长边完全显示 | | ||
| | `'cover'` | `'aspectFill'` | 保持纵横比,填满容器 | | ||
| | `'stretch'` | `'scaleToFill'` | 不保持比例,完全拉伸(xtaro 默认值) | | ||
| | `'center'` | `'center'` | 不缩放,只显示中间区域 | | ||
| | `'repeat'` | *无对应* | xtaro rn 端无此值 → 退化 `'scaleToFill'` + 写入 §7 QA warn | | ||
| | `'contain'` | `'aspectFit'` | ✅ rn 支持,保持纵横比,长边完全显示 | | ||
| | `'cover'` | `'aspectFill'` | ✅ rn 支持,保持纵横比,填满容器 | | ||
| | `'stretch'` | `'scaleToFill'` | ✅ rn 支持,不保持比例,完全拉伸(xtaro 默认值) | | ||
| | `'center'` | `'scaleToFill'` | ⚠️ rn 端不支持 `center`,退化 `scaleToFill` + QA warn | | ||
| | `'repeat'` | `'scaleToFill'` | ⚠️ rn 端不支持,退化 `scaleToFill` + QA warn | | ||
| **注**:若 xtaro.json 尚未把 `resizeMode` 改名成 `mode`,agent 需**同时**改 prop 名和值(不能只改值)。建议保持 xtaro.json 与本手册联动。 | ||
| **改写示例**: | ||
| ```tsx | ||
| // §5.5.3b propMap 处理后(prop 名已改) | ||
| <XImage src={require('./bg.png')} style={styles.bg} resizeMode="cover" /> | ||
| // §5.5.3c valueMap 处理后(prop 值同步改) | ||
| <XImage src={require('./bg.png')} style={styles.bg} mode="aspectFill" /> | ||
| ``` | ||
| **注**:遗留场景兜底 — 若某天回滚了 propMap,agent 需**同时**改 prop 名和值(等价于本节 + §5.5.3b 的 `resizeMode → mode`)。 | ||
| ### 1.2 `TextInput.keyboardType` (RN) → `type` (xtaro) | ||
| **说明**:xtaro rn 端支持的 type 是子集,超出的一律退化 + QA warn。 | ||
| **说明**:xtaro.json propMap 已把 `keyboardType` 改名成 `type`,agent **只改属性值**。xtaro rn 端支持的 type 是子集,超出的一律退化 + QA warn。 | ||
| xtaro `XInput.type` 完整取值(来自 `XInput.d.ts` `XInputProps.Type`):`text` / `number` / `idcard` / `digit` / `safe-password` / `nickname` / `numberpad` / `digitpad` / `idcardpad` / `email`;其中 `@supported rn` 标注的有 `text` / `number` / `idcard` / `digit` / `email`。 | ||
| | RN keyboardType | xtaro type | 备注 | | ||
@@ -49,8 +62,10 @@ |---|---|---| | ||
| | `'number-pad'` | `'number'` | 同 numeric | | ||
| | `'ascii-capable'` / `'url'` / 其他 | `'text'` | 全部退化 text + QA warn | | ||
| | `'ascii-capable'` / `'url'` / `'name-phone-pad'` / `'twitter'` / `'web-search'` / 其他 | `'text'` | 全部退化 text + QA warn | | ||
| ### 1.3 `TextInput.returnKeyType` (RN) → `confirmType` (xtaro) | ||
| **说明**:值域**基本一致**,但 RN 有 xtaro 没有的 `'default'` / `'previous'` 等,退化处理。 | ||
| **说明**:xtaro.json propMap 已把 `returnKeyType` 改名成 `confirmType`,agent **只改属性值**。值域**基本一致**,但 RN 有 xtaro 没有的 `'default'` / `'previous'` 等,退化处理。 | ||
| xtaro `XInput.confirmType` 完整取值(来自 `XInput.d.ts` `XInputProps.ConfirmType`):`send` / `search` / `next` / `go` / `done`。 | ||
| | RN returnKeyType | xtaro confirmType | 备注 | | ||
@@ -63,5 +78,5 @@ |---|---|---| | ||
| | `'send'` | `'send'` | 一致 | | ||
| | `'default'` / `'previous'` / `'yahoo'` / `'google'` / `'route'` | `'done'` | 全部退化 + QA warn | | ||
| | `'default'` / `'previous'` / `'yahoo'` / `'google'` / `'route'` / `'join'` / `'emergency-call'` | `'done'` | 全部退化 + QA warn | | ||
| **注**:值域大部分重合,建议 agent 命中不在上表的值时才写 §7 QA warn;重合值静默改。**prop 名需要改**(returnKeyType → confirmType),这一步可以放 xtaro.json propMap,也可以在此手册里改;当前 xtaro.json 未声明,agent 在 §5.5.3c 时改名 + 改值。 | ||
| **注**:值域大部分重合,建议 agent 命中不在上表的值时才写 §7 QA warn;重合值静默改。 | ||
@@ -223,20 +238,110 @@ --- | ||
| 以下属性 xtaro rn 端不支持,agent 在 §5.5.3c 时**删除属性** + 写入 §7 QA warn 段(列出文件名 + 行号 + 属性名)。 | ||
| 以下属性 xtaro rn 端不支持,agent 在 §5.5.3c 时**删除属性** + 写入 §7 QA warn 段(列出文件名 + 行号 + 属性名)。清单依据 `@ctrip/xtaro-types/types/component/*.d.ts` 每个 prop 的 `@supported` 注释,凡是**明确不含 `rn`** 的属性都在此表。 | ||
| | 原属性(RN) | 原标签 | 丢弃原因 | | ||
| ### 5.1 `Text` (RN) → `XText` | ||
| | RN 属性 | xtaro XText 是否支持 rn | 处理策略 | | ||
| |---|---|---| | ||
| | `numberOfLines` | `Text` | xtaro XText 只在 alipay 端支持;rn 端多行省略需在业务代码用 CSS `-webkit-line-clamp` 或 Text 手工截断 | | ||
| | `ellipsizeMode` | `Text` | 无对应 | | ||
| | `selectable` | `Text` | ✅ 一致,不丢(此项仅列为对照,agent 保留原属性) | | ||
| | `showsHorizontalScrollIndicator` | `ScrollView` | 无内置;xtaro 靠 CSS `::-webkit-scrollbar` 隐藏,业务侧手工写 | | ||
| | `showsVerticalScrollIndicator` | `ScrollView` | 同上 | | ||
| | `pagingEnabled` | `ScrollView` | xtaro 只 weapp 支持;rn 端无 | | ||
| | `onMomentumScrollEnd` | `ScrollView` | xtaro 只 weapp 支持 | | ||
| | `multiline` | `TextInput` | 无;若确需多行,改用 `XTextarea`(不同标签,超出本预设覆盖范围,QA error) | | ||
| | `keyboardAppearance` | `TextInput` | 无对应 | | ||
| | `blurOnSubmit` | `TextInput` | 无对应 | | ||
| | `caretHidden` | `TextInput` | 无对应 | | ||
| | `numberOfLines` | ❌ 只支持 `alipay` | 静默删 + QA warn(rn 端多行省略需 CSS `-webkit-line-clamp` 或代码截断) | | ||
| | `ellipsizeMode` | ❌ 无 | 静默删 + QA warn | | ||
| | `adjustsFontSizeToFit` | ❌ 无 | 静默删 + QA warn | | ||
| | `minimumFontScale` | ❌ 无 | 静默删 + QA warn | | ||
| | `allowFontScaling` | ❌ 无 | 静默删 + QA warn | | ||
| | `maxFontSizeMultiplier` | ❌ 无 | 静默删 + QA warn | | ||
| | `selectable` | ✅ 保留 | 静默改(**不删**,列此仅作对照) | | ||
| | `onPress`(Text 上的) | ❌ RN Text 有 onPress;XText 无对应事件 | 静默删 + QA error(需要点击的文字通常应外包 XView 处理 onClick,超出 preset 覆盖) | | ||
| | `onLongPress`(Text 上的) | ❌ 同上 | 静默删 + QA warn | | ||
| **注**:`selectable` 列在这里只作对照(实际保留);其他项 agent 静默删 + §7 QA 段 warn。 | ||
| ### 5.2 `View` / `Pressable` (RN) → `XView` | ||
| | RN 属性 | xtaro XView 是否支持 rn | 处理策略 | | ||
| |---|---|---| | ||
| | `pointerEvents` | ❌ 无对应 prop(RN 直接支持 `pointerEvents`,xtaro 需在 style 里写 `pointerEvents: 'none'`) | 静默改进 style 内(注:该 style 属性 RN 端可用);无法搬迁则删 + QA warn | | ||
| | `needsOffscreenAlphaCompositing` | ❌ 无 | 静默删 + QA warn | | ||
| | `renderToHardwareTextureAndroid` | ❌ 无 | 静默删 + QA warn | | ||
| | `shouldRasterizeIOS` | ❌ 无 | 静默删 + QA warn | | ||
| | `collapsable` | ❌ 无 | 静默删 + QA warn | | ||
| | `Pressable.android_ripple` | ❌ 无 | 静默删 + QA warn | | ||
| | `Pressable.hitSlop` | ❌ 无(XView 用 style/CSS 实现类似效果) | 静默删 + QA warn | | ||
| | `Pressable.delayLongPress` | ❌ 无 | 静默删 + QA warn | | ||
| | `Pressable.disabled` | ❌ XView 无 disabled prop | 静默删 + QA warn(如需禁用点击,业务侧在 onClick 里判 return) | | ||
| **注**:XView 的 `onTouchStart/onTouchMove/onTouchEnd/onTouchCancel/onLongPress` 与 RN 语义基本一致,静默保留即可(见 `XView.d.ts` `EventProps`)。 | ||
| ### 5.3 `Image` (RN) → `XImage` | ||
| | RN 属性 | xtaro XImage 是否支持 rn | 处理策略 | | ||
| |---|---|---| | ||
| | `defaultSource` | ❌ 只支持 `alipay` | 静默删 + QA warn(可考虑外层套 XView 做占位背景) | | ||
| | `blurRadius` | ❌ 无对应 prop | 静默删 + QA warn | | ||
| | `fadeDuration` | ❌ 无 | 静默删 + QA warn | | ||
| | `progressiveRenderingEnabled` | ❌ 无 | 静默删 + QA warn | | ||
| | `capInsets` | ❌ 无 | 静默删 + QA warn | | ||
| | `loadingIndicatorSource` | ❌ 无 | 静默删 + QA warn | | ||
| | `onProgress` | ❌ 无 | 静默删 + QA warn | | ||
| | `onPartialLoad` | ❌ 无 | 静默删 + QA warn | | ||
| | `onLoadStart` / `onLoadEnd` | ❌ 无(XImage 只有 `onLoad` / `onError`) | 静默删 + QA warn | | ||
| | `resizeMethod` | ❌ 无 | 静默删 + QA warn | | ||
| ### 5.4 `ScrollView` (RN) → `XScrollView` | ||
| | RN 属性 | xtaro XScrollView 是否支持 rn | 处理策略 | | ||
| |---|---|---| | ||
| | `showsHorizontalScrollIndicator` | ❌ 无(靠 CSS `::-webkit-scrollbar` 隐藏) | 静默删 + QA warn | | ||
| | `showsVerticalScrollIndicator` | ❌ 同上 | 静默删 + QA warn | | ||
| | `pagingEnabled` | ❌ 只 `weapp/swan` 支持 | 静默删 + QA warn | | ||
| | `onMomentumScrollBegin` | ❌ 只 `weapp` 支持 | 静默删 + QA warn | | ||
| | `onMomentumScrollEnd` | ❌ 只 `weapp` 支持 | 静默删 + QA warn | | ||
| | `onScrollBeginDrag` | ❌ 无(XScrollView `onDragStart` 只 `weapp` 支持) | 静默删 + QA warn | | ||
| | `onScrollEndDrag` | ❌ 同上 | 静默删 + QA warn | | ||
| | `refreshControl`(RefreshControl 组件) | ❌ 无对应 prop(XScrollView 的 refresher* 系列只 `weapp` 支持) | 静默删 + QA error(下拉刷新需业务侧改造,超出 preset 覆盖) | | ||
| | `stickyHeaderIndices` | ❌ 无 | 静默删 + QA warn | | ||
| | `keyboardShouldPersistTaps` | ❌ 无 | 静默删 + QA warn | | ||
| | `keyboardDismissMode` | ❌ 无 | 静默删 + QA warn | | ||
| | `bounces` | ❌ 只 `weapp/swan` 支持 | 静默删 + QA warn | | ||
| | `decelerationRate` | ❌ 无(xtaro `fastDeceleration` 只 `weapp`) | 静默删 + QA warn | | ||
| | `snapToInterval` / `snapToAlignment` / `snapToOffsets` / `snapToStart` / `snapToEnd` | ❌ 无 | 静默删 + QA warn | | ||
| | `overScrollMode` | ❌ 无 | 静默删 + QA warn | | ||
| | `nestedScrollEnabled` | ❌ 无 | 静默删 + QA warn | | ||
| | `contentInset` / `contentInsetAdjustmentBehavior` / `contentOffset` | ❌ 无对应 prop(XScrollView 用 `scrollTop/scrollLeft` 表达位置,`padding` 只 `weapp`) | 静默删 + QA warn(初始滚动位置可用 `scrollTop`/`scrollLeft` 替代,业务侧手工核对) | | ||
| | `automaticallyAdjustContentInsets` | ❌ 无 | 静默删 + QA warn | | ||
| | `alwaysBounceHorizontal` / `alwaysBounceVertical` | ❌ 无 | 静默删 + QA warn | | ||
| | `directionalLockEnabled` | ❌ 无 | 静默删 + QA warn | | ||
| | `maintainVisibleContentPosition` | ❌ 无 | 静默删 + QA warn | | ||
| | `scrollEnabled` | ❌ 无对应 prop(禁用滚动可写 `scrollX={false} scrollY={false}` 但语义不同) | 静默删 + QA warn | | ||
| | `scrollEventThrottle` | ✅ 保留(XScrollView 支持 `scrollEventThrottle`) | 静默改(**不删**,列此仅作对照) | | ||
| | `onEndReachedThreshold` | ⚠️ 单位不同 | 见 §3.4,不在本表 | | ||
| ### 5.5 `TextInput` (RN) → `XInput` | ||
| | RN 属性 | xtaro XInput 是否支持 rn | 处理策略 | | ||
| |---|---|---| | ||
| | `multiline` | ❌ 无;若确需多行须改用 `XTextarea`(不同标签) | 静默删 + QA error(超出 preset 覆盖范围) | | ||
| | `numberOfLines` | ❌ 无 | 静默删 + QA warn | | ||
| | `keyboardAppearance` | ❌ 无 | 静默删 + QA warn | | ||
| | `blurOnSubmit` | ❌ 无 | 静默删 + QA warn | | ||
| | `caretHidden` | ❌ 无 | 静默删 + QA warn | | ||
| | `contextMenuHidden` | ❌ 无 | 静默删 + QA warn | | ||
| | `selectionColor` | ❌ 无(xtaro 用 `placeholderTextColor` 只控 placeholder 颜色) | 静默删 + QA warn | | ||
| | `underlineColorAndroid` | ❌ 无 | 静默删 + QA warn | | ||
| | `textContentType` | ❌ 无 | 静默删 + QA warn | | ||
| | `autoCorrect` | ❌ 无 | 静默删 + QA warn | | ||
| | `autoCapitalize` | ❌ 无 | 静默删 + QA warn | | ||
| | `autoComplete` | ❌ 无 | 静默删 + QA warn | | ||
| | `spellCheck` | ❌ 无 | 静默删 + QA warn | | ||
| | `clearButtonMode` / `clearTextOnFocus` | ❌ 无 | 静默删 + QA warn | | ||
| | `enablesReturnKeyAutomatically` | ❌ 无 | 静默删 + QA warn | | ||
| | `passwordRules` | ❌ 无 | 静默删 + QA warn | | ||
| | `rejectResponderTermination` | ❌ 无 | 静默删 + QA warn | | ||
| | `scrollEnabled`(TextInput 上的) | ❌ 无 | 静默删 + QA warn | | ||
| | `selectTextOnFocus` | ❌ 无 | 静默删 + QA warn | | ||
| | `showSoftInputOnFocus` | ❌ 无 | 静默删 + QA warn | | ||
| | `onKeyPress` | ❌ 无(XInput 只有 `onInput/onFocus/onBlur/onConfirm`) | 静默删 + QA warn | | ||
| | `onSelectionChange` | ❌ 无 | 静默删 + QA warn | | ||
| | `onContentSizeChange` | ❌ 无 | 静默删 + QA warn | | ||
| | `onEndEditing` | ❌ 无 | 静默删 + QA warn | | ||
| | `onScroll`(TextInput 上的) | ❌ 无 | 静默删 + QA warn | | ||
| **注**:表中标 ✅ 的行(`Text.selectable` / `ScrollView.scrollEventThrottle`)只作对照,agent **保留原属性**。其他项静默删 + §7 QA 段 warn。 | ||
| --- | ||
@@ -248,8 +353,8 @@ | ||
| 1. **值域映射**(§一):`resizeMode`(Image)/ `keyboardType`(TextInput)/ `returnKeyType`(TextInput) 命中 → 按 valueMap 改值 + 必要时改 prop 名 | ||
| 1. **值域映射**(§一):`Image.mode`(propMap 已改名,值需按 §1.1 映射)/ `TextInput.type`(propMap 已改名,值按 §1.2)/ `TextInput.confirmType`(propMap 已改名,值按 §1.3) 命中 → 只改属性值 | ||
| 2. **布尔取反**(§二):`editable`(TextInput) 命中 → 改成 `disabled` + 值取反 | ||
| 3. **事件签名**(§三):`onChangeText` / `onPress` / `onPressIn` / `onPressOut` / `onEndReachedThreshold` 命中 → 改 prop 名 + 改回调函数体(§3.1 需读回调体) | ||
| 4. **结构变化**(§四):`ScrollView.horizontal` 命中 → 删原 prop + 加 scrollX/scrollY;`contentContainerStyle` 命中 → 包 XView | ||
| 5. **丢弃属性**(§五):表内属性命中 → 删属性 + 写入 §7 QA warn 段 | ||
| 5. **丢弃属性**(§五):表内属性命中 → 删属性 + 写入 §7 QA warn 段(逐组件 5.1~5.5,共 60+ 项) | ||
| **未在本手册出现的属性**一律**保留原样**;`style / key / ref / children / className` 永远不改。 |
@@ -8,5 +8,2 @@ { | ||
| }, | ||
| "figma": { | ||
| "token": "" | ||
| }, | ||
| "merge": { | ||
@@ -13,0 +10,0 @@ "mode": "component" |
@@ -8,5 +8,2 @@ { | ||
| }, | ||
| "figma": { | ||
| "token": "" | ||
| }, | ||
| "merge": { | ||
@@ -13,0 +10,0 @@ "mode": "component" |
@@ -50,2 +50,37 @@ #!/usr/bin/env node | ||
| // 极简 .env 解析:只处理 KEY=VALUE 行,支持引号包裹和 # 注释,不做变量插值 | ||
| // 之所以自己写而不引入 dotenv:pp 仓库承诺零 npm 依赖,figma.mjs 现在只用 Node 18+ 内置能力 | ||
| function parseEnvFile(text) { | ||
| const out = {} | ||
| for (const rawLine of text.split(/\r?\n/)) { | ||
| const line = rawLine.trim() | ||
| if (!line || line.startsWith('#')) continue | ||
| const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/) | ||
| if (!m) continue | ||
| let val = m[2] | ||
| // 剥单/双引号(常见于 KEY="xxx" 写法) | ||
| if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { | ||
| val = val.slice(1, -1) | ||
| } | ||
| out[m[1]] = val | ||
| } | ||
| return out | ||
| } | ||
| // token 读取优先级:process.env > 项目根 .env FIGMA_TOKEN > config.figma.token(向后兼容) | ||
| // v1.0.2 起主推 .env,老项目 config 里的 token 由 install.js 迁移;此处保留兼容读取避免旧项目立即断 | ||
| function loadFigmaToken(config, projectRoot) { | ||
| if (process.env.FIGMA_TOKEN) return process.env.FIGMA_TOKEN | ||
| const envPath = path.join(projectRoot, '.env') | ||
| if (fs.existsSync(envPath)) { | ||
| try { | ||
| const parsed = parseEnvFile(fs.readFileSync(envPath, 'utf8')) | ||
| if (parsed.FIGMA_TOKEN) return parsed.FIGMA_TOKEN | ||
| } catch (_) { | ||
| // .env 读失败不阻塞,继续走 config 兜底 | ||
| } | ||
| } | ||
| return config?.figma?.token || '' | ||
| } | ||
| function parseFlags(argv) { | ||
@@ -126,5 +161,5 @@ const positional = [] | ||
| async function cmdVerifyToken() { | ||
| const { config } = loadConfig() | ||
| const token = config.figma?.token | ||
| if (!token) return fail('figma.token 未在 pp-d2c.config.json 配置') | ||
| const { config, projectRoot } = loadConfig() | ||
| const token = loadFigmaToken(config, projectRoot) | ||
| if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)') | ||
| try { | ||
@@ -145,4 +180,4 @@ const me = await figmaFetch('/v1/me', token) | ||
| const { config, projectRoot } = loadConfig() | ||
| const token = config.figma?.token | ||
| if (!token) return fail('figma.token 未配置') | ||
| const token = loadFigmaToken(config, projectRoot) | ||
| if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)') | ||
@@ -197,4 +232,4 @@ const paths = cachePaths(projectRoot, fileKey) | ||
| const { config, projectRoot } = loadConfig() | ||
| const token = config.figma?.token | ||
| if (!token) return fail('figma.token 未配置') | ||
| const token = loadFigmaToken(config, projectRoot) | ||
| if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)') | ||
@@ -241,4 +276,4 @@ const paths = cachePaths(projectRoot, fileKey) | ||
| const { config, projectRoot } = loadConfig() | ||
| const token = config.figma?.token | ||
| if (!token) return fail('figma.token 未配置') | ||
| const token = loadFigmaToken(config, projectRoot) | ||
| if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)') | ||
@@ -294,4 +329,4 @@ const assetsDir = config.images?.assetsDir || 'static/' | ||
| const { config, projectRoot } = loadConfig() | ||
| const token = config.figma?.token | ||
| if (!token) return fail('figma.token 未配置') | ||
| const token = loadFigmaToken(config, projectRoot) | ||
| if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)') | ||
@@ -353,3 +388,4 @@ const dir = tmpScreenshotsDir(projectRoot) | ||
| 所有命令都从 cwd 向上查找 pp-d2c.config.json 拿 figma.token 和 assetsDir。 | ||
| 所有命令都从 cwd 向上查找 pp-d2c.config.json 拿 assetsDir; | ||
| FIGMA_TOKEN 从项目根 .env 读(优先级 process.env > .env > config.figma.token 兜底)。 | ||
| 输出统一为 stdout 一行 JSON: {ok: true, data: {...}} 或 {ok: false, error: "..."}。 | ||
@@ -356,0 +392,0 @@ 退出码 0 表示成功,非零表示失败。 |
@@ -50,2 +50,37 @@ #!/usr/bin/env node | ||
| // 极简 .env 解析:只处理 KEY=VALUE 行,支持引号包裹和 # 注释,不做变量插值 | ||
| // 之所以自己写而不引入 dotenv:pp 仓库承诺零 npm 依赖,figma.mjs 现在只用 Node 18+ 内置能力 | ||
| function parseEnvFile(text) { | ||
| const out = {} | ||
| for (const rawLine of text.split(/\r?\n/)) { | ||
| const line = rawLine.trim() | ||
| if (!line || line.startsWith('#')) continue | ||
| const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/) | ||
| if (!m) continue | ||
| let val = m[2] | ||
| // 剥单/双引号(常见于 KEY="xxx" 写法) | ||
| if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { | ||
| val = val.slice(1, -1) | ||
| } | ||
| out[m[1]] = val | ||
| } | ||
| return out | ||
| } | ||
| // token 读取优先级:process.env > 项目根 .env FIGMA_TOKEN > config.figma.token(向后兼容) | ||
| // v1.0.2 起主推 .env,老项目 config 里的 token 由 install.js 迁移;此处保留兼容读取避免旧项目立即断 | ||
| function loadFigmaToken(config, projectRoot) { | ||
| if (process.env.FIGMA_TOKEN) return process.env.FIGMA_TOKEN | ||
| const envPath = path.join(projectRoot, '.env') | ||
| if (fs.existsSync(envPath)) { | ||
| try { | ||
| const parsed = parseEnvFile(fs.readFileSync(envPath, 'utf8')) | ||
| if (parsed.FIGMA_TOKEN) return parsed.FIGMA_TOKEN | ||
| } catch (_) { | ||
| // .env 读失败不阻塞,继续走 config 兜底 | ||
| } | ||
| } | ||
| return config?.figma?.token || '' | ||
| } | ||
| function parseFlags(argv) { | ||
@@ -126,5 +161,5 @@ const positional = [] | ||
| async function cmdVerifyToken() { | ||
| const { config } = loadConfig() | ||
| const token = config.figma?.token | ||
| if (!token) return fail('figma.token 未在 pp-d2c.config.json 配置') | ||
| const { config, projectRoot } = loadConfig() | ||
| const token = loadFigmaToken(config, projectRoot) | ||
| if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)') | ||
| try { | ||
@@ -145,4 +180,4 @@ const me = await figmaFetch('/v1/me', token) | ||
| const { config, projectRoot } = loadConfig() | ||
| const token = config.figma?.token | ||
| if (!token) return fail('figma.token 未配置') | ||
| const token = loadFigmaToken(config, projectRoot) | ||
| if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)') | ||
@@ -197,4 +232,4 @@ const paths = cachePaths(projectRoot, fileKey) | ||
| const { config, projectRoot } = loadConfig() | ||
| const token = config.figma?.token | ||
| if (!token) return fail('figma.token 未配置') | ||
| const token = loadFigmaToken(config, projectRoot) | ||
| if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)') | ||
@@ -241,4 +276,4 @@ const paths = cachePaths(projectRoot, fileKey) | ||
| const { config, projectRoot } = loadConfig() | ||
| const token = config.figma?.token | ||
| if (!token) return fail('figma.token 未配置') | ||
| const token = loadFigmaToken(config, projectRoot) | ||
| if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)') | ||
@@ -294,4 +329,4 @@ const assetsDir = config.images?.assetsDir || 'static/' | ||
| const { config, projectRoot } = loadConfig() | ||
| const token = config.figma?.token | ||
| if (!token) return fail('figma.token 未配置') | ||
| const token = loadFigmaToken(config, projectRoot) | ||
| if (!token) return fail('FIGMA_TOKEN 未配置 (请在项目根 .env 写 FIGMA_TOKEN=xxx)') | ||
@@ -353,3 +388,4 @@ const dir = tmpScreenshotsDir(projectRoot) | ||
| 所有命令都从 cwd 向上查找 pp-d2c.config.json 拿 figma.token 和 assetsDir。 | ||
| 所有命令都从 cwd 向上查找 pp-d2c.config.json 拿 assetsDir; | ||
| FIGMA_TOKEN 从项目根 .env 读(优先级 process.env > .env > config.figma.token 兜底)。 | ||
| 输出统一为 stdout 一行 JSON: {ok: true, data: {...}} 或 {ok: false, error: "..."}。 | ||
@@ -356,0 +392,0 @@ 退出码 0 表示成功,非零表示失败。 |
@@ -35,3 +35,3 @@ # pp-doctor Skill | ||
| 请检查 `pp-d2c.config.json` 里的 `figma.token`: | ||
| 请检查项目根 `.env` 里的 `FIGMA_TOKEN`: | ||
| 1. 是否已配置且未过期(Figma 网页版右上角头像 → Settings → Security → Personal access tokens) | ||
@@ -38,0 +38,0 @@ 2. Token 权限是否包含 File content: Read-only |
@@ -16,3 +16,3 @@ # pp-d2c 命名与实现规则 | ||
| | `project.styleFormat` | 样式方案(见下表) | | ||
| | `figma.token` | Figma Personal Access Token(REST API 切图用) | | ||
| | `FIGMA_TOKEN` (项目根 `.env`) | Figma Personal Access Token(REST API 切图用;v1.0.2 起从 config 迁到 .env) | | ||
| | `images.assetsDir` | 图片下载目录 | | ||
@@ -217,7 +217,7 @@ | `images.imageBaseUrl` | 代码中图片 src 前缀 | | ||
| ### L0 主路径(figma.token 非空时,必须走此路径) | ||
| ### L0 主路径(`FIGMA_TOKEN` 非空时,必须走此路径) | ||
| ```bash | ||
| # PNG 2倍图,严格按 bbox(不含 effect / 父背景色) | ||
| curl -H "X-Figma-Token: {figma.token}" \ | ||
| curl -H "X-Figma-Token: {FIGMA_TOKEN}" \ | ||
| "https://api.figma.com/v1/images/{fileKey}?ids={nodeId}&format=png&scale=2&use_absolute_bounds=true" \ | ||
@@ -227,3 +227,3 @@ -o {projectRoot}/{assetsDir}/{filename}.png | ||
| # SVG(矢量图层优先) | ||
| curl -H "X-Figma-Token: {figma.token}" \ | ||
| curl -H "X-Figma-Token: {FIGMA_TOKEN}" \ | ||
| "https://api.figma.com/v1/images/{fileKey}?ids={nodeId}&format=svg&use_absolute_bounds=true" \ | ||
@@ -380,3 +380,3 @@ -o {projectRoot}/{assetsDir}/{filename}.svg | ||
| - 禁止父容器同时有 `bgc-` 和 `bg-` 时只写 `background-image` 不写 bgc- 的其他属性 | ||
| - 禁止 `figma.token` 存在且非空时使用 MCP `download_assets` 导出图片(token 有效必须走 L0) | ||
| - 禁止 `FIGMA_TOKEN` 存在且非空时使用 MCP `download_assets` 导出图片(token 有效必须走 L0) | ||
| - 禁止调用 `/v1/images` 时省略 `use_absolute_bounds=true`(除非 nodeId 在 `preserveEffectIds` 中) | ||
@@ -383,0 +383,0 @@ - 禁止把 `bg-` 节点的父容器当成切图源(切图源 nodeId 必须是 `bg-` 节点自己) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
487730
12.59%25
31.58%1778
21.45%356
0.56%10
100%