@af-mobile/eslint-plugin 2.1.0 → 2.2.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.
@@ -1,72 +1,72 @@
1
- // L3-2 af-mobile/wc-shadow-use-token(error)
2
- // 检测:Shadow 组件 CSS 字符串中颜色/间距/字号/圆角硬编码(非 var(--*))
3
- // 例外:backdrop 遮罩半透明黑
4
- import postcss from 'postcss';
5
-
6
- // 硬编码模式:颜色(#xxx / rgb / rgba / 命名色)、间距(Npx / Nrem)、字号、圆角
7
- const COLOR_RE = /(#([0-9a-f]{3,8})\b|rgb\(|rgba\(|\b(red|blue|green|white|black|gray|yellow|orange|purple|pink|brown|cyan)\b)/i;
8
- const SIZE_RE = /\b\d+(px|rem)\b/;
9
-
10
- // 需要检查的 CSS 属性
11
- const CHECKED_PROPS = new Set([
12
- 'color', 'background', 'background-color', 'border-color', 'border',
13
- 'padding', 'margin', 'font-size', 'border-radius', 'width', 'height',
14
- 'top', 'right', 'bottom', 'left', 'gap', 'box-shadow',
15
- ]);
16
-
17
- // 例外属性值(放行)
18
- const EXCEPTIONS = /backdrop|mask/i;
19
-
20
- export default {
21
- meta: {
22
- type: 'problem',
23
- docs: { description: 'Shadow 组件 CSS 必须使用 token 变量' },
24
- schema: [],
25
- messages: {
26
- hardcoded: "'{{prop}}: {{value}}' in Shadow CSS must use var(--*). Use token variables for cross-theme visual consistency",
27
- },
28
- },
29
- create(context) {
30
- const filename = context.filename || context.getFilename();
31
- if (!/src[\\/](?:charts[\\/])?components[\\/].*\.js$/.test(filename)) return {};
32
-
33
- const sourceCode = context.sourceCode || context.getSourceCode();
34
- const source = sourceCode.getText();
35
- // 仅检查 Shadow 组件(含 useShadow = true)
36
- if (!/useShadow\s*=\s*true/.test(source)) return {};
37
-
38
- // 提取 JS 中所有模板字符串内容(CSS 常量)
39
- function checkCss(cssText, node) {
40
- if (!cssText.includes(':')) return;
41
- try {
42
- const root = postcss.parse(cssText);
43
- root.walkDecls(decl => {
44
- if (!CHECKED_PROPS.has(decl.prop)) return;
45
- // 放行 var(--*) 值
46
- if (/^var\(/.test(decl.value.trim())) return;
47
- // 放行 backdrop 例外:选择器含 ::backdrop(遮罩半透明黑,L1 无 mask token,见 L4 §3.4 L3-2)
48
- const selector = decl.parent?.selector || '';
49
- if (EXCEPTIONS.test(selector) || EXCEPTIONS.test(decl.prop) || EXCEPTIONS.test(decl.value)) return;
50
- // 检测硬编码
51
- if (COLOR_RE.test(decl.value) || SIZE_RE.test(decl.value)) {
52
- context.report({
53
- node,
54
- loc: {
55
- start: { line: decl.source.start.line, column: decl.source.start.column - 1 },
56
- end: { line: decl.source.end.line, column: decl.source.end.column },
57
- },
58
- messageId: 'hardcoded',
59
- data: { prop: decl.prop, value: decl.value },
60
- });
61
- }
62
- });
63
- } catch { /* 非 CSS 内容跳过 */ }
64
- }
65
-
66
- return {
67
- TemplateElement(node) {
68
- if (node.value?.raw) checkCss(node.value.raw, node);
69
- },
70
- };
71
- },
72
- };
1
+ // L3-2 af-mobile/wc-shadow-use-token(error)
2
+ // 检测:Shadow 组件 CSS 字符串中颜色/间距/字号/圆角硬编码(非 var(--*))
3
+ // 例外:backdrop 遮罩半透明黑
4
+ import postcss from 'postcss';
5
+
6
+ // 硬编码模式:颜色(#xxx / rgb / rgba / 命名色)、间距(Npx / Nrem)、字号、圆角
7
+ const COLOR_RE = /(#([0-9a-f]{3,8})\b|rgb\(|rgba\(|\b(red|blue|green|white|black|gray|yellow|orange|purple|pink|brown|cyan)\b)/i;
8
+ const SIZE_RE = /\b\d+(px|rem)\b/;
9
+
10
+ // 需要检查的 CSS 属性
11
+ const CHECKED_PROPS = new Set([
12
+ 'color', 'background', 'background-color', 'border-color', 'border',
13
+ 'padding', 'margin', 'font-size', 'border-radius', 'width', 'height',
14
+ 'top', 'right', 'bottom', 'left', 'gap', 'box-shadow',
15
+ ]);
16
+
17
+ // 例外属性值(放行)
18
+ const EXCEPTIONS = /backdrop|mask/i;
19
+
20
+ export default {
21
+ meta: {
22
+ type: 'problem',
23
+ docs: { description: 'Shadow 组件 CSS 必须使用 token 变量' },
24
+ schema: [],
25
+ messages: {
26
+ hardcoded: "'{{prop}}: {{value}}' in Shadow CSS must use var(--*). Use token variables for cross-theme visual consistency",
27
+ },
28
+ },
29
+ create(context) {
30
+ const filename = context.filename || context.getFilename();
31
+ if (!/src[\\/](?:charts[\\/])?components[\\/].*\.js$/.test(filename)) return {};
32
+
33
+ const sourceCode = context.sourceCode || context.getSourceCode();
34
+ const source = sourceCode.getText();
35
+ // 仅检查 Shadow 组件(含 useShadow = true)
36
+ if (!/useShadow\s*=\s*true/.test(source)) return {};
37
+
38
+ // 提取 JS 中所有模板字符串内容(CSS 常量)
39
+ function checkCss(cssText, node) {
40
+ if (!cssText.includes(':')) return;
41
+ try {
42
+ const root = postcss.parse(cssText);
43
+ root.walkDecls(decl => {
44
+ if (!CHECKED_PROPS.has(decl.prop)) return;
45
+ // 放行 var(--*) 值
46
+ if (/^var\(/.test(decl.value.trim())) return;
47
+ // 放行 backdrop 例外:选择器含 ::backdrop(遮罩半透明黑,L1 无 mask token,见 L4 §3.4 L3-2)
48
+ const selector = decl.parent?.selector || '';
49
+ if (EXCEPTIONS.test(selector) || EXCEPTIONS.test(decl.prop) || EXCEPTIONS.test(decl.value)) return;
50
+ // 检测硬编码
51
+ if (COLOR_RE.test(decl.value) || SIZE_RE.test(decl.value)) {
52
+ context.report({
53
+ node,
54
+ loc: {
55
+ start: { line: decl.source.start.line, column: decl.source.start.column - 1 },
56
+ end: { line: decl.source.end.line, column: decl.source.end.column },
57
+ },
58
+ messageId: 'hardcoded',
59
+ data: { prop: decl.prop, value: decl.value },
60
+ });
61
+ }
62
+ });
63
+ } catch { /* 非 CSS 内容跳过 */ }
64
+ }
65
+
66
+ return {
67
+ TemplateElement(node) {
68
+ if (node.value?.raw) checkCss(node.value.raw, node);
69
+ },
70
+ };
71
+ },
72
+ };
@@ -1,101 +1,101 @@
1
- {
2
- "af-dialog": {
3
- "role": "dialog",
4
- "ariaLabel": true,
5
- "description": "模态对话框需要 role=dialog 和 aria-label"
6
- },
7
- "af-toast": {
8
- "ariaLive": true,
9
- "role": "status",
10
- "description": "轻提示需要 role=status 和 aria-live=polite"
11
- },
12
- "af-tabs": {
13
- "role": "tablist",
14
- "description": "标签页容器需要 role=tablist"
15
- },
16
- "af-action-sheet": {
17
- "role": "dialog",
18
- "ariaLabel": true,
19
- "description": "底部弹窗需要 role=dialog 和 aria-label"
20
- },
21
- "af-list": {
22
- "role": "list",
23
- "description": "列表容器需要 role=list"
24
- },
25
- "af-swiper": {
26
- "role": "region",
27
- "ariaLabel": true,
28
- "description": "轮播图需要 role=region 和 aria-label"
29
- },
30
- "af-dropdown": {
31
- "role": "listbox",
32
- "description": "下拉菜单触发器是 combobox,列表用 role=listbox(WAI-ARIA combobox 模式)"
33
- },
34
- "af-backtop": {
35
- "ariaLabel": true,
36
- "description": "返回顶部按钮需要 aria-label"
37
- },
38
- "af-picker": {
39
- "role": "listbox",
40
- "description": "选择器列需要 role=listbox"
41
- },
42
- "af-number-keyboard": {
43
- "role": "dialog",
44
- "ariaLabel": true,
45
- "description": "数字键盘弹层需要 role=dialog 和 aria-label;按键为原生 button 天然支持键盘操作"
46
- },
47
- "af-password-input": {
48
- "ariaLabel": true,
49
- "description": "密码格子容器需要 aria-label(含输入进度播报)"
50
- },
51
- "af-img": {
52
- "description": "图片组件无强制 ARIA 要求"
53
- },
54
- "af-switch": {
55
- "role": "switch",
56
- "ariaChecked": true,
57
- "description": "开关需要 role=switch 和 aria-checked"
58
- },
59
- "af-search-bar": {
60
- "description": "搜索栏无强制容器 ARIA;清除按钮需 aria-label(由组件内置)"
61
- },
62
- "af-skeleton-page": {
63
- "role": "status",
64
- "ariaLive": true,
65
- "description": "骨架屏需要 role=status 和 aria-live=polite"
66
- },
67
- "af-upload": {
68
- "ariaLabel": true,
69
- "description": "上传组件的触发按钮需 aria-label;预览网格用 role=list"
70
- },
71
- "af-chart-line": {
72
- "role": "img",
73
- "ariaLabel": true,
74
- "description": "折线图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
75
- },
76
- "af-chart-bar": {
77
- "role": "img",
78
- "ariaLabel": true,
79
- "description": "柱状图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
80
- },
81
- "af-chart-pie": {
82
- "role": "img",
83
- "ariaLabel": true,
84
- "description": "饼图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
85
- },
86
- "af-chart-radar": {
87
- "role": "img",
88
- "ariaLabel": true,
89
- "description": "雷达图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
90
- },
91
- "af-chart-funnel": {
92
- "role": "img",
93
- "ariaLabel": true,
94
- "description": "漏斗图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
95
- },
96
- "af-chat": {
97
- "role": "log",
98
- "ariaLive": true,
99
- "description": "对话记录容器需要 role=log 和 aria-live=polite(流式文本 aria-hidden,完成时 sr-only 一次性播报)"
100
- }
101
- }
1
+ {
2
+ "af-dialog": {
3
+ "role": "dialog",
4
+ "ariaLabel": true,
5
+ "description": "模态对话框需要 role=dialog 和 aria-label"
6
+ },
7
+ "af-toast": {
8
+ "ariaLive": true,
9
+ "role": "status",
10
+ "description": "轻提示需要 role=status 和 aria-live=polite"
11
+ },
12
+ "af-tabs": {
13
+ "role": "tablist",
14
+ "description": "标签页容器需要 role=tablist"
15
+ },
16
+ "af-action-sheet": {
17
+ "role": "dialog",
18
+ "ariaLabel": true,
19
+ "description": "底部弹窗需要 role=dialog 和 aria-label"
20
+ },
21
+ "af-list": {
22
+ "role": "list",
23
+ "description": "列表容器需要 role=list"
24
+ },
25
+ "af-swiper": {
26
+ "role": "region",
27
+ "ariaLabel": true,
28
+ "description": "轮播图需要 role=region 和 aria-label"
29
+ },
30
+ "af-dropdown": {
31
+ "role": "listbox",
32
+ "description": "下拉菜单触发器是 combobox,列表用 role=listbox(WAI-ARIA combobox 模式)"
33
+ },
34
+ "af-backtop": {
35
+ "ariaLabel": true,
36
+ "description": "返回顶部按钮需要 aria-label"
37
+ },
38
+ "af-picker": {
39
+ "role": "listbox",
40
+ "description": "选择器列需要 role=listbox"
41
+ },
42
+ "af-number-keyboard": {
43
+ "role": "dialog",
44
+ "ariaLabel": true,
45
+ "description": "数字键盘弹层需要 role=dialog 和 aria-label;按键为原生 button 天然支持键盘操作"
46
+ },
47
+ "af-password-input": {
48
+ "ariaLabel": true,
49
+ "description": "密码格子容器需要 aria-label(含输入进度播报)"
50
+ },
51
+ "af-img": {
52
+ "description": "图片组件无强制 ARIA 要求"
53
+ },
54
+ "af-switch": {
55
+ "role": "switch",
56
+ "ariaChecked": true,
57
+ "description": "开关需要 role=switch 和 aria-checked"
58
+ },
59
+ "af-search-bar": {
60
+ "description": "搜索栏无强制容器 ARIA;清除按钮需 aria-label(由组件内置)"
61
+ },
62
+ "af-skeleton-page": {
63
+ "role": "status",
64
+ "ariaLive": true,
65
+ "description": "骨架屏需要 role=status 和 aria-live=polite"
66
+ },
67
+ "af-upload": {
68
+ "ariaLabel": true,
69
+ "description": "上传组件的触发按钮需 aria-label;预览网格用 role=list"
70
+ },
71
+ "af-chart-line": {
72
+ "role": "img",
73
+ "ariaLabel": true,
74
+ "description": "折线图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
75
+ },
76
+ "af-chart-bar": {
77
+ "role": "img",
78
+ "ariaLabel": true,
79
+ "description": "柱状图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
80
+ },
81
+ "af-chart-pie": {
82
+ "role": "img",
83
+ "ariaLabel": true,
84
+ "description": "饼图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
85
+ },
86
+ "af-chart-radar": {
87
+ "role": "img",
88
+ "ariaLabel": true,
89
+ "description": "雷达图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
90
+ },
91
+ "af-chart-funnel": {
92
+ "role": "img",
93
+ "ariaLabel": true,
94
+ "description": "漏斗图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
95
+ },
96
+ "af-chat": {
97
+ "role": "log",
98
+ "ariaLive": true,
99
+ "description": "对话记录容器需要 role=log 和 aria-live=polite(流式文本 aria-hidden,完成时 sr-only 一次性播报)"
100
+ }
101
+ }
package/utils/helpers.js CHANGED
@@ -1,106 +1,106 @@
1
- // af-mobile UI —— ESLint 规则共享工具
2
- import { readFileSync } from 'node:fs';
3
- import { fileURLToPath } from 'node:url';
4
- import { resolve, dirname } from 'node:path';
5
-
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const whitelistPath = resolve(__dirname, 'whitelist-v1.json');
8
- const whitelist = JSON.parse(readFileSync(whitelistPath, 'utf8'));
9
-
10
- // 所有合法 class 集合(recipe + atomic)
11
- export const ALL_CLASSES = new Set([
12
- ...whitelist.classes.recipe,
13
- ...whitelist.classes.atomic,
14
- ]);
15
-
16
- // 所有合法组件 tagName 集合
17
- export const ALL_COMPONENTS = new Set(whitelist.components);
18
-
19
- // 从字符串中提取所有 class="xxx" 的 class 列表
20
- export function extractClassLists(str) {
21
- const results = [];
22
- const re = /class\s*=\s*"([^"]*)"/g;
23
- let m;
24
- while ((m = re.exec(str))) {
25
- const classes = m[1].split(/\s+/).filter(Boolean);
26
- if (classes.length) results.push({ classes, offset: m.index, raw: m[0] });
27
- }
28
- return results;
29
- }
30
-
31
- // 从字符串中提取所有 class='xxx'(单引号)的 class 列表
32
- export function extractClassListsSingle(str) {
33
- const results = [];
34
- const re = /class\s*=\s*'([^']*)'/g;
35
- let m;
36
- while ((m = re.exec(str))) {
37
- const classes = m[1].split(/\s+/).filter(Boolean);
38
- if (classes.length) results.push({ classes, offset: m.index, raw: m[0] });
39
- }
40
- return results;
41
- }
42
-
43
- // 合并提取双引号和单引号的 class
44
- export function extractAllClassLists(str) {
45
- return [...extractClassLists(str), ...extractClassListsSingle(str)];
46
- }
47
-
48
- // 从字符串中提取所有自定义元素标签 <af-xxx>
49
- export function extractCustomElements(str) {
50
- const results = [];
51
- const re = /<([a-z]+-[a-z-]+)/g;
52
- let m;
53
- while ((m = re.exec(str))) results.push(m[1]);
54
- return results;
55
- }
56
-
57
- // 编辑距离(Levenshtein):最近邻建议用,候选集小(<200)无需提前剪枝
58
- function levenshtein(a, b) {
59
- const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
60
- for (let j = 0; j <= b.length; j++) dp[0][j] = j;
61
- for (let i = 1; i <= a.length; i++) {
62
- for (let j = 1; j <= b.length; j++) {
63
- dp[i][j] = Math.min(
64
- dp[i - 1][j] + 1,
65
- dp[i][j - 1] + 1,
66
- dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
67
- );
68
- }
69
- }
70
- return dp[a.length][b.length];
71
- }
72
-
73
- // 最近邻建议:返回 " Did you mean 'xxx'?" 或 ''(无足够接近的候选)
74
- // 阈值随长度放宽:≤3 字符允许 1 距离,更长允许 2
75
- export function suggestNearest(name, candidates) {
76
- const max = name.length <= 3 ? 1 : 2;
77
- let best = null, bestDist = max + 1;
78
- for (const c of candidates) {
79
- if (Math.abs(c.length - name.length) > max) continue;
80
- const d = levenshtein(name, c);
81
- if (d < bestDist) { best = c; bestDist = d; }
82
- }
83
- return best ? ` Did you mean '${best}'?` : '';
84
- }
85
-
86
- // k 层导入源:发布名 @af-mobile/ui/k,或仓库内相对路径 k/index.js / k/flow.js
87
- const K_SOURCE_RE = /^@af-mobile\/ui\/k$|(?:^|[\\/])k[\\/](?:index|flow)\.[cm]?js$/;
88
-
89
- // 收集从 k 入口导入的绑定名(含别名):{ html: Set<localName>, For: Set<localName> }
90
- // k 层规则(k-no-bare-and 等)只对从 k 入口导入的 html/For 生效——
91
- // 主包 html``(返回字符串)语义不同,不受 k 规则约束
92
- export function collectKImports(programBody) {
93
- const names = { html: new Set(), For: new Set() };
94
- for (const stmt of programBody) {
95
- if (stmt.type !== 'ImportDeclaration') continue;
96
- if (!K_SOURCE_RE.test(stmt.source.value)) continue;
97
- for (const spec of stmt.specifiers) {
98
- if (spec.type !== 'ImportSpecifier') continue;
99
- const imported = spec.imported.type === 'Identifier'
100
- ? spec.imported.name
101
- : String(spec.imported.value);
102
- if (names[imported]) names[imported].add(spec.local.name);
103
- }
104
- }
105
- return names;
106
- }
1
+ // af-mobile UI —— ESLint 规则共享工具
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { resolve, dirname } from 'node:path';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const whitelistPath = resolve(__dirname, 'whitelist-v1.json');
8
+ const whitelist = JSON.parse(readFileSync(whitelistPath, 'utf8'));
9
+
10
+ // 所有合法 class 集合(recipe + atomic)
11
+ export const ALL_CLASSES = new Set([
12
+ ...whitelist.classes.recipe,
13
+ ...whitelist.classes.atomic,
14
+ ]);
15
+
16
+ // 所有合法组件 tagName 集合
17
+ export const ALL_COMPONENTS = new Set(whitelist.components);
18
+
19
+ // 从字符串中提取所有 class="xxx" 的 class 列表
20
+ export function extractClassLists(str) {
21
+ const results = [];
22
+ const re = /class\s*=\s*"([^"]*)"/g;
23
+ let m;
24
+ while ((m = re.exec(str))) {
25
+ const classes = m[1].split(/\s+/).filter(Boolean);
26
+ if (classes.length) results.push({ classes, offset: m.index, raw: m[0] });
27
+ }
28
+ return results;
29
+ }
30
+
31
+ // 从字符串中提取所有 class='xxx'(单引号)的 class 列表
32
+ export function extractClassListsSingle(str) {
33
+ const results = [];
34
+ const re = /class\s*=\s*'([^']*)'/g;
35
+ let m;
36
+ while ((m = re.exec(str))) {
37
+ const classes = m[1].split(/\s+/).filter(Boolean);
38
+ if (classes.length) results.push({ classes, offset: m.index, raw: m[0] });
39
+ }
40
+ return results;
41
+ }
42
+
43
+ // 合并提取双引号和单引号的 class
44
+ export function extractAllClassLists(str) {
45
+ return [...extractClassLists(str), ...extractClassListsSingle(str)];
46
+ }
47
+
48
+ // 从字符串中提取所有自定义元素标签 <af-xxx>
49
+ export function extractCustomElements(str) {
50
+ const results = [];
51
+ const re = /<([a-z]+-[a-z-]+)/g;
52
+ let m;
53
+ while ((m = re.exec(str))) results.push(m[1]);
54
+ return results;
55
+ }
56
+
57
+ // 编辑距离(Levenshtein):最近邻建议用,候选集小(<200)无需提前剪枝
58
+ function levenshtein(a, b) {
59
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
60
+ for (let j = 0; j <= b.length; j++) dp[0][j] = j;
61
+ for (let i = 1; i <= a.length; i++) {
62
+ for (let j = 1; j <= b.length; j++) {
63
+ dp[i][j] = Math.min(
64
+ dp[i - 1][j] + 1,
65
+ dp[i][j - 1] + 1,
66
+ dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
67
+ );
68
+ }
69
+ }
70
+ return dp[a.length][b.length];
71
+ }
72
+
73
+ // 最近邻建议:返回 " Did you mean 'xxx'?" 或 ''(无足够接近的候选)
74
+ // 阈值随长度放宽:≤3 字符允许 1 距离,更长允许 2
75
+ export function suggestNearest(name, candidates) {
76
+ const max = name.length <= 3 ? 1 : 2;
77
+ let best = null, bestDist = max + 1;
78
+ for (const c of candidates) {
79
+ if (Math.abs(c.length - name.length) > max) continue;
80
+ const d = levenshtein(name, c);
81
+ if (d < bestDist) { best = c; bestDist = d; }
82
+ }
83
+ return best ? ` Did you mean '${best}'?` : '';
84
+ }
85
+
86
+ // k 层导入源:发布名 @af-mobile/ui/k,或仓库内相对路径 k/index.js / k/flow.js
87
+ const K_SOURCE_RE = /^@af-mobile\/ui\/k$|(?:^|[\\/])k[\\/](?:index|flow)\.[cm]?js$/;
88
+
89
+ // 收集从 k 入口导入的绑定名(含别名):{ html: Set<localName>, For: Set<localName> }
90
+ // k 层规则(k-no-bare-and 等)只对从 k 入口导入的 html/For 生效——
91
+ // 主包 html``(返回字符串)语义不同,不受 k 规则约束
92
+ export function collectKImports(programBody) {
93
+ const names = { html: new Set(), For: new Set() };
94
+ for (const stmt of programBody) {
95
+ if (stmt.type !== 'ImportDeclaration') continue;
96
+ if (!K_SOURCE_RE.test(stmt.source.value)) continue;
97
+ for (const spec of stmt.specifiers) {
98
+ if (spec.type !== 'ImportSpecifier') continue;
99
+ const imported = spec.imported.type === 'Identifier'
100
+ ? spec.imported.name
101
+ : String(spec.imported.value);
102
+ if (names[imported]) names[imported].add(spec.local.name);
103
+ }
104
+ }
105
+ return names;
106
+ }
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "version": "v1",
3
- "af-mobileVersion": "1.7.0",
3
+ "af-mobileVersion": "1.9.0",
4
4
  "classes": {
5
5
  "recipe": [
6
6
  "actions",
7
+ "app-shell",
7
8
  "avatar",
8
9
  "badge",
9
10
  "body",