@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.
Files changed (54) hide show
  1. package/README.md +47 -0
  2. package/assets/schemas/custom-card-schema.json +102 -0
  3. package/bin/vibecoding +89 -0
  4. package/bin/vibecoding-dev +308 -0
  5. package/bin/vibecoding-interest-card.mjs +35 -0
  6. package/bin/vibecoding.mjs +9 -0
  7. package/package.json +58 -0
  8. package/src/adapters/interest-card/opencard/commands.mjs +62 -0
  9. package/src/adapters/interest-card/opencard/dev-watch.mjs +203 -0
  10. package/src/adapters/interest-card/opencard/dev_run.sh +126 -0
  11. package/src/adapters/interest-card/opencard/eslint-plugin-opencard-taro.mjs +278 -0
  12. package/src/adapters/interest-card/opencard/lint-lynx-css.mjs +676 -0
  13. package/src/adapters/interest-card/opencard/lint.sh +131 -0
  14. package/src/adapters/interest-card/opencard/lynx-css-rules.mjs +508 -0
  15. package/src/adapters/interest-card/opencard/pack.sh +27 -0
  16. package/src/adapters/interest-card/opencard/validate-opencard-structure.mjs +65 -0
  17. package/src/adapters/interest-card/opencard/validate-widget-shell.mjs +902 -0
  18. package/src/adapters/interest-card/opencard/validate.mjs +11 -0
  19. package/src/adapters/interest-card/opencard/write-web-config.mjs +17 -0
  20. package/src/build/opencard-environment.mjs +49 -0
  21. package/src/build/turbo-config.mjs +140 -0
  22. package/src/cli.mjs +104 -0
  23. package/src/commands/build.mjs +138 -0
  24. package/src/commands/db.mjs +14 -0
  25. package/src/commands/dev.mjs +319 -0
  26. package/src/commands/lint.mjs +176 -0
  27. package/src/commands/pack.mjs +169 -0
  28. package/src/commands/turbo.mjs +36 -0
  29. package/src/commands/validate.mjs +26 -0
  30. package/src/commands/verify.mjs +602 -0
  31. package/src/core/args.mjs +101 -0
  32. package/src/core/context.mjs +101 -0
  33. package/src/core/environment.mjs +55 -0
  34. package/src/core/errors.mjs +14 -0
  35. package/src/core/ports.mjs +76 -0
  36. package/src/core/process-output.mjs +25 -0
  37. package/src/core/runner.mjs +90 -0
  38. package/src/core/service-log-context.mjs +27 -0
  39. package/src/core/web-config.mjs +13 -0
  40. package/src/lint/lint-lynx-css.mjs +244 -0
  41. package/src/lint/lynx-css-rules.mjs +138 -0
  42. package/src/preview/dev-watch.mjs +38 -0
  43. package/src/preview/start-preview.mjs +213 -0
  44. package/src/preview/watch-card-routing.mjs +89 -0
  45. package/src/scripts/pack.mjs +15 -0
  46. package/src/validators/dependencies.mjs +34 -0
  47. package/src/validators/empty-project.mjs +29 -0
  48. package/src/validators/feed-card.cjs +156 -0
  49. package/src/validators/feed-card.mjs +32 -0
  50. package/src/validators/opencard-structure.mjs +58 -0
  51. package/src/validators/routes.cjs +470 -0
  52. package/src/validators/routes.mjs +17 -0
  53. package/src/validators/sql.mjs +57 -0
  54. package/src/validators/widget-shell.mjs +107 -0
