@qlover/create-app 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
 
2
2
 
3
+ ## [0.1.11](https://github.com/qlover/fe-base/compare/create-app-v0.1.10...create-app-v0.1.11) (2025-03-12)
4
+
5
+ ## [0.1.10](https://github.com/qlover/fe-base/compare/create-app-v0.1.9...create-app-v0.1.10) (2025-02-20)
6
+
3
7
  ## [0.1.9](https://github.com/qlover/fe-base/compare/create-app-v0.1.8...create-app-v0.1.9) (2025-02-20)
4
8
 
5
9
 
@@ -40,7 +40,7 @@ jobs:
40
40
  npm install -g pnpm
41
41
  pnpm -v
42
42
 
43
- - name: Install dependencies
43
+ - name: Pnpm Install dependencies
44
44
  run: pnpm install
45
45
 
46
46
  - name: Pnpm Lint
@@ -90,6 +90,9 @@ jobs:
90
90
  npm install -g pnpm
91
91
  pnpm -v
92
92
 
93
+ - name: Pnpm Install dependencies
94
+ run: pnpm install
95
+
93
96
  - name: Pnpm Lint
94
97
  run: pnpm lint
95
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlover/create-app",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/es/index.js",
@@ -0,0 +1,64 @@
1
+ import { LazyExoticComponent, PropsWithChildren } from 'react';
2
+ import { RouteObject } from 'react-router-dom';
3
+
4
+ import { TFunction } from 'i18next';
5
+ import { UseTranslationResponse } from 'react-i18next';
6
+
7
+ export interface BasePageProvider {
8
+ meta: RouteMeta;
9
+ i18n: UseTranslationResponse<string, string>;
10
+ t: TFunction<string, string>;
11
+ }
12
+
13
+ export type RouteCategory = 'main' | 'auth' | 'common';
14
+
15
+ export interface RouteMeta {
16
+ category?: RouteCategory;
17
+ /**
18
+ * from app.router.json
19
+ */
20
+ title?: string;
21
+ icon?: string;
22
+ /**
23
+ * from app.router.json
24
+ *
25
+ * @default 'common'
26
+ */
27
+ localNamespace?: string;
28
+ }
29
+
30
+ export type RouteType = RouteObject & {
31
+ meta?: RouteMeta;
32
+ };
33
+
34
+ export type RouteConfig = {
35
+ routes: RouteType[];
36
+ };
37
+
38
+ export type PagesMaps = Record<
39
+ string,
40
+ | (() => LazyExoticComponent<React.ComponentType<unknown>>)
41
+ | (() => React.ComponentType<unknown>)
42
+ >;
43
+
44
+ export type LoadProps = {
45
+ pagesMaps: PagesMaps;
46
+ componentPath: string;
47
+ route: RouteType;
48
+ Provider?: React.ComponentType<PropsWithChildren<RouteMeta>>;
49
+ };
50
+
51
+
52
+ type ComponentValue = React.ComponentType<unknown> | LazyExoticComponent<React.ComponentType<unknown>>;
53
+
54
+ export type ComponentMaps = {
55
+ /**
56
+ * key: ./xxx/bbb.(jsx,js,tsx,ts)
57
+ */
58
+ [key: string]: ComponentValue | (() => Promise<ComponentValue>) | (() => ComponentValue);
59
+ }
60
+
61
+ // new RouterManager({
62
+ // routes: [],
63
+ // componentMaps:
64
+ // });
@@ -0,0 +1,90 @@
1
+ import { RouteObject } from 'react-router-dom';
2
+ import isString from 'lodash/isString';
3
+ import type { ComponentType, LazyExoticComponent, ReactNode } from 'react';
4
+
5
+ type ComponentValue = Record<string, () => unknown>;
6
+
7
+ type RouteComponentType<T = unknown> =
8
+ | ComponentType<T>
9
+ | LazyExoticComponent<ComponentType<T>>;
10
+
11
+ export type RouteConfigValue = Omit<RouteObject, 'element' | 'children'> & {
12
+ /**
13
+ * 路径
14
+ *
15
+ * FIXME: support `ReactNode`
16
+ */
17
+ element?: string;
18
+ children?: RouteConfigValue[];
19
+ meta?: Record<string, unknown>;
20
+ };
21
+
22
+ export type RouterLoaderRender = (
23
+ route: Omit<RouteConfigValue, 'element'> & {
24
+ element: () => RouteComponentType;
25
+ }
26
+ ) => ReactNode;
27
+
28
+ export type RouterLoaderOptions = {
29
+ /**
30
+ * 路由配置
31
+ */
32
+ routes?: RouteConfigValue[];
33
+
34
+ /**
35
+ * 组件映射表
36
+ */
37
+ componentMaps?: ComponentValue;
38
+
39
+ /**
40
+ * 渲染路由
41
+ */
42
+ render: RouterLoaderRender;
43
+ };
44
+
45
+ export class RouterLoader {
46
+ constructor(private readonly options: RouterLoaderOptions) {
47
+ if (!options.render) {
48
+ throw new Error('RouterLoader render is required');
49
+ }
50
+ }
51
+
52
+ getComponentMaps(): ComponentValue {
53
+ const { componentMaps = {} } = this.options;
54
+
55
+ return componentMaps;
56
+ }
57
+
58
+ getComponent(element: string): () => RouteComponentType {
59
+ const maps = this.getComponentMaps();
60
+
61
+ const component = maps[element];
62
+
63
+ if (!component) {
64
+ throw new Error(`Component not found: ${element}`);
65
+ }
66
+
67
+ return component as () => RouteComponentType;
68
+ }
69
+
70
+ toRoute(route: RouteConfigValue): RouteObject {
71
+ const { render } = this.options;
72
+ const { element, children, ...rest } = route;
73
+
74
+ if (!element || !isString(element)) {
75
+ console.warn(
76
+ `Invalid route, path is: ${route.path}, element is: ${element}`
77
+ );
78
+ }
79
+
80
+ const componet = this.getComponent(element || '404');
81
+ const Element = render({ ...rest, element: componet });
82
+
83
+ // @ts-expect-error
84
+ return {
85
+ ...rest,
86
+ element: Element,
87
+ children: children?.map((child) => this.toRoute(child))
88
+ };
89
+ }
90
+ }
@@ -1,14 +1,35 @@
1
1
  import '@/uikit/styles/css/index.css';
