miaoda-expo-devkit 0.1.1-beta.1 → 0.1.1-beta.11

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 (36) hide show
  1. package/README.md +169 -0
  2. package/biome-config.json +14 -0
  3. package/dist/babel/plugin-jsx-source.d.ts +48 -0
  4. package/dist/babel/plugin-jsx-source.js +108 -18
  5. package/dist/babel/preset.d.ts +24 -0
  6. package/dist/babel/preset.js +50 -0
  7. package/dist/cli/lint.js +21 -0
  8. package/dist/metro.d.mts +198 -55
  9. package/dist/metro.d.ts +198 -55
  10. package/dist/metro.js +200 -66
  11. package/dist/metro.mjs +200 -66
  12. package/dist/rules/no-duplicate-expo-router-url.js +134 -0
  13. package/dist/rules/no-missing-css-import.js +98 -0
  14. package/dist/rules/no-rn-alert.js +57 -0
  15. package/dist/rules/no-undeclared-expo-plugin.js +159 -0
  16. package/dist/rules/no-unstable-expo-router.js +63 -0
  17. package/dist/stubs/css-control.js +8 -6
  18. package/dist/stubs/expo-camera-stub.js +28 -0
  19. package/dist/stubs/expo-image-stub.js +28 -0
  20. package/dist/stubs/lgui-control.js +163 -60
  21. package/oxlint-config.json +49 -0
  22. package/package.json +36 -6
  23. package/pnpm-config.json +8 -0
  24. package/dist/babel/plugin-jsx-source.js.map +0 -1
  25. package/dist/index.js.map +0 -1
  26. package/dist/index.mjs.map +0 -1
  27. package/dist/metro.js.map +0 -1
  28. package/dist/metro.mjs.map +0 -1
  29. package/dist/stubs/css-control.js.map +0 -1
  30. package/dist/stubs/entry-inject.js.map +0 -1
  31. package/dist/stubs/expo-router-entry-stub.js.map +0 -1
  32. package/dist/stubs/hmr-control.js.map +0 -1
  33. package/dist/stubs/lgui-control.js.map +0 -1
  34. package/dist/stubs/no-op-logbox.js.map +0 -1
  35. package/dist/stubs/router-control.js.map +0 -1
  36. package/dist/stubs/sentry-react-native-stub.js.map +0 -1
package/README.md CHANGED
@@ -263,5 +263,174 @@ sentry-react-native-stub.js
263
263
  |---|---|---|
264
264
  | `.` | `dist/index.js` | `SentryCapture`、`MetroSymbolicator`、全部类型 |
265
265
  | `./metro` | `dist/metro.js` | `withDevStubs`、`withEntryInjection` |
266
+ | `./babel-plugin-jsx-source` | `dist/babel/plugin-jsx-source.js` | Babel 插件:为 JSX 注入 source 信息 |
266
267
  | `./sentry-react-native-stub` | `dist/stubs/sentry-react-native-stub.js` | `@sentry/react-native` 模块替换 stub |
267
268
  | `./no-op-logbox` | `dist/stubs/no-op-logbox.js` | LogBox no-op stub |
