@4399ywkf/core 5.1.5 → 5.3.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.
@@ -1,7 +1,7 @@
1
- import { Q as YwkfPlugin, e as PluginHooks, Y as YwkfConfig, f as PluginConfig, U as RouteItem } from '../schema-CuEnqbqQ.js';
2
- export { y as BuildHooks, C as CodeInjection, V as DefinePlugin, F as GeneratedFile, G as GeneratorContext, J as GeneratorHooks, W as PluginAPI, K as PluginContext, X as PluginLogger, L as RouterHooks, N as RuntimeHooks } from '../schema-CuEnqbqQ.js';
1
+ import { Q as YwkfPlugin, e as PluginHooks, Y as YwkfConfig, f as PluginConfig, U as RouteItem } from '../schema-QzJlmn39.js';
2
+ export { y as BuildHooks, C as CodeInjection, V as DefinePlugin, F as GeneratedFile, G as GeneratorContext, J as GeneratorHooks, W as PluginAPI, K as PluginContext, X as PluginLogger, L as RouterHooks, N as RuntimeHooks } from '../schema-QzJlmn39.js';
3
3
  import { Configuration } from '@rspack/core';
4
- import { a as AppConfig, P as ProviderConfig } from '../types-BYqzyGAY.js';
4
+ import { a as AppConfig, P as ProviderConfig } from '../types-ZJhd87ue.js';
5
5
  import 'react';
6
6
  import 'react-router';
7
7
 
@@ -242,12 +242,13 @@ function LazyRoute({
242
242
 
243
243
  import React, { lazy, Suspense } from "react";
244
244
  import { createBrowserRouter, type RouteObject } from "react-router";
245
+ import { RouteErrorBoundary } from "@4399ywkf/core/runtime";
246
+ ${errorImports.length > 0 ? `
247
+ // \u540C\u6B65\u9519\u8BEF\u8FB9\u754C\u7EC4\u4EF6
248
+ ${errorImports.join("\n")}` : ""}
245
249
 
246
250
  // \u61D2\u52A0\u8F7D\u9875\u9762\u7EC4\u4EF6
247
251
  ${lazyImports.join("\n")}
248
- ${errorImports.length > 0 ? `
249
- // \u9519\u8BEF\u8FB9\u754C\u7EC4\u4EF6
250
- ${errorImports.join("\n")}` : ""}
251
252
  ${loadingImports.length > 0 ? `
252
253
  // \u52A0\u8F7D\u72B6\u6001\u7EC4\u4EF6
253
254
  ${loadingImports.join("\n")}` : ""}
@@ -282,7 +283,7 @@ export default routes;
282
283
  }
283
284
  if (route.errorFile) {
284
285
  errorImports.push(
285
- `const ${name}Error = lazy(() => import("@/pages/${toImportPath(route.errorFile)}"));`
286
+ `import ${name}Error from "@/pages/${toImportPath(route.errorFile)}";`
286
287
  );
287
288
  }
288
289
  if (route.loadingFile) {
@@ -297,13 +298,13 @@ export default routes;
297
298
  }
298
299
  }
299
300
  }