2
2
  import { createBrowserRouter, RouterProvider } from 'react-router-dom';
3
- import { createFeReactRoutes } from './pages';
4
- import { useMemo } from 'react';
5
- import { IOC } from './core';
6
- import { RouterController } from './uikit/controllers/RouterController';
3
+ import { lazy, useMemo } from 'react';
4
+ import { RouterLoader } from '@lib/router-loader/RouterLoader';
5
+ import appConfig from '@config/app.router.json';
6
+ import { RouterRenderComponent } from './components/RouterRenderComponent';
7
+ import { PagesMaps } from './base/types/Page';
8
+
9
+ function getAllPages() {
10
+ const modules = import.meta.glob('./pages/**/*.tsx');
11
+ return Object.keys(modules).reduce((acc, path) => {
12
+ const componentName = path.replace(/^\.\/pages\/(.*)\.tsx$/, '$1');
13
+ acc[componentName] = () =>
14
+ lazy(
15
+ modules[path] as () => Promise<{
16
+ default: React.ComponentType<unknown>;
17
+ }>
18
+ );
19
+ return acc;
20
+ }, {} as PagesMaps);
21
+ }
22
+
23
+ const routerLoader = new RouterLoader({
24
+ componentMaps: getAllPages(),
25
+ render: RouterRenderComponent
26
+ });
7
27
 