269
+
270
+ ---
271
+
272
+ ## Stubs 模块说明
273
+
274
+ ### entry-inject.js
275
+
276
+ 由 `expo-router-entry-stub.js` 在 `expo-router/entry-classic` 执行前 require,因此代码运行时机早于 expo-router 初始化和任何路由渲染。
277
+
278
+ **功能:**
279
+ - 设置全局标记 `globalThis.__DEVKIT_INJECTED__ = true`(供测试验证)
280
+ - 安装 HMR postMessage 控制器
281
+ - 安装 LGUI 可视化编辑器控制器
282
+ - 安装 CSS 注入控制器
283
+ - 安装路由消息控制器
284
+
285
+ ---
286
+
287
+ ### expo-router-entry-stub.js
288
+
289
+ Metro 的 `resolveRequest` 将 `expo-router/entry-classic` 重定向到此文件。
290
+
291
+ **执行顺序:**
292
+ 1. entry-inject(注入脚本)
293
+ 2. expo-router/entry-classic(原 expo-router 入口,负责注册 App)
294
+
295
+ ---
296
+
297
+ ### hmr-control.js
298
+
299
+ HMR postMessage 控制器,监听 window 上的 postMessage 消息来控制 HMR。
300
+
301
+ **消息格式:**
302
+ ```ts
303
+ window.postMessage({ type: 'devkit:hmr', action: 'enable' | 'disable' }, '*')
304
+ ```
305
+
306
+ **副作用:**
307
+ - 设置 `globalThis.__DEVKIT_HMR_ENABLED__`(初始值 true),随每条消息更新
308
+ - 在 `__DEV__` 模式下调用 `expo/src/async-require/hmr` 的 `enable()` / `disable()`,实际暂停或恢复 Fast Refresh
309
+
310
+ ---
311
+
312
+ ### css-control.js
313
+
314
+ CSS 注入控制器,为 LGUI 编辑器提供可视化高亮样式。
315
+
316
+ **高亮属性:**
317
+ - `data-editor-active`:选中元素(2px 实线边框)
318
+ - `data-editor-hover`:悬停元素(1px 实线边框)
319
+ - `data-editor-each`:同源兄弟元素(1px 虚线边框)
320
+ - `data-editor-full-width`:全宽元素(使用内缩 offset)
321
+
322
+ **导出函数:**
323
+ - `injectSelectorModeStyle()`:注入选择模式样式,返回移除函数
324
+ - `setupCSSInjectionControl()`:注入编辑器高亮 CSS(页面加载时注入,常驻)
325
+
326
+ ---
327
+
328
+ ### lgui-control.js
329
+
330
+ LGUI 可视化编辑器 postMessage 控制器,实现可视化编辑器与 iframe 内页面的双向通信。
331
+
332
+ **消息格式(父窗口 → iframe):**
333
+ ```ts
334
+ window.postMessage({ type: 'editor-inject' }, '*') // 初始化编辑器
335
+ window.postMessage({ type: 'editor-destroy' }, '*') // 销毁编辑器
336
+ ```
337
+
338
+ **消息格式(iframe → 父窗口):**
339
+ ```ts
340
+ parent.postMessage({ type: 'iframe-target-change', target: ElementInfo }, '*')
341
+ parent.postMessage({ type: 'iframe-scroll', target: Rect }, '*')
342
+ ```
343
+
344
+ **功能:**
345
+ - 监听鼠标事件(mouseover、mouseleave、click),处理 hover/active 状态
346
+ - 与父窗口通信,发送选中元素信息(位置、组件名、源码位置等)
347
+ - 观察选中节点属性变化,实时同步信息
348
+ - 高亮同源兄弟节点
349
+
350
+ ---
351
+
352
+ ### router-control.js
353
+
354
+ LGUI 路由控制器,监听父窗口 postMessage 处理路由导航和页面刷新。
355
+
356
+ **消息格式(父窗口 → iframe):**
357
+ ```ts
358
+ window.postMessage({ type: 'editor-location-update', pageName: string }, '*') // 更新路由
359
+ window.postMessage({ type: 'editor-refresh' }, '*') // 刷新页面
360
+ ```
361
+
362
+ **功能:**
363
+ - `editor-location-update`:更新路由(使用 `history.pushState`)
364
+ - `editor-refresh`:刷新页面(使用 `location.reload`)
365
+
366
+ ---
367
+
368
+ ### sentry-react-native-stub.js
369
+
370
+ `@sentry/react-native` 模块替换 stub,由 `withDevStubs()` 在 Metro 层自动注入。
371
+
372
+ **功能:**
373
+ - 将用户传入的 DSN 替换为无害的覆盖值(默认 `https://stubPublicKey@o0.ingest.sentry.io/0`)
374
+ - 自动注入内置 SentryCapture,监听并符号化 Sentry 错误事件
375
+ - 串联调用方传入的 `beforeSend` / `beforeBreadcrumb`(内置捕获器先执行)
376
+ - 向父窗口发送 `GLOBAL_ERROR` 事件,携带错误信息
377
+
378
+ **环境变量:**
379
+ - `SENTRY_OVERRIDE_DSN`:自定义覆盖 DSN(如指向本地 relay)
380
+
381
+ ---
382
+
383
+ ### no-op-logbox.js
384
+
385
+ LogBox no-op stub,用于 web 平台禁用 Expo 全屏错误遮罩。
386
+
387
+ 由 `withDevStubs()` 在 Metro 层将 `@expo/log-box` 和 `ErrorOverlayWebControls` 重定向到此文件。
388
+
389
+ ---
390
+
391
+ ## Babel 插件:jsx-source
392
+
393
+ `babel-plugin-jsx-source` 为 JSX 元素注入 source 属性(文件路径、行列号),用于开发调试。通过 `dataSet` 对象注入,这是 React Web 可识别的数据通道。
394
+
395
+ ### 配置
396
+
397
+ ```js
398
+ // babel.config.js
399
+ module.exports = {
400
+ plugins: [
401
+ ['miaoda-expo-devkit/babel-plugin-jsx-source', { rootDir: __dirname }]
402
+ ]
403
+ };
404
+ ```
405
+
406
+ ### 转换效果
407
+
408
+ 普通 JSX 元素:
409
+ ```tsx
410
+ // 转换前
411
+ <View style={styles.container} />
412
+
413
+ // 转换后
414
+ <View style={styles.container} dataSet={{"mdId": "path/to/file.tsx:10:4"}} />
415
+ ```
416
+
417
+ 纯文本节点(会添加 `componentContent`,使用 URL 编码格式):
418
+ ```tsx
419
+ // 转换前
420
+ <div>hello world</div>
421
+
422
+ // 转换后
423
+ <div dataSet={{"mdId": "path/to/file.tsx:10:4", "componentContent": "%7B%22text%22%3A%22hello%20world%22%7D"}}>hello world</div>
424
+ ```
425
+
426
+ `componentContent` 解码后为:
427
+ ```json
428
+ {"text":"hello world"}
429
+ ```
430
+
431
+ ### 选项
432
+
433
+ | 选项 | 类型 | 默认值 | 说明 |
434
+ |---|---|---|---|
435
+ | `rootDir` | `string` | - | 项目根目录,用于计算相对路径。不提供则使用绝对路径 |
436
+ | `excludePaths` | `string[]` | `[]` | 跳过注入的路径模式列表(相对于 rootDir 的路径片段) |
@@ -0,0 +1,14 @@
1
+ {
2
+ "linter": {
3
+ "enabled": true,
4
+ "rules": {
5
+ "recommended": false,
6
+ "correctness": {
7
+ "noUndeclaredDependencies": "error"
8
+ }
9
+ }
10
+ },
11
+ "formatter": {
12
+ "enabled": false
13
+ }
14
+ }
@@ -0,0 +1,48 @@
1
+ import { types, PluginObj } from '@babel/core';
2
+
3
+ /**
4
+ * babel-plugin-jsx-source
5
+ *
6
+ * 为 JSX 元素注入 source 属性,包含文件路径、行列号信息,用于开发调试。
7
+ * 通过 dataSet 对象注入,这是 React Web 可识别的数据通道。
8
+ *
9
+ * 转换前:
10
+ * <View style={styles.container} />
11
+ *
12
+ * 转换后:
13
+ * <View style={styles.container} dataSet={{"mdId": "path/to/file.tsx:10:4"}} />
14
+ *
15
+ * 对于纯文本节点:
16
+ * <div>hello world</div>
17
+ * <div>{"hello world"}</div>
18
+ * 转换后:
19
+ * <div dataSet={{"mdId": "path/to/file.tsx:10:4", "componentContent": "{\"text\":\"hello world\"}"}}>hello world</div>
20
+ * <div dataSet={{"mdId": "path/to/file.tsx:11:4", "componentContent": "{\"text\":\"hello world\"}"}}>{"hello world"}</div>
21
+ *
22
+ * 用法(babel.config.js):
23
+ *
24
+ * module.exports = {
25
+ * plugins: [
26
+ * ['miaoda-expo-devkit/babel-plugin-jsx-source', { rootDir: __dirname }]
27
+ * ]
28
+ * };
29
+ *
30
+ * 选项:
31
+ * - rootDir: 项目根目录,用于计算相对路径(可选,默认使用绝对路径)
32
+ */
33
+
34
+ /** 插件选项 */
35
+ interface JsxSourcePluginOptions {
36
+ /** 项目根目录,用于计算相对路径。若不提供则使用绝对路径 */
37
+ rootDir?: string;
38
+ /** 需要跳过注入的路径模式列表(相对于 rootDir 的路径片段) */
39
+ excludePaths?: string[];
40
+ }
41
+ /**
42
+ * Babel 插件:为 JSX 元素注入 dataSet
43
+ */
44
+ declare function babelPluginJsxSource({ types: t, }: {
45
+ types: typeof types;
46
+ }): PluginObj;
47
+
48
+ export { type JsxSourcePluginOptions, babelPluginJsxSource as default };
@@ -23,20 +23,65 @@ __export(plugin_jsx_source_exports, {
23
23
  default: () => babelPluginJsxSource
24
24
  });
