@open-dy/vibecoding-sdk-taro 0.1.0 → 0.1.1-beta.0

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.
@@ -217,15 +217,26 @@ const pagesMustUseCustomNavSpace = {
217
217
  if (!/\/src\/pages\/.+\/index\.[jt]sx$/.test(filePath)) return {};
218
218
 
219
219
  let used = false;
220
+ const sarImports = new Set();
221
+ const renderedComponents = new Set();
220
222
 
221
223
  return {
222
224
  ImportDeclaration(node) {
223
225
  if (typeof node.source.value === 'string' && CUSTOM_NAV_IMPORTS.has(node.source.value)) {
224
226
  used = true;
225
227
  }
228
+ // SAR owns HUD safe spacing; adding host spacing would shrink the canvas.
229
+ if (typeof node.source.value === 'string' && /^(?:@\/|(?:\.\.\/)+)components\/(?:SarGame|sar-game)(?:\/index)?$/.test(node.source.value)) {
230
+ for (const specifier of node.specifiers) {
231
+ if (specifier.type === 'ImportDefaultSpecifier') sarImports.add(specifier.local.name);
232
+ }
233
+ }
234
+ },
235
+ JSXOpeningElement(node) {
236
+ if (node.name.type === 'JSXIdentifier') renderedComponents.add(node.name.name);
226
237
  },
227
238
  'Program:exit'(node) {
228
- if (used) return;
239
+ if (used || [...sarImports].some(name => renderedComponents.has(name))) return;
229
240
  context.report({
230
241
  node,
231
242
  message:
@@ -0,0 +1,103 @@
1
+ // Inspect only statically known options; dynamic request builders need runtime tests.
2
+ const unknown = Symbol('unknown');
3
+
4
+ function propertyName(node) {
5
+ if (!node.computed && node.key.type === 'Identifier') return node.key.name;
6
+ return node.key.type === 'Literal' ? node.key.value : unknown;
7
+ }
8
+
9
+ function property(object, name, ignoreCase = false) {
10
+ if (!object || object.type !== 'ObjectExpression') return unknown;
11
+ let result;
12
+ for (const item of object.properties) {
13
+ if (item.type !== 'Property') {
14
+ result = unknown;
15
+ continue;
16
+ }
17
+ const key = propertyName(item);
18
+ if (key === unknown) {
19
+ result = unknown;
20
+ } else if (typeof key === 'string' && (ignoreCase ? key.toLowerCase() === name : key === name)) {
21
+ result = item.value;
22
+ }
23
+ }
24
+ return result;
25
+ }
26
+
27
+ function literal(node) {
28
+ if (node?.type === 'Literal') return node.value;
29
+ if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) {
30
+ return node.quasis[0].value.cooked;
31
+ }
32
+ return unknown;
33
+ }
34
+
35
+ function variableFor(context, node) {
36
+ let scope = context.sourceCode.getScope(node);
37
+ while (scope) {
38
+ const variable = scope.set.get(node.name);
39
+ if (variable) return variable;
40
+ scope = scope.upper;
41
+ }
42
+ }
43
+
44
+ function isNetworkImport(context, node) {
45
+ if (node?.type !== 'Identifier') return false;
46
+ return variableFor(context, node)?.defs.some(def =>
47
+ def.type === 'ImportBinding' &&
48
+ def.node.type === 'ImportSpecifier' &&
49
+ (def.node.imported.name ?? def.node.imported.value) === 'Network' &&
50
+ /(?:^|\/)network(?:\.ts)?$/.test(def.parent.source.value),
51
+ ) ?? false;
52
+ }
53
+
54
+ function isEmptyBody(context, node) {
55
+ if (node === undefined) return true;
56
+ if (node === unknown) return false;
57
+ if (node.type === 'UnaryExpression' && node.operator === 'void') return true;
58
+ if (node.type === 'Identifier' && node.name === 'undefined') {
59
+ return !variableFor(context, node)?.defs.length;
60
+ }
61
+ const value = literal(node);
62
+ return value === null || (typeof value === 'string' && value.trim() === '');
63
+ }
64
+
65
+ export default {
66
+ rules: {
67
+ 'require-delete-json-body': {
68
+ meta: {
69
+ type: 'problem',
70
+ schema: [],
71
+ docs: { description: 'Require a non-empty JSON body for Network.request DELETE calls.' },
72
+ messages: {
73
+ empty: 'Network.request 的 DELETE 请求使用默认或显式 JSON Content-Type 时,必须传非空 data;无业务参数请传 data: {}。否则真机可能触发 Fastify FST_ERR_CTP_EMPTY_JSON_BODY。',
74
+ },
75
+ },
76
+ create(context) {
77
+ return {
78
+ CallExpression(node) {
79
+ const callee = node.callee;
80
+ if (callee.type !== 'MemberExpression' || !isNetworkImport(context, callee.object)) return;
81
+ const methodName = callee.computed ? literal(callee.property) : callee.property.name;
82
+ if (methodName !== 'request') return;
83
+ const options = node.arguments[0];
84
+ const method = literal(property(options, 'method'));
85
+ if (typeof method !== 'string' || method.toUpperCase() !== 'DELETE') return;
86
+ const header = property(options, 'header');
87
+ if (header !== undefined) {
88
+ const contentType = property(header, 'content-type', true);
89
+ if (contentType === unknown) return;
90
+ if (contentType !== undefined) {
91
+ const value = literal(contentType);
92
+ if (typeof value !== 'string' || !/^application\/json(?:\s*;|\s*$)/i.test(value.trim())) return;
93
+ }
94
+ }
95
+ if (isEmptyBody(context, property(options, 'data'))) {
96
+ context.report({ node: options, messageId: 'empty' });
97
+ }
98
+ },
99
+ };
100
+ },
101
+ },
102
+ },
103
+ };
@@ -7,6 +7,7 @@ import importPlugin from 'eslint-plugin-import';
7
7
  import globals from 'globals';
8
8
  import microappPages from './eslint-plugin-microapp-pages.mjs';
9
9
  import customNavigation from './eslint-plugin-custom-navigation.mjs';
10
+ import networkJsonBody from './eslint-plugin-network-json-body.mjs';
10
11
 
11
12
  const REMOTE_CSS_IMPORT_PATTERN =
12
13
  /@import\s+(?:url\(\s*['"]?((?:https?:)?\/\/[^'")\s]+)['"]?\s*\)|['"]((?:https?:)?\/\/[^'"\s]+)['"])/g;
@@ -474,6 +475,7 @@ export function createMicroappTaroEslintConfig(options = {}) {
474
475
  files: ['src/**/*.{js,jsx,ts,tsx}'],
475
476
  ignores: ['src/network.ts'],
476
477
  plugins: {
478
+ networkJsonBody,
477
479
  lucide: lucideColorCheckPlugin,
478
480
  microappPages,
479
481
  customNavigation,
@@ -482,6 +484,7 @@ export function createMicroappTaroEslintConfig(options = {}) {
482
484
  rules: {
483
485
  'customNavigation/require-custom-navigation-style': 'error',
484
486
  'customNavigation/pages-must-use-custom-nav-space': 'error',
487
+ 'networkJsonBody/require-delete-json-body': 'error',
485
488
  'microappPages/nav-url-in-app-pages': 'error',
486
489
  // 'microappPages/page-dirs-in-app-pages': 'error',
487
490
  'microappPages/declared-pages-must-exist': 'error',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-dy/vibecoding-sdk-taro",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-beta.0",
4
4
  "description": "Runtime, UI, build and ESLint capabilities for Douyin Taro templates.",
5
5
  "type": "module",
6
6
  "files": [
@@ -101,7 +101,7 @@
101
101
  },
102
102
  "publishConfig": {
103
103
  "registry": "https://registry.npmjs.org/",
104
- "tag": "latest",
104
+ "tag": "beta",
105
105
  "access": "public"
106
106
  }
107
107
  }
@@ -2,14 +2,14 @@
2
2
  "schema": 1,
3
3
  "producer": "vibecoding-sdk-taro",
4
4
  "packageName": "@open-dy/vibecoding-sdk-taro",
5
- "version": "0.1.0",
5
+ "version": "0.1.1-beta.0",
6
6
  "source": {
7
- "digest": "sha256-18af3af6ac61deaa125856b188413a1e2792a73cb9799dae78d9ff509d382d9f",
8
- "files": 136
7
+ "digest": "sha256-ed51d3ff798eecb98c494ae74426924e47bf1b144b9ea49f3e6d58deb4701adc",
8
+ "files": 141
9
9
  },
10
- "payload": "sha256-164386e236cc31177b0e6b412f4604e4ab502249f9be21c03b1e26bbd79f92c2",
10
+ "payload": "sha256-7a8761ffaa34826c59b00e4ea58f11ecf9023f0bf78ba587c33cf9557768ef54",
11
11
  "toolchain": {
12
- "node": "v22.22.3",
12
+ "node": "v22.23.1",
13
13
  "packageManager": null
14
14
  }
15
15
  }
@@ -217,15 +217,26 @@ const pagesMustUseCustomNavSpace = {
217
217
  if (!/\/src\/pages\/.+\/index\.[jt]sx$/.test(filePath)) return {};
218
218
 
219
219
  let used = false;
220
+ const sarImports = new Set();
221
+ const renderedComponents = new Set();
220
222
 
221
223
  return {
222
224
  ImportDeclaration(node) {
223
225
  if (typeof node.source.value === 'string' && CUSTOM_NAV_IMPORTS.has(node.source.value)) {
224
226
  used = true;
225
227
  }
228
+ // SAR owns HUD safe spacing; adding host spacing would shrink the canvas.
229
+ if (typeof node.source.value === 'string' && /^(?:@\/|(?:\.\.\/)+)components\/(?:SarGame|sar-game)(?:\/index)?$/.test(node.source.value)) {
230
+ for (const specifier of node.specifiers) {
231
+ if (specifier.type === 'ImportDefaultSpecifier') sarImports.add(specifier.local.name);
232
+ }
233
+ }
234
+ },
235
+ JSXOpeningElement(node) {
236
+ if (node.name.type === 'JSXIdentifier') renderedComponents.add(node.name.name);
226
237
  },
227
238
  'Program:exit'(node) {
228
- if (used) return;
239
+ if (used || [...sarImports].some(name => renderedComponents.has(name))) return;
229
240
  context.report({
230
241
  node,
231
242
  message:
@@ -0,0 +1,103 @@
1
+ // Inspect only statically known options; dynamic request builders need runtime tests.
2
+ const unknown = Symbol('unknown');
3
+
4
+ function propertyName(node) {
5
+ if (!node.computed && node.key.type === 'Identifier') return node.key.name;
6
+ return node.key.type === 'Literal' ? node.key.value : unknown;
7
+ }
8
+
9
+ function property(object, name, ignoreCase = false) {
10
+ if (!object || object.type !== 'ObjectExpression') return unknown;
11
+ let result;
12
+ for (const item of object.properties) {
13
+ if (item.type !== 'Property') {
14
+ result = unknown;
15
+ continue;
16
+ }
17
+ const key = propertyName(item);
18
+ if (key === unknown) {
19
+ result = unknown;
20
+ } else if (typeof key === 'string' && (ignoreCase ? key.toLowerCase() === name : key === name)) {
21
+ result = item.value;
22
+ }
23
+ }
24
+ return result;
25
+ }
26
+
27
+ function literal(node) {
28
+ if (node?.type === 'Literal') return node.value;
29
+ if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) {
30
+ return node.quasis[0].value.cooked;
31
+ }
32
+ return unknown;
33
+ }
34
+
35
+ function variableFor(context, node) {
36
+ let scope = context.sourceCode.getScope(node);
37
+ while (scope) {
38
+ const variable = scope.set.get(node.name);
39
+ if (variable) return variable;
40
+ scope = scope.upper;
41
+ }
42
+ }
43
+
44
+ function isNetworkImport(context, node) {
45
+ if (node?.type !== 'Identifier') return false;
46
+ return variableFor(context, node)?.defs.some(def =>
47
+ def.type === 'ImportBinding' &&
48
+ def.node.type === 'ImportSpecifier' &&
49
+ (def.node.imported.name ?? def.node.imported.value) === 'Network' &&
50
+ /(?:^|\/)network(?:\.ts)?$/.test(def.parent.source.value),
51
+ ) ?? false;
52
+ }
53
+
54
+ function isEmptyBody(context, node) {
55
+ if (node === undefined) return true;
56
+ if (node === unknown) return false;
57
+ if (node.type === 'UnaryExpression' && node.operator === 'void') return true;
58
+ if (node.type === 'Identifier' && node.name === 'undefined') {
59
+ return !variableFor(context, node)?.defs.length;
60
+ }
61
+ const value = literal(node);
62
+ return value === null || (typeof value === 'string' && value.trim() === '');
63
+ }
64
+
65
+ export default {
66
+ rules: {
67
+ 'require-delete-json-body': {
68
+ meta: {
69
+ type: 'problem',
70
+ schema: [],
71
+ docs: { description: 'Require a non-empty JSON body for Network.request DELETE calls.' },
72
+ messages: {
73
+ empty: 'Network.request 的 DELETE 请求使用默认或显式 JSON Content-Type 时,必须传非空 data;无业务参数请传 data: {}。否则真机可能触发 Fastify FST_ERR_CTP_EMPTY_JSON_BODY。',
74
+ },
75
+ },
76
+ create(context) {
77
+ return {
78
+ CallExpression(node) {
79
+ const callee = node.callee;
80
+ if (callee.type !== 'MemberExpression' || !isNetworkImport(context, callee.object)) return;
81
+ const methodName = callee.computed ? literal(callee.property) : callee.property.name;
82
+ if (methodName !== 'request') return;
83
+ const options = node.arguments[0];
84
+ const method = literal(property(options, 'method'));
85
+ if (typeof method !== 'string' || method.toUpperCase() !== 'DELETE') return;
86
+ const header = property(options, 'header');
87
+ if (header !== undefined) {
88
+ const contentType = property(header, 'content-type', true);
89
+ if (contentType === unknown) return;
90
+ if (contentType !== undefined) {
91
+ const value = literal(contentType);
92
+ if (typeof value !== 'string' || !/^application\/json(?:\s*;|\s*$)/i.test(value.trim())) return;
93
+ }
94
+ }
95
+ if (isEmptyBody(context, property(options, 'data'))) {
96
+ context.report({ node: options, messageId: 'empty' });
97
+ }
98
+ },
99
+ };
100
+ },
101
+ },
102
+ },
103
+ };
@@ -7,6 +7,7 @@ import importPlugin from 'eslint-plugin-import';
7
7
  import globals from 'globals';
8
8
  import microappPages from './eslint-plugin-microapp-pages.mjs';
9
9
  import customNavigation from './eslint-plugin-custom-navigation.mjs';
10
+ import networkJsonBody from './eslint-plugin-network-json-body.mjs';
10
11
 
11
12
  const REMOTE_CSS_IMPORT_PATTERN =
12
13
  /@import\s+(?:url\(\s*['"]?((?:https?:)?\/\/[^'")\s]+)['"]?\s*\)|['"]((?:https?:)?\/\/[^'"\s]+)['"])/g;
@@ -474,6 +475,7 @@ export function createMicroappTaroEslintConfig(options = {}) {
474
475
  files: ['src/**/*.{js,jsx,ts,tsx}'],
475
476
  ignores: ['src/network.ts'],
476
477
  plugins: {
478
+ networkJsonBody,
477
479
  lucide: lucideColorCheckPlugin,
478
480
  microappPages,
479
481
  customNavigation,
@@ -482,6 +484,7 @@ export function createMicroappTaroEslintConfig(options = {}) {
482
484
  rules: {
483
485
  'customNavigation/require-custom-navigation-style': 'error',
484
486
  'customNavigation/pages-must-use-custom-nav-space': 'error',
487
+ 'networkJsonBody/require-delete-json-body': 'error',
485
488
  'microappPages/nav-url-in-app-pages': 'error',
486
489
  // 'microappPages/page-dirs-in-app-pages': 'error',
487
490
  'microappPages/declared-pages-must-exist': 'error',