miaoda-expo-devkit 0.1.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.
package/dist/metro.js ADDED
@@ -0,0 +1,193 @@
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/metro.ts
31
+ var metro_exports = {};
32
+ __export(metro_exports, {
33
+ withDevStubs: () => withDevStubs,
34
+ withEntryInjection: () => withEntryInjection,
35
+ withRouteEndpoint: () => withRouteEndpoint,
36
+ withWorkspaceNodeModules: () => withWorkspaceNodeModules
37
+ });
38
+ module.exports = __toCommonJS(metro_exports);
39
+ var import_connect = __toESM(require("connect"));
40
+ var import_fs2 = __toESM(require("fs"));
41
+ var import_path2 = __toESM(require("path"));
42
+
43
+ // src/routes.ts
44
+ var import_fs = __toESM(require("fs"));
45
+ var import_path = __toESM(require("path"));
46
+ var ROUTE_EXT = /\.(tsx|ts|jsx|js)$/;
47
+ function getRoutes(appDir) {
48
+ const routes = [];
49
+ function scan(dir, prefix) {
50
+ let entries;
51
+ try {
52
+ entries = import_fs.default.readdirSync(dir, { withFileTypes: true });
53
+ } catch {
54
+ return;
55
+ }
56
+ for (const entry of entries) {
57
+ const fullPath = import_path.default.join(dir, entry.name);
58
+ if (entry.isDirectory()) {
59
+ const isGroup = /^\(.*\)$/.test(entry.name);
60
+ const nextPrefix = isGroup ? prefix : `${prefix}/${entry.name}`;
61
+ scan(fullPath, nextPrefix);
62
+ } else if (ROUTE_EXT.test(entry.name)) {
63
+ const baseName = entry.name.replace(ROUTE_EXT, "");
64
+ if (baseName === "_layout") continue;
65
+ const segment = baseName === "index" ? "" : `/${baseName}`;
66
+ const routePath = `${prefix}${segment}` || "/";
67
+ const isDynamic = /\[.*\]/.test(baseName);
68
+ routes.push({
69
+ title: routePath,
70
+ pageId: routePath,
71
+ pageName: routePath,
72
+ visible: !isDynamic,
73
+ path: import_path.default.relative(appDir, fullPath)
74
+ });
75
+ }
76
+ }
77
+ }
78
+ scan(appDir, "");
79
+ return { routes };
80
+ }
81
+ function createRouteHandler(appDir) {
82
+ return (_req, res) => {
83
+ const routeTree = getRoutes(appDir);
84
+ res.writeHead(200, { "Content-Type": "application/json" });
85
+ res.end(JSON.stringify(routeTree, null, 2));
86
+ };
87
+ }
88
+
89
+ // src/metro.ts
90
+ var SENTRY_STUB_FILENAME = "sentry-react-native-stub.js";
91
+ var SENTRY_STUB_PATH = import_path2.default.resolve(__dirname, "stubs", SENTRY_STUB_FILENAME);
92
+ var NO_OP_LOGBOX_STUB_PATH = import_path2.default.resolve(__dirname, "stubs", "no-op-logbox.js");
93
+ function withDevStubs(config) {
94
+ const upstream = config.resolver?.resolveRequest ?? null;
95
+ const resolveRequest = (context, moduleName, platform) => {
96
+ if (platform === "web" && __DEV__) {
97
+ if (moduleName === "@expo/log-box" || moduleName === "@expo/log-box/lib" || moduleName.endsWith("ErrorOverlayWebControls")) {
98
+ return { filePath: NO_OP_LOGBOX_STUB_PATH, type: "sourceFile" };
99
+ }
100
+ }
101
+ if (moduleName === "@sentry/react-native" && !context.originModulePath.includes(SENTRY_STUB_FILENAME)) {
102
+ return { filePath: SENTRY_STUB_PATH, type: "sourceFile" };
103
+ }
104
+ if (upstream) {
105
+ return upstream(context, moduleName, platform);
106
+ }
107
+ return context.resolveRequest(context, moduleName, platform);
108
+ };
109
+ return {
110
+ ...config,
111
+ resolver: {
112
+ ...config.resolver,
113
+ resolveRequest
114
+ }
115
+ };
116
+ }
117
+ var EXPO_ROUTER_ENTRY_STUB_PATH = import_path2.default.resolve(__dirname, "stubs", "expo-router-entry-stub.js");
118
+ var EXPO_ROUTER_ENTRY_STUB_FILENAME = "expo-router-entry-stub.js";
119
+ function withEntryInjection(config, options) {
120
+ void options;
121
+ const upstream = config.resolver?.resolveRequest ?? null;
122
+ const resolveRequest = (context, moduleName, platform) => {
123
+ if (__DEV__ && moduleName === "expo-router/entry-classic" && platform === "web" && !context.originModulePath.includes(EXPO_ROUTER_ENTRY_STUB_FILENAME)) {
124
+ return { filePath: EXPO_ROUTER_ENTRY_STUB_PATH, type: "sourceFile" };
125
+ }
126
+ if (upstream) {
127
+ return upstream(context, moduleName, platform);
128
+ }
129
+ return context.resolveRequest(context, moduleName, platform);
130
+ };
131
+ return {
132
+ ...config,
133
+ resolver: {
134
+ ...config.resolver,
135
+ resolveRequest
136
+ }
137
+ };
138
+ }
139
+ var DEFAULT_ROUTE_ENDPOINT = "/__routes";
140
+ function withRouteEndpoint(config, options) {
141
+ const { appDir, endpoint = DEFAULT_ROUTE_ENDPOINT } = options;
142
+ const upstream = config.server?.enhanceMiddleware ?? null;
143
+ const enhanceMiddleware = (middleware, metroServer) => {
144
+ const enhanced = upstream ? upstream(middleware, metroServer) : middleware;
145
+ return (0, import_connect.default)().use(enhanced).use(endpoint, createRouteHandler(appDir));
146
+ };
147
+ return {
148
+ ...config,
149
+ server: {
150
+ ...config.server,
151
+ enhanceMiddleware
152
+ }
153
+ };
154
+ }
155
+ function withWorkspaceNodeModules(config) {
156
+ const projectRoot = config.projectRoot ?? process.cwd();
157
+ if (import_fs2.default.existsSync(import_path2.default.join(projectRoot, "node_modules"))) {
158
+ return config;
159
+ }
160
+ let dir = import_path2.default.dirname(projectRoot);
161
+ let found = null;
162
+ while (true) {
163
+ const candidate = import_path2.default.join(dir, "node_modules");
164
+ if (import_fs2.default.existsSync(candidate)) {
165
+ found = candidate;
166
+ break;
167
+ }
168
+ const parent = import_path2.default.dirname(dir);
169
+ if (parent === dir) break;
170
+ dir = parent;
171
+ }
172
+ if (!found) {
173
+ return config;
174
+ }
175
+ const existingWatchFolders = config.watchFolders ?? [];
176
+ const existingNodeModulesPaths = config.resolver?.nodeModulesPaths ?? [];
177
+ return {
178
+ ...config,
179
+ watchFolders: [...existingWatchFolders, found],
180
+ resolver: {
181
+ ...config.resolver,
182
+ nodeModulesPaths: [...existingNodeModulesPaths, found]
183
+ }
184
+ };
185
+ }
186
+ // Annotate the CommonJS export names for ESM import in node:
187
+ 0 && (module.exports = {
188
+ withDevStubs,
189
+ withEntryInjection,
190
+ withRouteEndpoint,
191
+ withWorkspaceNodeModules
192
+ });
193
+ //# sourceMappingURL=metro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/metro.ts","../src/routes.ts"],"sourcesContent":["/**\n * miaoda-expo-devkit/metro\n *\n * 提供 Metro 配置增强函数:\n * - withDevStubs(config) 模块替换:Sentry DSN 覆盖、LogBox 屏蔽\n * - withEntryInjection(config) bundle 首部脚本注入\n * - withRouteEndpoint(config) 添加 /__routes 端点,返回路由树 JSON\n * - withWorkspaceNodeModules(config) 修复 node_modules 位于项目父级目录时的资源加载 500 问题\n *\n * 用法(metro.config.js):\n *\n * const { getDefaultConfig } = require('expo/metro-config');\n * const { withDevStubs, withEntryInjection, withRouteEndpoint } = require('miaoda-expo-devkit/metro');\n *\n * const config = getDefaultConfig(__dirname);\n * module.exports = withRouteEndpoint(\n * withEntryInjection(withDevStubs(config)),\n * { appDir: path.join(__dirname, 'src', 'app') }\n * );\n *\n * 链式用法(与其他 Metro wrapper 组合):\n *\n * module.exports = withNativeWind(withEntryInjection(withDevStubs(config)), { input: './global.css' });\n */\n\nimport connect, { type HandleFunction, type Server } from 'connect';\nimport fs from 'fs';\nimport path from 'path';\nimport type { MetroConfig } from 'metro-config';\nimport type { CustomResolver } from 'metro-resolver';\nimport { createRouteHandler } from './routes';\n\n/**\n * sentry-react-native-stub.js 的文件名,用于在 resolver 中检测自身 import 以规避循环依赖。\n * 必须与编译产物 dist/stubs/sentry-react-native-stub.js 的文件名保持同步。\n */\nconst SENTRY_STUB_FILENAME = 'sentry-react-native-stub.js';\n\n/** Stub 文件的绝对路径。__dirname 指向编译产物所在的 dist/ 目录。 */\nconst SENTRY_STUB_PATH = path.resolve(__dirname, 'stubs', SENTRY_STUB_FILENAME);\nconst NO_OP_LOGBOX_STUB_PATH = path.resolve(__dirname, 'stubs', 'no-op-logbox.js');\n\n/**\n * 将开发阶段 stub 注入到 Metro config 中。\n *\n * 注入行为:\n * 1. web 平台:@expo/log-box 及 ErrorOverlayWebControls → no-op-logbox stub(屏蔽全屏错误遮罩)\n * 2. 所有平台:@sentry/react-native → sentry-react-native-stub(替换 DSN,注入内置捕获器)\n *\n * 自动保留并调用已有的 resolveRequest,支持与其他 Metro 配置链式组合。\n *\n * @param config Metro config 对象(来自 getDefaultConfig 或 getSentryExpoConfig)\n * @returns 注入 resolveRequest 后的新 Metro config\n */\nfunction withDevStubs(config: MetroConfig): MetroConfig {\n const upstream = config.resolver?.resolveRequest ?? null;\n\n const resolveRequest: CustomResolver = (context, moduleName, platform) => {\n // 1. web 平台:替换 LogBox 为 no-op,屏蔽全屏错误遮罩\n if (platform === 'web' && __DEV__) {\n if (\n moduleName === '@expo/log-box' ||\n moduleName === '@expo/log-box/lib' ||\n moduleName.endsWith('ErrorOverlayWebControls')\n ) {\n return { filePath: NO_OP_LOGBOX_STUB_PATH, type: 'sourceFile' };\n }\n }\n\n // 2. 所有平台:将 @sentry/react-native 替换为 stub\n // 检查 originModulePath 确保 stub 内部的 import 不被再次拦截(规避循环依赖)\n if (\n moduleName === '@sentry/react-native' &&\n !context.originModulePath.includes(SENTRY_STUB_FILENAME)\n ) {\n return { filePath: SENTRY_STUB_PATH, type: 'sourceFile' };\n }\n\n // 3. 回退到上游 resolver\n if (upstream) {\n return upstream(context, moduleName, platform);\n }\n return context.resolveRequest(context, moduleName, platform);\n };\n\n return {\n ...config,\n resolver: {\n ...config.resolver,\n resolveRequest,\n },\n };\n}\n\n// ─── withEntryInjection ────────────────────────────────────────────────────────\n\n/**\n * expo-router-entry-stub.js 的绝对路径。__dirname 指向编译产物所在的 dist/ 目录。\n * 当 Metro 解析 expo-router/entry-classic 时,resolver 将其重定向到此文件。\n */\nconst EXPO_ROUTER_ENTRY_STUB_PATH = path.resolve(__dirname, 'stubs', 'expo-router-entry-stub.js');\n\n/**\n * expo-router-entry-stub.js 的文件名,用于在 resolver 中检测自身 import 以规避循环依赖。\n * 必须与编译产物 dist/stubs/expo-router-entry-stub.js 的文件名保持同步。\n */\nconst EXPO_ROUTER_ENTRY_STUB_FILENAME = 'expo-router-entry-stub.js';\n\n/** withEntryInjection 的配置选项(预留,供未来支持自定义注入脚本路径扩展) */\nexport interface InjectOptions {\n // 预留字段,未来版本将支持传入自定义注入模块路径\n}\n\n/**\n * 在 expo-router 启动前注入一段脚本。\n *\n * 注入原理:\n * 拦截 Metro 对 expo-router/entry-classic 的解析,将其重定向到\n * dist/stubs/expo-router-entry-stub.js。expo-router-entry-stub.js 先 require entry-inject.js\n * (注入脚本),再 require expo-router/entry-classic(原入口),\n * 从而在 expo-router 启动前执行注入逻辑。\n *\n * 拦截 entry-classic 而非 entry 本身,原因:\n * - expo-router/entry 以 <script src=\"...entry.bundle...\"> 方式直接加载,\n * 不经过 resolveRequest。\n * - expo-router/entry 内部第一行是 import 'expo-router/entry-classic',\n * 这条 import 语句经过 resolveRequest,可以被拦截。\n *\n * 执行顺序(在 bundle 中):\n * entry-inject.js(注入脚本:设置 __DEVKIT_INJECTED__ + 安装 HMR 控制器)\n * → expo-router/entry-classic(原 expo-router 入口)\n * → renderRootComponent(App)\n *\n * 注入的脚本经过完整 Babel 编译,支持现代语法,web + native 双平台均有效。\n *\n * 支持与 withDevStubs 及其他 Metro wrapper 链式组合:\n * module.exports = withEntryInjection(withDevStubs(config));\n *\n * @param config Metro config 对象\n * @param options 注入选项(当前预留,未来支持自定义注入脚本路径)\n * @returns 注入 resolveRequest 后的新 Metro config\n */\nfunction withEntryInjection(config: MetroConfig, options?: InjectOptions): MetroConfig {\n void options; // 当前未使用,预留供未来扩展\n\n const upstream = config.resolver?.resolveRequest ?? null;\n\n const resolveRequest: CustomResolver = (context, moduleName, platform) => {\n // 拦截 expo-router/entry-classic,重定向到 stub 入口(expo-router-entry-stub.js)\n // 用 originModulePath 检查来源,避免 expo-router-entry-stub.js 自身 import\n // expo-router/entry-classic 时再次触发拦截,导致循环依赖。\n if (\n __DEV__ &&\n moduleName === 'expo-router/entry-classic' &&\n platform === 'web' &&\n !context.originModulePath.includes(EXPO_ROUTER_ENTRY_STUB_FILENAME)\n ) {\n return { filePath: EXPO_ROUTER_ENTRY_STUB_PATH, type: 'sourceFile' };\n }\n\n // 回退到上游 resolver(withDevStubs 或默认 resolver)\n if (upstream) {\n return upstream(context, moduleName, platform);\n }\n return context.resolveRequest(context, moduleName, platform);\n };\n\n return {\n ...config,\n resolver: {\n ...config.resolver,\n resolveRequest,\n },\n };\n}\n\n// ─── withRouteEndpoint ────────────────────────────────────────────────────────\n\n/** withRouteEndpoint 的配置选项 */\nexport interface RouteEndpointOptions {\n /** app 目录的绝对路径(包含路由文件的目录) */\n appDir: string;\n /** 自定义端点路径,默认 /__routes */\n endpoint?: string;\n}\n\n/** Metro enhanceMiddleware 的函数签名 */\ntype EnhanceMiddleware = (\n middleware: HandleFunction,\n metroServer: unknown\n) => HandleFunction | Server;\n\nconst DEFAULT_ROUTE_ENDPOINT = '/__routes';\n\n/**\n * 为 Metro server 添加 /__routes 端点,返回路由树 JSON。\n *\n * 端点返回格式:\n * {\n * \"routes\": [\n * { \"title\": \"首页\", \"pageId\": \"/\", \"pageName\": \"index\", \"visible\": true, \"path\": \"index.tsx\" },\n * { \"title\": \"id\", \"pageId\": \"/user/[id]\", \"pageName\": \"user-[id]\", \"visible\": false, \"path\": \"user/[id].tsx\" }\n * ]\n * }\n *\n * @param config Metro config 对象\n * @param options 配置选项,必须提供 appDir\n * @returns 增强 server.enhanceMiddleware 后的新 Metro config\n */\nfunction withRouteEndpoint(config: MetroConfig, options: RouteEndpointOptions): MetroConfig {\n const { appDir, endpoint = DEFAULT_ROUTE_ENDPOINT } = options;\n const upstream = config.server?.enhanceMiddleware ?? null;\n\n const enhanceMiddleware: EnhanceMiddleware = (middleware, metroServer) => {\n // 先调用上游 enhanceMiddleware(如果存在)\n const enhanced = upstream ? upstream(middleware, metroServer) : middleware;\n return connect().use(enhanced).use(endpoint, createRouteHandler(appDir));\n };\n\n return {\n ...config,\n server: {\n ...config.server,\n enhanceMiddleware,\n },\n };\n}\n\n// ─── withWorkspaceNodeModules ──────────────────────────────────────────────────\n\n/**\n * 修复沙箱环境中 node_modules 位于项目父级目录时,资源文件(字体、图片等)加载返回 500 的问题。\n *\n * 问题根因:\n * pnpm 在沙箱中将 node_modules 安装到 projectRoot 的上层目录(如 /workspace/node_modules),\n * 而非 projectRoot 内(如 /workspace/app/node_modules)。\n * Metro 处理 /assets/?unstable_path=./node_modules/.pnpm/... 请求时,会将路径解析为\n * path.resolve(projectRoot, './node_modules/.pnpm/...'),得到一个不存在的路径,返回 500。\n * 实际文件在父级的 node_modules/.pnpm/ 下。\n *\n * 修复方式:\n * 向上查找实际的 node_modules 目录,将其加入 watchFolders(让 Metro 索引其中的文件)\n * 和 resolver.nodeModulesPaths(让模块解析也能找到包)。\n *\n * 如果 node_modules 就在 projectRoot 下,则不做任何修改,直接返回原始 config。\n *\n * @param config Metro config 对象(来自 getDefaultConfig)\n * @returns 修正 watchFolders 和 nodeModulesPaths 后的新 Metro config\n */\nfunction withWorkspaceNodeModules(config: MetroConfig): MetroConfig {\n const projectRoot = config.projectRoot ?? process.cwd();\n\n // node_modules 就在 projectRoot 下,无需处理\n if (fs.existsSync(path.join(projectRoot, 'node_modules'))) {\n return config;\n }\n\n // 向上逐级查找 node_modules\n let dir = path.dirname(projectRoot);\n let found: string | null = null;\n while (true) {\n const candidate = path.join(dir, 'node_modules');\n if (fs.existsSync(candidate)) {\n found = candidate;\n break;\n }\n const parent = path.dirname(dir);\n if (parent === dir) break; // 到达文件系统根目录\n dir = parent;\n }\n\n if (!found) {\n return config;\n }\n\n const existingWatchFolders = config.watchFolders ?? [];\n const existingNodeModulesPaths = config.resolver?.nodeModulesPaths ?? [];\n\n return {\n ...config,\n watchFolders: [...existingWatchFolders, found],\n resolver: {\n ...config.resolver,\n nodeModulesPaths: [...existingNodeModulesPaths, found],\n },\n };\n}\n\nexport { withDevStubs, withEntryInjection, withRouteEndpoint, withWorkspaceNodeModules };\n","/**\n * miaoda-expo-devkit/routes\n *\n * 提供 Expo Router 路由收集功能:\n * - getRoutes(appDir) 扫描目录生成路由树\n * - createRouteHandler(appDir) 创建路由端点的请求处理器\n */\n\nimport fs from 'fs';\nimport path from 'path';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/** 单条路由信息 */\nexport interface RouteInfo {\n /** 页面标题 */\n title: string;\n /** 路由标识,如 /、/settings */\n pageId: string;\n /** 路由名称 */\n pageName: string;\n /** 是否可见(动态路由默认不可见) */\n visible: boolean;\n /** 相对于 appDir 的文件路径 */\n path: string;\n}\n\n/** getRoutes 返回的路由树 */\nexport interface RouteTree {\n routes: RouteInfo[];\n}\n\n/** HTTP 响应对象的最小接口 */\ninterface RouteResponse {\n writeHead: (status: number, headers: Record<string, string>) => void;\n end: (body: string) => void;\n}\n\n/** 支持的路由文件扩展名 */\nconst ROUTE_EXT = /\\.(tsx|ts|jsx|js)$/;\n\n/**\n * 扫描 Expo Router 的 app 目录,生成路由树。\n *\n * 扫描规则:\n * - 目录:递归扫描,路由组 (groupName) 不生成路径片段\n * - 文件:.tsx/.ts/.jsx/.js 文件生成路由\n * - _layout 文件:布局文件,跳过\n * - index 文件:映射到父目录路径\n * - [param] 文件:标记为动态路由,默认不可见\n *\n * @param appDir app 目录的绝对路径\n * @returns 路由树对象\n */\nexport function getRoutes(appDir: string): RouteTree {\n const routes: RouteInfo[] = [];\n\n function scan(dir: string, prefix: string): void {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n // 目录不存在或无权限,返回空\n return;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n // 路由组 (groupName) 不生成路径片段,直接透传\n const isGroup = /^\\(.*\\)$/.test(entry.name);\n const nextPrefix = isGroup ? prefix : `${prefix}/${entry.name}`;\n scan(fullPath, nextPrefix);\n } else if (ROUTE_EXT.test(entry.name)) {\n const baseName = entry.name.replace(ROUTE_EXT, '');\n\n // _layout 是布局文件,不是路由页面\n if (baseName === '_layout') continue;\n\n // index 文件映射到父目录,其他文件生成对应路径段\n const segment = baseName === 'index' ? '' : `/${baseName}`;\n const routePath = `${prefix}${segment}` || '/';\n const isDynamic = /\\[.*\\]/.test(baseName);\n\n routes.push({\n title: routePath,\n pageId: routePath,\n pageName: routePath,\n visible: !isDynamic,\n path: path.relative(appDir, fullPath),\n });\n }\n }\n }\n\n scan(appDir, '');\n\n return { routes };\n}\n\n// ─── createRouteHandler ───────────────────────────────────────────────────────\n\n/**\n * 创建路由端点的请求处理器。\n *\n * @param appDir app 目录的绝对路径\n * @returns connect 中间件处理函数\n */\nexport function createRouteHandler(appDir: string) {\n return (_req: unknown, res: RouteResponse) => {\n const routeTree = getRoutes(appDir);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(routeTree, null, 2));\n };\n}\n"],"mappings":"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","names":["import_fs","import_path","fs","path","path","connect","fs"]}
package/dist/metro.mjs ADDED
@@ -0,0 +1,155 @@
1
+ // src/metro.ts
2
+ import connect from "connect";
3
+ import fs2 from "fs";
4
+ import path2 from "path";
5
+
6
+ // src/routes.ts
7
+ import fs from "fs";
8
+ import path from "path";
9
+ var ROUTE_EXT = /\.(tsx|ts|jsx|js)$/;
10
+ function getRoutes(appDir) {
11
+ const routes = [];
12
+ function scan(dir, prefix) {
13
+ let entries;
14
+ try {
15
+ entries = fs.readdirSync(dir, { withFileTypes: true });
16
+ } catch {
17
+ return;
18
+ }
19
+ for (const entry of entries) {
20
+ const fullPath = path.join(dir, entry.name);
21
+ if (entry.isDirectory()) {
22
+ const isGroup = /^\(.*\)$/.test(entry.name);
23
+ const nextPrefix = isGroup ? prefix : `${prefix}/${entry.name}`;
24
+ scan(fullPath, nextPrefix);
25
+ } else if (ROUTE_EXT.test(entry.name)) {
26
+ const baseName = entry.name.replace(ROUTE_EXT, "");
27
+ if (baseName === "_layout") continue;
28
+ const segment = baseName === "index" ? "" : `/${baseName}`;
29
+ const routePath = `${prefix}${segment}` || "/";
30
+ const isDynamic = /\[.*\]/.test(baseName);
31
+ routes.push({
32
+ title: routePath,
33
+ pageId: routePath,
34
+ pageName: routePath,
35
+ visible: !isDynamic,
36
+ path: path.relative(appDir, fullPath)
37
+ });
38
+ }
39
+ }
40
+ }
41
+ scan(appDir, "");
42
+ return { routes };
43
+ }
44
+ function createRouteHandler(appDir) {
45
+ return (_req, res) => {
46
+ const routeTree = getRoutes(appDir);
47
+ res.writeHead(200, { "Content-Type": "application/json" });
48
+ res.end(JSON.stringify(routeTree, null, 2));
49
+ };
50
+ }
51
+
52
+ // src/metro.ts
53
+ var SENTRY_STUB_FILENAME = "sentry-react-native-stub.js";
54
+ var SENTRY_STUB_PATH = path2.resolve(__dirname, "stubs", SENTRY_STUB_FILENAME);
55
+ var NO_OP_LOGBOX_STUB_PATH = path2.resolve(__dirname, "stubs", "no-op-logbox.js");
56
+ function withDevStubs(config) {
57
+ const upstream = config.resolver?.resolveRequest ?? null;
58
+ const resolveRequest = (context, moduleName, platform) => {
59
+ if (platform === "web" && __DEV__) {
60
+ if (moduleName === "@expo/log-box" || moduleName === "@expo/log-box/lib" || moduleName.endsWith("ErrorOverlayWebControls")) {
61
+ return { filePath: NO_OP_LOGBOX_STUB_PATH, type: "sourceFile" };
62
+ }
63
+ }
64
+ if (moduleName === "@sentry/react-native" && !context.originModulePath.includes(SENTRY_STUB_FILENAME)) {
65
+ return { filePath: SENTRY_STUB_PATH, type: "sourceFile" };
66
+ }
67
+ if (upstream) {
68
+ return upstream(context, moduleName, platform);
69
+ }
70
+ return context.resolveRequest(context, moduleName, platform);
71
+ };
72
+ return {
73
+ ...config,
74
+ resolver: {
75
+ ...config.resolver,
76
+ resolveRequest
77
+ }
78
+ };
79
+ }
80
+ var EXPO_ROUTER_ENTRY_STUB_PATH = path2.resolve(__dirname, "stubs", "expo-router-entry-stub.js");
81
+ var EXPO_ROUTER_ENTRY_STUB_FILENAME = "expo-router-entry-stub.js";
82
+ function withEntryInjection(config, options) {
83
+ void options;
84
+ const upstream = config.resolver?.resolveRequest ?? null;
85
+ const resolveRequest = (context, moduleName, platform) => {
86
+ if (__DEV__ && moduleName === "expo-router/entry-classic" && platform === "web" && !context.originModulePath.includes(EXPO_ROUTER_ENTRY_STUB_FILENAME)) {
87
+ return { filePath: EXPO_ROUTER_ENTRY_STUB_PATH, type: "sourceFile" };
88
+ }
89
+ if (upstream) {
90
+ return upstream(context, moduleName, platform);
91
+ }
92
+ return context.resolveRequest(context, moduleName, platform);
93
+ };
94
+ return {
95
+ ...config,
96
+ resolver: {
97
+ ...config.resolver,
98
+ resolveRequest
99
+ }
100
+ };
101
+ }
102
+ var DEFAULT_ROUTE_ENDPOINT = "/__routes";
103
+ function withRouteEndpoint(config, options) {
104
+ const { appDir, endpoint = DEFAULT_ROUTE_ENDPOINT } = options;
105
+ const upstream = config.server?.enhanceMiddleware ?? null;
106
+ const enhanceMiddleware = (middleware, metroServer) => {
107
+ const enhanced = upstream ? upstream(middleware, metroServer) : middleware;
108
+ return connect().use(enhanced).use(endpoint, createRouteHandler(appDir));
109
+ };
110
+ return {
111
+ ...config,
112
+ server: {
113
+ ...config.server,
114
+ enhanceMiddleware
115
+ }
116
+ };
117
+ }
118
+ function withWorkspaceNodeModules(config) {
119
+ const projectRoot = config.projectRoot ?? process.cwd();
120
+ if (fs2.existsSync(path2.join(projectRoot, "node_modules"))) {
121
+ return config;
122
+ }
123
+ let dir = path2.dirname(projectRoot);
124
+ let found = null;
125
+ while (true) {
126
+ const candidate = path2.join(dir, "node_modules");
127
+ if (fs2.existsSync(candidate)) {
128
+ found = candidate;
129
+ break;
130
+ }
131
+ const parent = path2.dirname(dir);
132
+ if (parent === dir) break;
133
+ dir = parent;
134
+ }
135
+ if (!found) {
136
+ return config;
137
+ }
138
+ const existingWatchFolders = config.watchFolders ?? [];
139
+ const existingNodeModulesPaths = config.resolver?.nodeModulesPaths ?? [];
140
+ return {
141
+ ...config,
142
+ watchFolders: [...existingWatchFolders, found],
143
+ resolver: {
144
+ ...config.resolver,
145
+ nodeModulesPaths: [...existingNodeModulesPaths, found]
146
+ }
147
+ };
148
+ }
149
+ export {
150
+ withDevStubs,
151
+ withEntryInjection,
152
+ withRouteEndpoint,
153
+ withWorkspaceNodeModules
154
+ };
155
+ //# sourceMappingURL=metro.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/metro.ts","../src/routes.ts"],"sourcesContent":["/**\n * miaoda-expo-devkit/metro\n *\n * 提供 Metro 配置增强函数:\n * - withDevStubs(config) 模块替换:Sentry DSN 覆盖、LogBox 屏蔽\n * - withEntryInjection(config) bundle 首部脚本注入\n * - withRouteEndpoint(config) 添加 /__routes 端点,返回路由树 JSON\n * - withWorkspaceNodeModules(config) 修复 node_modules 位于项目父级目录时的资源加载 500 问题\n *\n * 用法(metro.config.js):\n *\n * const { getDefaultConfig } = require('expo/metro-config');\n * const { withDevStubs, withEntryInjection, withRouteEndpoint } = require('miaoda-expo-devkit/metro');\n *\n * const config = getDefaultConfig(__dirname);\n * module.exports = withRouteEndpoint(\n * withEntryInjection(withDevStubs(config)),\n * { appDir: path.join(__dirname, 'src', 'app') }\n * );\n *\n * 链式用法(与其他 Metro wrapper 组合):\n *\n * module.exports = withNativeWind(withEntryInjection(withDevStubs(config)), { input: './global.css' });\n */\n\nimport connect, { type HandleFunction, type Server } from 'connect';\nimport fs from 'fs';\nimport path from 'path';\nimport type { MetroConfig } from 'metro-config';\nimport type { CustomResolver } from 'metro-resolver';\nimport { createRouteHandler } from './routes';\n\n/**\n * sentry-react-native-stub.js 的文件名,用于在 resolver 中检测自身 import 以规避循环依赖。\n * 必须与编译产物 dist/stubs/sentry-react-native-stub.js 的文件名保持同步。\n */\nconst SENTRY_STUB_FILENAME = 'sentry-react-native-stub.js';\n\n/** Stub 文件的绝对路径。__dirname 指向编译产物所在的 dist/ 目录。 */\nconst SENTRY_STUB_PATH = path.resolve(__dirname, 'stubs', SENTRY_STUB_FILENAME);\nconst NO_OP_LOGBOX_STUB_PATH = path.resolve(__dirname, 'stubs', 'no-op-logbox.js');\n\n/**\n * 将开发阶段 stub 注入到 Metro config 中。\n *\n * 注入行为:\n * 1. web 平台:@expo/log-box 及 ErrorOverlayWebControls → no-op-logbox stub(屏蔽全屏错误遮罩)\n * 2. 所有平台:@sentry/react-native → sentry-react-native-stub(替换 DSN,注入内置捕获器)\n *\n * 自动保留并调用已有的 resolveRequest,支持与其他 Metro 配置链式组合。\n *\n * @param config Metro config 对象(来自 getDefaultConfig 或 getSentryExpoConfig)\n * @returns 注入 resolveRequest 后的新 Metro config\n */\nfunction withDevStubs(config: MetroConfig): MetroConfig {\n const upstream = config.resolver?.resolveRequest ?? null;\n\n const resolveRequest: CustomResolver = (context, moduleName, platform) => {\n // 1. web 平台:替换 LogBox 为 no-op,屏蔽全屏错误遮罩\n if (platform === 'web' && __DEV__) {\n if (\n moduleName === '@expo/log-box' ||\n moduleName === '@expo/log-box/lib' ||\n moduleName.endsWith('ErrorOverlayWebControls')\n ) {\n return { filePath: NO_OP_LOGBOX_STUB_PATH, type: 'sourceFile' };\n }\n }\n\n // 2. 所有平台:将 @sentry/react-native 替换为 stub\n // 检查 originModulePath 确保 stub 内部的 import 不被再次拦截(规避循环依赖)\n if (\n moduleName === '@sentry/react-native' &&\n !context.originModulePath.includes(SENTRY_STUB_FILENAME)\n ) {\n return { filePath: SENTRY_STUB_PATH, type: 'sourceFile' };\n }\n\n // 3. 回退到上游 resolver\n if (upstream) {\n return upstream(context, moduleName, platform);\n }\n return context.resolveRequest(context, moduleName, platform);\n };\n\n return {\n ...config,\n resolver: {\n ...config.resolver,\n resolveRequest,\n },\n };\n}\n\n// ─── withEntryInjection ────────────────────────────────────────────────────────\n\n/**\n * expo-router-entry-stub.js 的绝对路径。__dirname 指向编译产物所在的 dist/ 目录。\n * 当 Metro 解析 expo-router/entry-classic 时,resolver 将其重定向到此文件。\n */\nconst EXPO_ROUTER_ENTRY_STUB_PATH = path.resolve(__dirname, 'stubs', 'expo-router-entry-stub.js');\n\n/**\n * expo-router-entry-stub.js 的文件名,用于在 resolver 中检测自身 import 以规避循环依赖。\n * 必须与编译产物 dist/stubs/expo-router-entry-stub.js 的文件名保持同步。\n */\nconst EXPO_ROUTER_ENTRY_STUB_FILENAME = 'expo-router-entry-stub.js';\n\n/** withEntryInjection 的配置选项(预留,供未来支持自定义注入脚本路径扩展) */\nexport interface InjectOptions {\n // 预留字段,未来版本将支持传入自定义注入模块路径\n}\n\n/**\n * 在 expo-router 启动前注入一段脚本。\n *\n * 注入原理:\n * 拦截 Metro 对 expo-router/entry-classic 的解析,将其重定向到\n * dist/stubs/expo-router-entry-stub.js。expo-router-entry-stub.js 先 require entry-inject.js\n * (注入脚本),再 require expo-router/entry-classic(原入口),\n * 从而在 expo-router 启动前执行注入逻辑。\n *\n * 拦截 entry-classic 而非 entry 本身,原因:\n * - expo-router/entry 以 <script src=\"...entry.bundle...\"> 方式直接加载,\n * 不经过 resolveRequest。\n * - expo-router/entry 内部第一行是 import 'expo-router/entry-classic',\n * 这条 import 语句经过 resolveRequest,可以被拦截。\n *\n * 执行顺序(在 bundle 中):\n * entry-inject.js(注入脚本:设置 __DEVKIT_INJECTED__ + 安装 HMR 控制器)\n * → expo-router/entry-classic(原 expo-router 入口)\n * → renderRootComponent(App)\n *\n * 注入的脚本经过完整 Babel 编译,支持现代语法,web + native 双平台均有效。\n *\n * 支持与 withDevStubs 及其他 Metro wrapper 链式组合:\n * module.exports = withEntryInjection(withDevStubs(config));\n *\n * @param config Metro config 对象\n * @param options 注入选项(当前预留,未来支持自定义注入脚本路径)\n * @returns 注入 resolveRequest 后的新 Metro config\n */\nfunction withEntryInjection(config: MetroConfig, options?: InjectOptions): MetroConfig {\n void options; // 当前未使用,预留供未来扩展\n\n const upstream = config.resolver?.resolveRequest ?? null;\n\n const resolveRequest: CustomResolver = (context, moduleName, platform) => {\n // 拦截 expo-router/entry-classic,重定向到 stub 入口(expo-router-entry-stub.js)\n // 用 originModulePath 检查来源,避免 expo-router-entry-stub.js 自身 import\n // expo-router/entry-classic 时再次触发拦截,导致循环依赖。\n if (\n __DEV__ &&\n moduleName === 'expo-router/entry-classic' &&\n platform === 'web' &&\n !context.originModulePath.includes(EXPO_ROUTER_ENTRY_STUB_FILENAME)\n ) {\n return { filePath: EXPO_ROUTER_ENTRY_STUB_PATH, type: 'sourceFile' };\n }\n\n // 回退到上游 resolver(withDevStubs 或默认 resolver)\n if (upstream) {\n return upstream(context, moduleName, platform);\n }\n return context.resolveRequest(context, moduleName, platform);\n };\n\n return {\n ...config,\n resolver: {\n ...config.resolver,\n resolveRequest,\n },\n };\n}\n\n// ─── withRouteEndpoint ────────────────────────────────────────────────────────\n\n/** withRouteEndpoint 的配置选项 */\nexport interface RouteEndpointOptions {\n /** app 目录的绝对路径(包含路由文件的目录) */\n appDir: string;\n /** 自定义端点路径,默认 /__routes */\n endpoint?: string;\n}\n\n/** Metro enhanceMiddleware 的函数签名 */\ntype EnhanceMiddleware = (\n middleware: HandleFunction,\n metroServer: unknown\n) => HandleFunction | Server;\n\nconst DEFAULT_ROUTE_ENDPOINT = '/__routes';\n\n/**\n * 为 Metro server 添加 /__routes 端点,返回路由树 JSON。\n *\n * 端点返回格式:\n * {\n * \"routes\": [\n * { \"title\": \"首页\", \"pageId\": \"/\", \"pageName\": \"index\", \"visible\": true, \"path\": \"index.tsx\" },\n * { \"title\": \"id\", \"pageId\": \"/user/[id]\", \"pageName\": \"user-[id]\", \"visible\": false, \"path\": \"user/[id].tsx\" }\n * ]\n * }\n *\n * @param config Metro config 对象\n * @param options 配置选项,必须提供 appDir\n * @returns 增强 server.enhanceMiddleware 后的新 Metro config\n */\nfunction withRouteEndpoint(config: MetroConfig, options: RouteEndpointOptions): MetroConfig {\n const { appDir, endpoint = DEFAULT_ROUTE_ENDPOINT } = options;\n const upstream = config.server?.enhanceMiddleware ?? null;\n\n const enhanceMiddleware: EnhanceMiddleware = (middleware, metroServer) => {\n // 先调用上游 enhanceMiddleware(如果存在)\n const enhanced = upstream ? upstream(middleware, metroServer) : middleware;\n return connect().use(enhanced).use(endpoint, createRouteHandler(appDir));\n };\n\n return {\n ...config,\n server: {\n ...config.server,\n enhanceMiddleware,\n },\n };\n}\n\n// ─── withWorkspaceNodeModules ──────────────────────────────────────────────────\n\n/**\n * 修复沙箱环境中 node_modules 位于项目父级目录时,资源文件(字体、图片等)加载返回 500 的问题。\n *\n * 问题根因:\n * pnpm 在沙箱中将 node_modules 安装到 projectRoot 的上层目录(如 /workspace/node_modules),\n * 而非 projectRoot 内(如 /workspace/app/node_modules)。\n * Metro 处理 /assets/?unstable_path=./node_modules/.pnpm/... 请求时,会将路径解析为\n * path.resolve(projectRoot, './node_modules/.pnpm/...'),得到一个不存在的路径,返回 500。\n * 实际文件在父级的 node_modules/.pnpm/ 下。\n *\n * 修复方式:\n * 向上查找实际的 node_modules 目录,将其加入 watchFolders(让 Metro 索引其中的文件)\n * 和 resolver.nodeModulesPaths(让模块解析也能找到包)。\n *\n * 如果 node_modules 就在 projectRoot 下,则不做任何修改,直接返回原始 config。\n *\n * @param config Metro config 对象(来自 getDefaultConfig)\n * @returns 修正 watchFolders 和 nodeModulesPaths 后的新 Metro config\n */\nfunction withWorkspaceNodeModules(config: MetroConfig): MetroConfig {\n const projectRoot = config.projectRoot ?? process.cwd();\n\n // node_modules 就在 projectRoot 下,无需处理\n if (fs.existsSync(path.join(projectRoot, 'node_modules'))) {\n return config;\n }\n\n // 向上逐级查找 node_modules\n let dir = path.dirname(projectRoot);\n let found: string | null = null;\n while (true) {\n const candidate = path.join(dir, 'node_modules');\n if (fs.existsSync(candidate)) {\n found = candidate;\n break;\n }\n const parent = path.dirname(dir);\n if (parent === dir) break; // 到达文件系统根目录\n dir = parent;\n }\n\n if (!found) {\n return config;\n }\n\n const existingWatchFolders = config.watchFolders ?? [];\n const existingNodeModulesPaths = config.resolver?.nodeModulesPaths ?? [];\n\n return {\n ...config,\n watchFolders: [...existingWatchFolders, found],\n resolver: {\n ...config.resolver,\n nodeModulesPaths: [...existingNodeModulesPaths, found],\n },\n };\n}\n\nexport { withDevStubs, withEntryInjection, withRouteEndpoint, withWorkspaceNodeModules };\n","/**\n * miaoda-expo-devkit/routes\n *\n * 提供 Expo Router 路由收集功能:\n * - getRoutes(appDir) 扫描目录生成路由树\n * - createRouteHandler(appDir) 创建路由端点的请求处理器\n */\n\nimport fs from 'fs';\nimport path from 'path';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/** 单条路由信息 */\nexport interface RouteInfo {\n /** 页面标题 */\n title: string;\n /** 路由标识,如 /、/settings */\n pageId: string;\n /** 路由名称 */\n pageName: string;\n /** 是否可见(动态路由默认不可见) */\n visible: boolean;\n /** 相对于 appDir 的文件路径 */\n path: string;\n}\n\n/** getRoutes 返回的路由树 */\nexport interface RouteTree {\n routes: RouteInfo[];\n}\n\n/** HTTP 响应对象的最小接口 */\ninterface RouteResponse {\n writeHead: (status: number, headers: Record<string, string>) => void;\n end: (body: string) => void;\n}\n\n/** 支持的路由文件扩展名 */\nconst ROUTE_EXT = /\\.(tsx|ts|jsx|js)$/;\n\n/**\n * 扫描 Expo Router 的 app 目录,生成路由树。\n *\n * 扫描规则:\n * - 目录:递归扫描,路由组 (groupName) 不生成路径片段\n * - 文件:.tsx/.ts/.jsx/.js 文件生成路由\n * - _layout 文件:布局文件,跳过\n * - index 文件:映射到父目录路径\n * - [param] 文件:标记为动态路由,默认不可见\n *\n * @param appDir app 目录的绝对路径\n * @returns 路由树对象\n */\nexport function getRoutes(appDir: string): RouteTree {\n const routes: RouteInfo[] = [];\n\n function scan(dir: string, prefix: string): void {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n // 目录不存在或无权限,返回空\n return;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n // 路由组 (groupName) 不生成路径片段,直接透传\n const isGroup = /^\\(.*\\)$/.test(entry.name);\n const nextPrefix = isGroup ? prefix : `${prefix}/${entry.name}`;\n scan(fullPath, nextPrefix);\n } else if (ROUTE_EXT.test(entry.name)) {\n const baseName = entry.name.replace(ROUTE_EXT, '');\n\n // _layout 是布局文件,不是路由页面\n if (baseName === '_layout') continue;\n\n // index 文件映射到父目录,其他文件生成对应路径段\n const segment = baseName === 'index' ? '' : `/${baseName}`;\n const routePath = `${prefix}${segment}` || '/';\n const isDynamic = /\\[.*\\]/.test(baseName);\n\n routes.push({\n title: routePath,\n pageId: routePath,\n pageName: routePath,\n visible: !isDynamic,\n path: path.relative(appDir, fullPath),\n });\n }\n }\n }\n\n scan(appDir, '');\n\n return { routes };\n}\n\n// ─── createRouteHandler ───────────────────────────────────────────────────────\n\n/**\n * 创建路由端点的请求处理器。\n *\n * @param appDir app 目录的绝对路径\n * @returns connect 中间件处理函数\n */\nexport function createRouteHandler(appDir: string) {\n return (_req: unknown, res: RouteResponse) => {\n const routeTree = getRoutes(appDir);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(routeTree, null, 2));\n };\n}\n"],"mappings":"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","names":["fs","path","path","fs"]}
@@ -0,0 +1,101 @@
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
+ var css_control_exports = {};
20
+ __export(css_control_exports, {
21
+ injectSelectorModeStyle: () => injectSelectorModeStyle,
22
+ setupCSSInjectionControl: () => setupCSSInjectionControl
23
+ });
24
+ module.exports = __toCommonJS(css_control_exports);
25
+ const HIGHLIGHT_COLOR = "hsl(220, 89%, 55%)";
26
+ const OVERFLOW_HIDDEN = ':is([class*="overflow-hidden"], [class*="overflow-clip"])';
27
+ const REPLACED = ":is(img, video, iframe, canvas)";
28
+ const DEVKIT_CSS = `
29
+ /* ---- \u9009\u4E2D\u5143\u7D20 ---- */
30
+ [data-editor-active] {
31
+ outline: 2px solid ${HIGHLIGHT_COLOR} !important;
32
+ outline-offset: 0 !important;
33
+ }
34
+
35
+ /* ---- \u60AC\u505C\u5143\u7D20 ---- */
36
+ [data-editor-hover] {
37
+ outline: 1px solid ${HIGHLIGHT_COLOR} !important;
38
+ outline-offset: 0 !important;
39
+ }
40
+
41
+ /* ---- \u540C\u6E90\u5144\u5F1F\u5143\u7D20 ---- */
42
+ [data-editor-each] {
43
+ outline: 1px dashed ${HIGHLIGHT_COLOR} !important;
44
+ outline-offset: 0 !important;
45
+ }
46
+
47
+ /* ---- \u5168\u5BBD\u5143\u7D20\u4F7F\u7528\u5185\u7F29 offset ---- */
48
+ :is([data-editor-active], [data-editor-hover])[data-editor-full-width] {
49
+ outline-offset: -5px !important;
50
+ }
51
+
52
+ /* ---- overflow-hidden \u7236\u5143\u7D20\u4EE3\u7406 outline ---- */
53
+ ${OVERFLOW_HIDDEN} > ${REPLACED}:is([data-editor-hover], [data-editor-active]) {
54
+ outline: none !important;
55
+ }
56
+ ${OVERFLOW_HIDDEN}:has(> ${REPLACED}[data-editor-hover]) {
57
+ outline: 1px solid ${HIGHLIGHT_COLOR} !important;
58
+ outline-offset: 0 !important;
59
+ }
60
+ ${OVERFLOW_HIDDEN}:has(> ${REPLACED}[data-editor-active]) {
61
+ outline: 2px solid ${HIGHLIGHT_COLOR} !important;
62
+ outline-offset: 1px !important;
63
+ }
64
+ `;
65
+ const SELECTOR_MODE_CSS = `
66
+ * {
67
+ scroll-behavior: auto !important;
68
+ cursor: default !important;
69
+ }
70
+ [contenteditable="true"], [contenteditable="true"] * {
71
+ cursor: text !important;
72
+ }
73
+ :is(button, a, [role="button"], [role="link"]) svg {
74
+ pointer-events: auto !important;
75
+ }
76
+ `;
77
+ const SELECTOR_STYLE_ATTR = "data-devkit-selector";
78
+ function injectSelectorModeStyle() {
79
+ if (typeof document === "undefined") return () => {
80
+ };
81
+ const style = document.createElement("style");
82
+ style.setAttribute(SELECTOR_STYLE_ATTR, "");
83
+ style.textContent = SELECTOR_MODE_CSS;
84
+ document.head.appendChild(style);
85
+ return () => style.remove();
86
+ }
87
+ const STYLE_ATTR = "data-devkit-css";
88
+ function setupCSSInjectionControl() {
89
+ if (typeof document === "undefined" || typeof window === "undefined") return;
90
+ if (document.querySelector(`style[${STYLE_ATTR}]`)) return;
91
+ const style = document.createElement("style");
92
+ style.setAttribute(STYLE_ATTR, "");
93
+ style.textContent = DEVKIT_CSS;
94
+ document.head.appendChild(style);
95
+ }
96
+ // Annotate the CommonJS export names for ESM import in node:
97
+ 0 && (module.exports = {
98
+ injectSelectorModeStyle,
99
+ setupCSSInjectionControl
100
+ });
101
+ //# sourceMappingURL=css-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stubs/css-control.ts"],"sourcesContent":["/**\n * CSS 注入控制器。\n *\n * 在页面加载时注入一段固定的 <style>,为 LGUI 编辑器的\n * data-editor-active / data-editor-hover / data-editor-each\n * 属性提供可视化高亮样式。\n *\n * 副作用:\n * - 在 <head> 中追加一个 <style data-devkit-css> 元素\n *\n * 设计考量:\n * - 样式写死在代码中,无需外部传入,保持零配置\n * - 使用 data-devkit-css 标记避免重复注入\n * - 非 browser 环境(typeof document === 'undefined')直接返回,React Native 安全\n */\n\nconst HIGHLIGHT_COLOR = 'hsl(220, 89%, 55%)';\n\nconst OVERFLOW_HIDDEN = ':is([class*=\"overflow-hidden\"], [class*=\"overflow-clip\"])';\nconst REPLACED = ':is(img, video, iframe, canvas)';\n\nconst DEVKIT_CSS = `\n/* ---- 选中元素 ---- */\n[data-editor-active] {\n outline: 2px solid ${HIGHLIGHT_COLOR} !important;\n outline-offset: 0 !important;\n}\n\n/* ---- 悬停元素 ---- */\n[data-editor-hover] {\n outline: 1px solid ${HIGHLIGHT_COLOR} !important;\n outline-offset: 0 !important;\n}\n\n/* ---- 同源兄弟元素 ---- */\n[data-editor-each] {\n outline: 1px dashed ${HIGHLIGHT_COLOR} !important;\n outline-offset: 0 !important;\n}\n\n/* ---- 全宽元素使用内缩 offset ---- */\n:is([data-editor-active], [data-editor-hover])[data-editor-full-width] {\n outline-offset: -5px !important;\n}\n\n/* ---- overflow-hidden 父元素代理 outline ---- */\n${OVERFLOW_HIDDEN} > ${REPLACED}:is([data-editor-hover], [data-editor-active]) {\n outline: none !important;\n}\n${OVERFLOW_HIDDEN}:has(> ${REPLACED}[data-editor-hover]) {\n outline: 1px solid ${HIGHLIGHT_COLOR} !important;\n outline-offset: 0 !important;\n}\n${OVERFLOW_HIDDEN}:has(> ${REPLACED}[data-editor-active]) {\n outline: 2px solid ${HIGHLIGHT_COLOR} !important;\n outline-offset: 1px !important;\n}\n`;\n\n// ============================================================================\n// 选择模式样式(进入编辑器时注入,退出时移除)\n// ============================================================================\n\nconst SELECTOR_MODE_CSS = `\n* {\n scroll-behavior: auto !important;\n cursor: default !important;\n}\n[contenteditable=\"true\"], [contenteditable=\"true\"] * {\n cursor: text !important;\n}\n:is(button, a, [role=\"button\"], [role=\"link\"]) svg {\n pointer-events: auto !important;\n}\n`;\n\nconst SELECTOR_STYLE_ATTR = 'data-devkit-selector';\n\n/** 注入选择模式样式,返回移除函数 */\nexport function injectSelectorModeStyle(): () => void {\n if (typeof document === 'undefined') return () => {};\n const style = document.createElement('style');\n style.setAttribute(SELECTOR_STYLE_ATTR, '');\n style.textContent = SELECTOR_MODE_CSS;\n document.head.appendChild(style);\n return () => style.remove();\n}\n\n// ============================================================================\n// 高亮样式(页面加载时注入,常驻)\n// ============================================================================\n\n/** 防重复注入的标记属性 */\nconst STYLE_ATTR = 'data-devkit-css';\n\n/**\n * 注入编辑器高亮 CSS。\n * 仅在 browser 环境下执行,React Native 安全。\n */\nexport function setupCSSInjectionControl(): void {\n // 仅在真正的 browser 环境下安装监听器\n // React Native 中 window 存在但 document 不存在,用 document 判断更可靠\n if (typeof document === 'undefined' || typeof window === 'undefined') return;\n\n // 防止重复注入\n if (document.querySelector(`style[${STYLE_ATTR}]`)) return;\n\n const style = document.createElement('style');\n style.setAttribute(STYLE_ATTR, '');\n style.textContent = DEVKIT_CSS;\n document.head.appendChild(style);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,MAAM,kBAAkB;AAExB,MAAM,kBAAkB;AACxB,MAAM,WAAW;AAEjB,MAAM,aAAa;AAAA;AAAA;AAAA,uBAGI,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAMf,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAMd,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUrC,eAAe,MAAM,QAAQ;AAAA;AAAA;AAAA,EAG7B,eAAe,UAAU,QAAQ;AAAA,uBACZ,eAAe;AAAA;AAAA;AAAA,EAGpC,eAAe,UAAU,QAAQ;AAAA,uBACZ,eAAe;AAAA;AAAA;AAAA;AAStC,MAAM,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAa1B,MAAM,sBAAsB;AAGrB,SAAS,0BAAsC;AACpD,MAAI,OAAO,aAAa,YAAa,QAAO,MAAM;AAAA,EAAC;AACnD,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,aAAa,qBAAqB,EAAE;AAC1C,QAAM,cAAc;AACpB,WAAS,KAAK,YAAY,KAAK;AAC/B,SAAO,MAAM,MAAM,OAAO;AAC5B;AAOA,MAAM,aAAa;AAMZ,SAAS,2BAAiC;AAG/C,MAAI,OAAO,aAAa,eAAe,OAAO,WAAW,YAAa;AAGtE,MAAI,SAAS,cAAc,SAAS,UAAU,GAAG,EAAG;AAEpD,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,aAAa,YAAY,EAAE;AACjC,QAAM,cAAc;AACpB,WAAS,KAAK,YAAY,KAAK;AACjC;","names":[]}
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ var import_hmr_control = require("./hmr-control");
3
+ var import_lgui_control = require("./lgui-control");
4
+ var import_css_control = require("./css-control");
5
+ var import_router_control = require("./router-control");
6
+ globalThis.__DEVKIT_INJECTED__ = true;
7
+ (0, import_hmr_control.setupHMRMessageControl)();
8
+ (0, import_lgui_control.setupLGUIMessageControl)();
9
+ (0, import_css_control.setupCSSInjectionControl)();
10
+ (0, import_router_control.setupRouterControl)();
11
+ //# sourceMappingURL=entry-inject.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stubs/entry-inject.ts"],"sourcesContent":["/**\n * withEntryInjection 默认注入模块。\n *\n * 由 expo-router-entry-stub.ts 在 expo-router/entry-classic 执行前 require,\n * 因此此处代码运行时机早于 expo-router 初始化、早于任何路由渲染。\n *\n * 当前行为:\n * 1. 设置全局注入标记(供测试验证)\n * 2. 安装 HMR postMessage 控制器(监听消息以启动或停止 HMR)\n */\n\nimport { setupHMRMessageControl } from './hmr-control';\nimport { setupLGUIMessageControl } from './lgui-control';\nimport { setupCSSInjectionControl } from './css-control';\nimport { setupRouterControl } from './router-control';\n\n// 设置全局注入标记:测试可通过 globalThis.__DEVKIT_INJECTED__ 验证注入是否生效\nglobalThis.__DEVKIT_INJECTED__ = true;\n\n// 安装 HMR postMessage 控制器\n// 接受 { type: 'devkit:hmr', action: 'enable' | 'disable' } 消息以启动或停止 HMR\nsetupHMRMessageControl();\n\n// 安装 LGUI postMessage 控制器\nsetupLGUIMessageControl();\n\n// 安装 CSS 注入控制器\nsetupCSSInjectionControl();\n\n// 安装路由消息控制器\nsetupRouterControl();\n"],"mappings":";AAWA,yBAAuC;AACvC,0BAAwC;AACxC,yBAAyC;AACzC,4BAAmC;AAGnC,WAAW,sBAAsB;AAAA,IAIjC,2CAAuB;AAAA,IAGvB,6CAAwB;AAAA,IAGxB,6CAAyB;AAAA,IAGzB,0CAAmB;","names":[]}
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ var import_entry_inject = require("./entry-inject");
3
+ var import_entry_classic = require("expo-router/entry-classic");
4
+ //# sourceMappingURL=expo-router-entry-stub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stubs/expo-router-entry-stub.ts"],"sourcesContent":["/**\n * expo-router/entry-classic 的 stub 入口。\n *\n * Metro 的 resolveRequest(由 withEntryInjection 注入)将\n * expo-router/entry-classic 重定向到此文件。\n *\n * 执行顺序:\n * 1. entry-inject(注入脚本:设置全局标记 + 安装 HMR 控制器)\n * 2. expo-router/entry-classic(原 expo-router 入口,负责注册 App)\n *\n * 注意:expo-router 由宿主应用提供,不在 devkit 依赖中;\n * Metro 在构建时从应用目录向上查找并解析 expo-router/entry-classic。\n */\n\nimport './entry-inject';\n// expo-router 由宿主应用提供,Metro 在构建时从应用目录解析;此处 import 仅声明运行时依赖\nimport 'expo-router/entry-classic';\n"],"mappings":";AAcA,0BAAO;AAEP,2BAAO;","names":[]}
@@ -0,0 +1,51 @@
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
+ var hmr_control_exports = {};
20
+ __export(hmr_control_exports, {
21
+ setupHMRMessageControl: () => setupHMRMessageControl
22
+ });
23
+ module.exports = __toCommonJS(hmr_control_exports);
24
+ function isDevkitHMRMessage(data) {
25
+ return typeof data === "object" && data !== null && data.type === "devkit:hmr" && (data.action === "enable" || data.action === "disable");
26
+ }
27
+ function setupHMRMessageControl() {
28
+ if (typeof document === "undefined" || typeof window === "undefined") return;
29
+ const w = window;
30
+ globalThis.__DEVKIT_HMR_ENABLED__ = true;
31
+ w.addEventListener("message", (event) => {
32
+ const data = event.data;
33
+ if (!isDevkitHMRMessage(data)) return;
34
+ const enabled = data.action === "enable";
35
+ globalThis.__DEVKIT_HMR_ENABLED__ = enabled;
36
+ if (__DEV__) {
37
+ try {
38
+ const hmr = require("expo/src/async-require/hmr");
39
+ if (hmr.default) {
40
+ enabled ? hmr.default.enable() : hmr.default.disable();
41
+ }
42
+ } catch {
43
+ }
44
+ }
45
+ });
46
+ }
47
+ // Annotate the CommonJS export names for ESM import in node:
48
+ 0 && (module.exports = {
49
+ setupHMRMessageControl
50
+ });
51
+ //# sourceMappingURL=hmr-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stubs/hmr-control.ts"],"sourcesContent":["/**\n * HMR postMessage 控制器。\n *\n * 监听 window 上的 postMessage 消息,根据消息内容启动或停止 HMR。\n *\n * 消息格式:\n * window.postMessage({ type: 'devkit:hmr', action: 'enable' | 'disable' }, '*')\n *\n * 副作用:\n * - 在 globalThis 上写入 __DEVKIT_HMR_ENABLED__(初始值 true),\n * 随每条消息更新,供测试验证消息处理是否生效。\n * - 在 __DEV__ 模式下调用 expo/src/async-require/hmr 的 enable()/disable(),\n * 实际暂停或恢复 Fast Refresh。\n *\n * 设计考量:\n * - 全局状态标记的更新不依赖 __DEV__,测试在静态导出(生产构建)中也能验证消息处理。\n * - HMR 客户端调用仅在 __DEV__ 下执行,生产包不引入 dev-only 模块。\n * - 非 browser 环境(typeof document === 'undefined')直接返回,React Native 安全。\n */\n\ndeclare const __DEV__: boolean;\n\n/** devkit HMR 消息格式。 */\ninterface DevkitHMRMessage {\n type: 'devkit:hmr';\n action: 'enable' | 'disable';\n}\n\nfunction isDevkitHMRMessage(data: unknown): data is DevkitHMRMessage {\n return (\n typeof data === 'object' &&\n data !== null &&\n (data as Record<string, unknown>).type === 'devkit:hmr' &&\n ((data as Record<string, unknown>).action === 'enable' ||\n (data as Record<string, unknown>).action === 'disable')\n );\n}\n\n/**\n * 仅声明 devkit 实际用到的 window 最小接口,避免依赖 DOM lib。\n * window 不在 global.d.ts 中声明,以防与 tsconfig.check.json 的 DOM lib 冲突。\n */\ninterface BrowserWindow {\n addEventListener(type: string, listener: (event: MessageEvent) => void): void;\n}\n\nexport function setupHMRMessageControl(): void {\n // 仅在真正的 browser 环境下安装监听器\n // React Native 中 window 存在但 document 不存在,用 document 判断更可靠\n if (typeof document === 'undefined' || typeof window === 'undefined') return;\n const w = window as unknown as BrowserWindow;\n\n // 初始化全局状态标记:测试可在任意时刻读取 globalThis.__DEVKIT_HMR_ENABLED__\n globalThis.__DEVKIT_HMR_ENABLED__ = true;\n\n w.addEventListener('message', (event: MessageEvent) => {\n const data: unknown = event.data;\n\n // 过滤非 devkit:hmr 消息\n if (!isDevkitHMRMessage(data)) return;\n\n const enabled: boolean = data.action === 'enable';\n\n // 更新全局状态标记(生产构建下同样有效,便于测试断言)\n globalThis.__DEVKIT_HMR_ENABLED__ = enabled;\n\n // 仅在开发模式下控制真实 HMR 客户端\n if (__DEV__) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const hmr = require('expo/src/async-require/hmr') as {\n default?: { enable(): void; disable(): void };\n };\n if (hmr.default) {\n enabled ? hmr.default.enable() : hmr.default.disable();\n }\n } catch {\n // 静态导出或非 Metro dev 环境下 HMR 客户端不可用,静默忽略\n }\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BA,SAAS,mBAAmB,MAAyC;AACnE,SACE,OAAO,SAAS,YAChB,SAAS,QACR,KAAiC,SAAS,iBACzC,KAAiC,WAAW,YAC3C,KAAiC,WAAW;AAEnD;AAUO,SAAS,yBAA+B;AAG7C,MAAI,OAAO,aAAa,eAAe,OAAO,WAAW,YAAa;AACtE,QAAM,IAAI;AAGV,aAAW,yBAAyB;AAEpC,IAAE,iBAAiB,WAAW,CAAC,UAAwB;AACrD,UAAM,OAAgB,MAAM;AAG5B,QAAI,CAAC,mBAAmB,IAAI,EAAG;AAE/B,UAAM,UAAmB,KAAK,WAAW;AAGzC,eAAW,yBAAyB;AAGpC,QAAI,SAAS;AACX,UAAI;AAEF,cAAM,MAAM,QAAQ,4BAA4B;AAGhD,YAAI,IAAI,SAAS;AACf,oBAAU,IAAI,QAAQ,OAAO,IAAI,IAAI,QAAQ,QAAQ;AAAA,QACvD;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,CAAC;AACH;","names":[]}