25
25
  module.exports = __toCommonJS(plugin_jsx_source_exports);
26
- function createSourceObject(t, fileName, lineNumber, columnNumber) {
27
- return t.objectExpression([
28
- t.objectProperty(t.identifier("fileName"), t.stringLiteral(fileName)),
29
- t.objectProperty(t.identifier("lineNumber"), t.numericLiteral(lineNumber)),
30
- t.objectProperty(t.identifier("columnNumber"), t.numericLiteral(columnNumber))
31
- ]);
26
+ var DATASET_PROP_NAME = "dataSet";
27
+ var MD_ID_KEY = "mdId";
28
+ var MD_CONTENT_KEY = "componentContent";
29
+ var REACT_SPECIAL_COMPONENTS = /* @__PURE__ */ new Set([
30
+ "Fragment",
31
+ "Suspense",
32
+ "StrictMode",
33
+ "Profiler"
34
+ ]);
35
+ function findDataSetAttr(t, attributes) {
36
+ return attributes.find(
37
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: DATASET_PROP_NAME })
38
+ );
39
+ }
40
+ function getMdIdFromDataSet(t, obj) {
41
+ for (const p of obj.properties) {
42
+ if (t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === MD_ID_KEY && t.isStringLiteral(p.value)) {
43
+ return p.value.value;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ function getComponentContentFromDataSet(t, obj) {
49
+ for (const p of obj.properties) {
50
+ if (t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === MD_CONTENT_KEY && t.isStringLiteral(p.value)) {
51
+ return p.value.value;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ function removeMdIdFromDataSet(t, obj) {
57
+ obj.properties = obj.properties.filter(
58
+ (p) => !(t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === MD_ID_KEY)
59
+ );
32
60
  }
33
- function hasSourceAttribute(t, attributes) {
34
- return attributes.some(
35
- (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: SOURCE_PROP_NAME })
61
+ function removeComponentContentFromDataSet(t, obj) {
62
+ obj.properties = obj.properties.filter(
63
+ (p) => !(t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === MD_CONTENT_KEY)
36
64
  );
37
65
  }
38
- var SOURCE_PROP_NAME = "__jsxsource";
39
- var REACT_SPECIAL_COMPONENTS = /* @__PURE__ */ new Set(["Fragment", "Suspense", "StrictMode", "Profiler"]);
66
+ function getTextNodeContent(t, path) {
67
+ const parent = path.parent;
68
+ if (!t.isJSXElement(parent)) return null;
69
+ const children = parent.children;
70
+ if (children.length !== 1) return null;
71
+ const child = children[0];
72
+ if (t.isJSXText(child)) {
73
+ const text = child.value.trim();
74
+ return text || null;
75
+ }
76
+ if (t.isJSXExpressionContainer(child) && t.isStringLiteral(child.expression)) {
77
+ const text = child.expression.value.trim();
78
+ return text || null;
79
+ }
80
+ if (t.isJSXExpressionContainer(child) && t.isNumericLiteral(child.expression)) {
81
+ return String(child.expression.value);
82
+ }
83
+ return null;
84
+ }
40
85
  function shouldSkipElement(t, name) {
41
86
  if (t.isJSXIdentifier(name)) {
42
87
  return REACT_SPECIAL_COMPONENTS.has(name.name);
@@ -67,23 +112,68 @@ function babelPluginJsxSource({
67
112
  if (filename.includes("node_modules")) return;
68
113
  if (opts.excludePaths?.some((p) => filename.includes(p))) return;
69
114
  if (shouldSkipElement(t, path.node.name)) return;
70
- if (hasSourceAttribute(t, path.node.attributes)) return;
71
115
  const loc = path.node.loc;
72
116
  if (!loc) return;
73
117
  let filePath = filename;
74
118
  if (opts.rootDir && filename.startsWith(opts.rootDir)) {
75
119
  filePath = filename.slice(opts.rootDir.length);
76
- if (filePath.startsWith("/")) {
77
- filePath = filePath.slice(1);
120
+ if (filePath.startsWith("/")) filePath = filePath.slice(1);
121
+ }
122
+ const value = `${filePath}:${loc.start.line}:${loc.start.column}`;
123
+ const attrs = path.node.attributes;
124
+ const existing = findDataSetAttr(t, attrs);
125
+ if (existing) {
126
+ if (t.isJSXExpressionContainer(existing.value) && t.isObjectExpression(existing.value.expression)) {
127
+ const obj = existing.value.expression;
128
+ const oldMdId = getMdIdFromDataSet(t, obj);
129
+ if (oldMdId !== value) {
130
+ removeMdIdFromDataSet(t, obj);
131
+ obj.properties.push(
132
+ t.objectProperty(
133
+ t.identifier(MD_ID_KEY),
134
+ t.stringLiteral(value)
135
+ )
136
+ );
137
+ }
138
+ const textContent2 = getTextNodeContent(t, path);
139
+ const newComponentContent = textContent2 ? encodeURIComponent(JSON.stringify({ text: textContent2 })) : null;
140
+ const oldComponentContent = getComponentContentFromDataSet(t, obj);
141
+ if (oldComponentContent !== newComponentContent) {
142
+ removeComponentContentFromDataSet(t, obj);
143
+ if (newComponentContent) {
144
+ obj.properties.push(
145
+ t.objectProperty(
146
+ t.identifier(MD_CONTENT_KEY),
147
+ t.stringLiteral(newComponentContent)
148
+ )
149
+ );
150
+ }
151
+ }
78
152
  }
153
+ return;
154
+ }
155
+ const properties = [
156
+ t.objectProperty(
157
+ t.identifier(MD_ID_KEY),
158
+ t.stringLiteral(value)
159
+ )
160
+ ];
161
+ const textContent = getTextNodeContent(t, path);
162
+ if (textContent) {
163
+ properties.push(
164
+ t.objectProperty(
165
+ t.identifier(MD_CONTENT_KEY),
166
+ t.stringLiteral(encodeURIComponent(JSON.stringify({ text: textContent })))
167
+ )
168
+ );
79
169
  }
80
- const sourceAttribute = t.jsxAttribute(
81
- t.jsxIdentifier(SOURCE_PROP_NAME),
170
+ const attr = t.jsxAttribute(
171
+ t.jsxIdentifier(DATASET_PROP_NAME),
82
172
  t.jsxExpressionContainer(
83
- createSourceObject(t, filePath, loc.start.line, loc.start.column)
173
+ t.objectExpression(properties)
84
174
  )
85
175
  );
86
- path.node.attributes.push(sourceAttribute);
176
+ attrs.push(attr);
87
177
  }
88
178
  }
89
179
  };
@@ -0,0 +1,24 @@
1
+ import { ConfigAPI, TransformOptions } from '@babel/core';
2
+
3
+ /**
4
+ * babel-preset-expo-devkit
5
+ *
6
+ * 封装 Expo + NativeWind 标准 babel 配置,供基于本模板的项目直接引用。
7
+ * 升级 devkit 版本即可获得最新配置,无需手动维护 babel.config.js。
8
+ */
9
+
10
+ interface PresetOptions {
11
+ /**
12
+ * 项目根目录,用于 babel-plugin-jsx-source 计算相对路径。
13
+ * 默认为 process.cwd()(Babel 始终从项目根运行)。
14
+ */
15
+ rootDir?: string;
16
+ /**
17
+ * 需要跳过 jsx-source 注入的路径前缀列表(相对于 rootDir)。
18
+ * 例如:['src/components/ui']
19
+ */
20
+ excludePaths?: string[];
21
+ }
22
+ declare function presetExpoDevkit(api: ConfigAPI, options?: PresetOptions): TransformOptions;
23
+
24
+ export { presetExpoDevkit as default };
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/babel/preset.ts
31
+ var preset_exports = {};
32
+ __export(preset_exports, {
33
+ default: () => presetExpoDevkit
34
+ });
35
+ module.exports = __toCommonJS(preset_exports);
36
+ function presetExpoDevkit(api, options = {}) {
37
+ const isDev = api.env("development");
38
+ const { rootDir = process.cwd(), excludePaths = [] } = options;
39
+ return {
40
+ presets: [
41
+ ["babel-preset-expo", { jsxImportSource: "nativewind" }],
42
+ "nativewind/babel"
43
+ ],
44
+ plugins: isDev ? [
45
+ // require.resolve 确保引用绝对路径,避免 Babel 解析歧义
46
+ [require.resolve("./plugin-jsx-source"), { rootDir, excludePaths }]
47
+ ] : []
48
+ };
49
+ }
50
+ //# sourceMappingURL=preset.js.map
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli/lint.ts
5
+ var import_node_child_process = require("child_process");
6
+ var import_node_path = require("path");
7
+ var devkitRoot = (0, import_node_path.resolve)(__dirname, "../..");
8
+ var oxlintConfig = (0, import_node_path.join)(devkitRoot, "oxlint-config.json");
9
+ var biomeConfig = (0, import_node_path.join)(devkitRoot, "biome-config.json");
10
+ var targets = process.argv.slice(2);
11
+ var lintTargets = targets.length > 0 ? targets : ["src/"];
12
+ var failed = false;
13
+ var oxlint = (0, import_node_child_process.spawnSync)("oxlint", ["--config", oxlintConfig, ...lintTargets], {
14
+ stdio: "inherit"
15
+ });
16
+ if ((oxlint.status ?? 1) !== 0) failed = true;
17
+ var biome = (0, import_node_child_process.spawnSync)("biome", ["lint", "--config-path", biomeConfig, ...lintTargets], {
18
+ stdio: "inherit"
19
+ });
20
+ if ((biome.status ?? 1) !== 0) failed = true;
21
+ process.exit(failed ? 1 : 0);