8
28
  function App() {
9
29
  const routerBase = useMemo(() => {
10
- const routerController = IOC(RouterController);
11
- const routes = createFeReactRoutes(routerController.getRoutes());
30
+ const routes = appConfig.base.routes.map((route) =>
31
+ routerLoader.toRoute(route)
32
+ );
12
33
  const router = createBrowserRouter(routes);
13
34
  return router;
14
35
  }, []);
@@ -3,6 +3,7 @@ import { RouteObject } from 'react-router-dom';
3
3
 
4
4
  import { TFunction } from 'i18next';
5
5
  import { UseTranslationResponse } from 'react-i18next';
6
+ import { RouteConfigValue } from '@lib/router-loader/RouterLoader';
6
7
 
7
8
  export interface BasePageProvider {
8
9
  meta: RouteMeta;
@@ -27,12 +28,8 @@ export interface RouteMeta {
27
28
  localNamespace?: string;
28
29
  }
29
30
 
30
- export type RouteType = RouteObject & {
31
- meta?: RouteMeta;
32
- };
33
-
34
31
  export type RouteConfig = {
35
- routes: RouteType[];
32
+ routes: RouteConfigValue[];
36
33
  };
37
34
 
38
35
  export type PagesMaps = Record<
@@ -0,0 +1,16 @@
1
+ import { Suspense } from 'react';
2
+ import { Loading } from './Loading';
3
+ import { RouterLoaderRender } from '@lib/router-loader/RouterLoader';
4
+ import BaseRouteProvider from '@/uikit/providers/BaseRouteProvider';
5
+
6
+ export const RouterRenderComponent: RouterLoaderRender = (route) => {
7
+ const Component = route.element();
8
+
9
+ return (
10
+ <Suspense fallback={<Loading fullscreen />}>
11
+ <BaseRouteProvider {...route.meta}>
12
+ <Component />
13
+ </BaseRouteProvider>
14
+ </Suspense>
15
+ );
16
+ };
@@ -1,7 +1,7 @@
1
1
  import * as feGlobals from '@/core/globals';
2
2
  import { IOC } from '.';
3
3
  import { IOCInterface } from '@/base/port/IOCInterface';
4
- import { I18nService } from '@/services/i18n';
4
+ import { I18nService } from '@/services/I18nService';
5
5
 
6
6
  export class Bootstrap {
7
7
  constructor(private IOCContainer: IOCInterface) {}
@@ -5,15 +5,15 @@ import HttpApi from 'i18next-http-backend';
5
5
  import merge from 'lodash/merge';
6
6
  import { i18nConfig, I18nServiceLocale } from '@config/i18n';
7
7
 
8
+ const { supportedLngs, fallbackLng } = i18nConfig;
9
+
8
10
  // custom language detector
9
11
  const pathLanguageDetector = {
10
12
  name: 'pathLanguageDetector',
11
13
  lookup() {
12
14
  const path = window.location.pathname.split('/');
13
15
  const language = path[1];
14
- return I18nService.isValidLanguage(language)
15
- ? language
16
- : i18nConfig.fallbackLng;
16
+ return I18nService.isValidLanguage(language) ? language : fallbackLng;
17
17
  },
18
18
  cacheUserLanguage() {
19
19
  // no cache, because we get language from URL
@@ -39,12 +39,16 @@ export class I18nService {
39
39
  i18n.services.languageDetector.addDetector(pathLanguageDetector);
40
40
  }
41
41
 
42
+ static getCurrentLanguage(): I18nServiceLocale {
43
+ return i18n.language as I18nServiceLocale;
44
+ }
45
+
42
46
  /**
43
47
  * check if the language is supported
44
48
  * @param language - language to check
45
49
  * @returns true if the language is supported, false otherwise
46
50
  */
47
51
  static isValidLanguage(language: string): language is I18nServiceLocale {
48
- return i18nConfig.supportedLngs.includes(language as I18nServiceLocale);
52
+ return supportedLngs.includes(language as I18nServiceLocale);
49
53
  }
50
54
  }
@@ -22,8 +22,8 @@ export class ProcesserService {
22
22
  }
23
23
 
24
24
  init(): Promise<unknown> {
25
- return this.executor.exec(this.handler).catch(() => {
26
- this.options.logger.error('PageProcesser init failed');
25
+ return this.executor.exec(this.handler).catch((err) => {
26
+ this.options.logger.error('PageProcesser init failed', err);
27
27
  });
28
28
  }
29
29
  }
@@ -1,12 +1,11 @@
1
- import { I18nService } from '@/services/i18n';
2
- import { RouteConfig, RouteType } from '@/base/types/Page';
1
+ import { I18nService } from '@/services/I18nService';
3
2
  import { UIDependenciesInterface } from '@/base/port/UIDependenciesInterface';
4
- import { i18nConfig } from '@config/i18n';
5
3
  import { Logger } from '@qlover/fe-utils';
6
4
  import { NavigateFunction, NavigateOptions } from 'react-router-dom';
5
+ import { RouteConfigValue } from '@lib/router-loader/RouterLoader';
6
+ import { RouteConfig } from '@/base/types/Page';
7
7
 
8
8
  export type RouterControllerDependencies = {
9
- location: globalThis.Location;
10
9
  navigate: NavigateFunction;
11
10
  };
12
11
 
@@ -16,7 +15,7 @@ export type RouterControllerOptions = {
16
15
  };
17
16
 
18
17
  export type RouterControllerState = {
19
- routes: RouteType[];
18
+ routes: RouteConfigValue[];
20
19
  };
21
20
 
22
21
  export class RouterController
@@ -49,6 +48,11 @@ export class RouterController
49
48
  return navigate;
50
49
  }
51
50
 
51
+ static composePath(path: string): string {
52
+ const targetLang = I18nService.getCurrentLanguage();
53
+ return `/${targetLang}${path}`;
54
+ }
55
+
52
56
  /**
53
57
  * @override
54
58
  */
@@ -59,24 +63,17 @@ export class RouterController
59
63
  ) as RouterControllerDependencies;
60
64
  }
61
65
 
62
- getRoutes(): RouteType[] {
66
+ getRoutes(): RouteConfigValue[] {
63
67
  return this.state.routes;
64
68
  }
65
69
 
66
- changeRoutes(routes: RouteType[]): void {
70
+ changeRoutes(routes: RouteConfigValue[]): void {
67
71
  this.state.routes = routes;
68
72
  }
69
73
 
70
74
  goto(path: string, options?: NavigateOptions): void {
71
- console.trace('path');
72
-
73
- const tryLng = path.split('/')[0];
74
- if (!I18nService.isValidLanguage(tryLng)) {
75
- path = i18nConfig.fallbackLng + path.replace(tryLng, '');
76
- }
77
-
78
- this.navigate?.(path, options);
79
-
75
+ path = RouterController.composePath(path);
76
+ this.logger.debug('Goto path => ', path);
80
77
  this.navigate?.(path, options);
81
78
  }
82
79
 
@@ -1,4 +1,4 @@
1
- import { I18nService } from '@/services/i18n';
1
+ import { I18nService } from '@/services/I18nService';
2
2
  import { I18nServiceLocale } from '@config/i18n';
3
3
  import { useEffect } from 'react';
4
4
  import { useNavigate } from 'react-router-dom';
@@ -26,9 +26,7 @@ export function ProcessProvider({ children }: { children: React.ReactNode }) {
26
26
  }, []);
27
27
 
28
28
  useEffect(() => {
29
- IOC(RouterController).setDependencies({
30
- navigate
31
- });
29
+ IOC(RouterController).setDependencies({ navigate });
32
30
  }, [navigate]);
33
31
 
34
32
  if (!success) {
@@ -1,113 +0,0 @@
1
- import { lazy, Suspense } from 'react';
2
- import isString from 'lodash/isString';
3
- import {
4
- LoadProps,
5
- PagesMaps,
6
- RouteCategory,
7
- RouteType
8
- } from '@/base/types/Page';
9
- import { Loading } from '@/components/Loading';
10
- import BaseRouteProvider from '../uikit/providers/BaseRouteProvider';
11
- import NotFound from './404';
12
- import NotFound500 from './500';
13
-
14
- const staticComponentsMaps: Record<string, () => React.ComponentType<unknown>> =
15
- {
16
- '404': () => NotFound as React.ComponentType<unknown>,
17
- '500': () => NotFound500 as React.ComponentType<unknown>
18
- };
19
-
20
- const getRealComponents = () => {
21
- return import.meta.glob('./*/**/*.tsx');
22
- };
23
-
24
- const getLazyComponentMaps = () => {
25
- const modules = getRealComponents();
26
-
27
- const pagesMaps: PagesMaps = {};
28
-
29
- for (const path in modules) {
30
- const componentName = path.replace(/^\.\/(.*)\.tsx$/, '$1');
31
-
32
- pagesMaps[componentName] = () =>
33
- lazy(
34
- modules[path] as () => Promise<{
35
- default: React.ComponentType<unknown>;
36
- }>
37
- );
38
- }
39
-
40
- return pagesMaps;
41
- };
42
-
43
- // 懒加载组件
44
- const lazyLoad = ({ pagesMaps, componentPath, route, Provider }: LoadProps) => {
45
- // first try static
46
- let loadedComponent = staticComponentsMaps[componentPath];
47
- if (!loadedComponent) {
48
- loadedComponent = pagesMaps[componentPath];
49
- }
50
-
51
- if (!loadedComponent) {
52
- console.warn(`Route ${componentPath} not found`);
53
- return <NotFound route={componentPath} />;
54
- }
55
-
56
- const Component = loadedComponent();
57
-
58
- return (
59
- <Suspense fallback={<Loading fullscreen />}>
60
- {Provider ? (
61
- <Provider {...route.meta}>
62
- <Component />
63
- </Provider>
64
- ) : (
65
- <Component />
66
- )}
67
- </Suspense>
68
- );
69
- };
70
-
71
- // 转换单个路由
72
- const transformRoute = (
73
- route: RouteType,
74
- options: Pick<LoadProps, 'pagesMaps' | 'Provider'>
75
- ): RouteType => {
76
- const result: RouteType = {
77
- ...route,
78
- path: route.path
79
- };
80
-
81
- if (isString(route.element)) {
82
- result.element = lazyLoad({
83
- ...options,
84
- componentPath: route.element,
85
- route
86
- });
87
- }
88
-
89
- if (route.children) {
90
- result.children = route.children.map((child) =>
91
- transformRoute(child, options)
92
- );
93
- }
94
-
95
- return result;
96
- };
97
-
98
- export function createFeReactRoutes(routes: RouteType[]) {
99
- const pagesMaps = getLazyComponentMaps();
100
-
101
- return routes.map((route) =>
102
- transformRoute(route, { pagesMaps, Provider: BaseRouteProvider })
103
- );
104
- }
105
-
106
- export function filterRoutesByCategory(
107
- routes: RouteType[],
108
- category: RouteCategory[]
109
- ): RouteType[] {
110
- return routes.filter((route) =>
111
- route.meta?.category ? category.includes(route.meta?.category) : true
112
- );
113
- }