@thanhphi/create-360-miniapp 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +328 -2
- package/bin/create-360-miniapp.cjs +54 -0
- package/lib/create.cjs +56 -0
- package/package.json +24 -4
- package/template/.eslintrc.cjs +1 -0
- package/template/.prettierrc.cjs +1 -0
- package/template/.watchmanconfig +1 -0
- package/template/App.tsx +2 -0
- package/template/README.md +215 -0
- package/template/__tests__/auth.test.ts +287 -0
- package/template/aliases.cjs +16 -0
- package/template/app.config.js +23 -0
- package/template/app.json +17 -0
- package/template/assets/.gitkeep +0 -0
- package/template/babel.config.js +7 -0
- package/template/declare/.gitkeep +0 -0
- package/template/declare/assets.d.ts +11 -0
- package/template/env/beta.json +16 -0
- package/template/env/prod.json +16 -0
- package/template/env/staging.json +16 -0
- package/template/gitignore +19 -0
- package/template/index.js +2 -0
- package/template/index.repack.js +14 -0
- package/template/jest.config.cjs +29 -0
- package/template/metro.config.js +6 -0
- package/template/package.json +88 -0
- package/template/pipelines/beta.yml +91 -0
- package/template/pipelines/production.yml +91 -0
- package/template/pipelines/staging.yml +91 -0
- package/template/pipelines/templates/build-and-zip.yml +89 -0
- package/template/pipelines/templates/upload-and-register.yml +89 -0
- package/template/react-native.config.js +3 -0
- package/template/rspack.config.cjs +53 -0
- package/template/scripts/clean-mini-app-output.js +9 -0
- package/template/scripts/sync-env.js +29 -0
- package/template/src/App.tsx +50 -0
- package/template/src/StandaloneApp.tsx +43 -0
- package/template/src/assets/icons/.gitkeep +1 -0
- package/template/src/assets/images/.gitkeep +1 -0
- package/template/src/assets/svg/.gitkeep +1 -0
- package/template/src/bootstrap.tsx +7 -0
- package/template/src/common/mini-app/provider.tsx +45 -0
- package/template/src/common/mini-app/types.ts +13 -0
- package/template/src/config/.gitkeep +0 -0
- package/template/src/constants/.gitkeep +0 -0
- package/template/src/hooks/.gitkeep +0 -0
- package/template/src/library/components/.gitkeep +0 -0
- package/template/src/library/networking/client.ts +53 -0
- package/template/src/library/networking/errors.ts +25 -0
- package/template/src/library/utils/responsive.ts +7 -0
- package/template/src/library/utils/storage/.gitkeep +0 -0
- package/template/src/modules/__MODULE__/assets/.gitkeep +0 -0
- package/template/src/modules/__MODULE__/components/ProfileCard.tsx +38 -0
- package/template/src/modules/__MODULE__/constants/.gitkeep +0 -0
- package/template/src/modules/__MODULE__/navigation/ModuleNavigator.tsx +18 -0
- package/template/src/modules/__MODULE__/redux/home-slice.ts +13 -0
- package/template/src/modules/__MODULE__/screens/home/hooks/use-home.ts +30 -0
- package/template/src/modules/__MODULE__/screens/home/index.tsx +100 -0
- package/template/src/modules/__MODULE__/services/profile.service.ts +32 -0
- package/template/src/modules/__MODULE__/types/.gitkeep +0 -0
- package/template/src/modules/auth/components/.gitkeep +0 -0
- package/template/src/modules/auth/constants/.gitkeep +0 -0
- package/template/src/modules/auth/hooks/use-auth.ts +55 -0
- package/template/src/modules/auth/navigation/AuthenticationNavigator.tsx +16 -0
- package/template/src/modules/auth/redux/auth-slice.ts +36 -0
- package/template/src/modules/auth/redux/selectors.ts +6 -0
- package/template/src/modules/auth/screens/LoginScreen.tsx +78 -0
- package/template/src/modules/auth/services/oidc.ts +64 -0
- package/template/src/modules/auth/services/standalone-session.ts +121 -0
- package/template/src/navigation/MainStack.tsx +46 -0
- package/template/src/redux/RootReducers.ts +8 -0
- package/template/src/redux/hooks.ts +5 -0
- package/template/src/redux/store.ts +10 -0
- package/template/src/services/application.ts +87 -0
- package/template/src/themes/colors/dark.ts +90 -0
- package/template/src/themes/colors/light.ts +90 -0
- package/template/src/themes/index.ts +38 -0
- package/template/src/themes/standalone.ts +6 -0
- package/template/src/themes/ui-geometry.ts +12 -0
- package/template/src/types/.gitkeep +0 -0
- package/template/tsconfig.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thanhphi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,329 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @thanhphi/create-360-miniapp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Tạo mini app React Native chạy bên trong MySGR, có thể chạy độc lập để phát triển và kiểm thử.
|
|
4
|
+
|
|
5
|
+
Project được tạo sẵn gồm TypeScript, navigation, đăng nhập qua host, HTTP client có refresh token, màn hồ sơ mẫu, theme 360, cấu hình môi trường và pipeline Azure DevOps.
|
|
6
|
+
|
|
7
|
+
## Mục lục
|
|
8
|
+
|
|
9
|
+
- [Bắt đầu nhanh](#bắt-đầu-nhanh)
|
|
10
|
+
- [Yêu cầu](#yêu-cầu)
|
|
11
|
+
- [Các lựa chọn khi tạo app](#các-lựa-chọn-khi-tạo-app)
|
|
12
|
+
- [Cấu trúc project](#cấu-trúc-project)
|
|
13
|
+
- [Cấu hình môi trường và API](#cấu-hình-môi-trường-và-api)
|
|
14
|
+
- [Chạy trong MySGR](#chạy-trong-mysgr)
|
|
15
|
+
- [Chạy độc lập](#chạy-độc-lập)
|
|
16
|
+
- [Thêm màn hình và gọi API](#thêm-màn-hình-và-gọi-api)
|
|
17
|
+
- [Màu sắc và kích thước UI](#màu-sắc-và-kích-thước-ui)
|
|
18
|
+
- [Đóng gói và triển khai](#đóng-gói-và-triển-khai)
|
|
19
|
+
- [Xử lý vấn đề thường gặp](#xử-lý-vấn-đề-thường-gặp)
|
|
20
|
+
|
|
21
|
+
## Bắt đầu nhanh
|
|
22
|
+
|
|
23
|
+
Ví dụ tạo mini app quản lý nghỉ phép:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx @thanhphi/create-360-miniapp@latest leave-management --module leave --app-code LVE --port 8085
|
|
27
|
+
cd leave-management
|
|
28
|
+
npm install
|
|
29
|
+
npm run env:staging
|
|
30
|
+
npm run start:mf
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Lệnh cuối mở Re.Pack dev server. Để xem giao diện, mở app đã đăng ký từ **host MySGR có cấu hình trỏ tới dev server này**. Đây không phải lệnh khởi chạy một ứng dụng độc lập trên điện thoại.
|
|
34
|
+
|
|
35
|
+
Có thể tạo project bằng npm create thay cho npx:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm create @thanhphi/360-miniapp@latest leave-management -- --module leave --app-code LVE --port 8085
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Generator chỉ tạo thư mục mới, không ghi đè project có sẵn và không tự tạo Git repository. App vừa sinh có README riêng với các giá trị cấu hình đã được điền.
|
|
42
|
+
|
|
43
|
+
## Yêu cầu
|
|
44
|
+
|
|
45
|
+
Bộ phiên bản của template:
|
|
46
|
+
|
|
47
|
+
| Thành phần | Phiên bản |
|
|
48
|
+
| ------------------------- | ----------------------------------- |
|
|
49
|
+
| Node.js | Từ 20.19.4; khuyến nghị Node 22 LTS |
|
|
50
|
+
| Expo | SDK 55 |
|
|
51
|
+
| React / React Native | 19.2.0 / 0.83.6 |
|
|
52
|
+
| UI360 / miniapp-sdk | 0.2.0 / 0.1.0 |
|
|
53
|
+
| Unistyles / Nitro Modules | 3.2.5 / 0.36.5 |
|
|
54
|
+
| Re.Pack | 5.2.x |
|
|
55
|
+
|
|
56
|
+
- Chạy trong MySGR: cần host cung cấp các native dependencies tương thích, cấu hình tải remote và `hostCapabilities` theo SDK.
|
|
57
|
+
- Chạy độc lập trên iOS: cần macOS, Xcode và CocoaPods.
|
|
58
|
+
- Chạy độc lập trên Android: cần Android SDK, JDK và emulator hoặc thiết bị thật.
|
|
59
|
+
- Không dùng Expo Go cho template này; các native dependencies yêu cầu development build.
|
|
60
|
+
- UI360 được đóng gói trong mini app, không bắt buộc host dùng cùng phiên bản UI360. React, Unistyles và các native dependencies vẫn dùng chung với host.
|
|
61
|
+
|
|
62
|
+
## Các lựa chọn khi tạo app
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npx @thanhphi/create-360-miniapp@latest leave-management --module leave --container LeaveManagement --app-code LVE --display-name "Quản lý nghỉ phép" --bundle-id com.sungroup.leave --port 8085 --env staging
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Lựa chọn | Ý nghĩa | Mặc định |
|
|
69
|
+
| ---------------------- | --------------------------------------------------- | -------------------------------------------------- |
|
|
70
|
+
| Tên project | Tên thư mục và npm package, dạng `leave-management` | Bắt buộc |
|
|
71
|
+
| `--module` | Thư mục nghiệp vụ trong `src/modules/` | Tên project |
|
|
72
|
+
| `--container` | Tên container Re.Pack, dạng PascalCase | Tên project chuyển thành PascalCase |
|
|
73
|
+
| `--app-code` | Mã nghiệp vụ của app trên backend | Tên project bỏ dấu gạch, viết hoa, tối đa 12 ký tự |
|
|
74
|
+
| `--display-name` | Tên hiển thị khi chạy độc lập | Tên container |
|
|
75
|
+
| `--bundle-id` | ID ứng dụng native, phải riêng cho mỗi app | `com.sungroup.<tên-project-bỏ-dấu-gạch>` |
|
|
76
|
+
| `--port` | Port Re.Pack, từ 1024 đến 65535 | `8085` |
|
|
77
|
+
| `--env` | Môi trường ban đầu: `staging`, `beta`, `prod` | `staging` |
|
|
78
|
+
| `--install` | Tạo project rồi chạy npm install | Không tự cài |
|
|
79
|
+
| `--native` | Cài dependencies và chạy Expo prebuild | Không tạo ios/android |
|
|
80
|
+
| `--help` / `--version` | Xem trợ giúp / phiên bản generator | — |
|
|
81
|
+
|
|
82
|
+
App ID trong catalog mặc định bằng tên project, ví dụ `leave-management`. App code, container và app ID là ba giá trị khác nhau; kiểm tra bảng cấu hình trong README của project được sinh trước khi tích hợp host.
|
|
83
|
+
|
|
84
|
+
Không đặt module là `auth`, `common`, `library`, `redux`, `navigation` hoặc `themes`; đây là các thư mục của base.
|
|
85
|
+
|
|
86
|
+
## Cấu trúc project
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
env/ staging.json, beta.json, prod.json
|
|
90
|
+
pipelines/ Pipeline build và deployment
|
|
91
|
+
scripts/ Chọn env, dọn output bundle
|
|
92
|
+
src/
|
|
93
|
+
common/mini-app/ Tích hợp host, provider và runtime
|
|
94
|
+
library/networking/ HTTP client dùng miniapp-sdk
|
|
95
|
+
modules/
|
|
96
|
+
auth/ Trạng thái phiên và đăng nhập standalone
|
|
97
|
+
<module>/ Màn hình, hooks, services, Redux nghiệp vụ
|
|
98
|
+
navigation/ Root navigator
|
|
99
|
+
redux/ Store và typed hooks
|
|
100
|
+
services/application.ts Khởi tạo store, QueryClient và HTTP client
|
|
101
|
+
themes/ Palette, geometry và cấu hình standalone
|
|
102
|
+
App.tsx Entry được host tải
|
|
103
|
+
StandaloneApp.tsx Entry chạy độc lập
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Phần lớn code tính năng mới đặt trong `src/modules/<module>/`. Không đặt endpoint nghiệp vụ hoặc logic màn hình trong provider/runtime.
|
|
107
|
+
|
|
108
|
+
Commit lockfile được sinh sau khi cài dependencies. Các lần cài tiếp theo và CI dùng `npm ci` để giữ phiên bản nhất quán.
|
|
109
|
+
|
|
110
|
+
## Cấu hình môi trường và API
|
|
111
|
+
|
|
112
|
+
Sửa `env/staging.json`, `env/beta.json` và `env/prod.json`. Những URL, issuer và client ID mẫu phải được thay bằng cấu hình của app.
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"ENV": "staging",
|
|
117
|
+
"BASE_URL": "https://api.example.com",
|
|
118
|
+
"API_TIMEOUT_MS": 15000,
|
|
119
|
+
"AUTH": {
|
|
120
|
+
"issuer": "https://identity.example.com",
|
|
121
|
+
"clientId": "native-client-id",
|
|
122
|
+
"redirectUrl": "com.sungroup.leave.auth://oauth/redirect",
|
|
123
|
+
"scopes": ["openid", "profile", "offline_access"]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Chọn môi trường trước khi chạy Re.Pack hoặc bundle:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
npm run env:staging
|
|
132
|
+
# Hoặc npm run env:beta / npm run env:prod
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Script sinh `env/env-config.ts`; không sửa file này bằng tay. Đổi env cần restart dev server.
|
|
136
|
+
|
|
137
|
+
**Các giá trị env được đưa vào bundle. Không đặt mật khẩu, client secret, private key hoặc token vào đây.** Lưu secret deployment trong cấu hình secret của CI.
|
|
138
|
+
|
|
139
|
+
Màn mẫu chỉ gọi API khi người dùng nhấn **Tải hồ sơ**. Endpoint mặc định là `/me`; sửa `PROFILE_PATH` và mapping response trong `src/modules/<module>/services/profile.service.ts` theo backend thật.
|
|
140
|
+
|
|
141
|
+
## Chạy trong MySGR
|
|
142
|
+
|
|
143
|
+
### Cấu hình phía host
|
|
144
|
+
|
|
145
|
+
Đăng ký các thông tin sau trong registry/resolver của host và catalog tương ứng:
|
|
146
|
+
|
|
147
|
+
- App ID và app code.
|
|
148
|
+
- Tên container, ví dụ `LeaveManagement`.
|
|
149
|
+
- Module được expose: `LeaveManagement/MiniApp`.
|
|
150
|
+
- Dev server URL/port khi phát triển, hoặc URL artifact khi triển khai.
|
|
151
|
+
|
|
152
|
+
Các giá trị này có trong README được tạo cùng project. CLI không tự sửa repository host hay đăng ký dữ liệu backend.
|
|
153
|
+
|
|
154
|
+
### Chạy local
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
npm run env:staging
|
|
158
|
+
npm run start:mf
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Dùng thiết bị thật:
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
npm run start:mf:device
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Host phải trỏ tới IP LAN của máy phát triển và port đã chọn. Kiểm tra thiết bị truy cập được dev server; `localhost` trên điện thoại không phải máy tính.
|
|
168
|
+
|
|
169
|
+
### Phiên đăng nhập
|
|
170
|
+
|
|
171
|
+
Host truyền `hostCapabilities` vào mini app. Contract có `auth.getSnapshot()`, `auth.subscribe()`, `auth.refreshIfNeeded()` và `closeMiniApp()`.
|
|
172
|
+
|
|
173
|
+
Base đã nối sẵn các chức năng này. HTTP client tự gắn access token, nhờ host refresh và retry tối đa một lần sau HTTP 401. Code màn hình/service không cần tự lưu token hoặc đăng nhập lại.
|
|
174
|
+
|
|
175
|
+
- Host sở hữu refresh token và persistence.
|
|
176
|
+
- Không persist token do host cấp trong mini app.
|
|
177
|
+
- Session đổi khi người dùng đổi tài khoản; refresh access token trong cùng session không reset dữ liệu nghiệp vụ.
|
|
178
|
+
- `param_supper_app` chứa dữ liệu mở app/thông báo, không dùng thay cho auth.
|
|
179
|
+
- Nếu thiếu `hostCapabilities`, kiểm tra cấu hình tích hợp; hosted entry không tự chuyển sang đăng nhập standalone.
|
|
180
|
+
|
|
181
|
+
## Chạy độc lập
|
|
182
|
+
|
|
183
|
+
Cấu hình `AUTH` trong env trước, đăng ký OAuth native client và redirect URL với nhà cung cấp đăng nhập, sau đó:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
npm run env:staging
|
|
187
|
+
npx expo prebuild
|
|
188
|
+
npm run ios:staging
|
|
189
|
+
# Hoặc npm run android:staging
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Sau khi có development build, có thể dùng `npm run start:staging` để chạy Metro.
|
|
193
|
+
|
|
194
|
+
- Đăng nhập OIDC sử dụng `react-native-app-auth`.
|
|
195
|
+
- Token standalone mặc định chỉ giữ trong bộ nhớ; đóng app/reset runtime sẽ mất phiên.
|
|
196
|
+
- Scopes mẫu không tự cấp quyền API. Cấu hình audience/scopes đúng với backend.
|
|
197
|
+
- Nếu backend yêu cầu đổi token OIDC sang token riêng, bổ sung flow exchange trong service auth.
|
|
198
|
+
- Đổi redirect URL hoặc thêm native dependency cần cập nhật native build; đổi JavaScript không nhất thiết cần prebuild lại.
|
|
199
|
+
|
|
200
|
+
Standalone là entry riêng; không xác định chế độ chạy chỉ bằng việc thiếu props host.
|
|
201
|
+
|
|
202
|
+
## Thêm màn hình và gọi API
|
|
203
|
+
|
|
204
|
+
1. Thêm screen/hook/service trong `src/modules/<module>/`.
|
|
205
|
+
2. Đăng ký route trong navigator của module.
|
|
206
|
+
3. Lấy HTTP client đã được tạo sẵn từ `useApplicationServices()`.
|
|
207
|
+
4. Dùng Redux cho state nghiệp vụ và TanStack Query cho server data.
|
|
208
|
+
|
|
209
|
+
Ví dụ service:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import type { ApiClient } from "../../../library/networking/client";
|
|
213
|
+
|
|
214
|
+
export function getItems(api: ApiClient, signal?: AbortSignal) {
|
|
215
|
+
return api.get<{ id: string; name: string }[]>("/items", { signal });
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Ví dụ trong component/hook; điều chỉnh relative imports theo vị trí file:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import { useQuery } from "@tanstack/react-query";
|
|
223
|
+
import { useApplicationServices } from "./common/mini-app/provider";
|
|
224
|
+
|
|
225
|
+
const { api } = useApplicationServices();
|
|
226
|
+
const query = useQuery({
|
|
227
|
+
queryKey: ["items"],
|
|
228
|
+
queryFn: ({ signal }) => getItems(api, signal),
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Ví dụ trên giả định API trả trực tiếp một array. Nếu backend trả envelope, unwrap trong service module. Nếu API báo hết hạn token trong HTTP 200, cấu hình thêm `isAuthExpired` trong HTTP client theo contract backend.
|
|
233
|
+
|
|
234
|
+
Khi query chứa dữ liệu cá nhân, đưa session ID vào query key như `useHome()` mẫu. Không lưu token trong query key.
|
|
235
|
+
|
|
236
|
+
## Màu sắc và kích thước UI
|
|
237
|
+
|
|
238
|
+
- `StyleSheet.configure()` cấp theme cho Unistyles.
|
|
239
|
+
- `UIThemeProvider` cấp theme cho component UI360.
|
|
240
|
+
- `createUITheme()` tạo object dữ liệu; không đăng ký hoặc thay đổi theme global.
|
|
241
|
+
- `create360Theme()` tạo cùng dữ liệu và thêm `type/color` để dùng với Unistyles.
|
|
242
|
+
|
|
243
|
+
Khi hosted, base giữ nguyên mode/màu của Unistyles đang hoạt động. Không map tên màu legacy, không trộn palette khác và không ghi đè runtime host.
|
|
244
|
+
|
|
245
|
+
Geometry được tạo sẵn trong `src/themes/ui-geometry.ts` từ factory UI360, scale theo chiều rộng thiết kế 375. Provider và style mẫu cùng dùng geometry này; không cần host cung cấp spacing/radius/typography. Không nhân scale lại tại màn hình.
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
249
|
+
import { uiGeometry } from "./themes/ui-geometry";
|
|
250
|
+
|
|
251
|
+
const { spacing, radius } = uiGeometry;
|
|
252
|
+
const styles = StyleSheet.create(({ color }) => ({
|
|
253
|
+
card: {
|
|
254
|
+
padding: spacing.xxl,
|
|
255
|
+
borderRadius: radius.lg,
|
|
256
|
+
backgroundColor: color.background,
|
|
257
|
+
},
|
|
258
|
+
}));
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Màu guideline: `content1` là chữ nội dung, `textPrimary` là chữ trên primary, `background` là nền card và `base3` là nền màn.
|
|
262
|
+
|
|
263
|
+
Palette local nằm trong `src/themes/colors/light.ts` và `dark.ts`, được dùng để khởi tạo khi standalone. Cấu hình standalone nằm trong `src/themes/standalone.ts`.
|
|
264
|
+
|
|
265
|
+
**Tách phiên bản UI360 không tự tách theme Unistyles.** Host vẫn cần cung cấp các tên màu mà app sử dụng; factory không tự đổi `bg_1` thành `background`. Nếu cần palette riêng khi hosted, phải thiết kế phạm vi theme riêng; template không tự bật cơ chế đó.
|
|
266
|
+
|
|
267
|
+
## Đóng gói và triển khai
|
|
268
|
+
|
|
269
|
+
Chọn env rồi tạo remote bundles:
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
npm run env:staging
|
|
273
|
+
npm run bundle:mf:android
|
|
274
|
+
npm run bundle:mf:ios
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Artifact cần triển khai nằm ở:
|
|
278
|
+
|
|
279
|
+
```text
|
|
280
|
+
build/outputs/android/remotes/
|
|
281
|
+
build/outputs/ios/remotes/
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
ZIP **toàn bộ nội dung** thư mục `remotes/` của từng platform, gồm container và các chunk; loại bỏ `*.map`. Không chỉ upload file container. Không dùng `build/generated/<platform>/index.bundle` làm remote artifact.
|
|
285
|
+
|
|
286
|
+
Các lệnh trên chỉ tạo JavaScript/assets, không tạo IPA/APK. Thêm native module mới vẫn cần binary host hỗ trợ trước khi deploy remote.
|
|
287
|
+
|
|
288
|
+
### Azure DevOps
|
|
289
|
+
|
|
290
|
+
Chọn pipeline `pipelines/staging.yml`, `beta.yml` hoặc `production.yml`.
|
|
291
|
+
|
|
292
|
+
Mặc định `deploy: false`: pipeline cài dependencies, kiểm tra, bundle hai platform, đóng ZIP và lưu artifacts. Chỉ bật deployment sau khi cấu hình:
|
|
293
|
+
|
|
294
|
+
| Biến | Giá trị cần cung cấp |
|
|
295
|
+
| ------------------------------------ | -------------------------- |
|
|
296
|
+
| `MINIAPP_AZURE_SUBSCRIPTION` | Azure service connection |
|
|
297
|
+
| `MINIAPP_STORAGE_ACCOUNT` | Storage Account |
|
|
298
|
+
| `MINIAPP_STORAGE_CONTAINER` | Blob container |
|
|
299
|
+
| `DB_HOST`, `DB_LOGIN`, `DB_DATABASE` | Kết nối PostgreSQL catalog |
|
|
300
|
+
| `DB_PASSWORD` | Mật khẩu, đánh dấu secret |
|
|
301
|
+
|
|
302
|
+
Kiểm tra `appName`, `appId`, `appCode`, `envBuild`, `envFolder` và `miniAppVersion` trong YAML. Agent deployment cần Azure CLI, psql và quyền truy cập hạ tầng. Pipeline không tự tạo Storage Account, database/schema hay đăng ký registry trong code host.
|
|
303
|
+
|
|
304
|
+
### Trước khi phát hành
|
|
305
|
+
|
|
306
|
+
```sh
|
|
307
|
+
npm run typecheck
|
|
308
|
+
npm run lint
|
|
309
|
+
npm test
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Sau đó kiểm tra trên host: mở/đóng app, refresh token, đổi tài khoản, lỗi mạng, hiển thị theme, dialog/loading và remote bundle đúng platform. Xác nhận các dependencies native tương thích trước khi cập nhật catalog.
|
|
313
|
+
|
|
314
|
+
## Xử lý vấn đề thường gặp
|
|
315
|
+
|
|
316
|
+
| Hiện tượng | Kiểm tra |
|
|
317
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
318
|
+
| Không tải được mini app | Container/module name, port hoặc URL resolver; artifact phải đủ chunk |
|
|
319
|
+
| Thiếu hostCapabilities | Cách host mở app và object capabilities truyền vào |
|
|
320
|
+
| Lỗi đọc spacing.xxl | Dùng provider/geometry của base mới; không lấy geometry từ theme host chưa có token này |
|
|
321
|
+
| Thiếu màu hoặc màu không đúng | Tên token Unistyles mà host cung cấp; không thêm mapping màu chỉ để sửa geometry |
|
|
322
|
+
| Standalone không đăng nhập được | Issuer, client ID, redirect URL, scopes và native build |
|
|
323
|
+
| API trả 401 sau đăng nhập | Audience/scopes hoặc flow token exchange của backend |
|
|
324
|
+
| Thêm native package nhưng remote vẫn lỗi | Binary host phải có module native tương ứng |
|
|
325
|
+
| Thay template nhưng app cũ không đổi | Generator chỉ tạo project; cập nhật app đã sinh theo từng thay đổi, không chạy đè lên thư mục cũ |
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
MIT.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawnSync } = require('node:child_process');
|
|
3
|
+
const { createApp } = require('../lib/create.cjs');
|
|
4
|
+
|
|
5
|
+
const help = `Create a MySGR mini app (Expo 55 / React Native 0.83 / Re.Pack 5)
|
|
6
|
+
|
|
7
|
+
npx @thanhphi/create-360-miniapp my-app [options]
|
|
8
|
+
npm create @thanhphi/360-miniapp@latest my-app -- [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--module <slug> Feature module folder (default: app name)
|
|
12
|
+
--container <Name> Federation container (default: PascalCase app name)
|
|
13
|
+
--app-code <CODE> Backend mini-app code
|
|
14
|
+
--bundle-id <id> Unique Android/iOS application ID
|
|
15
|
+
--display-name <name> Native display name
|
|
16
|
+
--port <number> Re.Pack dev server port (default: 8085)
|
|
17
|
+
--env <flavor> Initial staging, beta or prod (default: staging)
|
|
18
|
+
--install Install dependencies and create package-lock.json
|
|
19
|
+
--native Install if needed, then generate ios/ and android/
|
|
20
|
+
--help Show usage
|
|
21
|
+
--version Show generator version
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
function main(argv) {
|
|
25
|
+
if (argv.includes('--help') || argv.includes('-h')) return process.stdout.write(help);
|
|
26
|
+
if (argv.includes('--version')) return process.stdout.write(require('../package.json').version + '\n');
|
|
27
|
+
const options = {};
|
|
28
|
+
const values = { '--module': 'module', '--container': 'container', '--app-code': 'appCode', '--bundle-id': 'bundleId', '--display-name': 'displayName', '--port': 'port', '--env': 'env' };
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
const arg = argv[i];
|
|
31
|
+
if (arg === '--') continue;
|
|
32
|
+
if (arg === '--install' || arg === '--native') options[arg.slice(2)] = true;
|
|
33
|
+
else if (values[arg]) {
|
|
34
|
+
const value = argv[++i];
|
|
35
|
+
if (!value || value.startsWith('--')) throw new Error(`Missing value for ${arg}.`);
|
|
36
|
+
options[values[arg]] = value;
|
|
37
|
+
} else if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}.`);
|
|
38
|
+
else if (options.name) throw new Error(`Unexpected argument: ${arg}.`);
|
|
39
|
+
else options.name = arg;
|
|
40
|
+
}
|
|
41
|
+
const app = createApp(options);
|
|
42
|
+
process.stdout.write(`Created ${app.appName} at ${app.destination}\n`);
|
|
43
|
+
function run(args) {
|
|
44
|
+
const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { cwd: app.destination, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
45
|
+
if (result.error) throw result.error;
|
|
46
|
+
if (result.status !== 0) throw new Error(`npm ${args.join(' ')} failed. App files remain in ${app.destination}; fix the error and retry there.`);
|
|
47
|
+
}
|
|
48
|
+
if (options.install || options.native) run(['install']);
|
|
49
|
+
if (options.native) run(['exec', '--', 'expo', 'prebuild']);
|
|
50
|
+
process.stdout.write(`\nNext:\n cd ${options.name}\n${options.install || options.native ? '' : ' npm install\n'} npm run env:${app.environment}\n npm run start:mf\n\nStandalone: npm run ios:${app.environment} or android:${app.environment}\nRead README.md for env, auth, host registration and pipeline setup.\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try { main(process.argv.slice(2)); }
|
|
54
|
+
catch (error) { process.stderr.write(`create-360-miniapp: ${error.message}\n`); process.exitCode = 1; }
|
package/lib/create.cjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const template = path.resolve(__dirname, '../template');
|
|
5
|
+
const reserved = new Set(['auth', 'common', 'library', 'redux', 'navigation', 'themes']);
|
|
6
|
+
|
|
7
|
+
function createApp(options, cwd = process.cwd()) {
|
|
8
|
+
const name = options.name;
|
|
9
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(name || '')) {
|
|
10
|
+
throw new Error('App name must be a lowercase slug, e.g. leave-management.');
|
|
11
|
+
}
|
|
12
|
+
const moduleName = options.module || name;
|
|
13
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(moduleName) || reserved.has(moduleName)) {
|
|
14
|
+
throw new Error('Module must be a lowercase feature name; auth and framework folders are reserved.');
|
|
15
|
+
}
|
|
16
|
+
const appName = options.container || name.split('-').map(part => part[0].toUpperCase() + part.slice(1)).join('');
|
|
17
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(appName)) throw new Error('Container must be a PascalCase identifier.');
|
|
18
|
+
const appCode = options.appCode || name.replace(/-/g, '').slice(0, 12).toUpperCase();
|
|
19
|
+
if (!/^[A-Z][A-Z0-9_]{0,31}$/.test(appCode)) throw new Error('App code must contain uppercase letters, digits or underscore.');
|
|
20
|
+
const port = Number(options.port || 8085);
|
|
21
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error('Port must be an integer between 1024 and 65535.');
|
|
22
|
+
const bundleId = options.bundleId || `com.sungroup.${name.replace(/-/g, '')}`;
|
|
23
|
+
if (!/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*){2,}$/.test(bundleId)) throw new Error('Bundle ID must be a lowercase reverse-domain identifier without underscores.');
|
|
24
|
+
const displayName = options.displayName || appName;
|
|
25
|
+
// The same replacement is used in JS, JSON, Markdown and YAML. Keep it safe in all formats.
|
|
26
|
+
if (!/^[\p{L}\p{N} _.-]{1,80}$/u.test(displayName)) throw new Error('Display name can contain letters, numbers, spaces, dots, underscores and hyphens.');
|
|
27
|
+
const environment = options.env || 'staging';
|
|
28
|
+
if (!['staging', 'beta', 'prod'].includes(environment)) throw new Error('Environment must be staging, beta or prod.');
|
|
29
|
+
const destination = path.resolve(cwd, name);
|
|
30
|
+
if (fs.existsSync(destination)) throw new Error(`Destination already exists: ${destination}. Choose a new directory.`);
|
|
31
|
+
const replacements = {
|
|
32
|
+
__APP_NAME__: appName, __DISPLAY_NAME__: displayName, __PACKAGE_NAME__: name,
|
|
33
|
+
__APP_ID__: name, __APP_CODE__: appCode, __MODULE__: moduleName,
|
|
34
|
+
__PORT__: String(port), __BUNDLE_ID__: bundleId,
|
|
35
|
+
};
|
|
36
|
+
function render(value) {
|
|
37
|
+
return value.replace(/__(?:APP_NAME|DISPLAY_NAME|PACKAGE_NAME|APP_ID|APP_CODE|MODULE|PORT|BUNDLE_ID)__/g, key => replacements[key]);
|
|
38
|
+
}
|
|
39
|
+
function copy(directory, target) {
|
|
40
|
+
fs.mkdirSync(target, { recursive: true });
|
|
41
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
42
|
+
const source = path.join(directory, entry.name);
|
|
43
|
+
// npm strips .gitignore; keep it as a normal file inside the published template.
|
|
44
|
+
const output = path.join(target, entry.name === 'gitignore' ? '.gitignore' : render(entry.name));
|
|
45
|
+
if (entry.isDirectory()) copy(source, output);
|
|
46
|
+
else if (entry.isFile()) fs.writeFileSync(output, render(fs.readFileSync(source, 'utf8')));
|
|
47
|
+
else throw new Error(`Unsupported template entry: ${source}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
copy(template, destination);
|
|
51
|
+
const { syncEnv } = require(path.join(destination, 'scripts/sync-env.js'));
|
|
52
|
+
syncEnv(environment, destination);
|
|
53
|
+
return { destination, appName, moduleName, environment, port };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { createApp };
|
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thanhphi/create-360-miniapp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Create a MySGR Expo and Re.Pack mini app with modules, host auth and Azure pipelines",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-360-miniapp": "bin/create-360-miniapp.cjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"template",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test tests/*.test.cjs"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"registry": "https://registry.npmjs.org/"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = { root: true, extends: '@react-native', ignorePatterns: ['build/', 'env/env-config.ts'] };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = { singleQuote: true, trailingComma: 'all', arrowParens: 'avoid' };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
package/template/App.tsx
ADDED