300
- emitRouteArray(routes, parentPath) {
301
- const items = routes.map((r) => this.emitRouteObject(r, parentPath));
301
+ emitRouteArray(routes, parentPath, depth = 0) {
302
+ const items = routes.map((r) => this.emitRouteObject(r, parentPath, depth));
302
303
  return `[
303
304
  ${items.join(",\n ")}
304
305
  ]`;
305
306
  }
306
- emitRouteObject(route, parentPath) {
307
+ emitRouteObject(route, parentPath, depth) {
307
308
  const parts = [];
308
309
  const name = route.name;
309
310
  if (route.index) {
@@ -324,11 +325,13 @@ export default routes;
324
325
  parts.push(`element: <LazyRoute Component={${name}Page} />`);
325
326
  }
326
327
  if (route.errorFile) {
327
- parts.push(`errorElement: <LazyRoute Component={${name}Error} />`);
328
+ parts.push(`errorElement: <RouteErrorBoundary fallback={<${name}Error />} />`);
329
+ } else if (depth === 0) {
330
+ parts.push(`errorElement: <RouteErrorBoundary />`);
328
331
  }
329
332
  if (route.children && route.children.length > 0) {
330
333
  const childParent = route.pathless ? parentPath : route.path;
331
- parts.push(`children: ${this.emitRouteArray(route.children, childParent)}`);
334
+ parts.push(`children: ${this.emitRouteArray(route.children, childParent, depth + 1)}`);
332
335
  }
333
336
  return `{
334
337
  ${parts.join(",\n ")}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/router/generator.ts","../../src/router/plugin.ts"],"sourcesContent":["import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from \"fs\";\nimport { join, relative } from \"path\";\n\n/** 将 Windows 路径统一为 POSIX 正斜杠(import 路径必须用 /) */\nfunction winPath(path: string): string {\n const isExtendedLengthPath = /^\\\\\\\\\\?\\\\/.test(path);\n if (isExtendedLengthPath) {\n return path;\n }\n return path.replace(/\\\\/g, \"/\");\n}\n\n/**\n * 路由节点配置\n */\nexport interface RouteConfig {\n /** 路由路径 */\n path: string;\n /** 页面文件(page.tsx)相对路径 */\n file?: string;\n /** 布局文件(layout.tsx)相对路径 */\n layoutFile?: string;\n /** 错误边界文件(error.tsx)相对路径 */\n errorFile?: string;\n /** 加载状态文件(loading.tsx)相对路径 */\n loadingFile?: string;\n /** 通配路由文件($.tsx)相对路径 */\n catchAllFile?: string;\n /** 组件名称 */\n name: string;\n /** 子路由 */\n children?: RouteConfig[];\n /** 是否为 index 路由 */\n index?: boolean;\n /** 是否为布局路由 */\n isLayout?: boolean;\n /** 是否为无路径布局(__prefix) */\n pathless?: boolean;\n}\n\nexport interface GeneratorOptions {\n /** 页面目录 */\n pagesDir: string;\n /** 输出目录 */\n outputDir: string;\n /** 路由 basename */\n basename?: string;\n /**\n * 加载状态配置\n * - `false`:禁用全局默认加载状态(Suspense fallback 为 null,但仍可通过 loading.tsx 覆盖)\n * - `{ component: string }`:自定义默认加载组件的导入路径,例如 `\"@/components/Loading\"`\n * - `undefined`:使用内置默认加载组件\n */\n loading?: false | { component: string };\n}\n\n/**\n * 不参与路由扫描的目录名\n */\nconst EXCLUDED_DIRS = new Set([\n \"components\",\n \"hooks\",\n \"utils\",\n \"services\",\n \"models\",\n \"assets\",\n \"types\",\n \"constants\",\n \"styles\",\n]);\n\n/**\n * 约定文件名匹配\n */\nconst CONVENTION_FILES = {\n page: /^page\\.(tsx?|jsx?)$/,\n layout: /^layout\\.(tsx?|jsx?)$/,\n error: /^error\\.(tsx?|jsx?)$/,\n loading: /^loading\\.(tsx?|jsx?)$/,\n catchAll: /^\\$\\.(tsx?|jsx?)$/,\n};\n\n/**\n * Modern.js 风格约定式路由生成器\n *\n * 文件约定:\n * - page.tsx — 页面内容(叶子组件)\n * - layout.tsx — 布局组件(使用 <Outlet> 渲染子路由)\n * - error.tsx — 错误边界组件\n * - loading.tsx — 加载状态组件\n * - $.tsx — 通配/404 路由\n *\n * 目录约定:\n * - [id]/ — 动态路由 → /:id\n * - [id$]/ — 可选动态路由 → /:id?\n * - [...slug]/ — 全匹配路由 → /*\n * - __auth/ — 无路径布局(不生成 URL 片段)\n * - user.profile/ — 扁平路由(. 分隔 → /user/profile)\n */\nexport class ConventionalRouteGenerator {\n private options: GeneratorOptions;\n\n constructor(options: GeneratorOptions) {\n this.options = options;\n }\n\n generate(): RouteConfig[] {\n const { pagesDir } = this.options;\n if (!existsSync(pagesDir)) {\n console.warn(`[ywkf] 页面目录不存在: ${pagesDir}`);\n return [];\n }\n return this.scanDirectory(pagesDir, \"/\");\n }\n\n /**\n * 扫描目录,生成路由树\n */\n private scanDirectory(dir: string, routePath: string): RouteConfig[] {\n const entries = readdirSync(dir);\n\n // 查找约定文件\n const layoutFile = entries.find((e) => CONVENTION_FILES.layout.test(e));\n const pageFile = entries.find((e) => CONVENTION_FILES.page.test(e));\n const errorFile = entries.find((e) => CONVENTION_FILES.error.test(e));\n const loadingFile = entries.find((e) => CONVENTION_FILES.loading.test(e));\n const catchAllFile = entries.find((e) => CONVENTION_FILES.catchAll.test(e));\n\n // 收集子目录\n const subDirs = entries.filter((e) => {\n if (e.startsWith(\".\")) return false;\n if (EXCLUDED_DIRS.has(e)) return false;\n return statSync(join(dir, e)).isDirectory();\n });\n\n // 递归子目录\n const childRoutes: RouteConfig[] = [];\n for (const subDir of subDirs) {\n const subDirPath = join(dir, subDir);\n\n // __prefix:无路径布局\n if (subDir.startsWith(\"__\")) {\n const pathlessRoutes = this.scanDirectory(subDirPath, routePath);\n // 如果该无路径目录有 layout,将子路由包装在其中\n const pathlessLayout = readdirSync(subDirPath).find((e) => CONVENTION_FILES.layout.test(e));\n if (pathlessLayout) {\n const pathlessChildren = this.scanDirectory(subDirPath, routePath);\n const pathlessLayoutRel = winPath(\n relative(this.options.pagesDir, join(subDirPath, pathlessLayout)),\n );\n childRoutes.push({\n path: routePath,\n name: this.generateRouteName(subDir.slice(2)),\n layoutFile: pathlessLayoutRel,\n isLayout: true,\n pathless: true,\n children: pathlessChildren.filter((r) => r.layoutFile !== pathlessLayoutRel),\n });\n } else {\n childRoutes.push(...pathlessRoutes);\n }\n continue;\n }\n\n // 计算路由路径\n const subRoutePath = this.resolveRoutePath(subDir, routePath);\n childRoutes.push(...this.scanDirectory(subDirPath, subRoutePath));\n }\n\n // 构建当前目录的路由节点\n const routeName = this.generateRouteName(routePath);\n const relPath = (file: string) => winPath(relative(this.options.pagesDir, join(dir, file)));\n\n // 有 layout.tsx → 创建布局路由节点,children 为子路由\n if (layoutFile) {\n const layoutChildren: RouteConfig[] = [];\n\n // page.tsx 作为 index 子路由\n if (pageFile) {\n layoutChildren.push({\n path: routePath,\n file: relPath(pageFile),\n name: `${routeName}Index`,\n index: true,\n });\n }\n\n // 通配路由\n if (catchAllFile) {\n layoutChildren.push({\n path: \"*\",\n file: relPath(catchAllFile),\n name: `${routeName}CatchAll`,\n });\n }\n\n layoutChildren.push(...childRoutes);\n\n return [\n {\n path: routePath,\n layoutFile: relPath(layoutFile),\n errorFile: errorFile ? relPath(errorFile) : undefined,\n loadingFile: loadingFile ? relPath(loadingFile) : undefined,\n name: routeName,\n isLayout: true,\n children: layoutChildren,\n },\n ];\n }\n\n // 无 layout → page.tsx 是叶子页面\n const result: RouteConfig[] = [];\n\n if (pageFile) {\n result.push({\n path: routePath,\n file: relPath(pageFile),\n errorFile: errorFile ? relPath(errorFile) : undefined,\n name: routeName,\n });\n }\n\n if (catchAllFile) {\n result.push({\n path: \"*\",\n file: relPath(catchAllFile),\n name: `${routeName}CatchAll`,\n });\n }\n\n result.push(...childRoutes);\n return result;\n }\n\n /**\n * 解析目录名到路由路径\n *\n * - [id] → :id\n * - [id$] → :id?\n * - [...slug] → *\n * - user.profile → user/profile\n */\n private resolveRoutePath(dirName: string, parentPath: string): string {\n let segment: string;\n\n // [...slug] → *\n if (dirName.match(/^\\[\\.\\.\\.(.+)\\]$/)) {\n segment = \"*\";\n }\n // [id$] → :id? (可选动态)\n else if (dirName.match(/^\\[(.+)\\$\\]$/)) {\n const param = dirName.slice(1, -2);\n segment = `:${param}?`;\n }\n // [id] → :id\n else if (dirName.match(/^\\[(.+)\\]$/)) {\n const param = dirName.slice(1, -1);\n segment = `:${param}`;\n }\n // user.profile → user/profile (扁平路由)\n else if (dirName.includes(\".\")) {\n segment = dirName.replace(/\\./g, \"/\");\n } else {\n segment = dirName;\n }\n\n return parentPath === \"/\" ? `/${segment}` : `${parentPath}/${segment}`;\n }\n\n /**\n * 生成有效的 JS 标识符名称\n */\n private generateRouteName(path: string): string {\n const name = path\n .split(\"/\")\n .filter(Boolean)\n .map((s) => s.replace(/^:/, \"Param\").replace(/\\?$/, \"Optional\").replace(/^\\*$/, \"CatchAll\"))\n .map((s) => {\n const cleaned = s.replace(/[^a-zA-Z0-9]/g, \"\");\n if (!cleaned) return \"\";\n return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);\n })\n .join(\"\");\n\n if (!name || /^\\d/.test(name)) {\n return `Page${name || \"Root\"}`;\n }\n return name;\n }\n\n // ===== 代码生成 =====\n\n generateCode(): string {\n const routes = this.generate();\n const lazyImports: string[] = [];\n const errorImports: string[] = [];\n const loadingImports: string[] = [];\n\n this.collectImports(routes, lazyImports, errorImports, loadingImports);\n\n const { loading } = this.options;\n const hasCustomLoading = loading && typeof loading === \"object\" && loading.component;\n const loadingDisabled = loading === false;\n\n let loadingSection: string;\n if (loadingDisabled) {\n loadingSection = `\n// 懒加载包装(加载状态已禁用)\nfunction LazyRoute({\n Component,\n Loading,\n}: {\n Component: React.LazyExoticComponent<React.ComponentType<unknown>>;\n Loading?: React.ComponentType;\n}) {\n const fallback = Loading ? <Loading /> : null;\n return (\n <Suspense fallback={fallback}>\n <Component />\n </Suspense>\n );\n}`;\n } else if (hasCustomLoading) {\n loadingSection = `\nimport CustomDefaultLoading from \"${loading.component}\";\n\n// 懒加载包装(使用自定义默认加载组件)\nfunction LazyRoute({\n Component,\n Loading = CustomDefaultLoading,\n}: {\n Component: React.LazyExoticComponent<React.ComponentType<unknown>>;\n Loading?: React.ComponentType;\n}) {\n return (\n <Suspense fallback={<Loading />}>\n <Component />\n </Suspense>\n );\n}`;\n } else {\n loadingSection = `\n// 默认加载状态\nconst DefaultLoading = () => <div style={{ padding: 24, textAlign: \"center\" }}>加载中...</div>;\n\n// 懒加载包装\nfunction LazyRoute({\n Component,\n Loading = DefaultLoading,\n}: {\n Component: React.LazyExoticComponent<React.ComponentType<unknown>>;\n Loading?: React.ComponentType;\n}) {\n return (\n <Suspense fallback={<Loading />}>\n <Component />\n </Suspense>\n );\n}`;\n }\n\n return `// 此文件由 @4399ywkf/core 自动生成,请勿手动修改\n// Generated at: ${new Date().toISOString()}\n\nimport React, { lazy, Suspense } from \"react\";\nimport { createBrowserRouter, type RouteObject } from \"react-router\";\n\n// 懒加载页面组件\n${lazyImports.join(\"\\n\")}\n${errorImports.length > 0 ? `\\n// 错误边界组件\\n${errorImports.join(\"\\n\")}` : \"\"}\n${loadingImports.length > 0 ? `\\n// 加载状态组件\\n${loadingImports.join(\"\\n\")}` : \"\"}\n${loadingSection}\n\n// 路由配置\nexport const routes: RouteObject[] = ${this.emitRouteArray(routes)};\n\n/**\n * 创建路由实例\n */\nexport function createRouter(basename?: string) {\n return createBrowserRouter(routes, { basename: basename || \"/\" });\n}\n\nexport default routes;\n`;\n }\n\n private collectImports(\n routes: RouteConfig[],\n lazyImports: string[],\n errorImports: string[],\n loadingImports: string[],\n ): void {\n for (const route of routes) {\n const name = route.name;\n const toImportPath = (f: string) => f.replace(/\\.(tsx?|jsx?)$/, \"\");\n\n if (route.isLayout && route.layoutFile) {\n lazyImports.push(\n `const ${name}Layout = lazy(() => import(\"@/pages/${toImportPath(route.layoutFile)}\"));`,\n );\n }\n if (route.file) {\n lazyImports.push(\n `const ${name}Page = lazy(() => import(\"@/pages/${toImportPath(route.file)}\"));`,\n );\n }\n if (route.errorFile) {\n errorImports.push(\n `const ${name}Error = lazy(() => import(\"@/pages/${toImportPath(route.errorFile)}\"));`,\n );\n }\n if (route.loadingFile) {\n loadingImports.push(\n `const ${name}Loading = lazy(() => import(\"@/pages/${toImportPath(\n route.loadingFile,\n )}\"));`,\n );\n }\n\n if (route.children) {\n this.collectImports(route.children, lazyImports, errorImports, loadingImports);\n }\n }\n }\n\n private emitRouteArray(routes: RouteConfig[], parentPath?: string): string {\n const items = routes.map((r) => this.emitRouteObject(r, parentPath));\n return `[\\n ${items.join(\",\\n \")}\\n]`;\n }\n\n private emitRouteObject(route: RouteConfig, parentPath?: string): string {\n const parts: string[] = [];\n const name = route.name;\n\n // path\n if (route.index) {\n parts.push(\"index: true\");\n } else if (route.pathless) {\n // 无路径布局不输出 path\n } else if (route.path === \"*\") {\n parts.push(`path: \"*\"`);\n } else if (parentPath && route.path.startsWith(parentPath) && parentPath !== \"/\") {\n const rel = route.path.slice(parentPath.length + 1);\n parts.push(`path: \"${rel || \"\"}\"`);\n } else {\n parts.push(`path: \"${route.path}\"`);\n }\n\n // element\n if (route.isLayout && route.layoutFile) {\n const loadingProp = route.loadingFile ? ` Loading={${name}Loading}` : \"\";\n parts.push(`element: <LazyRoute Component={${name}Layout}${loadingProp} />`);\n } else if (route.file) {\n parts.push(`element: <LazyRoute Component={${name}Page} />`);\n }\n\n // errorElement\n if (route.errorFile) {\n parts.push(`errorElement: <LazyRoute Component={${name}Error} />`);\n }\n\n // children\n if (route.children && route.children.length > 0) {\n const childParent = route.pathless ? parentPath : route.path;\n parts.push(`children: ${this.emitRouteArray(route.children, childParent)}`);\n }\n\n return `{\\n ${parts.join(\",\\n \")}\\n }`;\n }\n\n /**\n * 写入路由文件\n */\n write(): void {\n const { outputDir } = this.options;\n const code = this.generateCode();\n if (!existsSync(outputDir)) {\n mkdirSync(outputDir, { recursive: true });\n }\n writeFileSync(join(outputDir, \"routes.tsx\"), code, \"utf-8\");\n if (process.env.DEBUG) console.log(`[ywkf] 约定式路由已生成`);\n }\n}\n\n/**\n * 生成约定式路由\n */\nexport function generateConventionalRoutes(options: GeneratorOptions): void {\n new ConventionalRouteGenerator(options).write();\n}\n","import type { Compiler } from \"@rspack/core\";\nimport { existsSync, watch } from \"fs\";\nimport { join } from \"path\";\nimport { ConventionalRouteGenerator } from \"./generator.js\";\n\nexport interface ConventionalRoutePluginOptions {\n /** 页面目录 */\n pagesDir: string;\n /** 输出目录 */\n outputDir: string;\n /** 路由 basename */\n basename?: string;\n /** 是否监听文件变化(开发模式) */\n watch?: boolean;\n}\n\n/**\n * 约定式路由 Rspack 插件\n *\n * 在编译开始前扫描 src/pages 目录,生成 .ywkf/routes.tsx\n */\nexport class ConventionalRoutePlugin {\n private options: ConventionalRoutePluginOptions;\n private isWatching = false;\n private hasGenerated = false;\n private lastGeneratedContent = \"\";\n\n constructor(options: ConventionalRoutePluginOptions) {\n this.options = options;\n }\n\n apply(compiler: Compiler): void {\n const pluginName = \"ConventionalRoutePlugin\";\n\n // 只在首次编译时生成路由\n compiler.hooks.beforeCompile.tapAsync(pluginName, (_params, callback) => {\n if (!this.hasGenerated) {\n this.generateRoutes();\n this.hasGenerated = true;\n }\n callback();\n });\n\n // 开发模式下监听文件变化\n if (this.options.watch && !this.isWatching) {\n this.watchPages(compiler);\n }\n }\n\n /**\n * 生成路由文件(带内容比对,避免不必要的重复生成)\n */\n private generateRoutes(): void {\n try {\n const generator = new ConventionalRouteGenerator({\n pagesDir: this.options.pagesDir,\n outputDir: this.options.outputDir,\n basename: this.options.basename,\n });\n\n const newContent = generator.generateCode();\n const _outputPath = join(this.options.outputDir, \"routes.tsx\");\n\n // 比对内容,如果相同则跳过(去掉时间戳后比较)\n const normalizedNew = this.normalizeContent(newContent);\n const normalizedOld = this.normalizeContent(this.lastGeneratedContent);\n\n if (normalizedNew === normalizedOld) {\n return; // 内容相同,跳过生成\n }\n\n generator.write();\n this.lastGeneratedContent = newContent;\n } catch (error) {\n console.error(\"[ywkf] 生成路由失败:\", error);\n }\n }\n\n /**\n * 标准化内容(去掉时间戳等动态部分)\n */\n private normalizeContent(content: string): string {\n return content.replace(/\\/\\/ Generated at: .+/g, \"\");\n }\n\n /**\n * 监听页面目录变化\n */\n private watchPages(compiler: Compiler): void {\n if (!existsSync(this.options.pagesDir)) {\n return;\n }\n\n this.isWatching = true;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n const watcher = watch(this.options.pagesDir, { recursive: true }, (_eventType, filename) => {\n // 只处理 tsx/jsx 文件\n if (!filename?.match(/\\.(tsx?|jsx?)$/)) {\n return;\n }\n\n // 防抖处理\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n }\n\n debounceTimer = setTimeout(() => {\n if (process.env.DEBUG) console.log(`[ywkf] 检测到页面变化: ${filename}`);\n this.generateRoutes();\n }, 500);\n });\n\n // 编译器关闭时(含 dev 热重启)关闭监听,避免泄漏\n compiler.hooks.shutdown.tap(\"ConventionalRoutePlugin\", () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n // 重置实例状态,使该插件实例在新编译器上可被安全复用\n this.isWatching = false;\n this.hasGenerated = false;\n this.lastGeneratedContent = \"\";\n });\n }\n}\n"],"mappings":"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","names":["existsSync","join","join","existsSync"]}
1
+ {"version":3,"sources":["../../src/router/generator.ts","../../src/router/plugin.ts"],"sourcesContent":["import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from \"fs\";\nimport { join, relative } from \"path\";\n\n/** 将 Windows 路径统一为 POSIX 正斜杠(import 路径必须用 /) */\nfunction winPath(path: string): string {\n const isExtendedLengthPath = /^\\\\\\\\\\?\\\\/.test(path);\n if (isExtendedLengthPath) {\n return path;\n }\n return path.replace(/\\\\/g, \"/\");\n}\n\n/**\n * 路由节点配置\n */\nexport interface RouteConfig {\n /** 路由路径 */\n path: string;\n /** 页面文件(page.tsx)相对路径 */\n file?: string;\n /** 布局文件(layout.tsx)相对路径 */\n layoutFile?: string;\n /** 错误边界文件(error.tsx)相对路径 */\n errorFile?: string;\n /** 加载状态文件(loading.tsx)相对路径 */\n loadingFile?: string;\n /** 通配路由文件($.tsx)相对路径 */\n catchAllFile?: string;\n /** 组件名称 */\n name: string;\n /** 子路由 */\n children?: RouteConfig[];\n /** 是否为 index 路由 */\n index?: boolean;\n /** 是否为布局路由 */\n isLayout?: boolean;\n /** 是否为无路径布局(__prefix) */\n pathless?: boolean;\n}\n\nexport interface GeneratorOptions {\n /** 页面目录 */\n pagesDir: string;\n /** 输出目录 */\n outputDir: string;\n /** 路由 basename */\n basename?: string;\n /**\n * 加载状态配置\n * - `false`:禁用全局默认加载状态(Suspense fallback 为 null,但仍可通过 loading.tsx 覆盖)\n * - `{ component: string }`:自定义默认加载组件的导入路径,例如 `\"@/components/Loading\"`\n * - `undefined`:使用内置默认加载组件\n */\n loading?: false | { component: string };\n}\n\n/**\n * 不参与路由扫描的目录名\n */\nconst EXCLUDED_DIRS = new Set([\n \"components\",\n \"hooks\",\n \"utils\",\n \"services\",\n \"models\",\n \"assets\",\n \"types\",\n \"constants\",\n \"styles\",\n]);\n\n/**\n * 约定文件名匹配\n */\nconst CONVENTION_FILES = {\n page: /^page\\.(tsx?|jsx?)$/,\n layout: /^layout\\.(tsx?|jsx?)$/,\n error: /^error\\.(tsx?|jsx?)$/,\n loading: /^loading\\.(tsx?|jsx?)$/,\n catchAll: /^\\$\\.(tsx?|jsx?)$/,\n};\n\n/**\n * Modern.js 风格约定式路由生成器\n *\n * 文件约定:\n * - page.tsx — 页面内容(叶子组件)\n * - layout.tsx — 布局组件(使用 <Outlet> 渲染子路由)\n * - error.tsx — 错误边界组件\n * - loading.tsx — 加载状态组件\n * - $.tsx — 通配/404 路由\n *\n * 目录约定:\n * - [id]/ — 动态路由 → /:id\n * - [id$]/ — 可选动态路由 → /:id?\n * - [...slug]/ — 全匹配路由 → /*\n * - __auth/ — 无路径布局(不生成 URL 片段)\n * - user.profile/ — 扁平路由(. 分隔 → /user/profile)\n */\nexport class ConventionalRouteGenerator {\n private options: GeneratorOptions;\n\n constructor(options: GeneratorOptions) {\n this.options = options;\n }\n\n generate(): RouteConfig[] {\n const { pagesDir } = this.options;\n if (!existsSync(pagesDir)) {\n console.warn(`[ywkf] 页面目录不存在: ${pagesDir}`);\n return [];\n }\n return this.scanDirectory(pagesDir, \"/\");\n }\n\n /**\n * 扫描目录,生成路由树\n */\n private scanDirectory(dir: string, routePath: string): RouteConfig[] {\n const entries = readdirSync(dir);\n\n // 查找约定文件\n const layoutFile = entries.find((e) => CONVENTION_FILES.layout.test(e));\n const pageFile = entries.find((e) => CONVENTION_FILES.page.test(e));\n const errorFile = entries.find((e) => CONVENTION_FILES.error.test(e));\n const loadingFile = entries.find((e) => CONVENTION_FILES.loading.test(e));\n const catchAllFile = entries.find((e) => CONVENTION_FILES.catchAll.test(e));\n\n // 收集子目录\n const subDirs = entries.filter((e) => {\n if (e.startsWith(\".\")) return false;\n if (EXCLUDED_DIRS.has(e)) return false;\n return statSync(join(dir, e)).isDirectory();\n });\n\n // 递归子目录\n const childRoutes: RouteConfig[] = [];\n for (const subDir of subDirs) {\n const subDirPath = join(dir, subDir);\n\n // __prefix:无路径布局\n if (subDir.startsWith(\"__\")) {\n const pathlessRoutes = this.scanDirectory(subDirPath, routePath);\n // 如果该无路径目录有 layout,将子路由包装在其中\n const pathlessLayout = readdirSync(subDirPath).find((e) => CONVENTION_FILES.layout.test(e));\n if (pathlessLayout) {\n const pathlessChildren = this.scanDirectory(subDirPath, routePath);\n const pathlessLayoutRel = winPath(\n relative(this.options.pagesDir, join(subDirPath, pathlessLayout)),\n );\n childRoutes.push({\n path: routePath,\n name: this.generateRouteName(subDir.slice(2)),\n layoutFile: pathlessLayoutRel,\n isLayout: true,\n pathless: true,\n children: pathlessChildren.filter((r) => r.layoutFile !== pathlessLayoutRel),\n });\n } else {\n childRoutes.push(...pathlessRoutes);\n }\n continue;\n }\n\n // 计算路由路径\n const subRoutePath = this.resolveRoutePath(subDir, routePath);\n childRoutes.push(...this.scanDirectory(subDirPath, subRoutePath));\n }\n\n // 构建当前目录的路由节点\n const routeName = this.generateRouteName(routePath);\n const relPath = (file: string) => winPath(relative(this.options.pagesDir, join(dir, file)));\n\n // 有 layout.tsx → 创建布局路由节点,children 为子路由\n if (layoutFile) {\n const layoutChildren: RouteConfig[] = [];\n\n // page.tsx 作为 index 子路由\n if (pageFile) {\n layoutChildren.push({\n path: routePath,\n file: relPath(pageFile),\n name: `${routeName}Index`,\n index: true,\n });\n }\n\n // 通配路由\n if (catchAllFile) {\n layoutChildren.push({\n path: \"*\",\n file: relPath(catchAllFile),\n name: `${routeName}CatchAll`,\n });\n }\n\n layoutChildren.push(...childRoutes);\n\n return [\n {\n path: routePath,\n layoutFile: relPath(layoutFile),\n errorFile: errorFile ? relPath(errorFile) : undefined,\n loadingFile: loadingFile ? relPath(loadingFile) : undefined,\n name: routeName,\n isLayout: true,\n children: layoutChildren,\n },\n ];\n }\n\n // 无 layout → page.tsx 是叶子页面\n const result: RouteConfig[] = [];\n\n if (pageFile) {\n result.push({\n path: routePath,\n file: relPath(pageFile),\n errorFile: errorFile ? relPath(errorFile) : undefined,\n name: routeName,\n });\n }\n\n if (catchAllFile) {\n result.push({\n path: \"*\",\n file: relPath(catchAllFile),\n name: `${routeName}CatchAll`,\n });\n }\n\n result.push(...childRoutes);\n return result;\n }\n\n /**\n * 解析目录名到路由路径\n *\n * - [id] → :id\n * - [id$] → :id?\n * - [...slug] → *\n * - user.profile → user/profile\n */\n private resolveRoutePath(dirName: string, parentPath: string): string {\n let segment: string;\n\n // [...slug] → *\n if (dirName.match(/^\\[\\.\\.\\.(.+)\\]$/)) {\n segment = \"*\";\n }\n // [id$] → :id? (可选动态)\n else if (dirName.match(/^\\[(.+)\\$\\]$/)) {\n const param = dirName.slice(1, -2);\n segment = `:${param}?`;\n }\n // [id] → :id\n else if (dirName.match(/^\\[(.+)\\]$/)) {\n const param = dirName.slice(1, -1);\n segment = `:${param}`;\n }\n // user.profile → user/profile (扁平路由)\n else if (dirName.includes(\".\")) {\n segment = dirName.replace(/\\./g, \"/\");\n } else {\n segment = dirName;\n }\n\n return parentPath === \"/\" ? `/${segment}` : `${parentPath}/${segment}`;\n }\n\n /**\n * 生成有效的 JS 标识符名称\n */\n private generateRouteName(path: string): string {\n const name = path\n .split(\"/\")\n .filter(Boolean)\n .map((s) => s.replace(/^:/, \"Param\").replace(/\\?$/, \"Optional\").replace(/^\\*$/, \"CatchAll\"))\n .map((s) => {\n const cleaned = s.replace(/[^a-zA-Z0-9]/g, \"\");\n if (!cleaned) return \"\";\n return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);\n })\n .join(\"\");\n\n if (!name || /^\\d/.test(name)) {\n return `Page${name || \"Root\"}`;\n }\n return name;\n }\n\n // ===== 代码生成 =====\n\n generateCode(): string {\n const routes = this.generate();\n const lazyImports: string[] = [];\n const errorImports: string[] = [];\n const loadingImports: string[] = [];\n\n this.collectImports(routes, lazyImports, errorImports, loadingImports);\n\n const { loading } = this.options;\n const hasCustomLoading = loading && typeof loading === \"object\" && loading.component;\n const loadingDisabled = loading === false;\n\n let loadingSection: string;\n if (loadingDisabled) {\n loadingSection = `\n// 懒加载包装(加载状态已禁用)\nfunction LazyRoute({\n Component,\n Loading,\n}: {\n Component: React.LazyExoticComponent<React.ComponentType<unknown>>;\n Loading?: React.ComponentType;\n}) {\n const fallback = Loading ? <Loading /> : null;\n return (\n <Suspense fallback={fallback}>\n <Component />\n </Suspense>\n );\n}`;\n } else if (hasCustomLoading) {\n loadingSection = `\nimport CustomDefaultLoading from \"${loading.component}\";\n\n// 懒加载包装(使用自定义默认加载组件)\nfunction LazyRoute({\n Component,\n Loading = CustomDefaultLoading,\n}: {\n Component: React.LazyExoticComponent<React.ComponentType<unknown>>;\n Loading?: React.ComponentType;\n}) {\n return (\n <Suspense fallback={<Loading />}>\n <Component />\n </Suspense>\n );\n}`;\n } else {\n loadingSection = `\n// 默认加载状态\nconst DefaultLoading = () => <div style={{ padding: 24, textAlign: \"center\" }}>加载中...</div>;\n\n// 懒加载包装\nfunction LazyRoute({\n Component,\n Loading = DefaultLoading,\n}: {\n Component: React.LazyExoticComponent<React.ComponentType<unknown>>;\n Loading?: React.ComponentType;\n}) {\n return (\n <Suspense fallback={<Loading />}>\n <Component />\n </Suspense>\n );\n}`;\n }\n\n return `// 此文件由 @4399ywkf/core 自动生成,请勿手动修改\n// Generated at: ${new Date().toISOString()}\n\nimport React, { lazy, Suspense } from \"react\";\nimport { createBrowserRouter, type RouteObject } from \"react-router\";\nimport { RouteErrorBoundary } from \"@4399ywkf/core/runtime\";\n${errorImports.length > 0 ? `\\n// 同步错误边界组件\\n${errorImports.join(\"\\n\")}` : \"\"}\n\n// 懒加载页面组件\n${lazyImports.join(\"\\n\")}\n${loadingImports.length > 0 ? `\\n// 加载状态组件\\n${loadingImports.join(\"\\n\")}` : \"\"}\n${loadingSection}\n\n// 路由配置\nexport const routes: RouteObject[] = ${this.emitRouteArray(routes)};\n\n/**\n * 创建路由实例\n */\nexport function createRouter(basename?: string) {\n return createBrowserRouter(routes, { basename: basename || \"/\" });\n}\n\nexport default routes;\n`;\n }\n\n private collectImports(\n routes: RouteConfig[],\n lazyImports: string[],\n errorImports: string[],\n loadingImports: string[],\n ): void {\n for (const route of routes) {\n const name = route.name;\n const toImportPath = (f: string) => f.replace(/\\.(tsx?|jsx?)$/, \"\");\n\n if (route.isLayout && route.layoutFile) {\n lazyImports.push(\n `const ${name}Layout = lazy(() => import(\"@/pages/${toImportPath(route.layoutFile)}\"));`,\n );\n }\n if (route.file) {\n lazyImports.push(\n `const ${name}Page = lazy(() => import(\"@/pages/${toImportPath(route.file)}\"));`,\n );\n }\n if (route.errorFile) {\n errorImports.push(\n `import ${name}Error from \"@/pages/${toImportPath(route.errorFile)}\";`,\n );\n }\n if (route.loadingFile) {\n loadingImports.push(\n `const ${name}Loading = lazy(() => import(\"@/pages/${toImportPath(\n route.loadingFile,\n )}\"));`,\n );\n }\n\n if (route.children) {\n this.collectImports(route.children, lazyImports, errorImports, loadingImports);\n }\n }\n }\n\n private emitRouteArray(routes: RouteConfig[], parentPath?: string, depth = 0): string {\n const items = routes.map((r) => this.emitRouteObject(r, parentPath, depth));\n return `[\\n ${items.join(\",\\n \")}\\n]`;\n }\n\n private emitRouteObject(route: RouteConfig, parentPath: string | undefined, depth: number): string {\n const parts: string[] = [];\n const name = route.name;\n\n // path\n if (route.index) {\n parts.push(\"index: true\");\n } else if (route.pathless) {\n // 无路径布局不输出 path\n } else if (route.path === \"*\") {\n parts.push(`path: \"*\"`);\n } else if (parentPath && route.path.startsWith(parentPath) && parentPath !== \"/\") {\n const rel = route.path.slice(parentPath.length + 1);\n parts.push(`path: \"${rel || \"\"}\"`);\n } else {\n parts.push(`path: \"${route.path}\"`);\n }\n\n // element\n if (route.isLayout && route.layoutFile) {\n const loadingProp = route.loadingFile ? ` Loading={${name}Loading}` : \"\";\n parts.push(`element: <LazyRoute Component={${name}Layout}${loadingProp} />`);\n } else if (route.file) {\n parts.push(`element: <LazyRoute Component={${name}Page} />`);\n }\n\n // errorElement\n if (route.errorFile) {\n parts.push(`errorElement: <RouteErrorBoundary fallback={<${name}Error />} />`);\n } else if (depth === 0) {\n // React Router 会在内部接住 lazy route rejection;顶层必须有同步 errorElement 才能触发恢复。\n parts.push(`errorElement: <RouteErrorBoundary />`);\n }\n\n // children\n if (route.children && route.children.length > 0) {\n const childParent = route.pathless ? parentPath : route.path;\n parts.push(`children: ${this.emitRouteArray(route.children, childParent, depth + 1)}`);\n }\n\n return `{\\n ${parts.join(\",\\n \")}\\n }`;\n }\n\n /**\n * 写入路由文件\n */\n write(): void {\n const { outputDir } = this.options;\n const code = this.generateCode();\n if (!existsSync(outputDir)) {\n mkdirSync(outputDir, { recursive: true });\n }\n writeFileSync(join(outputDir, \"routes.tsx\"), code, \"utf-8\");\n if (process.env.DEBUG) console.log(`[ywkf] 约定式路由已生成`);\n }\n}\n\n/**\n * 生成约定式路由\n */\nexport function generateConventionalRoutes(options: GeneratorOptions): void {\n new ConventionalRouteGenerator(options).write();\n}\n","import type { Compiler } from \"@rspack/core\";\nimport { existsSync, watch } from \"fs\";\nimport { join } from \"path\";\nimport { ConventionalRouteGenerator } from \"./generator.js\";\n\nexport interface ConventionalRoutePluginOptions {\n /** 页面目录 */\n pagesDir: string;\n /** 输出目录 */\n outputDir: string;\n /** 路由 basename */\n basename?: string;\n /** 是否监听文件变化(开发模式) */\n watch?: boolean;\n}\n\n/**\n * 约定式路由 Rspack 插件\n *\n * 在编译开始前扫描 src/pages 目录,生成 .ywkf/routes.tsx\n */\nexport class ConventionalRoutePlugin {\n private options: ConventionalRoutePluginOptions;\n private isWatching = false;\n private hasGenerated = false;\n private lastGeneratedContent = \"\";\n\n constructor(options: ConventionalRoutePluginOptions) {\n this.options = options;\n }\n\n apply(compiler: Compiler): void {\n const pluginName = \"ConventionalRoutePlugin\";\n\n // 只在首次编译时生成路由\n compiler.hooks.beforeCompile.tapAsync(pluginName, (_params, callback) => {\n if (!this.hasGenerated) {\n this.generateRoutes();\n this.hasGenerated = true;\n }\n callback();\n });\n\n // 开发模式下监听文件变化\n if (this.options.watch && !this.isWatching) {\n this.watchPages(compiler);\n }\n }\n\n /**\n * 生成路由文件(带内容比对,避免不必要的重复生成)\n */\n private generateRoutes(): void {\n try {\n const generator = new ConventionalRouteGenerator({\n pagesDir: this.options.pagesDir,\n outputDir: this.options.outputDir,\n basename: this.options.basename,\n });\n\n const newContent = generator.generateCode();\n const _outputPath = join(this.options.outputDir, \"routes.tsx\");\n\n // 比对内容,如果相同则跳过(去掉时间戳后比较)\n const normalizedNew = this.normalizeContent(newContent);\n const normalizedOld = this.normalizeContent(this.lastGeneratedContent);\n\n if (normalizedNew === normalizedOld) {\n return; // 内容相同,跳过生成\n }\n\n generator.write();\n this.lastGeneratedContent = newContent;\n } catch (error) {\n console.error(\"[ywkf] 生成路由失败:\", error);\n }\n }\n\n /**\n * 标准化内容(去掉时间戳等动态部分)\n */\n private normalizeContent(content: string): string {\n return content.replace(/\\/\\/ Generated at: .+/g, \"\");\n }\n\n /**\n * 监听页面目录变化\n */\n private watchPages(compiler: Compiler): void {\n if (!existsSync(this.options.pagesDir)) {\n return;\n }\n\n this.isWatching = true;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n const watcher = watch(this.options.pagesDir, { recursive: true }, (_eventType, filename) => {\n // 只处理 tsx/jsx 文件\n if (!filename?.match(/\\.(tsx?|jsx?)$/)) {\n return;\n }\n\n // 防抖处理\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n }\n\n debounceTimer = setTimeout(() => {\n if (process.env.DEBUG) console.log(`[ywkf] 检测到页面变化: ${filename}`);\n this.generateRoutes();\n }, 500);\n });\n\n // 编译器关闭时(含 dev 热重启)关闭监听,避免泄漏\n compiler.hooks.shutdown.tap(\"ConventionalRoutePlugin\", () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n // 重置实例状态,使该插件实例在新编译器上可被安全复用\n this.isWatching = false;\n this.hasGenerated = false;\n this.lastGeneratedContent = \"\";\n });\n }\n}\n"],"mappings":"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","names":["existsSync","join","join","existsSync"]}
@@ -1,6 +1,6 @@
1
1
  import { Configuration } from '@rspack/core';
2
- import { Y as YwkfConfig } from '../schema-CuEnqbqQ.js';
3
- import '../types-BYqzyGAY.js';
2
+ import { Y as YwkfConfig } from '../schema-QzJlmn39.js';
3
+ import '../types-ZJhd87ue.js';
4
4
  import 'react';
5
5
  import 'react-router';
6
6
 
@@ -874,7 +874,7 @@ function createMockMiddleware(mockPath, options) {
874
874
  scanMockFiles(mockPath);
875
875
  return async (_req, _res, next) => {
876
876
  if (delay > 0) {
877
- await new Promise((resolve3) => setTimeout(resolve3, delay));
877
+ await new Promise((resolve4) => setTimeout(resolve4, delay));
878
878
  }
879
879
  next();
880
880
  };
@@ -1828,11 +1828,11 @@ var zustandPlugin = createPlugin((options = {}) => ({
1828
1828
  const hooks = {
1829
1829
  modifyRspackConfig(rspackConfig) {
1830
1830
  logger.info("Zustand \u5DF2\u542F\u7528");
1831
- const resolve3 = rspackConfig.resolve || {};
1832
- const alias = resolve3.alias || {};
1831
+ const resolve4 = rspackConfig.resolve || {};
1832
+ const alias = resolve4.alias || {};
1833
1833
  alias["@store"] = storePath;
1834
- resolve3.alias = alias;
1835
- rspackConfig.resolve = resolve3;
1834
+ resolve4.alias = alias;
1835
+ rspackConfig.resolve = resolve4;
1836
1836
  return rspackConfig;
1837
1837
  },
1838
1838
  generateFiles(_ctx) {
@@ -2322,12 +2322,13 @@ function LazyRoute({
2322
2322
 
2323
2323
  import React, { lazy, Suspense } from "react";
2324
2324
  import { createBrowserRouter, type RouteObject } from "react-router";
2325
+ import { RouteErrorBoundary } from "@4399ywkf/core/runtime";
2326
+ ${errorImports.length > 0 ? `
2327
+ // \u540C\u6B65\u9519\u8BEF\u8FB9\u754C\u7EC4\u4EF6
2328
+ ${errorImports.join("\n")}` : ""}
2325
2329
 
2326
2330
  // \u61D2\u52A0\u8F7D\u9875\u9762\u7EC4\u4EF6
2327
2331
  ${lazyImports.join("\n")}
2328
- ${errorImports.length > 0 ? `
2329
- // \u9519\u8BEF\u8FB9\u754C\u7EC4\u4EF6
2330
- ${errorImports.join("\n")}` : ""}
2331
2332
  ${loadingImports.length > 0 ? `
2332
2333
  // \u52A0\u8F7D\u72B6\u6001\u7EC4\u4EF6
2333
2334
  ${loadingImports.join("\n")}` : ""}
@@ -2362,7 +2363,7 @@ export default routes;
2362
2363
  }
2363
2364
  if (route.errorFile) {
2364
2365
  errorImports.push(
2365
- `const ${name}Error = lazy(() => import("@/pages/${toImportPath(route.errorFile)}"));`
2366
+ `import ${name}Error from "@/pages/${toImportPath(route.errorFile)}";`
2366
2367
  );
2367
2368
  }
2368
2369
  if (route.loadingFile) {
@@ -2377,13 +2378,13 @@ export default routes;
2377
2378
  }
2378
2379
  }
2379
2380
  }
2380
- emitRouteArray(routes, parentPath) {
2381
- const items = routes.map((r) => this.emitRouteObject(r, parentPath));
2381
+ emitRouteArray(routes, parentPath, depth = 0) {
2382
+ const items = routes.map((r) => this.emitRouteObject(r, parentPath, depth));
2382
2383
  return `[
2383
2384
  ${items.join(",\n ")}
2384
2385
  ]`;
2385
2386
  }
2386
- emitRouteObject(route, parentPath) {
2387
+ emitRouteObject(route, parentPath, depth) {
2387
2388
  const parts = [];
2388
2389
  const name = route.name;
2389
2390
  if (route.index) {
@@ -2404,11 +2405,13 @@ export default routes;
2404
2405
  parts.push(`element: <LazyRoute Component={${name}Page} />`);
2405
2406
  }
2406
2407
  if (route.errorFile) {
2407
- parts.push(`errorElement: <LazyRoute Component={${name}Error} />`);
2408
+ parts.push(`errorElement: <RouteErrorBoundary fallback={<${name}Error />} />`);
2409
+ } else if (depth === 0) {
2410
+ parts.push(`errorElement: <RouteErrorBoundary />`);
2408
2411
  }
2409
2412
  if (route.children && route.children.length > 0) {
2410
2413
  const childParent = route.pathless ? parentPath : route.path;
2411
- parts.push(`children: ${this.emitRouteArray(route.children, childParent)}`);
2414
+ parts.push(`children: ${this.emitRouteArray(route.children, childParent, depth + 1)}`);
2412
2415
  }
2413
2416
  return `{
2414
2417
  ${parts.join(",\n ")}
@@ -3013,14 +3016,93 @@ var YwkfGeneratorPlugin = class {
3013
3016
  }
3014
3017
  };
3015
3018
 
3019
+ // src/rspack/client-env.ts
3020
+ import { existsSync as existsSync12, readFileSync as readFileSync4 } from "fs";
3021
+ import { resolve as resolve3 } from "path";
3022
+ import dotenv from "dotenv";
3023
+ var FRAMEWORK_ENV_KEYS = [
3024
+ "APP_CNAME",
3025
+ "APP_HOST",
3026
+ "APP_MODE",
3027
+ "APP_NAME",
3028
+ "APP_PORT",
3029
+ "DEBUG",
3030
+ "NODE_ENV",
3031
+ "OUTPUT_PATH",
3032
+ "PUBLIC_PATH",
3033
+ "YWKF_BUILD_ID",
3034
+ "YWKF_PREFIX_CLS"
3035
+ ];
3036
+ var DEFAULT_EXPOSE_PREFIXES = ["YWKF_PUBLIC_", "NEXT_PUBLIC_"];
3037
+ var SECRET_KEY_PATTERN = /TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE|AUTH|ACCESS_KEY|API_KEY|SIGNING_KEY/i;
3038
+ var SECRET_VALUE_PATTERN = /^(sntry[su]_|sk-|ghp_|gho_|ghu_|ghs_|github_pat_|glpat-|xox[abprs]-|AKIA|ya29\.)/;
3039
+ function readEnvFileKeys(filePath) {
3040
+ if (!existsSync12(filePath)) return [];
3041
+ try {
3042
+ return Object.keys(dotenv.parse(readFileSync4(filePath, "utf-8")));
3043
+ } catch {
3044
+ return [];
3045
+ }
3046
+ }
3047
+ function collectClientEnvKeys(options) {
3048
+ const { cwd, env = {}, mode = process.env.APP_MODE || "development" } = options;
3049
+ const keys = new Set(FRAMEWORK_ENV_KEYS);
3050
+ const files = [
3051
+ env.publicEnvFile ?? "config/env/.env.public",
3052
+ ".env.public",
3053
+ ".env",
3054
+ `${env.envDir ?? "config/env"}/.env.${mode}`,
3055
+ `.env.${mode}`
3056
+ ];
3057
+ for (const file of files) {
3058
+ for (const key of readEnvFileKeys(resolve3(cwd, file))) keys.add(key);
3059
+ }
3060
+ for (const key of env.expose ?? []) keys.add(key);
3061
+ return keys;
3062
+ }
3063
+ function buildClientEnv(options) {
3064
+ const { cwd, env = {}, mode, processEnv = process.env } = options;
3065
+ const prefixes = env.exposePrefixes ?? DEFAULT_EXPOSE_PREFIXES;
3066
+ const allowed = collectClientEnvKeys({ cwd, env, mode });
3067
+ const result = {};
3068
+ const dropped = [];
3069
+ for (const [key, value] of Object.entries(processEnv)) {
3070
+ if (value === void 0) continue;
3071
+ const isAllowed = allowed.has(key) || prefixes.some((prefix) => key.startsWith(prefix));
3072
+ if (!isAllowed) continue;
3073
+ if (SECRET_KEY_PATTERN.test(key) || SECRET_VALUE_PATTERN.test(value)) {
3074
+ dropped.push(key);
3075
+ continue;
3076
+ }
3077
+ result[key] = value;
3078
+ }
3079
+ return { env: result, dropped };
3080
+ }
3081
+
3016
3082
  // src/rspack/base.ts
3017
3083
  var require2 = createRequire(import.meta.url);
3018
3084
  var __dirname = dirname(fileURLToPath(import.meta.url));
3019
3085
  var coreNodeModules = join12(__dirname, "../../node_modules");
3086
+ function resolveYwkfBuildId(appName) {
3087
+ const releaseId = [
3088
+ process.env.YWKF_BUILD_ID,
3089
+ process.env.CI_COMMIT_SHA,
3090
+ process.env.GITHUB_SHA,
3091
+ process.env.GIT_COMMIT
3092
+ ].find((value) => value?.trim())?.trim();
3093
+ return `${appName}:${releaseId || Date.now().toString(36)}`;
3094
+ }
3020
3095
  function createBaseConfig(config, cwd, options = {}) {
3021
3096
  const isDev = options.isDev ?? process.env.NODE_ENV !== "production";
3022
3097
  const { resolveApp } = createPathResolver(cwd);
3023
3098
  const { appName, appCName, output, html, alias: userAlias, microFrontend, router, reactCompiler } = config;
3099
+ const buildId = resolveYwkfBuildId(appName);
3100
+ const { env: clientEnv, dropped: droppedEnvKeys } = buildClientEnv({ cwd, env: config.env });
3101
+ if (droppedEnvKeys.length > 0) {
3102
+ console.warn(
3103
+ `[ywkf] \u4EE5\u4E0B\u73AF\u5883\u53D8\u91CF\u7591\u4F3C\u5BC6\u94A5,\u5DF2\u4ECE\u5BA2\u6237\u7AEF\u4EA7\u7269\u4E2D\u5254\u9664:${droppedEnvKeys.join(", ")}\u3002\u5982\u786E\u4E3A\u516C\u5F00\u914D\u7F6E,\u8BF7\u91CD\u547D\u540D\u6216\u8C03\u6574 env.expose\u3002`
3104
+ );
3105
+ }
3024
3106
  const ywkfOutputDir = resolveApp(".ywkf");
3025
3107
  const defaultAlias = {
3026
3108
  "@": resolveApp("src"),
@@ -3144,7 +3226,11 @@ function createBaseConfig(config, cwd, options = {}) {
3144
3226
  Buffer: ["buffer", "Buffer"]
3145
3227
  }),
3146
3228
  new rspack2.DefinePlugin({
3147
- "process.env": JSON.stringify(process.env)
3229
+ "globalThis.__YWKF_BUILD_ID__": JSON.stringify(buildId),
3230
+ // 只内联白名单内的公开配置。整包 process.env 会把构建机环境(CI 凭据、内网地址)
3231
+ // 写进公网可下载的产物,见 client-env.ts。外层括号不可省:该值以 `{` 开头,
3232
+ // `() => process.env.X` 会被替换成 `() => {…}` 而当成函数块体,导致整条 chunk 语法失败。
3233
+ "process.env": `(${JSON.stringify(clientEnv)})`
3148
3234
  }),
3149
3235
  new rspack2.HtmlRspackPlugin({
3150
3236
  template: resolveApp(html.template ?? "public/index.html"),