@open-dy/vibecoding-cli 0.0.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/assets/schemas/custom-card-schema.json +102 -0
- package/bin/vibecoding +89 -0
- package/bin/vibecoding-dev +308 -0
- package/bin/vibecoding-interest-card.mjs +35 -0
- package/bin/vibecoding.mjs +9 -0
- package/package.json +58 -0
- package/src/adapters/interest-card/opencard/commands.mjs +62 -0
- package/src/adapters/interest-card/opencard/dev-watch.mjs +203 -0
- package/src/adapters/interest-card/opencard/dev_run.sh +126 -0
- package/src/adapters/interest-card/opencard/eslint-plugin-opencard-taro.mjs +278 -0
- package/src/adapters/interest-card/opencard/lint-lynx-css.mjs +676 -0
- package/src/adapters/interest-card/opencard/lint.sh +131 -0
- package/src/adapters/interest-card/opencard/lynx-css-rules.mjs +508 -0
- package/src/adapters/interest-card/opencard/pack.sh +27 -0
- package/src/adapters/interest-card/opencard/validate-opencard-structure.mjs +65 -0
- package/src/adapters/interest-card/opencard/validate-widget-shell.mjs +902 -0
- package/src/adapters/interest-card/opencard/validate.mjs +11 -0
- package/src/adapters/interest-card/opencard/write-web-config.mjs +17 -0
- package/src/build/opencard-environment.mjs +49 -0
- package/src/build/turbo-config.mjs +140 -0
- package/src/cli.mjs +104 -0
- package/src/commands/build.mjs +138 -0
- package/src/commands/db.mjs +14 -0
- package/src/commands/dev.mjs +319 -0
- package/src/commands/lint.mjs +176 -0
- package/src/commands/pack.mjs +169 -0
- package/src/commands/turbo.mjs +36 -0
- package/src/commands/validate.mjs +26 -0
- package/src/commands/verify.mjs +602 -0
- package/src/core/args.mjs +101 -0
- package/src/core/context.mjs +101 -0
- package/src/core/environment.mjs +55 -0
- package/src/core/errors.mjs +14 -0
- package/src/core/ports.mjs +76 -0
- package/src/core/process-output.mjs +25 -0
- package/src/core/runner.mjs +90 -0
- package/src/core/service-log-context.mjs +27 -0
- package/src/core/web-config.mjs +13 -0
- package/src/lint/lint-lynx-css.mjs +244 -0
- package/src/lint/lynx-css-rules.mjs +138 -0
- package/src/preview/dev-watch.mjs +38 -0
- package/src/preview/start-preview.mjs +213 -0
- package/src/preview/watch-card-routing.mjs +89 -0
- package/src/scripts/pack.mjs +15 -0
- package/src/validators/dependencies.mjs +34 -0
- package/src/validators/empty-project.mjs +29 -0
- package/src/validators/feed-card.cjs +156 -0
- package/src/validators/feed-card.mjs +32 -0
- package/src/validators/opencard-structure.mjs +58 -0
- package/src/validators/routes.cjs +470 -0
- package/src/validators/routes.mjs +17 -0
- package/src/validators/sql.mjs +57 -0
- package/src/validators/widget-shell.mjs +107 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# opencard lint:结构 / 底板 / Lynx CSS / tsc / eslint 并行,失败汇总后一次退出。
|
|
3
|
+
# LINT_INCREMENTAL=1 时 eslint 只扫 git 变更的 src 文件;仅 CSS 变更时跳过 eslint。
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT="${1:?Missing project root}"
|
|
7
|
+
shift
|
|
8
|
+
cd "$ROOT"
|
|
9
|
+
ADAPTER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
|
|
11
|
+
resolve_git_base() {
|
|
12
|
+
local top
|
|
13
|
+
top="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
14
|
+
if [[ -z "${top}" ]] || ! git -C "${top}" rev-parse --git-dir >/dev/null 2>&1; then
|
|
15
|
+
return 1
|
|
16
|
+
fi
|
|
17
|
+
if git -C "${top}" rev-parse --verify origin/main >/dev/null 2>&1; then
|
|
18
|
+
echo "origin/main"
|
|
19
|
+
elif git -C "${top}" rev-parse --verify origin/master >/dev/null 2>&1; then
|
|
20
|
+
echo "origin/master"
|
|
21
|
+
else
|
|
22
|
+
git -C "${top}" rev-list --max-parents=0 HEAD 2>/dev/null | tail -n 1
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
collect_opencard_changed() {
|
|
27
|
+
local top base changed rel_prefix="opencard/"
|
|
28
|
+
top="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
29
|
+
if [[ -z "${top}" ]]; then
|
|
30
|
+
return 1
|
|
31
|
+
fi
|
|
32
|
+
base="$(resolve_git_base || true)"
|
|
33
|
+
if [[ -z "${base:-}" ]]; then
|
|
34
|
+
return 1
|
|
35
|
+
fi
|
|
36
|
+
changed="$(git -C "${top}" diff --name-only "${base}"...HEAD -- 'opencard/src/**' 2>/dev/null || true)"
|
|
37
|
+
if [[ -z "${changed}" ]]; then
|
|
38
|
+
changed="$(git -C "${top}" diff --name-only HEAD -- 'opencard/src/**' 2>/dev/null || true)"
|
|
39
|
+
fi
|
|
40
|
+
if [[ -z "${changed}" ]]; then
|
|
41
|
+
return 1
|
|
42
|
+
fi
|
|
43
|
+
while IFS= read -r file; do
|
|
44
|
+
[[ -n "${file}" ]] && echo "${file#${rel_prefix}}"
|
|
45
|
+
done <<< "${changed}"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
CHANGED_FILES=()
|
|
49
|
+
if [[ "${LINT_INCREMENTAL:-}" == "1" ]]; then
|
|
50
|
+
while IFS= read -r file; do
|
|
51
|
+
[[ -n "${file}" ]] && CHANGED_FILES+=("${file}")
|
|
52
|
+
done <<< "$(collect_opencard_changed || true)"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
RUN_ESLINT=1
|
|
56
|
+
ESLINT_FILES=()
|
|
57
|
+
if [[ "${#CHANGED_FILES[@]}" -gt 0 ]]; then
|
|
58
|
+
echo "[opencard lint] incremental mode (${#CHANGED_FILES[@]} files)"
|
|
59
|
+
has_script=0
|
|
60
|
+
for file in "${CHANGED_FILES[@]}"; do
|
|
61
|
+
case "${file}" in
|
|
62
|
+
*.js|*.jsx|*.ts|*.tsx)
|
|
63
|
+
has_script=1
|
|
64
|
+
ESLINT_FILES+=("${file}")
|
|
65
|
+
;;
|
|
66
|
+
esac
|
|
67
|
+
done
|
|
68
|
+
if [[ "${has_script}" -eq 0 ]]; then
|
|
69
|
+
RUN_ESLINT=0
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
mkdir -p node_modules/.cache/eslint
|
|
74
|
+
|
|
75
|
+
TMPDIR="$(mktemp -d)"
|
|
76
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
77
|
+
|
|
78
|
+
NAMES=()
|
|
79
|
+
PIDS=()
|
|
80
|
+
|
|
81
|
+
launch() {
|
|
82
|
+
local name="$1"
|
|
83
|
+
shift
|
|
84
|
+
NAMES+=("${name}")
|
|
85
|
+
(
|
|
86
|
+
set +e
|
|
87
|
+
"$@" >"${TMPDIR}/${name}.log" 2>&1
|
|
88
|
+
echo $? >"${TMPDIR}/${name}.code"
|
|
89
|
+
) &
|
|
90
|
+
PIDS+=("$!")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
launch structure node "$ADAPTER_DIR/validate.mjs" "$ROOT" opencard-structure
|
|
94
|
+
launch widget-shell node "$ADAPTER_DIR/validate.mjs" "$ROOT" widget-shell
|
|
95
|
+
launch lynx-css node "$ADAPTER_DIR/validate.mjs" "$ROOT" lynx-css
|
|
96
|
+
launch tsc tsc --noEmit
|
|
97
|
+
|
|
98
|
+
if [[ "${RUN_ESLINT}" -eq 1 ]]; then
|
|
99
|
+
if [[ "${#ESLINT_FILES[@]}" -gt 0 ]]; then
|
|
100
|
+
launch eslint eslint --cache --cache-location node_modules/.cache/eslint/opencard-src "${ESLINT_FILES[@]}"
|
|
101
|
+
else
|
|
102
|
+
launch eslint eslint --cache --cache-location node_modules/.cache/eslint/opencard-src 'src/**/*.{js,jsx,ts,tsx}'
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
for pid in "${PIDS[@]}"; do
|
|
107
|
+
wait "${pid}" || true
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
FAILED=0
|
|
111
|
+
for name in "${NAMES[@]}"; do
|
|
112
|
+
code="$(cat "${TMPDIR}/${name}.code" 2>/dev/null || echo 1)"
|
|
113
|
+
if [[ "${code}" != "0" ]]; then
|
|
114
|
+
FAILED=1
|
|
115
|
+
echo "" >&2
|
|
116
|
+
echo "[opencard lint] ${name} failed (exit ${code})" >&2
|
|
117
|
+
if [[ -s "${TMPDIR}/${name}.log" ]]; then
|
|
118
|
+
cat "${TMPDIR}/${name}.log" >&2
|
|
119
|
+
fi
|
|
120
|
+
elif [[ -s "${TMPDIR}/${name}.log" ]]; then
|
|
121
|
+
echo "" >&2
|
|
122
|
+
echo "[opencard lint] ${name} warnings:" >&2
|
|
123
|
+
cat "${TMPDIR}/${name}.log" >&2
|
|
124
|
+
fi
|
|
125
|
+
done
|
|
126
|
+
|
|
127
|
+
if [[ "${FAILED}" -ne 0 ]]; then
|
|
128
|
+
echo "" >&2
|
|
129
|
+
echo "[opencard lint] ERROR: lint 失败,需要修改后重新检测,直到通过为止" >&2
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lynx 机械禁写规则(单一事实源)。
|
|
3
|
+
* rendering.md 解释差异和替代写法;本文件决定 `pnpm lint` 拦什么。
|
|
4
|
+
* 无法静态判定的布局语义(margin collapse、尺寸链路、flex-shrink 权重等)不在这里。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const HINT = '见 rendering.md';
|
|
8
|
+
|
|
9
|
+
const FORBIDDEN_AT_RULES = new Set([
|
|
10
|
+
'media',
|
|
11
|
+
'supports',
|
|
12
|
+
'layer',
|
|
13
|
+
'charset',
|
|
14
|
+
'namespace',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const FORBIDDEN_AT_RULE_HINT = {
|
|
18
|
+
media: 'Lynx 的 @media 不生效。文字保持 DESIGN.md 的 px 基线;容器用 flex、vw/vh 适配。',
|
|
19
|
+
supports: 'Lynx 不支持 @supports。能力分支放构建期或运行时 class。',
|
|
20
|
+
layer: 'Lynx 不支持 @layer。用常规 CSS 顺序。',
|
|
21
|
+
charset: '不要手写 @charset,交给构建系统。',
|
|
22
|
+
namespace: 'Lynx 不支持 @namespace。',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const FORBIDDEN_PROPERTIES = new Set([
|
|
26
|
+
'float',
|
|
27
|
+
'clear',
|
|
28
|
+
'grid-column',
|
|
29
|
+
'grid-row',
|
|
30
|
+
'grid-area',
|
|
31
|
+
'pointer-events',
|
|
32
|
+
'cursor',
|
|
33
|
+
'resize',
|
|
34
|
+
'scroll-behavior',
|
|
35
|
+
'overscroll-behavior',
|
|
36
|
+
'content',
|
|
37
|
+
'quotes',
|
|
38
|
+
'list-style',
|
|
39
|
+
'list-style-type',
|
|
40
|
+
'list-style-image',
|
|
41
|
+
'list-style-position',
|
|
42
|
+
'caption-side',
|
|
43
|
+
'table-layout',
|
|
44
|
+
'border-collapse',
|
|
45
|
+
'border-spacing',
|
|
46
|
+
'empty-cells',
|
|
47
|
+
'word-break',
|
|
48
|
+
'overflow-wrap',
|
|
49
|
+
'word-wrap',
|
|
50
|
+
'word-spacing',
|
|
51
|
+
'hyphens',
|
|
52
|
+
'tab-size',
|
|
53
|
+
'text-transform',
|
|
54
|
+
'unicode-bidi',
|
|
55
|
+
'writing-mode',
|
|
56
|
+
'text-orientation',
|
|
57
|
+
'transform-style',
|
|
58
|
+
'background-attachment',
|
|
59
|
+
'-webkit-line-clamp',
|
|
60
|
+
'-webkit-box-orient',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const FORBIDDEN_PROPERTY_HINT = {
|
|
64
|
+
float: '不要用 float/clear。改用 Flex / Grid。',
|
|
65
|
+
clear: '不要用 float/clear。改用 Flex / Grid。',
|
|
66
|
+
'grid-column': '不要用 grid-column 简写。改用 grid-column-start / grid-column-end。',
|
|
67
|
+
'grid-row': '不要用 grid-row 简写。改用 grid-row-start / grid-row-end。',
|
|
68
|
+
'grid-area': '不要用 grid-area。拆成 start/end。',
|
|
69
|
+
'pointer-events': 'Lynx 不支持 pointer-events。用条件渲染或事件层级。',
|
|
70
|
+
cursor: 'Lynx 不支持 cursor。不要生成。',
|
|
71
|
+
resize: 'Lynx 不支持 resize。用组件状态或 JS 调尺寸。',
|
|
72
|
+
'scroll-behavior': 'Lynx 不支持 scroll-behavior。纵向滚动仍禁止。',
|
|
73
|
+
'overscroll-behavior': 'Lynx 不支持 overscroll-behavior。不要生成纵向滚动容器。',
|
|
74
|
+
content: '不能配合伪元素生成内容。增加真实节点。',
|
|
75
|
+
quotes: 'Lynx 不支持 quotes。写在文本内容里。',
|
|
76
|
+
'list-style': '列表 marker 不可用。用真实节点模拟。',
|
|
77
|
+
'list-style-type': '列表 marker 不可用。用真实节点模拟。',
|
|
78
|
+
'list-style-image': '列表图片 marker 不可用;SVG marker 也不要用。',
|
|
79
|
+
'list-style-position': '用外层 View 的 Flex / Grid 组织 marker。',
|
|
80
|
+
'caption-side': 'table caption 不可用。用普通节点。',
|
|
81
|
+
'table-layout': 'table layout 不可用。用 CSS Grid。',
|
|
82
|
+
'border-collapse': 'table border 合并不可用。用 grid gap / 单元格边框。',
|
|
83
|
+
'border-spacing': 'table spacing 不可用。用 gap。',
|
|
84
|
+
'empty-cells': 'table empty-cell 不可用。条件渲染或空节点。',
|
|
85
|
+
'word-break': 'Lynx 不支持 word-break。控制宽度 / white-space / ellipsis。',
|
|
86
|
+
'overflow-wrap': 'Lynx 不支持 overflow-wrap。控制文本节点宽度。',
|
|
87
|
+
'word-wrap': 'Lynx 不支持 word-wrap。控制文本节点宽度。',
|
|
88
|
+
'word-spacing': 'Lynx 不支持 word-spacing。',
|
|
89
|
+
hyphens: 'Lynx 不支持 hyphens。',
|
|
90
|
+
'tab-size': 'Lynx 不支持 tab-size。数据层替换 tab。',
|
|
91
|
+
'text-transform': 'Lynx 不支持 text-transform。在数据层转换文本。',
|
|
92
|
+
'unicode-bidi': 'Lynx 不支持 unicode-bidi。',
|
|
93
|
+
'writing-mode': 'Lynx 不支持 writing-mode。必要时用 transform 旋转。',
|
|
94
|
+
'text-orientation': 'Lynx 不支持 text-orientation。',
|
|
95
|
+
'transform-style': 'Lynx 不支持 transform-style。',
|
|
96
|
+
'background-attachment': '不支持 background-attachment。只保留 color/image/repeat/position。',
|
|
97
|
+
'-webkit-line-clamp': '不要迁移 -webkit-line-clamp。单行用 nowrap + ellipsis;多行减行。',
|
|
98
|
+
'-webkit-box-orient': '不要迁移 -webkit-box。',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const FORBIDDEN_PROPERTY_PREFIXES = [
|
|
102
|
+
['scroll-snap-', 'Lynx 不支持 scroll-snap-*。横向轮播用 Swiper。'],
|
|
103
|
+
['counter-', 'CSS counter 不可用。在数据层计算序号。'],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const DISPLAY_FORBIDDEN = [
|
|
107
|
+
[/^inline\b/i, '不要用 inline / inline-block。结构用外层 View 的 Flex / Grid。'],
|
|
108
|
+
[/^table/i, 'table layout 不可用。用 CSS Grid。'],
|
|
109
|
+
[/^contents$/i, 'display: contents 不可用。保留容器或重构层级。'],
|
|
110
|
+
[/^list-item$/i, 'display: list-item 不可用。用真实节点模拟 marker。'],
|
|
111
|
+
[/-webkit-box/i, '不要迁移 display: -webkit-box / -webkit-line-clamp。'],
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const POSITION_FORBIDDEN = new Set(['static', 'sticky']);
|
|
115
|
+
|
|
116
|
+
const OVERFLOW_BLOCKED = new Set(['auto', 'scroll']);
|
|
117
|
+
|
|
118
|
+
const WHITE_SPACE_FORBIDDEN = new Set([
|
|
119
|
+
'pre',
|
|
120
|
+
'pre-wrap',
|
|
121
|
+
'pre-line',
|
|
122
|
+
'break-spaces',
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const FORBIDDEN_UNITS = new Set(['cm', 'mm', 'in', 'pt', 'pc', 'ch', 'ex']);
|
|
126
|
+
|
|
127
|
+
const FORBIDDEN_FUNCTIONS = {
|
|
128
|
+
min: 'Lynx 不支持 min()。用明确尺寸、JS 计算或 class 切换。',
|
|
129
|
+
max: 'Lynx 不支持 max()。用明确尺寸、JS 计算或 class 切换。',
|
|
130
|
+
clamp: 'Lynx 不支持 clamp()。文字保持 px 基线;容器用 flex、vw/vh 或明确尺寸。',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const CALC_ALLOW = new Set([
|
|
134
|
+
'width',
|
|
135
|
+
'height',
|
|
136
|
+
'min-width',
|
|
137
|
+
'max-width',
|
|
138
|
+
'min-height',
|
|
139
|
+
'max-height',
|
|
140
|
+
'top',
|
|
141
|
+
'right',
|
|
142
|
+
'bottom',
|
|
143
|
+
'left',
|
|
144
|
+
'inset',
|
|
145
|
+
'margin',
|
|
146
|
+
'padding',
|
|
147
|
+
'flex-basis',
|
|
148
|
+
'gap',
|
|
149
|
+
'row-gap',
|
|
150
|
+
'column-gap',
|
|
151
|
+
'grid-row-gap',
|
|
152
|
+
'grid-column-gap',
|
|
153
|
+
'font-size',
|
|
154
|
+
'text-indent',
|
|
155
|
+
'perspective',
|
|
156
|
+
'margin-top',
|
|
157
|
+
'margin-right',
|
|
158
|
+
'margin-bottom',
|
|
159
|
+
'margin-left',
|
|
160
|
+
'padding-top',
|
|
161
|
+
'padding-right',
|
|
162
|
+
'padding-bottom',
|
|
163
|
+
'padding-left',
|
|
164
|
+
'margin-inline-start',
|
|
165
|
+
'margin-inline-end',
|
|
166
|
+
'padding-inline-start',
|
|
167
|
+
'padding-inline-end',
|
|
168
|
+
'inset-inline-start',
|
|
169
|
+
'inset-inline-end',
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
const STEPS_DIRECTIONS = new Set([
|
|
173
|
+
'start',
|
|
174
|
+
'jump-start',
|
|
175
|
+
'end',
|
|
176
|
+
'jump-end',
|
|
177
|
+
'jump-none',
|
|
178
|
+
'jump-both',
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
const SAFE_AREA_ENV = /^safe-area-inset-(top|right|bottom|left)$/i;
|
|
182
|
+
|
|
183
|
+
const STRUCTURAL_PSEUDO =
|
|
184
|
+
/:(?:first-child|last-child|only-child|first-of-type|last-of-type|only-of-type|nth-child|nth-last-child|nth-of-type|nth-last-of-type|empty)\b/i;
|
|
185
|
+
|
|
186
|
+
const PSEUDO_ELEMENT = /::(?:before|after|first-line|first-letter|marker|backdrop)\b/i;
|
|
187
|
+
|
|
188
|
+
const LEGACY_PSEUDO_ELEMENT = /(?:^|[^:]):(?:before|after)\b/i;
|
|
189
|
+
|
|
190
|
+
const MODERN_PSEUDO_FN = /:(?:is|where|has)\s*\(/i;
|
|
191
|
+
|
|
192
|
+
const STATE_PSEUDO = /:(?:disabled|enabled|checked)\b/i;
|
|
193
|
+
|
|
194
|
+
const ROOT_PSEUDO = /(?:^|[^A-Za-z0-9_-]):root(?![A-Za-z0-9_-])/i;
|
|
195
|
+
|
|
196
|
+
const ATTR_TILDE = /\[[^\]]*~=/;
|
|
197
|
+
|
|
198
|
+
const UNIT_RE =
|
|
199
|
+
/(?:^|[^A-Za-z0-9_-])(?:\d*\.\d+|\d+)(cm|mm|in|pt|pc|ch|ex)(?![A-Za-z0-9_-])/gi;
|
|
200
|
+
|
|
201
|
+
const NEGATIVE_LENGTH = /(?:^|[,\s])-\d/;
|
|
202
|
+
|
|
203
|
+
export const TEXT_CONTAINER_PROPS = new Set([
|
|
204
|
+
'flex-direction',
|
|
205
|
+
'flex-wrap',
|
|
206
|
+
'flex-flow',
|
|
207
|
+
'justify-content',
|
|
208
|
+
'align-items',
|
|
209
|
+
'align-content',
|
|
210
|
+
'gap',
|
|
211
|
+
'row-gap',
|
|
212
|
+
'column-gap',
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
export function toKebab(prop) {
|
|
216
|
+
if (!prop) return '';
|
|
217
|
+
return String(prop)
|
|
218
|
+
.trim()
|
|
219
|
+
.replace(/^['"]|['"]$/g, '')
|
|
220
|
+
.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
|
|
221
|
+
.replace(/^ms-/, '-ms-')
|
|
222
|
+
.toLowerCase();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function normalizeValue(value) {
|
|
226
|
+
return String(value ?? '')
|
|
227
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
228
|
+
.replace(/\s*!important\s*$/i, '')
|
|
229
|
+
.replace(/\s+/g, ' ')
|
|
230
|
+
.trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function splitCssList(value) {
|
|
234
|
+
const parts = [];
|
|
235
|
+
let current = '';
|
|
236
|
+
let depth = 0;
|
|
237
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
238
|
+
const ch = value[i];
|
|
239
|
+
if (ch === '(') depth += 1;
|
|
240
|
+
if (ch === ')') depth = Math.max(0, depth - 1);
|
|
241
|
+
if (depth === 0 && /\s/.test(ch)) {
|
|
242
|
+
if (current) parts.push(current);
|
|
243
|
+
current = '';
|
|
244
|
+
} else {
|
|
245
|
+
current += ch;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (current) parts.push(current);
|
|
249
|
+
return parts;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function matchingParen(text, openIndex) {
|
|
253
|
+
let depth = 0;
|
|
254
|
+
for (let i = openIndex; i < text.length; i += 1) {
|
|
255
|
+
const ch = text[i];
|
|
256
|
+
if (ch === '"' || ch === "'") {
|
|
257
|
+
const q = ch;
|
|
258
|
+
i += 1;
|
|
259
|
+
while (i < text.length && text[i] !== q) {
|
|
260
|
+
if (text[i] === '\\') i += 1;
|
|
261
|
+
i += 1;
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (ch === '(') depth += 1;
|
|
266
|
+
else if (ch === ')') {
|
|
267
|
+
depth -= 1;
|
|
268
|
+
if (depth === 0) return i;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return text.length;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function forEachCssFunction(value, callback) {
|
|
275
|
+
const re = /([A-Za-z_-][A-Za-z0-9_-]*)\s*\(/g;
|
|
276
|
+
let match;
|
|
277
|
+
while ((match = re.exec(value))) {
|
|
278
|
+
const name = match[1];
|
|
279
|
+
const open = match.index + match[0].length - 1;
|
|
280
|
+
const close = matchingParen(value, open);
|
|
281
|
+
const inner = value.slice(open + 1, close);
|
|
282
|
+
callback(name.toLowerCase(), inner);
|
|
283
|
+
re.lastIndex = Math.max(re.lastIndex, open + 1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isCompleteFlex(value) {
|
|
288
|
+
if (value === 'none') return true;
|
|
289
|
+
return splitCssList(value).length >= 3;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function hasSvgSource(value) {
|
|
293
|
+
return (
|
|
294
|
+
/\.svg(?:\s*['")]|$|\?|#)/i.test(value) ||
|
|
295
|
+
/data:image\/svg\+xml/i.test(value) ||
|
|
296
|
+
/image\/svg\+xml/i.test(value)
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function finding(id, message) {
|
|
301
|
+
return { id, message };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function lintAtRule(name) {
|
|
305
|
+
const key = String(name || '').toLowerCase();
|
|
306
|
+
if (!FORBIDDEN_AT_RULES.has(key)) return [];
|
|
307
|
+
return [finding('forbidden-at-rule', FORBIDDEN_AT_RULE_HINT[key] || `Lynx 不支持 @${key}。`)];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function isTextContainerDeclaration(prop, value) {
|
|
311
|
+
const p = toKebab(prop);
|
|
312
|
+
if (TEXT_CONTAINER_PROPS.has(p)) return true;
|
|
313
|
+
if (p !== 'display') return false;
|
|
314
|
+
const v = normalizeValue(value).toLowerCase();
|
|
315
|
+
return v === 'flex' || v === 'grid';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function lintSelector(selector, { allowRoot = false } = {}) {
|
|
319
|
+
const findings = [];
|
|
320
|
+
const text = String(selector || '');
|
|
321
|
+
if (!allowRoot && ROOT_PSEUDO.test(text)) {
|
|
322
|
+
findings.push(
|
|
323
|
+
finding(
|
|
324
|
+
'root-selector',
|
|
325
|
+
'只有 opencard/src/app.css 允许 :root。变量上移 app.css,或写在局部 class 上。',
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (STRUCTURAL_PSEUDO.test(text)) {
|
|
330
|
+
findings.push(
|
|
331
|
+
finding(
|
|
332
|
+
'structural-pseudo',
|
|
333
|
+
'结构伪类(:first-child / :nth-child 等)解析但不匹配。在 JSX/数据层加 class。',
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (PSEUDO_ELEMENT.test(text) || LEGACY_PSEUDO_ELEMENT.test(text)) {
|
|
338
|
+
findings.push(
|
|
339
|
+
finding(
|
|
340
|
+
'pseudo-element',
|
|
341
|
+
'不要用 ::before / ::after 等伪元素。需要内容时增加真实节点。',
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
if (MODERN_PSEUDO_FN.test(text)) {
|
|
346
|
+
findings.push(
|
|
347
|
+
finding('modern-pseudo', ':is() / :where() / :has() 不可用。拆成明确 class。'),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (STATE_PSEUDO.test(text)) {
|
|
351
|
+
findings.push(
|
|
352
|
+
finding(
|
|
353
|
+
'state-pseudo',
|
|
354
|
+
':disabled / :enabled / :checked 不参与匹配。用 .disabled 等 state class。',
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (ATTR_TILDE.test(text)) {
|
|
359
|
+
findings.push(finding('attr-tilde', '[attr~="value"] 不可用。改用 class。'));
|
|
360
|
+
}
|
|
361
|
+
return findings;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function lintDeclaration(prop, rawValue) {
|
|
365
|
+
const p = toKebab(prop);
|
|
366
|
+
const value = normalizeValue(rawValue);
|
|
367
|
+
if (!p) return [];
|
|
368
|
+
const findings = [];
|
|
369
|
+
|
|
370
|
+
if (FORBIDDEN_PROPERTIES.has(p)) {
|
|
371
|
+
findings.push(
|
|
372
|
+
finding('forbidden-property', FORBIDDEN_PROPERTY_HINT[p] || `Lynx 不支持 ${p}。${HINT}`),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
for (const [prefix, hint] of FORBIDDEN_PROPERTY_PREFIXES) {
|
|
376
|
+
if (p.startsWith(prefix)) {
|
|
377
|
+
findings.push(finding('forbidden-property', hint));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (p === 'display') {
|
|
382
|
+
for (const [pattern, hint] of DISPLAY_FORBIDDEN) {
|
|
383
|
+
if (pattern.test(value)) findings.push(finding('forbidden-value', hint));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (p === 'position' && POSITION_FORBIDDEN.has(value.toLowerCase())) {
|
|
388
|
+
findings.push(
|
|
389
|
+
finding(
|
|
390
|
+
'forbidden-value',
|
|
391
|
+
value.toLowerCase() === 'sticky'
|
|
392
|
+
? 'opencard 不提供纵向滚动上下文,不要用 position: sticky。改用 relative / absolute。'
|
|
393
|
+
: '不要显式生成 position: static。',
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (p === 'overflow' || p === 'overflow-y') {
|
|
399
|
+
const tokens = splitCssList(value.toLowerCase());
|
|
400
|
+
if (tokens.some((token) => OVERFLOW_BLOCKED.has(token))) {
|
|
401
|
+
findings.push(
|
|
402
|
+
finding(
|
|
403
|
+
'forbidden-value',
|
|
404
|
+
p === 'overflow-y'
|
|
405
|
+
? '禁止 overflow-y: auto/scroll。纵向内容收敛,不要滚动承接。'
|
|
406
|
+
: '禁止 overflow: auto/scroll 承接纵向溢出。横向滚动只用 overflow-x。',
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (p === 'white-space' && WHITE_SPACE_FORBIDDEN.has(value.toLowerCase())) {
|
|
413
|
+
findings.push(
|
|
414
|
+
finding(
|
|
415
|
+
'forbidden-value',
|
|
416
|
+
'white-space 只生成 normal / nowrap。pre* 用真实换行节点或数据预处理。',
|
|
417
|
+
),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (p === 'flex' && !isCompleteFlex(value)) {
|
|
422
|
+
findings.push(
|
|
423
|
+
finding(
|
|
424
|
+
'flex-shorthand',
|
|
425
|
+
`不要写 flex: ${value}。使用完整三值,例如 flex: 1 1 0 或 flex: 0 0 auto。`,
|
|
426
|
+
),
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if ((p === 'flex-basis' || p === 'flex') && /\bmin-content\b/i.test(value)) {
|
|
431
|
+
findings.push(
|
|
432
|
+
finding(
|
|
433
|
+
'min-content-flex-basis',
|
|
434
|
+
'flex-basis 中不要用 min-content(会按 0px 处理)。用 auto、固定值或 JS 计算。',
|
|
435
|
+
),
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (p === 'padding' || p.startsWith('padding-')) {
|
|
440
|
+
if (NEGATIVE_LENGTH.test(value) || /^-\d/.test(value)) {
|
|
441
|
+
findings.push(finding('negative-padding', '不要生成负 padding。改为 0 或重写布局。'));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
UNIT_RE.lastIndex = 0;
|
|
446
|
+
if (UNIT_RE.test(value)) {
|
|
447
|
+
findings.push(
|
|
448
|
+
finding(
|
|
449
|
+
'forbidden-unit',
|
|
450
|
+
'不要用 cm/mm/in/pt/pc/ch/ex。物理单位改 px/rem;字体相关单位不要类推。',
|
|
451
|
+
),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (hasSvgSource(value)) {
|
|
456
|
+
findings.push(
|
|
457
|
+
finding(
|
|
458
|
+
'svg-url',
|
|
459
|
+
'不要在 CSS 中引用 SVG 图片(.svg / data:image/svg+xml)。改用 PNG/WebP/JPG,或按 components.md 用 Taro Svg。',
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
forEachCssFunction(value, (name, inner) => {
|
|
465
|
+
if (FORBIDDEN_FUNCTIONS[name]) {
|
|
466
|
+
findings.push(finding('forbidden-function', FORBIDDEN_FUNCTIONS[name]));
|
|
467
|
+
}
|
|
468
|
+
if (name === 'calc' && !CALC_ALLOW.has(p) && !p.startsWith('margin-') && !p.startsWith('padding-')) {
|
|
469
|
+
findings.push(
|
|
470
|
+
finding(
|
|
471
|
+
'calc-property',
|
|
472
|
+
`calc() 不能用于 ${p}。只用于尺寸、定位、margin/padding、gap、font-size 等 allowlist 属性。`,
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
if (name === 'steps') {
|
|
477
|
+
const args = splitCssList(inner.replace(/,/g, ' '));
|
|
478
|
+
const direction = args[1]?.toLowerCase();
|
|
479
|
+
if (args.length < 2 || !STEPS_DIRECTIONS.has(direction)) {
|
|
480
|
+
findings.push(
|
|
481
|
+
finding('steps-direction', 'steps() 必须包含 direction,例如 steps(5, end)。'),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (name === 'env' && !SAFE_AREA_ENV.test(inner.trim())) {
|
|
486
|
+
findings.push(
|
|
487
|
+
finding('env-function', `不支持 env(${inner.trim() || '...'} )。仅 safe-area-inset-* 可用且不要默认加到卡片根节点。`),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return uniqueFindings(findings);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function uniqueFindings(findings) {
|
|
496
|
+
const seen = new Set();
|
|
497
|
+
return findings.filter((item) => {
|
|
498
|
+
const key = `${item.id}:${item.message}`;
|
|
499
|
+
if (seen.has(key)) return false;
|
|
500
|
+
seen.add(key);
|
|
501
|
+
return true;
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function formatFinding(file, line, column, message) {
|
|
506
|
+
const loc = column ? `${line}:${column}` : String(line);
|
|
507
|
+
return `${file}:${loc}: ${message}`;
|
|
508
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -Eeuo pipefail
|
|
3
|
+
|
|
4
|
+
PACKAGE_ROOT="${1:?Missing project root}"
|
|
5
|
+
shift
|
|
6
|
+
ARTIFACTS_DIR="${ARTIFACTS_DIR:-/tmp/microapp_artifacts}"
|
|
7
|
+
DIST_DIR="${PACKAGE_ROOT}/dist"
|
|
8
|
+
ARTIFACT_PATH="${ARTIFACTS_DIR}/opencard.zip"
|
|
9
|
+
|
|
10
|
+
cd "${PACKAGE_ROOT}"
|
|
11
|
+
|
|
12
|
+
echo "Running opencard build:pack"
|
|
13
|
+
# CLI 启动此脚本前,根据宿主路由配置注入构建环境变量。
|
|
14
|
+
TARO_TOOLCHAIN_ENTRY="$(node -p 'require.resolve("@open-dy/vibecoding-sdk-opencard/build/taro-toolchain")')"
|
|
15
|
+
node "${TARO_TOOLCHAIN_ENTRY}" build --type tt
|
|
16
|
+
|
|
17
|
+
if [[ ! -d "${DIST_DIR}" ]]; then
|
|
18
|
+
echo "Error: ${DIST_DIR} not found after opencard build:pack"
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
node "$(dirname "${BASH_SOURCE[0]}")/write-web-config.mjs" "${DIST_DIR}"
|
|
23
|
+
|
|
24
|
+
mkdir -p "${ARTIFACTS_DIR}"
|
|
25
|
+
echo "Packing ${DIST_DIR} into ${ARTIFACT_PATH}"
|
|
26
|
+
rm -f "${ARTIFACT_PATH}"
|
|
27
|
+
(cd "${DIST_DIR}" && zip -rq "${ARTIFACT_PATH}" .)
|