miaoda-expo-devkit 0.1.1-dev.development-build.1 → 0.1.1-dev.development-build.2
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 +15 -7
- package/dist/babel/plugin-lucide-react-native.js +1 -1
- package/dist/rules/no-bare-web-crypto.js +235 -0
- package/dist/rules/no-expo-file-system-compat.js +2130 -0
- package/dist/rules/no-lucide-star-import.js +78 -0
- package/dist/rules/no-sentry-default-import.js +83 -0
- package/dist/rules/no-typeof-window-check.js +85 -0
- package/oxlint-config.json +11 -1
- package/package.json +6 -1
- package/pnpm-patches/@expo__cli@55.0.16-55.0.22.patch +230 -145
- package/pnpm-patches/expo@55.0.6.patch +78 -30
- package/pnpm-patches/metro-file-map@0.83.3.patch +7 -3
- package/pnpm-patches/metro-runtime@0.83.3.patch +5 -1
- package/pnpm-patches/react-native-worklets@0.7.4.patch +91 -0
- package/pnpmfile.cjs +85 -2
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Expo / React Native 开发环境工具集,通过 Metro 构建层注入以下
|
|
|
19
19
|
- **expo-haptics stub** — Web 中提供 no-op 实现,触觉 API 调用时弹 Dialog 提示参数信息,native 不受影响
|
|
20
20
|
- **expo-contacts stub** — Web / Expo Go 中提供 no-op 实现,联系人 API 调用时弹 Dialog 提示,Dev Build 透传真实模块
|
|
21
21
|
- **expo-image-picker stub** — 桌面 Web 中 `launchCameraAsync` 通过 `getUserMedia` 打开摄像头预览弹窗(浏览器原生 `capture` 属性在 PC 端被忽略),移动端浏览器透传 expo 原实现,Native 不受影响
|
|
22
|
-
- **devkit-lint** — 集成 Oxlint(
|
|
22
|
+
- **devkit-lint** — 集成 Oxlint(22 条自定义规则)、Biome、TypeScript 类型检查、以及 `app.json` 字段合法性校验,一条命令完成项目全量静态检查
|
|
23
23
|
|
|
24
24
|
## 安装
|
|
25
25
|
|
|
@@ -865,11 +865,19 @@ pnpmfile:
|
|
|
865
865
|
|
|
866
866
|
DevKit 当前管理以下补丁:
|
|
867
867
|
|
|
868
|
-
- `@
|
|
869
|
-
- `expo@55.0.6
|
|
870
|
-
- `
|
|
871
|
-
- `
|
|
872
|
-
- `react-native
|
|
868
|
+
- `@expo/cli@55.0.16-55.0.22`(Metro HMR 批量编辑控制的服务端,含 AI 错误采集;另按 55.0.16–55.0.22 每个小版本提供独立补丁文件)
|
|
869
|
+
- `expo@55.0.6`(HMR 批量编辑控制的客户端;并固定 `expo-audio@55.0.6` 和 `expo-video@55.0.5` 的 Expo 兼容版本表)
|
|
870
|
+
- `metro-runtime@0.83.3`(HMRClient 透传 `miaoda-hmr-control` 控制消息)
|
|
871
|
+
- `metro-file-map@0.83.3`(`miaodaFlushChanges` 文件监听栅栏,保证恢复时 watcher 已看到全部写入)
|
|
872
|
+
- `react-native@0.83.2`(LogBox 错误页增强:错误标题映射与一键复制)
|
|
873
|
+
- `expo-modules-core@55.0.15`(Android CMake 打包 worklets 原生库)
|
|
874
|
+
- `react-native-reanimated@4.2.1`(Android 打包 `libworklets.so` 并关联 worklets 构建任务)
|
|
875
|
+
- `react-native-worklets@0.7.4`(Babel 插件规范化 TypeScript 函数类型)
|
|
876
|
+
|
|
877
|
+
前四个补丁共同实现 [Metro HMR 批量编辑控制](../../docs/metro-hmr-control.md),须整体安装。
|
|
878
|
+
|
|
879
|
+
`react-native-css-interop@0.2.7` 的 box-shadow fallthrough 已由上游修复,新模板不再需要 DevKit patch。
|
|
880
|
+
为兼容升级 DevKit 的历史模板,DevKit 会在项目实际声明或 lockfile 中包含历史版本时注册已验证的 CSS interop(`0.2.6`)、Worklets(`0.7.2`)和 Skia(`@shopify/react-native-skia@2.4.18`,支持 `SKIA_DOWNLOAD_BASE_URL` 自定义二进制下载源)patch,由 pnpm 按实际安装版本匹配;新模板不会注册未使用的旧 patch。注册 patch 不会安装额外版本。历史项目升级后应重新生成并提交 lockfile。
|
|
873
881
|
|
|
874
882
|
迁移历史项目时,hook 会计算同名本地 patch 的 SHA-256:已知模板 patch 会自动切换到 DevKit 内置文件;内容不同的项目定制 patch 会继续优先,并输出警告。项目中其他 `patchedDependencies` 不受影响。迁移后必须重新生成并提交 `pnpm-lock.yaml`。
|
|
875
883
|
|
|
@@ -879,7 +887,7 @@ DevKit 当前管理以下补丁:
|
|
|
879
887
|
pnpm --dir packages/devkit test:patches
|
|
880
888
|
```
|
|
881
889
|
|
|
882
|
-
该命令会自动构建并打包 DevKit,在临时项目中模拟 pnpm 安装 Config Dependency 后的目录布局,安装受支持版本范围内的目标依赖,检查
|
|
890
|
+
该命令会自动构建并打包 DevKit,在临时项目中模拟 pnpm 安装 Config Dependency 后的目录布局,安装受支持版本范围内的目标依赖,检查 11 个托管补丁(18 个补丁文件,覆盖 18 个包版本,含 CSS interop、Worklets、Skia 的历史 legacy 版本)及 lockfile,然后执行 frozen-lockfile 复装,并运行真实 Metro 文件监听、HTTP 控制接口和 `/hot` WebSocket 回归,最后清理临时目录。它不会运行 Expo Prebuild、Gradle 或 Android 编译。
|
|
883
891
|
|
|
884
892
|
### 本地编辑 pnpm 补丁
|
|
885
893
|
|
|
@@ -14623,7 +14623,7 @@ function buildAliasMap(moduleDir, pkgRoot) {
|
|
|
14623
14623
|
if (import_node_fs.default.existsSync(cjsBundle)) {
|
|
14624
14624
|
const content = import_node_fs.default.readFileSync(cjsBundle, "utf-8");
|
|
14625
14625
|
const varToExportNames = /* @__PURE__ */ new Map();
|
|
14626
|
-
const re = /^exports\.(\w+)\s*=\s*(\w+)
|
|
14626
|
+
const re = /^exports\.(\w+)\s*=\s*(\w+)(?:\.\w+)?;/gm;
|
|
14627
14627
|
let m;
|
|
14628
14628
|
while ((m = re.exec(content)) !== null) {
|
|
14629
14629
|
const [, exportName, varName] = m;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/rules/no-bare-web-crypto.ts
|
|
21
|
+
var no_bare_web_crypto_exports = {};
|
|
22
|
+
__export(no_bare_web_crypto_exports, {
|
|
23
|
+
default: () => no_bare_web_crypto_default
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(no_bare_web_crypto_exports);
|
|
26
|
+
var HOST_GLOBALS = /* @__PURE__ */ new Set(["globalThis", "global", "window"]);
|
|
27
|
+
var TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
|
|
28
|
+
"TSAsExpression",
|
|
29
|
+
"TSSatisfiesExpression",
|
|
30
|
+
"TSTypeAssertion",
|
|
31
|
+
"TSNonNullExpression",
|
|
32
|
+
"TSInstantiationExpression",
|
|
33
|
+
"ChainExpression",
|
|
34
|
+
"ParenthesizedExpression"
|
|
35
|
+
]);
|
|
36
|
+
var WEB_ONLY_FILE = /\.web\.[jt]sx?$/;
|
|
37
|
+
var NODE_CONFIG_FILE = /(?:^|[/\\])[^/\\]*\.config\.[cm]?[jt]s$/;
|
|
38
|
+
var CRYPTO = "crypto";
|
|
39
|
+
function unwrap(node) {
|
|
40
|
+
let cur = node ?? null;
|
|
41
|
+
while (cur && TRANSPARENT_WRAPPERS.has(cur.type)) {
|
|
42
|
+
cur = cur.expression ?? null;
|
|
43
|
+
}
|
|
44
|
+
return cur;
|
|
45
|
+
}
|
|
46
|
+
function cryptoRefName(node) {
|
|
47
|
+
const n = unwrap(node);
|
|
48
|
+
if (!n) return null;
|
|
49
|
+
if (n.type === "Identifier" && n.name === CRYPTO) {
|
|
50
|
+
return CRYPTO;
|
|
51
|
+
}
|
|
52
|
+
if (n.type === "MemberExpression" && !n.computed) {
|
|
53
|
+
const obj = unwrap(n.object);
|
|
54
|
+
const prop = n.property;
|
|
55
|
+
if (obj?.type === "Identifier" && obj.name !== void 0 && HOST_GLOBALS.has(obj.name) && prop?.type === "Identifier" && prop.name === CRYPTO) {
|
|
56
|
+
return `${obj.name}.${CRYPTO}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function patternBindsCrypto(pattern) {
|
|
62
|
+
const p = unwrap(pattern);
|
|
63
|
+
if (!p) return false;
|
|
64
|
+
switch (p.type) {
|
|
65
|
+
case "Identifier":
|
|
66
|
+
return p.name === CRYPTO;
|
|
67
|
+
case "ObjectPattern":
|
|
68
|
+
return (p.properties ?? []).some((prop) => {
|
|
69
|
+
const q = unwrap(prop);
|
|
70
|
+
if (!q) return false;
|
|
71
|
+
if (q.type === "Property") return patternBindsCrypto(q.value);
|
|
72
|
+
if (q.type === "RestElement") return patternBindsCrypto(q.argument);
|
|
73
|
+
return patternBindsCrypto(q);
|
|
74
|
+
});
|
|
75
|
+
case "ArrayPattern":
|
|
76
|
+
return (p.elements ?? []).some(patternBindsCrypto);
|
|
77
|
+
case "AssignmentPattern":
|
|
78
|
+
return patternBindsCrypto(p.left);
|
|
79
|
+
case "RestElement":
|
|
80
|
+
return patternBindsCrypto(p.argument);
|
|
81
|
+
case "TSParameterProperty":
|
|
82
|
+
return patternBindsCrypto(p.parameter);
|
|
83
|
+
default:
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function eachNode(root, visit) {
|
|
88
|
+
const seen = /* @__PURE__ */ new Set();
|
|
89
|
+
const stack = [root];
|
|
90
|
+
while (stack.length > 0) {
|
|
91
|
+
const node = stack.pop();
|
|
92
|
+
if (seen.has(node)) continue;
|
|
93
|
+
seen.add(node);
|
|
94
|
+
visit(node);
|
|
95
|
+
for (const key of Object.keys(node)) {
|
|
96
|
+
if (key === "parent") continue;
|
|
97
|
+
const child = node[key];
|
|
98
|
+
if (Array.isArray(child)) {
|
|
99
|
+
for (const item of child) {
|
|
100
|
+
if (item && typeof item === "object") stack.push(item);
|
|
101
|
+
}
|
|
102
|
+
} else if (child && typeof child === "object") {
|
|
103
|
+
stack.push(child);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function fileDeclaresCrypto(program) {
|
|
109
|
+
let found = false;
|
|
110
|
+
eachNode(program, (node) => {
|
|
111
|
+
if (found) return;
|
|
112
|
+
switch (node.type) {
|
|
113
|
+
case "ImportDeclaration":
|
|
114
|
+
found = (node.specifiers ?? []).some((s) => s?.local?.name === CRYPTO);
|
|
115
|
+
break;
|
|
116
|
+
case "TSImportEqualsDeclaration":
|
|
117
|
+
found = node.id?.name === CRYPTO;
|
|
118
|
+
break;
|
|
119
|
+
case "VariableDeclarator":
|
|
120
|
+
found = patternBindsCrypto(node.id);
|
|
121
|
+
break;
|
|
122
|
+
case "FunctionDeclaration":
|
|
123
|
+
case "FunctionExpression":
|
|
124
|
+
found = node.id?.name === CRYPTO || (node.params ?? []).some(patternBindsCrypto);
|
|
125
|
+
break;
|
|
126
|
+
case "ArrowFunctionExpression":
|
|
127
|
+
found = (node.params ?? []).some(patternBindsCrypto);
|
|
128
|
+
break;
|
|
129
|
+
case "ClassDeclaration":
|
|
130
|
+
case "ClassExpression":
|
|
131
|
+
found = node.id?.name === CRYPTO;
|
|
132
|
+
break;
|
|
133
|
+
case "CatchClause":
|
|
134
|
+
found = patternBindsCrypto(node.param);
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return found;
|
|
141
|
+
}
|
|
142
|
+
function renderExpr(member, refName) {
|
|
143
|
+
if (member.computed) {
|
|
144
|
+
const prop2 = unwrap(member.property);
|
|
145
|
+
if (prop2?.type === "Literal" && typeof prop2.value === "string") {
|
|
146
|
+
return `${refName}['${prop2.value}']`;
|
|
147
|
+
}
|
|
148
|
+
return `${refName}[\u2026]`;
|
|
149
|
+
}
|
|
150
|
+
const prop = member.property;
|
|
151
|
+
if (prop?.type === "Identifier" && prop.name) {
|
|
152
|
+
return `${refName}.${prop.name}`;
|
|
153
|
+
}
|
|
154
|
+
return refName;
|
|
155
|
+
}
|
|
156
|
+
var noBareWebCryptoRule = {
|
|
157
|
+
meta: {
|
|
158
|
+
type: "problem",
|
|
159
|
+
docs: {
|
|
160
|
+
description: "Disallow the browser global `crypto` (crypto.randomUUID / getRandomValues / subtle); it does not exist in the React Native Hermes runtime and crashes on Android/iOS while web preview looks fine. Use expo-crypto instead."
|
|
161
|
+
},
|
|
162
|
+
schema: [],
|
|
163
|
+
messages: {
|
|
164
|
+
noGlobalCrypto: "'{{expr}}' uses the browser global `crypto`, which does NOT exist in the React Native (Hermes) runtime \u2014 this crashes on Android/iOS at runtime, while TypeScript and web preview both look fine. Do NOT work around it with `typeof crypto` guards or Math.random fallbacks \u2014 on native those silently take a degraded branch. Use Expo's official module instead: import * as Crypto from 'expo-crypto'; Crypto.randomUUID(), Crypto.getRandomValues(...), Crypto.digestAsync(...)."
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
create(context) {
|
|
168
|
+
const filename = typeof context.filename === "string" ? context.filename : "";
|
|
169
|
+
const skipFile = WEB_ONLY_FILE.test(filename) || NODE_CONFIG_FILE.test(filename);
|
|
170
|
+
let exempt = skipFile;
|
|
171
|
+
let program = null;
|
|
172
|
+
let bindingChecked = false;
|
|
173
|
+
let bindingExempt = false;
|
|
174
|
+
function suppressed() {
|
|
175
|
+
if (exempt) return true;
|
|
176
|
+
if (!bindingChecked) {
|
|
177
|
+
bindingChecked = true;
|
|
178
|
+
bindingExempt = program !== null && fileDeclaresCrypto(program);
|
|
179
|
+
}
|
|
180
|
+
return bindingExempt;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
Program(node) {
|
|
184
|
+
if (skipFile) return;
|
|
185
|
+
program = node;
|
|
186
|
+
for (const stmt of program.body ?? []) {
|
|
187
|
+
if (stmt?.type === "ExpressionStatement" && stmt.directive === "use dom") {
|
|
188
|
+
exempt = true;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
// crypto.randomUUID() / crypto.subtle.digest() / crypto?.x / crypto['x'] /
|
|
194
|
+
// globalThis.crypto.randomUUID() / window.crypto.getRandomValues() 等成员访问
|
|
195
|
+
MemberExpression(node) {
|
|
196
|
+
const member = node;
|
|
197
|
+
const ref = cryptoRefName(member.object);
|
|
198
|
+
if (!ref || suppressed()) return;
|
|
199
|
+
context.report({
|
|
200
|
+
node,
|
|
201
|
+
messageId: "noGlobalCrypto",
|
|
202
|
+
data: { expr: renderExpr(member, ref) }
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
// const c = crypto / const { randomUUID } = crypto / const c = window.crypto 等读取
|
|
206
|
+
VariableDeclarator(node) {
|
|
207
|
+
const decl = node;
|
|
208
|
+
const ref = cryptoRefName(decl.init);
|
|
209
|
+
if (!ref || suppressed()) return;
|
|
210
|
+
context.report({
|
|
211
|
+
node: decl.init,
|
|
212
|
+
messageId: "noGlobalCrypto",
|
|
213
|
+
data: { expr: ref }
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
// { ...crypto } / [ ...crypto ] / f(...crypto)
|
|
217
|
+
SpreadElement(node) {
|
|
218
|
+
const spread = node;
|
|
219
|
+
const ref = cryptoRefName(spread.argument);
|
|
220
|
+
if (!ref || suppressed()) return;
|
|
221
|
+
context.report({
|
|
222
|
+
node: spread.argument,
|
|
223
|
+
messageId: "noGlobalCrypto",
|
|
224
|
+
data: { expr: ref }
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var plugin = {
|
|
231
|
+
meta: { name: "no-bare-web-crypto" },
|
|
232
|
+
rules: { "no-bare-web-crypto": noBareWebCryptoRule }
|
|
233
|
+
};
|
|
234
|
+
var no_bare_web_crypto_default = plugin;
|
|
235
|
+
module.exports = module.exports.default;
|