@proteus-vue/compiler 0.3.0-beta.0 → 0.3.0-beta.3

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/dist/template.js DELETED
@@ -1,337 +0,0 @@
1
- // src/compiler/template.ts
2
- // 4-1-a Template → WXML
3
- // 标准 HTML 标签 / Vue 指令 → 小程序标签 / 指令(映射表见 LLM_IMPLEMENTATION_GUIDE §P4-1-a)
4
- import { parse as domParse, NodeTypes } from '@vue/compiler-dom';
5
- import { TAG_RULE_BY_TAG } from './transforms/template';
6
- import { resolveOverrides } from './overrides';
7
- function escapeXml(s) {
8
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
9
- }
10
- function kebabCase(s) {
11
- return s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
12
- }
13
- function camelToKebab(s) {
14
- return s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
15
- }
16
- /** 提取表达式节点的文本(兼容 Simple/Compound/Interpolation/Text/字符串) */
17
- function exprContent(exp) {
18
- if (exp == null)
19
- return '';
20
- if (typeof exp === 'string')
21
- return exp;
22
- const node = exp;
23
- if (node.type === NodeTypes.SIMPLE_EXPRESSION) {
24
- return typeof node.content === 'string' ? node.content : '';
25
- }
26
- if (node.type === NodeTypes.INTERPOLATION) {
27
- return exprContent(node.content);
28
- }
29
- if (node.type === NodeTypes.COMPOUND_EXPRESSION || Array.isArray(node.children)) {
30
- return node.children.map((c) => exprContent(c)).join('');
31
- }
32
- if (node.type === NodeTypes.TEXT) {
33
- return typeof node.content === 'string' ? node.content : '';
34
- }
35
- return '';
36
- }
37
- /** 解析 v-for 表达式:(item, idx) in list / item of items */
38
- function parseForExpr(exp) {
39
- const m = exp.trim().match(/^\(?\s*([\w$]+)\s*(?:,\s*([\w$]+))?\s*\)?\s+(?:in|of)\s+(.+)$/);
40
- if (!m)
41
- return { list: exp };
42
- return { list: m[3].trim(), item: m[1], index: m[2] };
43
- }
44
- /** 事件处理器:仅支持简单方法引用(方法名 / 方法名($event)) */
45
- function cleanHandler(exp, warnings) {
46
- const t = exp.trim();
47
- if (/^[\w$]+$/.test(t))
48
- return t;
49
- const m = t.match(/^([\w$]+)\(\$event\)$/);
50
- if (m)
51
- return m[1];
52
- warnings.push(`事件处理器 "${t}" 不是简单方法引用(MVP 仅支持方法名),已原样输出`);
53
- return t;
54
- }
55
- /** :class 绑定:对象语法 → 三元拼接,其余 → {{expr}} */
56
- function formatClassBinding(exp, warnings) {
57
- const t = exp.trim();
58
- if (t.startsWith('{')) {
59
- const parts = [];
60
- const re = /(['"]?)([\w-]+)\1\s*:\s*([^,}]+)/g;
61
- let m;
62
- while ((m = re.exec(t)))
63
- parts.push(`(${m[3].trim()}?'${m[2]} ':'')`);
64
- if (parts.length)
65
- return `{{${parts.join('+')}}}`;
66
- }
67
- if (t.startsWith('['))
68
- warnings.push(`:class 数组语法暂不支持(MVP),已按表达式输出`);
69
- return `{{${t}}}`;
70
- }
71
- /** :style 绑定:对象语法 → prop:{{expr}} 拼接,其余 → {{expr}} */
72
- function formatStyleBinding(exp) {
73
- const t = exp.trim();
74
- if (t.startsWith('{')) {
75
- const parts = [];
76
- const re = /(['"]?)([\w-]+)\1\s*:\s*([^,}]+)/g;
77
- let m;
78
- while ((m = re.exec(t)))
79
- parts.push(`${camelToKebab(m[2])}:{{${m[3].trim()}}}`);
80
- if (parts.length)
81
- return parts.join(';');
82
- }
83
- return `{{${t}}}`;
84
- }
85
- function serializeElement(node, ctx) {
86
- const hasVHtml = node.props.some((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'html');
87
- const hasClick = node.props.some((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'on');
88
- // 导航链接:<a href> / <router-link to>(元素上有 @click 时不作为导航链接,交给事件映射)
89
- const isNavLink = (node.tag === 'a' || node.tag === 'router-link') && !hasClick;
90
- // 标签映射(★生效配置:tags.ts 常量 + config 覆盖;规则被禁用则按未注册标签原样输出)
91
- const tagRuleId = TAG_RULE_BY_TAG[node.tag];
92
- let tag = hasVHtml ? 'rich-text' : (ctx.tagMap[node.tag] ?? kebabCase(node.tag));
93
- if (node.tag === 'router-link')
94
- tag = 'view';
95
- if (tagRuleId && ctx.disabled.has(tagRuleId)) {
96
- tag = kebabCase(node.tag);
97
- ctx.warnings.push(`规则 ${tagRuleId} 已被禁用(rules.disabled),<${node.tag}> 按未注册标签原样输出`);
98
- }
99
- // 决策 trace:标签映射
100
- if (!ctx.disabled.has('tag/unknown-kebab') && !(tagRuleId && ctx.disabled.has(tagRuleId))) {
101
- ctx.trace?.add(hasVHtml ? 'tag/rich-text' : node.tag === 'router-link' ? 'tag/router-link' : (TAG_RULE_BY_TAG[node.tag] ?? 'tag/unknown-kebab'), { line: node.loc.start.line, before: `<${node.tag}>`, after: `<${tag}>` });
102
- }
103
- // 语义标签基础类(h1-h6/p/a → proteus-*,样式侧注入 Web UA 等价默认样式;rich-text 不附加)
104
- const baseClass = hasVHtml ? '' : (ctx.semanticClass[node.tag] ?? '');
105
- // 语义类随标签规则联动:tag/* 规则被禁用时标签保持原样,基础类也一并取消(避免 class 无意义)
106
- const tagDisabled = Boolean(tagRuleId) && ctx.disabled.has(tagRuleId);
107
- if (baseClass && (ctx.disabled.has('semantic/base-class') || tagDisabled)) {
108
- ctx.warnings.push(`语义基础类已被禁用(${tagDisabled ? `${tagRuleId} 被禁用` : 'semantic/base-class 被禁用'},rules.disabled),不再附加`);
109
- }
110
- const effectiveBaseClass = baseClass && !ctx.disabled.has('semantic/base-class') && !tagDisabled ? baseClass : '';
111
- if (effectiveBaseClass) {
112
- ctx.trace?.add('semantic/base-class', { line: node.loc.start.line, before: node.tag, after: effectiveBaseClass });
113
- }
114
- const isInputLike = tag === 'input' || tag === 'textarea';
115
- const attrs = [];
116
- let hasNavTarget = false;
117
- for (const prop of node.props) {
118
- if (prop.type === NodeTypes.ATTRIBUTE) {
119
- const attr = prop;
120
- if (isNavLink && (attr.name === 'href' || attr.name === 'to' || attr.name === 'route-type')) {
121
- if (ctx.disabled.has('nav/navigate-link')) {
122
- ctx.warnings.push('规则 nav/navigate-link 已被禁用(rules.disabled),<a> 按普通 view 输出(无导航语义)');
123
- break;
124
- }
125
- if (attr.name === 'route-type' && attr.value) {
126
- attrs.push(`data-route-type="${escapeXml(attr.value.content)}"`);
127
- ctx.trace?.add('nav/route-type', { line: node.loc.start.line, before: `route-type="${attr.value.content}"`, after: `data-route-type="${attr.value.content}"` });
128
- }
129
- else if (attr.value) {
130
- attrs.push(`data-url="${escapeXml(attr.value.content)}"`);
131
- hasNavTarget = true;
132
- }
133
- continue;
134
- }
135
- if (attr.name === 'class' && effectiveBaseClass) {
136
- attrs.push(`class="${effectiveBaseClass}${attr.value ? ' ' + escapeXml(attr.value.content) : ''}"`);
137
- continue;
138
- }
139
- attrs.push(attr.value ? `${attr.name}="${escapeXml(attr.value.content)}"` : attr.name);
140
- continue;
141
- }
142
- const dir = prop;
143
- if (isNavLink && dir.name === 'bind' && (exprContent(dir.arg) === 'href' || exprContent(dir.arg) === 'to')) {
144
- const exp = exprContent(dir.exp);
145
- if (exp.trim().startsWith('{')) {
146
- ctx.warnings.push('路由链接 :to/:href 对象形式暂不支持(MVP),请改用字符串路径');
147
- }
148
- else if (!ctx.disabled.has('nav/navigate-link')) {
149
- attrs.push(`data-url="{{${exp}}}"`);
150
- hasNavTarget = true;
151
- }
152
- else {
153
- ctx.warnings.push('规则 nav/navigate-link 已被禁用(rules.disabled),<a> 按普通 view 输出(无导航语义)');
154
- }
155
- continue;
156
- }
157
- switch (dir.name) {
158
- case 'if':
159
- if (ctx.disabled.has('directive/v-if')) {
160
- ctx.warnings.push('规则 directive/v-if 已被禁用(rules.disabled),v-if 已忽略');
161
- break;
162
- }
163
- attrs.push(`wx:if="{{${exprContent(dir.exp)}}}"`);
164
- ctx.trace?.add('directive/v-if', { line: node.loc.start.line, before: `v-if="${exprContent(dir.exp)}"`, after: `wx:if="{{${exprContent(dir.exp)}}}"` });
165
- break;
166
- case 'else-if':
167
- if (ctx.disabled.has('directive/v-else-if'))
168
- break;
169
- attrs.push(`wx:elif="{{${exprContent(dir.exp)}}}"`);
170
- ctx.trace?.add('directive/v-else-if', { line: node.loc.start.line, before: 'v-else-if', after: 'wx:elif' });
171
- break;
172
- case 'else':
173
- if (ctx.disabled.has('directive/v-else'))
174
- break;
175
- attrs.push('wx:else');
176
- ctx.trace?.add('directive/v-else', { line: node.loc.start.line, before: 'v-else', after: 'wx:else' });
177
- break;
178
- case 'for': {
179
- if (ctx.disabled.has('directive/v-for')) {
180
- ctx.warnings.push('规则 directive/v-for 已被禁用(rules.disabled),v-for 已忽略');
181
- break;
182
- }
183
- const f = parseForExpr(exprContent(dir.exp));
184
- attrs.push(`wx:for="{{${f.list}}}"`);
185
- if (f.item)
186
- attrs.push(`wx:for-item="${f.item}"`);
187
- if (f.index)
188
- attrs.push(`wx:for-index="${f.index}"`);
189
- ctx.trace?.add('directive/v-for', { line: node.loc.start.line, before: exprContent(dir.exp), after: `wx:for="{{${f.list}}}"` });
190
- break;
191
- }
192
- case 'on': {
193
- if (ctx.disabled.has('event/click-to-tap') && ctx.disabled.has('event/modifier-catch')) {
194
- ctx.warnings.push('事件映射规则已全部禁用(rules.disabled),@事件 原样输出');
195
- const handler = cleanHandler(exprContent(dir.exp), ctx.warnings);
196
- attrs.push(`bind${exprContent(dir.arg)}="${handler}"`);
197
- break;
198
- }
199
- const raw = exprContent(dir.arg);
200
- const mapped = ctx.eventMap[raw] ?? raw;
201
- // 修饰符:运行时 modifiers 是 { content }[](与声明类型 string[] 不一致,做兼容)
202
- const mods = dir.modifiers.map((m) => typeof m === 'string' ? m : (m?.content ?? ''));
203
- const isCatch = (mods.includes('stop') || mods.includes('prevent')) && !ctx.disabled.has('event/modifier-catch');
204
- const handler = cleanHandler(exprContent(dir.exp), ctx.warnings);
205
- attrs.push(`${isCatch ? 'catch' : 'bind'}${mapped}="${handler}"`);
206
- ctx.trace?.add(isCatch ? 'event/modifier-catch' : 'event/click-to-tap', { line: node.loc.start.line, before: `@${raw}`, after: `${isCatch ? 'catch' : 'bind'}${mapped}` });
207
- break;
208
- }
209
- case 'bind': {
210
- const arg = exprContent(dir.arg);
211
- const exp = exprContent(dir.exp);
212
- if (arg === 'class') {
213
- if (ctx.disabled.has('directive/v-bind-class'))
214
- break;
215
- const cls = formatClassBinding(exp, ctx.warnings);
216
- attrs.push(`class="${effectiveBaseClass ? `${effectiveBaseClass} ` : ''}${cls}"`);
217
- ctx.trace?.add('directive/v-bind-class', { line: node.loc.start.line, before: `:class="${exp}"`, after: cls });
218
- }
219
- else if (arg === 'style') {
220
- if (ctx.disabled.has('directive/v-bind-style'))
221
- break;
222
- attrs.push(`style="${formatStyleBinding(exp)}"`);
223
- ctx.trace?.add('directive/v-bind-style', { line: node.loc.start.line, before: `:style="${exp}"`, after: formatStyleBinding(exp) });
224
- }
225
- else if (arg === 'key') {
226
- if (ctx.disabled.has('directive/v-bind-key'))
227
- break;
228
- if (/^[\w$]+$/.test(exp))
229
- attrs.push(`wx:key="${exp}"`);
230
- else
231
- ctx.warnings.push(`:key="${exp}" 不是简单标识符(MVP),wx:key 已忽略`);
232
- ctx.trace?.add('directive/v-bind-key', { line: node.loc.start.line, before: `:key="${exp}"`, after: `wx:key="${exp}"` });
233
- }
234
- else {
235
- if (ctx.disabled.has('directive/v-bind'))
236
- break;
237
- attrs.push(`${arg}="{{${exp}}}"`);
238
- ctx.trace?.add('directive/v-bind', { line: node.loc.start.line, before: `:${arg}`, after: `${arg}="{{${exp}}}"` });
239
- }
240
- break;
241
- }
242
- case 'model': {
243
- if (ctx.disabled.has('directive/v-model')) {
244
- ctx.warnings.push('规则 directive/v-model 已被禁用(rules.disabled),v-model 已忽略');
245
- break;
246
- }
247
- const model = exprContent(dir.exp);
248
- if (model && !ctx.vModelBindings.includes(model))
249
- ctx.vModelBindings.push(model);
250
- if (isInputLike)
251
- attrs.push(`value="{{${model}}}"`);
252
- // 方法名不用 __ 前缀(微信保留前缀,真机绑定可能失效)
253
- attrs.push(`bindinput="proteusOn${capitalize(model)}Input"`);
254
- ctx.trace?.add('directive/v-model', { line: node.loc.start.line, before: `v-model="${model}"`, after: `bindinput="proteusOn${capitalize(model)}Input"` });
255
- break;
256
- }
257
- case 'html':
258
- if (ctx.disabled.has('directive/v-html')) {
259
- ctx.warnings.push('规则 directive/v-html 已被禁用(rules.disabled),v-html 已忽略');
260
- break;
261
- }
262
- attrs.push(`nodes="{{${exprContent(dir.exp)}}}"`);
263
- ctx.trace?.add('directive/v-html', { line: node.loc.start.line, before: 'v-html', after: 'rich-text nodes' });
264
- break;
265
- case 'show':
266
- if (ctx.disabled.has('directive/v-show-limit'))
267
- break;
268
- ctx.warnings.push('v-show 暂不支持(MVP),已忽略,请改用 v-if');
269
- ctx.trace?.add('directive/v-show-limit', { line: node.loc.start.line, before: 'v-show', after: '(忽略 + 编译期警告)' });
270
- break;
271
- default:
272
- break; // v-slot / v-pre 等:MVP 忽略
273
- }
274
- }
275
- if (hasNavTarget && !ctx.disabled.has('nav/navigate-link')) {
276
- // 导航链接:绑定点击跳转(handler 由 script 转换自动注入;方法名避免 __ 前缀)
277
- attrs.push('bindtap="proteusNavigateTo"');
278
- ctx.usesNavigate = true;
279
- ctx.trace?.add('nav/navigate-link', { line: node.loc.start.line, before: `<${node.tag}>` + (node.tag === 'a' ? ' href/to' : ' to'), after: 'data-url + bindtap="proteusNavigateTo"' });
280
- }
281
- if (effectiveBaseClass && !attrs.some((a) => a.startsWith('class='))) {
282
- attrs.push(`class="${effectiveBaseClass}"`);
283
- }
284
- const attrStr = attrs.length ? ` ${attrs.join(' ')}` : '';
285
- // 反黑盒:注入源码行号注释(默认关闭,dev 调试开启)
286
- const lineNote = ctx.annotateLines ? `<!-- @${node.loc.start.line} ${node.tag} -->\n` : '';
287
- if (lineNote && !ctx.lineNoteTraced) {
288
- ctx.lineNoteTraced = true;
289
- ctx.trace?.add('annotation/line-note', { line: node.loc.start.line, before: `<${node.tag}>`, after: `<!-- @${node.loc.start.line} ${node.tag} -->` });
290
- }
291
- if (!node.children.length)
292
- return `${lineNote}<${tag}${attrStr} />`;
293
- const hasElementChild = node.children.some((c) => c.type === NodeTypes.ELEMENT);
294
- if (hasElementChild) {
295
- const inner = node.children.map((c) => serializeNode(c, ctx)).join('\n');
296
- return `${lineNote}<${tag}${attrStr}>\n${inner}\n</${tag}>`;
297
- }
298
- // 纯文本/插值子节点:紧凑单行(产物可读性)
299
- const inline = node.children.map((c) => serializeNode(c, ctx)).join('');
300
- return `${lineNote}<${tag}${attrStr}>${inline}</${tag}>`;
301
- }
302
- function serializeNode(node, ctx) {
303
- switch (node.type) {
304
- case NodeTypes.ELEMENT:
305
- return serializeElement(node, ctx);
306
- case NodeTypes.TEXT:
307
- return escapeXml(node.content);
308
- case NodeTypes.INTERPOLATION:
309
- ctx.trace?.add('node/interpolation', { line: node.loc.start.line, before: '{{ expr }}', after: '{{ expr }}(原样保留)' });
310
- return `{{ ${node.content.content} }}`;
311
- case NodeTypes.COMMENT:
312
- return `<!-- ${node.content} -->`;
313
- default:
314
- return '';
315
- }
316
- }
317
- function capitalize(s) {
318
- return s.charAt(0).toUpperCase() + s.slice(1);
319
- }
320
- /** template 源码 → WXML(纯函数,独立可测) */
321
- export function transformTemplateToWxml(source, opts = { px2rpx: true, rpxRatio: 2, annotateLines: false }) {
322
- const ctx = {
323
- vModelBindings: [],
324
- warnings: [],
325
- annotateLines: opts.annotateLines ?? false,
326
- filename: opts.filename,
327
- usesNavigate: false,
328
- trace: opts.trace,
329
- // ★底线循环 ①③:生效配置 = tags.ts 常量 + config 覆盖(规则改写/禁用即时生效)
330
- ...resolveOverrides(opts.rules),
331
- };
332
- const root = domParse(source, { onError: () => undefined });
333
- const wxml = root.children.map((c) => serializeNode(c, ctx)).join('\n');
334
- for (const w of ctx.warnings)
335
- console.warn(`[mp-transform] ${w}`);
336
- return { wxml, vModelBindings: ctx.vModelBindings, usesNavigate: ctx.usesNavigate, warnings: ctx.warnings };
337
- }
package/dist/trace.js DELETED
@@ -1,18 +0,0 @@
1
- /** 创建空收集器(phase 记录事件所属阶段,formatTransformTrace 分组用) */
2
- export function createTrace(phase = 'template') {
3
- return {
4
- events: [],
5
- add(ruleId, opts) {
6
- this.events.push({ ruleId, phase, ...opts });
7
- },
8
- };
9
- }
10
- /** 由字符偏移计算源码行号(1-based) */
11
- export function lineAt(source, index) {
12
- let line = 1;
13
- for (let i = 0; i < index && i < source.length; i++) {
14
- if (source[i] === '\n')
15
- line++;
16
- }
17
- return line;
18
- }
@@ -1,48 +0,0 @@
1
- import { TEMPLATE_RULES } from './template';
2
- import { SCRIPT_RULES } from './script';
3
- import { STYLE_RULES } from './style';
4
- import { VALIDATE_RULES } from './validate';
5
- /** 全量规则(聚合各阶段,ID 唯一性由 tests/transforms.test.ts 校验) */
6
- export const TRANSFORM_RULES = [
7
- ...TEMPLATE_RULES,
8
- ...SCRIPT_RULES,
9
- ...STYLE_RULES,
10
- ...VALIDATE_RULES,
11
- ];
12
- const byId = new Map(TRANSFORM_RULES.map((r) => [r.id, r]));
13
- /** 按阶段枚举规则(省略 phase 返回全部) */
14
- export function listTransformRules(phase) {
15
- return phase ? TRANSFORM_RULES.filter((r) => r.phase === phase) : [...TRANSFORM_RULES];
16
- }
17
- /** 按稳定 ID 查单条规则 */
18
- export function getTransformRule(id) {
19
- return byId.get(id);
20
- }
21
- /** 渲染单条规则的 AI 说明书(人可读文本,可直接喂给 AI / 写入文档) */
22
- export function formatTransformRule(rule) {
23
- const lines = [
24
- `## ${rule.id}(${rule.phase})`,
25
- `**${rule.title}** \`[${rule.status}]\``,
26
- `- 输入 → 输出:${rule.description}`,
27
- `- 为什么:${rule.why}`,
28
- `- 触发条件:${rule.when}`,
29
- `- 示例:`,
30
- ` - 源码:\`${rule.example.before}\``,
31
- ` - 产物:\`${rule.example.after}\``,
32
- `- 如何验证:${rule.verify}`,
33
- `- 实现位置:${rule.source}`,
34
- ];
35
- if (rule.decision)
36
- lines.push(`- 决策:${rule.decision}`);
37
- return lines.join('\n');
38
- }
39
- /** 渲染全量规则目录(按阶段分组) */
40
- export function formatTransformCatalog() {
41
- const phases = ['template', 'script', 'style', 'validate'];
42
- const blocks = phases.map((phase) => {
43
- const rules = listTransformRules(phase);
44
- const items = rules.map((r) => `- \`${r.id}\` \`[${r.status}]\` ${r.title}`).join('\n');
45
- return `### ${phase} 阶段(${rules.length} 条规则)\n\n${items}`;
46
- });
47
- return blocks.join('\n\n');
48
- }
@@ -1,179 +0,0 @@
1
- export const SCRIPT_RULES = [
2
- {
3
- id: 'script/const-to-data',
4
- phase: 'script',
5
- status: 'implemented',
6
- title: '顶层 const(ref/reactive/字面量)→ data',
7
- description: '零缩进顶层 const → data 字段;ref(0)/reactive({...})/字面量在构建期静态求值,多行数组/对象字面量完整提取',
8
- why: '小程序页面状态在 data 中,响应式声明(ref/reactive)编译期为初始值求值(决策 #60:括号平衡扫描支持多行字面量;只提取零缩进顶层 const,不误取函数体内局部 const)',
9
- when: 'script 顶层出现 const 声明且初始值为字面量/ref/reactive/shallowRef/readonly 时',
10
- example: {
11
- before: 'const count = ref(0)\nconst cards = ref([{ title: "a" }])',
12
- after: 'data: {\n count: 0,\n cards: [{ "title": "a" }],\n}',
13
- },
14
- verify: 'tests/mp-transform.test.ts data 提取用例;golden fixture showcase.js(多行数组)',
15
- source: 'src/compiler/script.ts → extractData + extractInitializer',
16
- decision: '#60 / #22',
17
- },
18
- {
19
- id: 'script/function-to-methods',
20
- phase: 'script',
21
- status: 'implemented',
22
- title: '顶层 function 声明 → methods',
23
- description: '顶层 function handleTap() {...} → methods 中的对象简写 handleTap() {...}',
24
- why: '小程序页面逻辑在 methods 中;对象字面量内不能输出裸 function 声明(决策 #14:产物方法用对象简写)',
25
- when: 'script 顶层出现 function 声明时',
26
- example: {
27
- before: 'function handleTap() {\n count.value++\n}',
28
- after: 'handleTap() {\n this.setData({ count: (this.data.count === undefined || this.data.count === null ? 0 : this.data.count) + 1 })\n},',
29
- },
30
- verify: 'tests/mp-transform.test.ts methods 提取用例',
31
- source: 'src/compiler/script.ts → extractMethods',
32
- decision: '#14',
33
- },
34
- {
35
- id: 'script/arrow-to-methods',
36
- phase: 'script',
37
- status: 'implemented',
38
- title: 'const 箭头函数 → methods',
39
- description: 'const fn = (params) => {...} → methods 中的 fn(params) {...}(支持 async)',
40
- why: '小程序页面逻辑在 methods 中;const 箭头函数同样提取为方法',
41
- when: 'script 顶层出现 const 箭头函数(含 async)时',
42
- example: {
43
- before: 'const load = async () => {\n const r = await fetchData()\n}',
44
- after: 'load() {\n const r = await fetchData()\n},',
45
- },
46
- verify: 'tests/mp-transform.test.ts methods 提取用例',
47
- source: 'src/compiler/script.ts → extractMethods(arrowRe 分支)',
48
- },
49
- {
50
- id: 'script/lifecycle-map',
51
- phase: 'script',
52
- status: 'implemented',
53
- title: '生命周期映射 onMounted → onReady / onUnmounted → onUnload',
54
- description: 'onMounted(() => {...}) → onReady() {...};onUnmounted → onUnload;onLoad 透传',
55
- why: 'Vue 组件生命周期与小程序页面生命周期不同名,编译期映射到小程序钩子',
56
- when: 'script 出现 onMounted / onUnmounted / onLoad 调用时',
57
- example: { before: 'onMounted(() => { doInit() })', after: 'onReady() {\n doInit()\n},' },
58
- verify: 'tests/runtime.test.ts + mp-transform.test.ts 生命周期用例',
59
- source: 'src/compiler/script.ts → extractLifecycles',
60
- },
61
- {
62
- id: 'script/ref-read',
63
- phase: 'script',
64
- status: 'implemented',
65
- title: '方法内 ref 读取 → this.data.name',
66
- description: '方法/生命周期体内的 name.value 读取 → this.data.name',
67
- why: '小程序运行期状态在 this.data 中,编译期把 setup ref 访问重写为 data 访问(决策 #22)',
68
- when: '方法或生命周期体内出现 ref.value 读取时',
69
- example: { before: 'const v = count.value', after: 'const v = this.data.count' },
70
- verify: 'tests/mp-transform.test.ts ref 重写用例',
71
- source: 'src/compiler/script.ts → rewriteRefAccess',
72
- decision: '#22',
73
- },
74
- {
75
- id: 'script/ref-write',
76
- phase: 'script',
77
- status: 'implemented',
78
- title: '方法内 ref 赋值 → this.setData',
79
- description: '方法/生命周期体内的 name.value = expr → this.setData({ name: expr })',
80
- why: '小程序更新视图的唯一通道是 setData,编译期把赋值重写为 setData 调用(决策 #22;排除 ==/===/复合赋值)',
81
- when: '方法或生命周期体内出现 name.value = expr 时',
82
- example: { before: 'count.value = count.value + 1', after: 'this.setData({ count: this.data.count + 1 })' },
83
- verify: 'tests/mp-transform.test.ts ref 重写用例',
84
- source: 'src/compiler/script.ts → rewriteRefAccess',
85
- decision: '#22',
86
- },
87
- {
88
- id: 'script/ref-incdec',
89
- phase: 'script',
90
- status: 'implemented',
91
- title: '方法内 ref 自增/自减 → this.setData',
92
- description: 'name.value++ / -- / ++name.value → this.setData({ name: (null 检查 ? 0 : this.data.name) + 1 })',
93
- why: '自增/自减无表达式可提取,编译期为显式 setData 并做 null 兜底(决策 #22;不用 ?? 运算符——真机预览报 SyntaxError,决策 #36)',
94
- when: '方法或生命周期体内出现 name.value 的自增/自减时',
95
- example: {
96
- before: 'count.value++',
97
- after: 'this.setData({ count: (this.data.count === undefined || this.data.count === null ? 0 : this.data.count) + 1 })',
98
- },
99
- verify: 'tests/mp-transform.test.ts ref 重写用例;Web E2E tap 计数用例',
100
- source: 'src/compiler/script.ts → rewriteRefAccess + numOrZero',
101
- decision: '#22 / #36',
102
- },
103
- {
104
- id: 'script/vmodel-handler',
105
- phase: 'script',
106
- status: 'implemented',
107
- title: 'v-model 自动 handler 注入',
108
- description: '模板出现 v-model="x" 时注入 proteusOnXInput(e) { this.setData({ x: e.detail.value }) }',
109
- why: '小程序无 v-model,回写方向由自动 handler 承担(决策 #29:方法名避免 __ 前缀,微信保留 _ 前缀可能导致绑定失效)',
110
- when: 'template 转换返回的 vModelBindings 非空时',
111
- example: {
112
- before: '(模板)<input v-model="name" />',
113
- after: 'proteusOnNameInput(e) { this.setData({ name: e.detail.value }) },',
114
- },
115
- verify: 'tests/mp-transform.test.ts v-model 用例',
116
- source: 'src/compiler/script.ts → transformScriptToPage(vModelBindings 循环)',
117
- decision: '#29',
118
- },
119
- {
120
- id: 'script/nav-handler',
121
- phase: 'script',
122
- status: 'implemented',
123
- title: '导航链接自动 handler 注入(proteusNavigateTo)',
124
- description: '模板出现导航链接时注入 proteusNavigateTo(e):读 data-url → wx.navigateTo(routeType 透传,fail 降级普通跳转)',
125
- why: '小程序导航统一走 wx.navigateTo;保留前导 / 为绝对路径(决策 #30 真机根因);fail 降级保证自定义路由失败仍可跳转(#28);方法名避开 __ 前缀(#29)',
126
- when: 'template 转换返回 usesNavigate=true 时',
127
- example: {
128
- before: '(模板)<a href="/pages/user/index">用户</a>',
129
- after: 'proteusNavigateTo(e) {\n const ds = e.currentTarget.dataset\n const url = String(ds.url || "")\n if (!url) return\n const nav = { url: url, fail: ... }\n if (ds.routeType) nav.routeType = ds.routeType\n wx.navigateTo(nav)\n},',
130
- },
131
- verify: 'tests/mp-transform.test.ts 导航链接用例;真机验证归档 #30',
132
- source: 'src/compiler/script.ts → transformScriptToPage(usesNavigate 分支)',
133
- decision: '#24 / #28 / #29 / #30',
134
- },
135
- {
136
- id: 'script/onload-params',
137
- phase: 'script',
138
- status: 'implemented',
139
- title: '默认 onLoad 参数自动 decode',
140
- description: '无显式 onLoad 时注入默认实现:遍历 options → decodeURIComponent → 结构化值({/[ 开头)JSON.parse → setData',
141
- why: '路由参数经 query 传递,页面需还原为原始类型(决策 #19:仅对结构化值 JSON.parse,普通标量保持字符串——对齐 P3 契约 options.id === "1")',
142
- when: 'script 无显式 onLoad 时(有显式 onLoad 则透传)',
143
- example: {
144
- before: '(无 onLoad)',
145
- after: 'onLoad(options) {\n const params = {}\n const keys = Object.keys(options || {})\n for (let i = 0; i < keys.length; i++) {\n ...\n }\n this.setData(params)\n},',
146
- },
147
- verify: 'tests/runtime.test.ts 参数 decode 用例',
148
- source: 'src/compiler/script.ts → transformScriptToPage(onLoad 分支)',
149
- decision: '#19 / #32(ES5 安全:索引循环 Object.keys)',
150
- },
151
- {
152
- id: 'script/component-mode',
153
- phase: 'script',
154
- status: 'implemented',
155
- title: '组件模式 → Component() 构造器',
156
- description: 'isComponent=true(components 目录下)→ Component({ data, methods, ... }),页面 → Page({ ... })',
157
- why: '小程序页面用 Page(),组件用 Component()(不同构造器形态,isComponent 分支已在 compiler/index.ts 透传)',
158
- when: '文件路径包含 components/ 目录时',
159
- example: { before: '(组件 SFC)', after: 'Component({ data: {...}, ... })' },
160
- verify: 'tests/mp-transform.test.ts 组件形态用例',
161
- source: 'src/compiler/script.ts → transformScriptToPage(isComponent 分支)',
162
- },
163
- {
164
- id: 'script/es5-safe',
165
- phase: 'script',
166
- status: 'implemented',
167
- title: '生成代码 ES5 安全',
168
- description: '产物规避 ?? / ?. / 数组解构 / 对象展开(显式 null 检查三元、索引循环 Object.keys、直接属性赋值)',
169
- why: '微信开发者工具将页面 JS 转 ES5 时依赖 babel helper 模块,helper 不在包内会报错(#32:arrayWithHoles 未定义);真机预览直接报 ?? 语法错误(#36)',
170
- when: '所有生成的 JS 产物(方法体 / onLoad / 自动 handler)',
171
- example: {
172
- before: '// 禁止出现在产物中\nconst [a, b] = arr\nconst v = x ?? 0',
173
- after: '// 产物实际形态\nconst a = arr[0], b = arr[1]\nconst v = (x === undefined || x === null ? 0 : x)',
174
- },
175
- verify: 'grep 扫描产物零残留(决策 #36 验证方式)',
176
- source: 'src/compiler/script.ts 各处(numOrZero / onLoad 实现 / nav handler)',
177
- decision: '#32 / #36',
178
- },
179
- ];
@@ -1,74 +0,0 @@
1
- // src/compiler/transforms/style.ts
2
- // style 阶段编译规则注册表 —— 每条规则一份 AI 说明书
3
- import { TAG_MAP, SEMANTIC_CLASS } from '../tags';
4
- export const STYLE_RULES = [
5
- {
6
- id: 'style/px-to-rpx',
7
- phase: 'style',
8
- status: 'implemented',
9
- title: 'px → rpx(仅 MP 端编译期生效)',
10
- description: 'CSS 数值 px → rpx(rpxRatio 默认 2:48px → 96rpx);Web 端永不转换(Vite 原生处理)',
11
- why: '小程序 rpx 是屏幕等比单位(750 设计稿),跨端 CSS 一致性的编译期吸收(决策 #9:MP 端 px→rpx,Web 端保持标准 CSS)',
12
- when: 'style 中出现数值 px(style.px2rpx=true)时',
13
- example: { before: 'padding: 48px;', after: 'padding: 96rpx;' },
14
- verify: 'tests/mp-transform.test.ts px→rpx 用例',
15
- source: 'src/compiler/style.ts → transformStyleToWxss(px2rpx 分支)',
16
- decision: '#9',
17
- },
18
- {
19
- id: 'style/selector-tag',
20
- phase: 'style',
21
- status: 'implemented',
22
- title: '选择器 HTML 标签 → 小程序标签',
23
- description: '选择器中的标签名重写为小程序标签(.links a → .links view、div > p → view > .proteus-p);属性选择器/类名/ID/长标识符不误伤',
24
- why: '模板已把 div/a/h1 等映射为 view/text,若样式选择器不重写则匹配不到元素(决策 #57 修复的 bug);命中条件=标签位于选择器起始或组合器之后,避免 .a/#input/tag-a 误伤',
25
- when: 'style 选择器中含 TAG_MAP 中的标签名时',
26
- example: { before: '.links a { color: #1a7af8; }', after: '.links .proteus-a { color: #1a7af8; }' },
27
- verify: 'tests/mp-transform.test.ts 选择器重写用例;产物验证 .links a → .links view 对齐 wxml(决策 #57)',
28
- source: 'src/compiler/style.ts → rewriteTagSelectors + TAG_SELECTOR_RE',
29
- decision: '#57',
30
- mapping: { ...TAG_MAP },
31
- },
32
- {
33
- id: 'style/selector-semantic',
34
- phase: 'style',
35
- status: 'implemented',
36
- title: '语义标签选择器 → proteus-* 类选择器',
37
- description: 'h1-h6/p/a 选择器映射为基础类选择器(.card h3 → .card .proteus-h3、.links a → .links .proteus-a),而非标签',
38
- why: 'h3 与 p 都映射为 text,若都映射为标签,同特异性规则后写覆盖先写(决策 #61 修复:.card p 的 color 曾污染 h3);模板侧已附加 proteus-* 类故精确匹配',
39
- when: '选择器中含 h1-h6/p/a 标签时',
40
- example: { before: '.card h3 { font-weight: 700; }', after: '.card .proteus-h3 { font-weight: 700; }' },
41
- verify: 'tests/mp-transform.test.ts 选择器重写用例(showcase.wxss 两条规则独立)',
42
- source: 'src/compiler/style.ts → rewriteTagSelectors(semantic 分支)+ src/compiler/tags.ts SEMANTIC_CLASS',
43
- decision: '#61',
44
- mapping: { ...SEMANTIC_CLASS },
45
- },
46
- {
47
- id: 'style/semantic-base-wxss',
48
- phase: 'style',
49
- status: 'implemented',
50
- title: '注入语义基础 WXSS(h1-h6/p/a 视觉还原)',
51
- description: '产物 WXSS 头部注入 .proteus-h1~h6/.proteus-p/.proteus-a 基础样式(对齐 HTML 标准附录 D:字号/字重/单边 em 段距/链接色),位于用户样式之前',
52
- why: 'Web 浏览器有 UA 默认样式,小程序 text/view 没有;注入基础类还原两端视觉一致(决策 #58);margin 用单边 bottom + em 相对自身字号——Skyline 自研引擎不折叠 margin,单边 em 与 Web 折叠在主流程组合下视觉一致(决策 #59)',
53
- when: '每次 style 转换(作为产物前缀)',
54
- example: {
55
- before: '// 源码无样式时产物仍含',
56
- after: '.proteus-h1 { display: block; font-size: 64rpx; font-weight: 700; margin: 0 0 0.67em; }\n.proteus-p { display: block; margin: 0 0 1em; }\n.proteus-a { color: #1a7af8; text-decoration: underline; }',
57
- },
58
- verify: 'golden fixture showcase.wxss;产物验证 <text class="proteus-h1"> + .proteus-h1(决策 #58)',
59
- source: 'src/compiler/style.ts → BASE_SEMANTIC_WXSS',
60
- decision: '#58 / #59',
61
- },
62
- {
63
- id: 'style/skyline-unsupported',
64
- phase: 'style',
65
- status: 'implemented',
66
- title: 'Skyline 不支持属性编译期警告',
67
- description: 'float、position: fixed 出现时编译期警告(不阻断构建)',
68
- why: 'Skyline 自研渲染引擎不支持这些布局属性,编译期警告让开发者提前知道(反黑盒原则:警告可见、可统计)',
69
- when: 'WXSS 中出现 float: 或 position: fixed 时',
70
- example: { before: '.banner { position: fixed; }', after: '警告:WXSS 检测到 Skyline 不支持的属性:position: fixed(编译期警告)' },
71
- verify: 'tests/mp-transform.test.ts 警告用例',
72
- source: 'src/compiler/style.ts → transformStyleToWxss(unsupported 分支)',
73
- },
74
- ];