@@ -0,0 +1,470 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function validateRoutes(context, options = {}) {
5
+ if (context.target !== 'taro') return;
6
+ const ROOT = context.root;
7
+ const routesDir = options.routesDir || '/tmp/routes';
8
+ const CLIENT_FILE = path.join(routesDir, 'client-routes.json');
9
+ const OPENCARD_CLIENT_FILE = path.join(routesDir, 'opencard-client-routes.json');
10
+ const SERVER_FILE = path.join(routesDir, 'server-routes.json');
11
+ const CLOSED_DOMAIN_SERVER_FILE = path.join(routesDir, 'closed-domain-server-routes.json');
12
+ const FEED_CARD_SCHEMA_FILE = path.join(ROOT, 'feed/feed-card.json');
13
+ const CARD_ROUTING_CONFIG_FILE = path.join(ROOT, 'config/card-routing.json');
14
+ const OPENCARD_DIR = path.join(ROOT, 'opencard');
15
+ const CLOSED_DOMAIN_SERVER_DIR = path.join(ROOT, 'closed-domain-server');
16
+ const OPEN_DOMAIN_SERVER_SRC_DIR = path.join(ROOT, 'server', 'src');
17
+ const OPENAPI_MODULE = '@open-dy/ai_coding_sdk/openapi';
18
+ const CLOSED_DOMAIN_OPENAPI_METHODS = new Set([
19
+ 'getUserConstellation',
20
+ 'getUserInterest',
21
+ 'getUserBasicData',
22
+ 'getUserFollowing',
23
+ 'getAIUserProfile',
24
+ 'getAIUserDataInfo',
25
+ 'getAIUserItemList',
26
+ ]);
27
+ const SERVER_SOURCE_EXTENSIONS = new Set([
28
+ '.js',
29
+ '.jsx',
30
+ '.ts',
31
+ '.tsx',
32
+ '.mjs',
33
+ '.cjs',
34
+ '.mts',
35
+ '.cts',
36
+ ]);
37
+
38
+ function loadJson(filePath) {
39
+ if (!fs.existsSync(filePath)) {
40
+ console.error(`文件不存在: ${filePath}`);
41
+ console.error('请确保前端和服务端都已经至少构建过一次。');
42
+ throw new Error('前后端路由一致性检测失败');
43
+ }
44
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
45
+ }
46
+
47
+ function withRouteSourceTag(routes, sourceLabel) {
48
+ return routes.map((route) => ({
49
+ ...route,
50
+ file: `${sourceLabel}/${route.file}`,
51
+ }));
52
+ }
53
+
54
+ function loadClientRoutes() {
55
+ const clientRoutes = withRouteSourceTag(loadJson(CLIENT_FILE), '主应用');
56
+ if (!fs.existsSync(OPENCARD_DIR)) {
57
+ return clientRoutes;
58
+ }
59
+
60
+ return clientRoutes.concat(
61
+ withRouteSourceTag(loadJson(OPENCARD_CLIENT_FILE), 'opencard'),
62
+ );
63
+ }
64
+
65
+ function normalizeUrl(url) {
66
+ return '/' + url.replace(/^\/+/, '').replace(/\/+$/, '');
67
+ }
68
+
69
+ function isStaticUrl(url) {
70
+ return !url.includes('${') && !url.includes('<dynamic>');
71
+ }
72
+
73
+ function matchRoute(clientUrl, serverRoutes) {
74
+ const normalized = normalizeUrl(clientUrl);
75
+
76
+ return serverRoutes.find((route) => {
77
+ const routePath = normalizeUrl(String(route.path));
78
+ if (normalized === routePath) return true;
79
+
80
+ const routeParts = routePath.split('/');
81
+ const clientParts = normalized.split('/');
82
+ if (routeParts.length !== clientParts.length) return false;
83
+
84
+ return routeParts.every((part, i) => part.startsWith(':') || part === clientParts[i]);
85
+ });
86
+ }
87
+
88
+ function loadCardRoutingConfig() {
89
+ if (!fs.existsSync(CARD_ROUTING_CONFIG_FILE)) {
90
+ console.warn(`未找到 ${CARD_ROUTING_CONFIG_FILE},按历史项目兼容逻辑默认检查开放域服务。`);
91
+ return null;
92
+ }
93
+
94
+ const config = loadJson(CARD_ROUTING_CONFIG_FILE);
95
+ const cardData = config?.cardData;
96
+ if (config?.version !== 1 || cardData?.path !== '/api/dy_card/data') {
97
+ console.error(`卡片路由配置非法: ${CARD_ROUTING_CONFIG_FILE}`);
98
+ throw new Error('前后端路由一致性检测失败');
99
+ }
100
+
101
+ if (
102
+ cardData.routes !== undefined &&
103
+ (!Array.isArray(cardData.routes) ||
104
+ cardData.routes.some(
105
+ (route) => typeof route !== 'string' || !route.trim(),
106
+ ))
107
+ ) {
108
+ console.error(`卡片路由配置非法: ${CARD_ROUTING_CONFIG_FILE} 的 cardData.routes 必须是非空字符串数组`);
109
+ throw new Error('前后端路由一致性检测失败');
110
+ }
111
+
112
+ const routes = Array.isArray(cardData.routes)
113
+ ? cardData.routes.map((route) => normalizeUrl(route))
114
+ : [];
115
+ if (!routes.includes(cardData.path)) {
116
+ routes.unshift(cardData.path);
117
+ }
118
+
119
+ return {
120
+ ...config,
121
+ cardData: {
122
+ ...cardData,
123
+ routes,
124
+ },
125
+ };
126
+ }
127
+
128
+ function loadCardDataServerRoutes(openDomainServerRoutes, cardRoutingConfig) {
129
+ if (!cardRoutingConfig) {
130
+ return openDomainServerRoutes;
131
+ }
132
+
133
+ const cardData = cardRoutingConfig.cardData;
134
+ switch (cardData.serviceDomain) {
135
+ case 'open':
136
+ return openDomainServerRoutes;
137
+ case 'closed':
138
+ if (!fs.existsSync(path.join(CLOSED_DOMAIN_SERVER_DIR, 'package.json'))) {
139
+ console.error(`card-routing.json 声明 cardData.serviceDomain=closed,但 ${CLOSED_DOMAIN_SERVER_DIR}/package.json 不存在。`);
140
+ throw new Error('前后端路由一致性检测失败');
141
+ }
142
+ return loadClosedDomainServerRoutes(true);
143
+ case 'none':
144
+ return [];
145
+ default:
146
+ console.error(`卡片服务域非法: ${String(cardData.serviceDomain)}`);
147
+ throw new Error('前后端路由一致性检测失败');
148
+ }
149
+ }
150
+
151
+ function assertCardRoutesUseOneService(
152
+ cardRoutingConfig,
153
+ openDomainServerRoutes,
154
+ closedDomainServerRoutes,
155
+ ) {
156
+ if (!cardRoutingConfig || cardRoutingConfig.cardData.serviceDomain === 'none') {
157
+ return;
158
+ }
159
+
160
+ for (const cardRoute of cardRoutingConfig.cardData.routes) {
161
+ const openMatch = matchRoute(cardRoute, openDomainServerRoutes);
162
+ const closedMatch = matchRoute(cardRoute, closedDomainServerRoutes);
163
+ if (openMatch && closedMatch) {
164
+ console.error(
165
+ `卡片接口 ${cardRoute} 同时注册在开放域和封闭域;同一张异形卡只能选择一个服务域。`,
166
+ );
167
+ throw new Error('前后端路由一致性检测失败');
168
+ }
169
+
170
+ const selectedRoutes =
171
+ cardRoutingConfig.cardData.serviceDomain === 'closed'
172
+ ? closedDomainServerRoutes
173
+ : openDomainServerRoutes;
174
+ if (!matchRoute(cardRoute, selectedRoutes)) {
175
+ console.error(
176
+ `卡片接口 ${cardRoute} 未在 ${cardRoutingConfig.cardData.serviceDomain} 服务注册。`,
177
+ );
178
+ throw new Error('前后端路由一致性检测失败');
179
+ }
180
+ }
181
+ }
182
+
183
+ function loadClosedDomainServerRoutes(required = false) {
184
+ if (!fs.existsSync(path.join(CLOSED_DOMAIN_SERVER_DIR, 'package.json'))) {
185
+ if (required) {
186
+ console.error(`card-routing.json 声明封闭域服务,但 ${CLOSED_DOMAIN_SERVER_DIR}/package.json 不存在。`);
187
+ throw new Error('前后端路由一致性检测失败');
188
+ }
189
+ return [];
190
+ }
191
+ if (!fs.existsSync(CLOSED_DOMAIN_SERVER_FILE)) {
192
+ if (required) {
193
+ console.error(`封闭域服务已选择,但路由文件不存在: ${CLOSED_DOMAIN_SERVER_FILE}`);
194
+ console.error('请先构建 closed-domain-server,再执行 check-routes。');
195
+ throw new Error('前后端路由一致性检测失败');
196
+ }
197
+ return [];
198
+ }
199
+ return loadJson(CLOSED_DOMAIN_SERVER_FILE);
200
+ }
201
+
202
+ function isClosedDomainUrl(url) {
203
+ return normalizeUrl(url).startsWith('/api/closed_domain/');
204
+ }
205
+
206
+ function maskCommentsAndStrings(source, maskStrings) {
207
+ let result = '';
208
+ let state = 'code';
209
+ let quote = '';
210
+
211
+ for (let index = 0; index < source.length; index += 1) {
212
+ const current = source[index];
213
+ const next = source[index + 1];
214
+
215
+ if (state === 'line-comment') {
216
+ if (current === '\n') {
217
+ state = 'code';
218
+ result += '\n';
219
+ } else {
220
+ result += ' ';
221
+ }
222
+ continue;
223
+ }
224
+
225
+ if (state === 'block-comment') {
226
+ if (current === '*' && next === '/') {
227
+ result += ' ';
228
+ index += 1;
229
+ state = 'code';
230
+ } else {
231
+ result += current === '\n' ? '\n' : ' ';
232
+ }
233
+ continue;
234
+ }
235
+
236
+ if (state === 'string') {
237
+ if (current === '\\') {
238
+ result += maskStrings ? ' ' : current;
239
+ if (next !== undefined) {
240
+ result += maskStrings ? (next === '\n' ? '\n' : ' ') : next;
241
+ index += 1;
242
+ }
243
+ } else {
244
+ result += maskStrings ? (current === '\n' ? '\n' : ' ') : current;
245
+ if (current === quote) {
246
+ state = 'code';
247
+ quote = '';
248
+ }
249
+ }
250
+ continue;
251
+ }
252
+
253
+ if (current === '/' && next === '/') {
254
+ result += ' ';
255
+ index += 1;
256
+ state = 'line-comment';
257
+ continue;
258
+ }
259
+ if (current === '/' && next === '*') {
260
+ result += ' ';
261
+ index += 1;
262
+ state = 'block-comment';
263
+ continue;
264
+ }
265
+ if (current === "'" || current === '"' || current === '\u0060') {
266
+ state = 'string';
267
+ quote = current;
268
+ result += maskStrings ? ' ' : current;
269
+ continue;
270
+ }
271
+ result += current;
272
+ }
273
+
274
+ return result;
275
+ }
276
+
277
+ function escapeRegExp(value) {
278
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
279
+ }
280
+
281
+ function isOpenAPIModule(moduleName) {
282
+ return moduleName === OPENAPI_MODULE || moduleName.startsWith(`${OPENAPI_MODULE}/`);
283
+ }
284
+
285
+ function closedDomainMethodFromSubpath(moduleName) {
286
+ if (!moduleName.startsWith(`${OPENAPI_MODULE}/`)) return null;
287
+ const method = moduleName.slice(OPENAPI_MODULE.length + 1).split('/').pop().replace(/\.[cm]?[jt]s$/, '');
288
+ return CLOSED_DOMAIN_OPENAPI_METHODS.has(method) ? method : null;
289
+ }
290
+
291
+ function bindingIsCalled(code, binding) {
292
+ return new RegExp(`\\b${escapeRegExp(binding)}\\s*(?:\\?\\.\\s*)?\\(`).test(code);
293
+ }
294
+
295
+ function namespaceMethodIsCalled(code, namespace, method) {
296
+ return new RegExp(
297
+ `\\b${escapeRegExp(namespace)}\\s*(?:\\?\\.|\\.)\\s*${escapeRegExp(method)}\\s*(?:\\?\\.\\s*)?\\(`,
298
+ ).test(code);
299
+ }
300
+
301
+ function parseNamedBindings(specifiers, aliasSeparator) {
302
+ return specifiers.split(',').map((specifier) => {
303
+ const normalized = specifier.trim().replace(/^type\s+/, '');
304
+ const parts = normalized.split(aliasSeparator).map((part) => part.trim());
305
+ return { imported: parts[0], local: parts[1] || parts[0] };
306
+ });
307
+ }
308
+
309
+ function findClosedDomainOpenAPICalls(source) {
310
+ const sourceWithoutComments = maskCommentsAndStrings(source, false);
311
+ const executableCode = maskCommentsAndStrings(source, true);
312
+ const methods = new Set();
313
+
314
+ const recordNamedBindings = (specifiers, aliasSeparator) => {
315
+ for (const { imported, local } of parseNamedBindings(specifiers, aliasSeparator)) {
316
+ if (CLOSED_DOMAIN_OPENAPI_METHODS.has(imported) && bindingIsCalled(executableCode, local)) {
317
+ methods.add(imported);
318
+ }
319
+ }
320
+ };
321
+
322
+ const namedImportPattern = /\bimport\s*\{([\s\S]*?)\}\s*from\s*(['"])([^'"]+)\2/g;
323
+ for (const match of sourceWithoutComments.matchAll(namedImportPattern)) {
324
+ if (isOpenAPIModule(match[3])) recordNamedBindings(match[1], /\s+as\s+/);
325
+ }
326
+
327
+ const namespaceImportPattern = /\bimport\s*\*\s*as\s*([A-Za-z_$][\w$]*)\s*from\s*(['"])([^'"]+)\2/g;
328
+ for (const match of sourceWithoutComments.matchAll(namespaceImportPattern)) {
329
+ if (!isOpenAPIModule(match[3])) continue;
330
+ for (const method of CLOSED_DOMAIN_OPENAPI_METHODS) {
331
+ if (namespaceMethodIsCalled(executableCode, match[1], method)) methods.add(method);
332
+ }
333
+ }
334
+
335
+ const defaultImportPattern = /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s*(['"])([^'"]+)\2/g;
336
+ for (const match of sourceWithoutComments.matchAll(defaultImportPattern)) {
337
+ const method = closedDomainMethodFromSubpath(match[3]);
338
+ if (method && bindingIsCalled(executableCode, match[1])) methods.add(method);
339
+ }
340
+
341
+ const namedRequirePattern = /\b(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*(['"])([^'"]+)\2\s*\)/g;
342
+ for (const match of sourceWithoutComments.matchAll(namedRequirePattern)) {
343
+ if (isOpenAPIModule(match[3])) recordNamedBindings(match[1], /\s*:\s*/);
344
+ }
345
+
346
+ const namespaceRequirePattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\s*\(\s*(['"])([^'"]+)\2\s*\)/g;
347
+ for (const match of sourceWithoutComments.matchAll(namespaceRequirePattern)) {
348
+ if (!isOpenAPIModule(match[3])) continue;
349
+ const subpathMethod = closedDomainMethodFromSubpath(match[3]);
350
+ if (subpathMethod && bindingIsCalled(executableCode, match[1])) methods.add(subpathMethod);
351
+ for (const method of CLOSED_DOMAIN_OPENAPI_METHODS) {
352
+ if (namespaceMethodIsCalled(executableCode, match[1], method)) methods.add(method);
353
+ }
354
+ }
355
+
356
+ const directRequirePattern = /\brequire\s*\(\s*(['"])([^'"]+)\1\s*\)\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)\s*(?:\?\.\s*)?\(/g;
357
+ for (const match of sourceWithoutComments.matchAll(directRequirePattern)) {
358
+ if (isOpenAPIModule(match[2]) && CLOSED_DOMAIN_OPENAPI_METHODS.has(match[3])) {
359
+ methods.add(match[3]);
360
+ }
361
+ }
362
+
363
+ return Array.from(methods).sort();
364
+ }
365
+
366
+ function listServerSourceFiles(directory) {
367
+ if (!fs.existsSync(directory)) return [];
368
+ const sourceFiles = [];
369
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
370
+ const entryPath = path.join(directory, entry.name);
371
+ if (entry.isDirectory()) {
372
+ sourceFiles.push(...listServerSourceFiles(entryPath));
373
+ } else if (entry.isFile() && SERVER_SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
374
+ sourceFiles.push(entryPath);
375
+ }
376
+ }
377
+ return sourceFiles.sort();
378
+ }
379
+
380
+ function assertOpenDomainServerDoesNotCallClosedDomainOpenAPI() {
381
+ const violations = [];
382
+ for (const filePath of listServerSourceFiles(OPEN_DOMAIN_SERVER_SRC_DIR)) {
383
+ const source = fs.readFileSync(filePath, 'utf-8');
384
+ for (const method of findClosedDomainOpenAPICalls(source)) {
385
+ violations.push({ filePath, method });
386
+ }
387
+ }
388
+
389
+ if (violations.length === 0) return;
390
+
391
+ console.error('--- 开放域服务调用封闭域 OpenAPI 校验失败 ---\n');
392
+ console.error('以下封闭域高敏 OpenAPI 只能在 closed-domain-server/ 中调用:');
393
+ for (const violation of violations) {
394
+ console.error(` ${path.relative(ROOT, violation.filePath)}: ${violation.method}()`);
395
+ }
396
+ throw new Error('前后端路由一致性检测失败');
397
+ }
398
+
399
+ function main() {
400
+ const clientRoutes = loadClientRoutes();
401
+ const openDomainServerRoutes = loadJson(SERVER_FILE);
402
+ const closedDomainServerRoutes = loadClosedDomainServerRoutes();
403
+ const cardRoutingConfig = loadCardRoutingConfig();
404
+
405
+ console.log('=== 前后端路由一致性检测报告 ===\n');
406
+
407
+ assertOpenDomainServerDoesNotCallClosedDomainOpenAPI();
408
+
409
+ // If feed-card.json exists, we require /api/dy_card/data to be present on server side.
410
+ // This is a hard requirement because the feed card renderer relies on this endpoint.
411
+ if (fs.existsSync(FEED_CARD_SCHEMA_FILE)) {
412
+ const requiredPath = '/api/dy_card/data';
413
+ const cardDataServerRoutes = loadCardDataServerRoutes(
414
+ openDomainServerRoutes,
415
+ cardRoutingConfig,
416
+ );
417
+ const matched = matchRoute(requiredPath, cardDataServerRoutes);
418
+ if (!matched) {
419
+ console.error('--- Feed 卡接口校验失败 ---\n');
420
+ console.error(`检测到 ${FEED_CARD_SCHEMA_FILE},但服务端未注册必需接口:${requiredPath}`);
421
+ console.error('请在 card-routing.json 声明的服务域实现并注册该接口(通常为 Controller: @Controller(\'api\') + @All(\'dy_card/data\'))。');
422
+ throw new Error('前后端路由一致性检测失败');
423
+ }
424
+ }
425
+
426
+ assertCardRoutesUseOneService(
427
+ cardRoutingConfig,
428
+ openDomainServerRoutes,
429
+ closedDomainServerRoutes,
430
+ );
431
+
432
+ const configuredCardRoutes = cardRoutingConfig?.cardData?.routes || [];
433
+ const mismatched = [];
434
+ for (const cr of clientRoutes) {
435
+ if (cr.url.startsWith('http://') || cr.url.startsWith('https://') || !isStaticUrl(cr.url)) {
436
+ continue;
437
+ }
438
+
439
+ const isCardRoute = configuredCardRoutes.some((cardRoute) =>
440
+ matchRoute(cr.url, [{ path: cardRoute }]),
441
+ );
442
+ const serverRoutes = isCardRoute
443
+ ? loadCardDataServerRoutes(openDomainServerRoutes, cardRoutingConfig)
444
+ : isClosedDomainUrl(cr.url)
445
+ ? closedDomainServerRoutes
446
+ : openDomainServerRoutes;
447
+ if (matchRoute(cr.url, serverRoutes)) {
448
+ continue;
449
+ } else {
450
+ mismatched.push(cr);
451
+ }
452
+ }
453
+
454
+ console.log('--- 检测结果 ---\n');
455
+
456
+ if (mismatched.length > 0) {
457
+ console.error(`未匹配(${mismatched.length} 条):以下前端请求路径未匹配到所选服务域的服务端路由,请检查前端请求路径、服务域配置,或确认服务端已注册对应路由`);
458
+ for (const r of mismatched) {
459
+ console.error(` [${r.method}] ${r.url} (来源: ${r.file})`);
460
+ }
461
+ throw new Error('前后端路由一致性检测失败');
462
+ } else {
463
+ console.log('所有前端静态请求路径均已在服务端注册对应路由,检测通过。');
464
+ }
465
+ }
466
+
467
+ return main();
468
+ }
469
+
470
+ module.exports = { validateRoutes };
@@ -0,0 +1,17 @@
1
+ import validator from './routes.cjs'
2
+ import { CliError } from '../core/errors.mjs'
3
+
4
+ /**
5
+ * @desc 校验工程中声明的页面路由与源码入口是否一致。
6
+ * @params {object} context CLI 工程上下文。
7
+ * @params {object} options 路由校验选项。
8
+ * @return {unknown} 校验器返回的结果;发现错误时抛出 CliError。
9
+ * @example validateRoutes(context, { target: 'taro' })
10
+ */
11
+ export function validateRoutes(context, options) {
12
+ try {
13
+ return validator.validateRoutes(context, options)
14
+ } catch (error) {
15
+ throw new CliError(error.message)
16
+ }
17
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { CliError } from '../core/errors.mjs'
4
+
5
+ const SOURCE_EXTENSIONS = /\.(?:cjs|cts|js|mjs|mts|ts)$/u
6
+ const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build'])
7
+ const UNSUPPORTED_PAGINATION = /\b(?:LIMIT|OFFSET)\s+\?/giu
8
+
9
+ function sourceFiles(root) {
10
+ if (!root || !existsSync(root)) return []
11
+ const files = []
12
+ const visit = (directory) => {
13
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
14
+ if (entry.isDirectory()) {
15
+ if (!IGNORED_DIRECTORIES.has(entry.name)) visit(path.join(directory, entry.name))
16
+ continue
17
+ }
18
+ if (SOURCE_EXTENSIONS.test(entry.name)) files.push(path.join(directory, entry.name))
19
+ }
20
+ }
21
+ visit(root)
22
+ return files
23
+ }
24
+
25
+ function lineNumber(source, offset) {
26
+ return source.slice(0, offset).split('\n').length
27
+ }
28
+
29
+ export function findUnsupportedPagination(root) {
30
+ const findings = []
31
+ for (const file of sourceFiles(root)) {
32
+ const source = readFileSync(file, 'utf8')
33
+ for (const match of source.matchAll(UNSUPPORTED_PAGINATION)) {
34
+ findings.push({
35
+ file: path.relative(root, file),
36
+ line: lineNumber(source, match.index ?? 0),
37
+ match: match[0],
38
+ })
39
+ }
40
+ }
41
+ return findings
42
+ }
43
+
44
+ export function validateSqlQueries(roots) {
45
+ const normalizedRoots = (Array.isArray(roots) ? roots : [roots]).filter(Boolean)
46
+ const findings = normalizedRoots.flatMap((root) =>
47
+ findUnsupportedPagination(path.join(root, 'src')),
48
+ )
49
+ if (findings.length === 0) return findings
50
+
51
+ const details = findings.map(({ file, line, match }) => `${file}:${line} (${match})`).join('\n')
52
+ throw new CliError(
53
+ 'SQL pagination placeholders are not supported by the project database proxy.\n' +
54
+ `${details}\n` +
55
+ 'Validate the page size and offset as integers, then use fixed SQL or safe integer literals; keep user data parameterized.',
56
+ )
57
+ }
@@ -0,0 +1,107 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ function listCssFiles(directory, files = []) {
5
+ if (!existsSync(directory)) return files
6
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
7
+ const target = path.join(directory, entry.name)
8
+ if (entry.isDirectory()) {
9
+ listCssFiles(target, files)
10
+ } else if (entry.name.endsWith('.css')) {
11
+ files.push(target)
12
+ }
13
+ }
14
+ return files
15
+ }
16
+
17
+ function rootRule(source) {
18
+ const match = /\.widget-root\s*\{([\s\S]*?)\}/m.exec(source)
19
+ return match?.[1] ?? ''
20
+ }
21
+
22
+ function hasDeclaration(css, property, value) {
23
+ const expression = new RegExp(
24
+ `${property.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:\\s*${value}`,
25
+ 'i',
26
+ )
27
+ return expression.test(css)
28
+ }
29
+
30
+ function formatFinding(file, line, column, message) {
31
+ return `${file}:${line}:${column} ${message}`
32
+ }
33
+
34
+ export function validateWidgetShell(context) {
35
+ const root = context.target === 'opencard' ? context.root : context.opencardRoot
36
+ if (!root) return { skipped: true, findings: [] }
37
+
38
+ const pageFile = path.join(root, 'src/pages/index/index.tsx')
39
+ const sourceDirectory = path.join(root, 'src')
40
+ const findings = []
41
+ if (!existsSync(pageFile)) {
42
+ findings.push({
43
+ file: 'src/pages/index/index.tsx',
44
+ line: 1,
45
+ column: 1,
46
+ message: '缺少 OpenCard 页面入口。',
47
+ })
48
+ } else {
49
+ const page = readFileSync(pageFile, 'utf8')
50
+ if (!/<View\b[^>]*className\s*=\s*(?:"[^"]*\bwidget-root\b[^"]*"|'[^']*\bwidget-root\b[^']*')/m.test(page)) {
51
+ findings.push({
52
+ file: 'src/pages/index/index.tsx',
53
+ line: 1,
54
+ column: 1,
55
+ message: '页面根 View 必须使用 widget-root 作为固定 widget 底板。',
56
+ })
57
+ }
58
+ }
59
+
60
+ const cssFiles = listCssFiles(sourceDirectory)
61
+ const css = cssFiles.map((file) => readFileSync(file, 'utf8')).join('\n')
62
+ const shell = rootRule(css)
63
+ if (!shell) {
64
+ findings.push({
65
+ file: 'src/pages/index/index.css',
66
+ line: 1,
67
+ column: 1,
68
+ message: '缺少 .widget-root CSS。',
69
+ })
70
+ } else {
71
+ const checks = [
72
+ [/box-sizing\s*:\s*border-box/i, '底板必须使用 box-sizing: border-box。'],
73
+ [/display\s*:\s*flex/i, '底板必须使用 Flex 布局。'],
74
+ [/height\s*:\s*100%/i, '底板必须使用 height: 100%,不能使用 100vh。'],
75
+ [/background(?:-color)?\s*:\s*(?!transparent|none)[^;]+/i, '底板必须有非透明背景。'],
76
+ [/border\s*:\s*1px\s+(?:solid|dashed|dotted)\s+[^;]+/i, '底板必须有 1px 描边。'],
77
+ [/border-radius\s*:\s*(?:12px|var\(--[A-Za-z0-9_-]+\))/i, '底板四角必须使用 12px 圆角。'],
78
+ ]
79
+ for (const [expression, message] of checks) {
80
+ if (!expression.test(shell)) {
81
+ findings.push({
82
+ file: 'src/pages/index/index.css',
83
+ line: 1,
84
+ column: 1,
85
+ message,
86
+ })
87
+ }
88
+ }
89
+ if (hasDeclaration(shell, 'height', '100vh')) {
90
+ findings.push({
91
+ file: 'src/pages/index/index.css',
92
+ line: 1,
93
+ column: 1,
94
+ message: '固定 widget 禁止使用 height: 100vh。',
95
+ })
96
+ }
97
+ }
98
+
99
+ if (findings.length > 0) {
100
+ console.warn(`WARN: OpenCard widget 底板校验未通过(${findings.length}),不阻断构建`)
101
+ for (const finding of findings) {
102
+ console.warn(formatFinding(finding.file, finding.line, finding.column, finding.message))
103
+ }
104
+ }
105
+
106
+ return { skipped: false, findings }
107
+ }