agent-flutter 0.1.24 → 0.1.26
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/package.json +1 -1
- package/templates/shared/TEMPLATES.md +27 -0
- package/templates/shared/rules/ci-cd-pr.md +3 -3
- package/templates/shared/rules/integration-api.md +12 -0
- package/templates/shared/rules/ui-refactor-convert.md +120 -0
- package/templates/shared/rules/ui.md +38 -2
- package/templates/shared/tools/jsx2flutter/jsx2flutter.mjs +88 -160
package/package.json
CHANGED
|
@@ -74,3 +74,30 @@ Finalize this feature:
|
|
|
74
74
|
- Screenshot evidence: [No UI OR before/after]
|
|
75
75
|
- Paired with: [Solo OR name]
|
|
76
76
|
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## **5. Refactor Converted UI (Post-Convert)**
|
|
81
|
+
|
|
82
|
+
Use this when code comes from convert script and needs production refactor:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
Refactor converted UI:
|
|
86
|
+
- Source file: [e.g. lib/src/ui/home_demo_figma/home_demo_figma_page.dart]
|
|
87
|
+
- Feature name: [e.g. home_demo]
|
|
88
|
+
- Keep exact Figma visual: [yes/no]
|
|
89
|
+
- Needs API integration now?: [yes/no]
|
|
90
|
+
- Normalize/rename convert assets?: [yes/no]
|
|
91
|
+
- Commit now?: [yes/no]
|
|
92
|
+
- Push now?: [yes/no]
|
|
93
|
+
- Create PR now?: [yes/no]
|
|
94
|
+
- Priority fixes: [naming, localization, App* widgets, token cleanup, asset-rename, overflow]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Expected behavior:
|
|
98
|
+
1. Apply `ui-refactor-convert.md` + `ui.md`.
|
|
99
|
+
2. Rename generated classes/files to semantic names.
|
|
100
|
+
3. Normalize convert-generated asset folder and names, then update `AppAssets` usages.
|
|
101
|
+
4. Replace hardcoded style/string with tokens + localization.
|
|
102
|
+
5. Extract components and pass UI correctness gates.
|
|
103
|
+
6. Ask and execute in order: commit -> push -> create PR.
|
|
@@ -21,7 +21,7 @@ This is the required "done" flow before feature handoff.
|
|
|
21
21
|
6. If answer is `yes`, push branch to remote.
|
|
22
22
|
7. Ask user explicitly: `Do you want me to create PR now? (yes/no)`
|
|
23
23
|
8. If answer is `yes`, create PR with mandatory template sections.
|
|
24
|
-
9. If
|
|
24
|
+
9. If answer is `no` at any step, stop at that step and return current status.
|
|
25
25
|
|
|
26
26
|
If any step is skipped, feature is not considered complete.
|
|
27
27
|
|
|
@@ -48,6 +48,6 @@ PR must follow `.github/pull_request_template.md` and include all sections:
|
|
|
48
48
|
|
|
49
49
|
## 6. Agent Behavior
|
|
50
50
|
- After finishing UI/API tasks, agent must ask in order: commit -> push -> create PR.
|
|
51
|
-
- Agent
|
|
51
|
+
- Agent executes only steps explicitly confirmed by user (`yes`).
|
|
52
52
|
- If user confirms all steps, agent returns PR URL as final output.
|
|
53
|
-
- If user declines at any step, agent stops and returns
|
|
53
|
+
- If user declines at any step, agent stops and returns repository status.
|
|
@@ -51,6 +51,9 @@ If any item is missing, stop implementation and request contract clarification f
|
|
|
51
51
|
- UI/domain-only models: `lib/src/core/model/`
|
|
52
52
|
- Never pass raw `Map<String, dynamic>` across UI layers for non-trivial APIs.
|
|
53
53
|
- Reuse-first: check existing models before creating new ones.
|
|
54
|
+
- Required API fields must not silently fallback to fake defaults in `fromJson`.
|
|
55
|
+
- Nullable/non-nullable and enum values must match backend contract explicitly.
|
|
56
|
+
- Date/number parsing must be guarded (safe parse/cast), not optimistic cast.
|
|
54
57
|
|
|
55
58
|
### D. Repository Boundary
|
|
56
59
|
- All remote calls must be wrapped by repository classes in `lib/src/core/repository/`.
|
|
@@ -83,6 +86,11 @@ If any item is missing, stop implementation and request contract clarification f
|
|
|
83
86
|
- Prefer typed fields and safe nullability.
|
|
84
87
|
- Add parser methods (`fromJson/toJson`) consistently.
|
|
85
88
|
- Apply reuse/evolution checklist from [dart-model-reuse](../skills/dart-model-reuse/SKILL.md).
|
|
89
|
+
- Validate model correctness against contract:
|
|
90
|
+
- Required/optional fields match schema.
|
|
91
|
+
- Enum/status values are constrained.
|
|
92
|
+
- No hidden fallback defaults for required fields.
|
|
93
|
+
- Update `spec/model-registry.md` for any model additions/changes.
|
|
86
94
|
|
|
87
95
|
### Step 4: Implement Repository Method
|
|
88
96
|
- Repository extends/reuses `Api`.
|
|
@@ -107,6 +115,7 @@ If any item is missing, stop implementation and request contract clarification f
|
|
|
107
115
|
- UI reads bloc state and renders loading/error/success.
|
|
108
116
|
- No API parsing logic in widget tree.
|
|
109
117
|
- All strings shown from API errors must pass localization policy in [getx-localization-standard](../skills/getx-localization-standard/SKILL.md).
|
|
118
|
+
- UI should consume repository-mapped typed models only (not raw response JSON/map).
|
|
110
119
|
|
|
111
120
|
## 5. Minimal Reference Pattern
|
|
112
121
|
|
|
@@ -137,6 +146,9 @@ class FeatureRepository extends Api {
|
|
|
137
146
|
## 7. Verification Checklist Before PR
|
|
138
147
|
- [ ] Endpoint added in `api_url.dart` and consumed from repository only.
|
|
139
148
|
- [ ] Request/response models are separated and typed.
|
|
149
|
+
- [ ] Model correctness validated against API contract (required/nullable/enum/date/number).
|
|
150
|
+
- [ ] No hidden fallback defaults for required contract fields in `fromJson`.
|
|
151
|
+
- [ ] UI/Bloc consumes typed models only (no raw JSON/map parsing).
|
|
140
152
|
- [ ] Bloc uses `PageState` lifecycle correctly.
|
|
141
153
|
- [ ] All displayed text/error is localized via `LocaleKey`.
|
|
142
154
|
- [ ] Flavor run verified at least on staging command.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
alwaysApply: false
|
|
3
|
+
---
|
|
4
|
+
# UI Refactor Rule for Convert Output (JSX/Figma -> Dart)
|
|
5
|
+
|
|
6
|
+
## 1. Goal
|
|
7
|
+
Turn generated UI code into production-ready Flutter code that fully complies with `ui.md`.
|
|
8
|
+
|
|
9
|
+
This rule applies after running convert scripts (for example files like `home_demo_figma_page.dart` and generated component trees), especially when convert output creates non-standard asset names/folders.
|
|
10
|
+
|
|
11
|
+
## 1.1 Required References
|
|
12
|
+
Always apply together with:
|
|
13
|
+
- [ui.md](./ui.md)
|
|
14
|
+
- [flutter-ui-widgets](../skills/flutter-ui-widgets/SKILL.md)
|
|
15
|
+
- [flutter-assets-management](../skills/flutter-assets-management/SKILL.md)
|
|
16
|
+
- [getx-localization-standard](../skills/getx-localization-standard/SKILL.md)
|
|
17
|
+
- [dart-best-practices](../skills/dart-best-practices/SKILL.md)
|
|
18
|
+
- [ci-cd-pr.md](./ci-cd-pr.md)
|
|
19
|
+
|
|
20
|
+
If API data is involved, also apply:
|
|
21
|
+
- [integration-api.md](./integration-api.md)
|
|
22
|
+
- [dart-model-reuse](../skills/dart-model-reuse/SKILL.md)
|
|
23
|
+
|
|
24
|
+
## 2. Trigger Conditions
|
|
25
|
+
Use this rule when at least one condition is true:
|
|
26
|
+
- Source file is generated by convert pipeline.
|
|
27
|
+
- Class/file names are non-semantic (for example: `UhomeUdemoFigma`, `Frame123`).
|
|
28
|
+
- Generated UI has hardcoded strings/colors/styles or deep nested widget trees.
|
|
29
|
+
- Converted UI works visually but fails architecture/readability/maintainability standards.
|
|
30
|
+
|
|
31
|
+
## 3. Refactor Workflow
|
|
32
|
+
|
|
33
|
+
### Step 1: Normalize Structure and Naming
|
|
34
|
+
- Keep page architecture:
|
|
35
|
+
- `lib/src/ui/<feature>/{binding,interactor,components}`
|
|
36
|
+
- Rename generated class/file names to semantic names.
|
|
37
|
+
- Keep page entry simple (`<feature>_page.dart` should not contain huge raw trees).
|
|
38
|
+
|
|
39
|
+
### Step 2: Apply Design Tokens and Shared Widgets
|
|
40
|
+
- Replace raw style values with:
|
|
41
|
+
- `AppColors`
|
|
42
|
+
- `AppStyles`
|
|
43
|
+
- `AppDimensions` and extensions (`n.width`, `n.height`)
|
|
44
|
+
- Replace raw controls with existing `App*` widgets where applicable.
|
|
45
|
+
- Keep exact Figma values when overriding tokens (radius/spacing/typography).
|
|
46
|
+
|
|
47
|
+
### Step 3: Remove Convert Artifacts
|
|
48
|
+
- Remove redundant wrappers (`ClipRRect + Container` duplicates, empty containers, unnecessary stacks).
|
|
49
|
+
- Reduce deeply nested UI into clear sub-components under `components/`.
|
|
50
|
+
- Add `const` where possible.
|
|
51
|
+
- Avoid `Get.width` direct usage when local layout constraints are more correct (`LayoutBuilder`, `MediaQuery` scoped use).
|
|
52
|
+
|
|
53
|
+
### Step 4: Localization and Content Cleanup
|
|
54
|
+
- Move all user-facing text to `LocaleKey` + `.tr`.
|
|
55
|
+
- Do not keep demo/static strings inside production widget tree.
|
|
56
|
+
- If temporary mock is needed, source it from `app_demo_data.dart`.
|
|
57
|
+
|
|
58
|
+
### Step 5: State and Behavior Correctness
|
|
59
|
+
- Ensure required states render correctly:
|
|
60
|
+
- `loading`, `success`, `empty`, `error` (plus `unauthorized` if relevant).
|
|
61
|
+
- Ensure UI behavior is not only visual:
|
|
62
|
+
- tap handlers wired,
|
|
63
|
+
- enabled/disabled state clear,
|
|
64
|
+
- no broken interaction overlays.
|
|
65
|
+
|
|
66
|
+
### Step 6: Asset and Icon Hygiene
|
|
67
|
+
- Use `AppAssets` only (no raw asset path in widget code).
|
|
68
|
+
- Keep icon/image naming consistent and avoid duplicates.
|
|
69
|
+
- Verify SVG rendering without unexpected tint/color shift.
|
|
70
|
+
- Normalize convert-generated asset folders and names:
|
|
71
|
+
- Detect non-standard convert folders (for example: `assets/figma/**`).
|
|
72
|
+
- Move/rename assets to project-standard locations (`assets/images/**`, `assets/images/icons/**`) when applicable.
|
|
73
|
+
- Rename files to meaningful `snake_case` names based on feature and purpose.
|
|
74
|
+
- Replace convert-style asset constants in `lib/src/utils/app_assets.dart` with UI-standard naming.
|
|
75
|
+
- Update all usages in Dart code to new `AppAssets` constants.
|
|
76
|
+
- Remove orphan/unused old asset files and constants.
|
|
77
|
+
|
|
78
|
+
### Step 7: Verify and Prepare for PR
|
|
79
|
+
- Run at minimum:
|
|
80
|
+
- `fvm flutter analyze`
|
|
81
|
+
- Validate UI correctness gates from `ui.md`:
|
|
82
|
+
- responsive widths `320/390/430`
|
|
83
|
+
- text scale `1.0/1.3`
|
|
84
|
+
- no overflow warnings in debug logs
|
|
85
|
+
|
|
86
|
+
### Step 8: Commit/Push/PR Confirmation (Mandatory)
|
|
87
|
+
- After refactor verification, ask user in strict order:
|
|
88
|
+
1. `Do you want me to commit now? (yes/no)`
|
|
89
|
+
2. `Do you want me to push now? (yes/no)`
|
|
90
|
+
3. `Do you want me to create PR now? (yes/no)`
|
|
91
|
+
- Execute only steps explicitly confirmed with `yes`.
|
|
92
|
+
- If user answers `no` at any step, stop there and return current status.
|
|
93
|
+
|
|
94
|
+
## 4. Mandatory Output of Refactor Task
|
|
95
|
+
After refactor, output must include:
|
|
96
|
+
- List of renamed files/classes.
|
|
97
|
+
- List of extracted components.
|
|
98
|
+
- List of localized keys added/updated.
|
|
99
|
+
- List of token replacements (`raw -> AppColors/AppStyles/AppDimensions`).
|
|
100
|
+
- Asset rename mapping (`old path/name -> new path/name`) and updated `AppAssets` constants.
|
|
101
|
+
- Commit/push/PR confirmation answers and execution results.
|
|
102
|
+
- Remaining known gaps (if any) with exact file paths.
|
|
103
|
+
|
|
104
|
+
## 5. Anti-Patterns (Do Not)
|
|
105
|
+
- Do not keep generated class names in final code.
|
|
106
|
+
- Do not leave hardcoded colors/text if a project token/key exists.
|
|
107
|
+
- Do not keep convert-only asset naming/folders in final code if they violate project standard.
|
|
108
|
+
- Do not parse API JSON directly in UI widgets.
|
|
109
|
+
- Do not submit refactor that only changes formatting but leaves architectural violations.
|
|
110
|
+
|
|
111
|
+
## 6. Prompt Template
|
|
112
|
+
Use this prompt to trigger this rule:
|
|
113
|
+
|
|
114
|
+
> Refactor converted UI using `ui-refactor-convert.md` + `ui.md`.
|
|
115
|
+
> Source: `<path-to-generated-page-or-component>`.
|
|
116
|
+
> Keep visual fidelity, but enforce architecture/tokens/localization/state correctness.
|
|
117
|
+
> Extract reusable components and remove convert artifacts.
|
|
118
|
+
> Normalize asset folder/naming and update `AppAssets` references.
|
|
119
|
+
> Then ask: commit now? push now? create PR now?
|
|
120
|
+
> Return changed files and verification checklist results.
|
|
@@ -14,6 +14,14 @@ alwaysApply: false
|
|
|
14
14
|
- **Shadows**: Normalize `BoxShadow` closest to CSS box-shadow; do not change blur/spread arbitrarily.
|
|
15
15
|
- **Backgrounds**: Use `DecorationImage` + appropriate `BoxFit` (cover/contain) and correct alignment.
|
|
16
16
|
|
|
17
|
+
### **UI Correctness Gate (Priority P1)**
|
|
18
|
+
- Every new/updated screen must pass visual + behavioral correctness, not only "looks similar".
|
|
19
|
+
- Required state coverage for each screen: `loading`, `success`, `empty`, `error` (and `permission/unauthorized` if applicable).
|
|
20
|
+
- Required responsive check: at least widths `320`, `390`, `430`.
|
|
21
|
+
- Required accessibility check: text scale `1.0` and `1.3` without overflow/clipping.
|
|
22
|
+
- No layout overflow warnings are allowed in debug console.
|
|
23
|
+
- Tap targets should remain usable (recommended minimum 44x44 when interactive).
|
|
24
|
+
|
|
17
25
|
### **Overrides (Per-Component)**
|
|
18
26
|
- App* widgets must allow overriding tokens: `size`, `radius`, `padding/margin`, `backgroundColor`, `textStyle`, `iconSize`, `constraints`.
|
|
19
27
|
- Defaults must come from tokens (`AppStyles`, `AppColors`, `AppDimensions`) and act only as fallbacks; do not hardcode.
|
|
@@ -91,6 +99,16 @@ alwaysApply: false
|
|
|
91
99
|
- **No dynamic data in UI**: Pages/components MUST receive typed models via constructor/BLoC state; do not hardcode demo lists in widgets.
|
|
92
100
|
- **Demo/Mock Data Location**: Keep all demo/sample data in `lib/src/utils/app_demo_data.dart` only. Import from there when previewing or testing UI.
|
|
93
101
|
- **Reuse-first**: Before creating a new model, check the [Model Registry](../../spec/model-registry.md) and follow [Dart Model Reuse Skill](../skills/dart-model-reuse/SKILL.md). Prefer extending existing models (optional fields) or composition over duplication.
|
|
102
|
+
- **Model Correctness Gate (Priority P1)**:
|
|
103
|
+
- Each model class must state purpose/source in class doc (Request/Response/UI).
|
|
104
|
+
- Do not use `dynamic`/`Object?` for API payload fields unless contract truly allows unknown schema.
|
|
105
|
+
- Required contract fields must not silently fallback to fake defaults (`''`, `0`, `false`) in `fromJson`.
|
|
106
|
+
- Nullable/non-nullable must match backend contract.
|
|
107
|
+
- Date/number parsing must use guarded conversion (`DateTime.tryParse`, safe numeric casting).
|
|
108
|
+
- UI layer must consume typed UI/domain model only, not raw response map/json.
|
|
109
|
+
- **Mapper Boundary (Required)**:
|
|
110
|
+
- If API response shape differs from UI needs, map in repository or mapper layer (`lib/src/core/mapper/`).
|
|
111
|
+
- Widgets and page blocs must not parse JSON directly.
|
|
94
112
|
|
|
95
113
|
### **E. Composition & State**
|
|
96
114
|
- **State**: Default to `StatelessWidget`. Use `StatefulWidget` only for local UI logic (animations, focus).
|
|
@@ -147,6 +165,7 @@ Follow these steps when creating a new UI screen:
|
|
|
147
165
|
- Check Figma/Design.
|
|
148
166
|
- Identify reusable components -> Are they in `lib/src/ui/widgets`?
|
|
149
167
|
- Identify unique components -> Plan to put them in `components/`.
|
|
168
|
+
- Define UI state matrix before coding: loading/success/empty/error + action states (enabled/disabled).
|
|
150
169
|
|
|
151
170
|
### **Step 2: Scaffolding**
|
|
152
171
|
- Create the folder structure (See [Lib Src Architecture Skill](../skills/flutter-standard-lib-src-architecture/SKILL.md)):
|
|
@@ -157,6 +176,11 @@ Follow these steps when creating a new UI screen:
|
|
|
157
176
|
### **Step 3: Data Modeling (If API involved)**
|
|
158
177
|
- Create Request model in `lib/src/core/model/request/`.
|
|
159
178
|
- Create Response model in `lib/src/core/model/response/`.
|
|
179
|
+
- Validate each model against API contract before UI wiring:
|
|
180
|
+
- Required vs nullable fields are correct.
|
|
181
|
+
- Enum/status values are represented safely (enum or controlled constants).
|
|
182
|
+
- No silent fallback for required fields.
|
|
183
|
+
- Update `spec/model-registry.md` when adding/changing model.
|
|
160
184
|
|
|
161
185
|
### **Step 4: Logic & Binding**
|
|
162
186
|
- Create the Controller/Bloc in `interactor/` (See [Bloc Skill](../skills/flutter-bloc-state-management/SKILL.md)).
|
|
@@ -174,9 +198,13 @@ Follow these steps when creating a new UI screen:
|
|
|
174
198
|
- Use `AppColors` / `AppStyles`.
|
|
175
199
|
- Use `App*` widgets.
|
|
176
200
|
- Use `LocaleKey.my_key.tr` for text.
|
|
177
|
-
- **Radius & Icon**:
|
|
201
|
+
- **Radius & Icon**:
|
|
178
202
|
- If Figma requires radius = 14, **must** use `BorderRadius.circular(14)` at the corresponding component.
|
|
179
203
|
- Icon 24×24; if framed, use container 40×40/44×44 per Figma.
|
|
204
|
+
- **Behavioral Correctness (STRICT)**:
|
|
205
|
+
- Implement and render all required states from Step 1 (loading/success/empty/error...).
|
|
206
|
+
- Avoid overflow on small width and long localization strings.
|
|
207
|
+
- UI must not depend on mock constants inside widget tree for production flow.
|
|
180
208
|
- **Documentation Comments (STRICT)**:
|
|
181
209
|
- Add top-of-class `///` description for every Page and Component, summarizing its purpose and main actions.
|
|
182
210
|
- Generator reads these docs to build `spec/ui-workflow.md` (run `dart run tool/generate_ui_workflow_spec.dart`).
|
|
@@ -190,7 +218,7 @@ Follow these steps when creating a new UI screen:
|
|
|
190
218
|
```
|
|
191
219
|
|
|
192
220
|
### **Step 7: Component Extraction**
|
|
193
|
-
- **Rule**: If a widget block is > 50 lines or reused twice, extract it to `components/` (See [
|
|
221
|
+
- **Rule**: If a widget block is > 50 lines or reused twice, extract it to `components/` (See [Flutter UI Widgets Skill](../skills/flutter-ui-widgets/SKILL.md)).
|
|
194
222
|
- Keep the main `build()` method clean and readable.
|
|
195
223
|
|
|
196
224
|
### **Step 8: Route Registration (Choose One)**
|
|
@@ -257,6 +285,14 @@ Follow these steps when creating a new UI screen:
|
|
|
257
285
|
- [ ] Page broken down into `components/`?
|
|
258
286
|
- [ ] Logic separated into `interactor/`?
|
|
259
287
|
- [ ] **Data Models Separated?** (Request/Response in `core/model/`)
|
|
288
|
+
- [ ] **Model Correctness Gate passed?** (required/nullable/enum/date/number mapping validated)
|
|
289
|
+
- [ ] **No hidden fallback defaults for required fields?** (`fromJson` does not hide bad contract)
|
|
290
|
+
- [ ] **Mapper Boundary respected?** (UI/Bloc does not parse raw JSON/Map)
|
|
291
|
+
- [ ] **Model Registry Updated?** (`spec/model-registry.md` reflects new/changed models)
|
|
292
|
+
- [ ] **UI Correctness Gate passed?** (loading/success/empty/error implemented and verified)
|
|
293
|
+
- [ ] **Responsive check passed?** (320/390/430 widths)
|
|
294
|
+
- [ ] **Accessibility text scale check passed?** (`1.0` and `1.3` no overflow/clipping)
|
|
295
|
+
- [ ] **No debug overflow/error logs from layout?**
|
|
260
296
|
- [ ] File naming matches `<feature>_<type>.dart`?
|
|
261
297
|
- [ ] **Route Registered?**
|
|
262
298
|
- [ ] If Full Screen: Added to `AppPages`?
|
|
@@ -1,48 +1,20 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { createRequire } from 'node:module'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
4
|
import { parse } from '@babel/parser'
|
|
6
5
|
import traverseModule from '@babel/traverse'
|
|
7
6
|
const traverse = traverseModule.default
|
|
8
7
|
import postcss from 'postcss'
|
|
9
8
|
import safeParser from 'postcss-safe-parser'
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
const __dirname = path.dirname(__filename)
|
|
10
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
|
13
11
|
const require = createRequire(import.meta.url)
|
|
14
12
|
let sass = null
|
|
15
13
|
try {
|
|
16
14
|
sass = require('sass')
|
|
17
15
|
} catch {}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
const fromCwd = process.cwd()
|
|
21
|
-
if (fs.existsSync(path.resolve(fromCwd, 'pubspec.yaml'))) {
|
|
22
|
-
return fromCwd
|
|
23
|
-
}
|
|
24
|
-
let current = __dirname
|
|
25
|
-
while (true) {
|
|
26
|
-
if (fs.existsSync(path.resolve(current, 'pubspec.yaml'))) {
|
|
27
|
-
return current
|
|
28
|
-
}
|
|
29
|
-
const parent = path.dirname(current)
|
|
30
|
-
if (parent === current) break
|
|
31
|
-
current = parent
|
|
32
|
-
}
|
|
33
|
-
return fromCwd
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function readPackageName(projectRoot) {
|
|
37
|
-
const pubspecPath = path.resolve(projectRoot, 'pubspec.yaml')
|
|
38
|
-
if (!fs.existsSync(pubspecPath)) return 'app'
|
|
39
|
-
const content = fs.readFileSync(pubspecPath, 'utf8')
|
|
40
|
-
const matched = content.match(/^name:\s*([A-Za-z0-9_]+)/m)
|
|
41
|
-
return matched?.[1] || 'app'
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const REPO_ROOT = resolveProjectRoot()
|
|
45
|
-
const PROJECT_PACKAGE_NAME = readPackageName(REPO_ROOT)
|
|
17
|
+
const REPO_ROOT = path.resolve(__dirname, '../..')
|
|
46
18
|
const APP_ASSETS_FILE = path.resolve(REPO_ROOT, 'lib/src/utils/app_assets.dart')
|
|
47
19
|
let ICONS_MAP = {}
|
|
48
20
|
try {
|
|
@@ -745,6 +717,47 @@ function textAlignFromProps(props) {
|
|
|
745
717
|
return null
|
|
746
718
|
}
|
|
747
719
|
|
|
720
|
+
function isInlineTextTag(tag) {
|
|
721
|
+
return tag === 'span' || tag === 'p'
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function mergeTextStyles(baseStyle, ownStyle) {
|
|
725
|
+
if (baseStyle && ownStyle) return `(${baseStyle}).merge(${ownStyle})`
|
|
726
|
+
return ownStyle || baseStyle || null
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function buildInlineTextSpans(el, cssMap, topClassName, skipRootDecoration, inheritedStyle = null) {
|
|
730
|
+
const className = getClassNameAttr(el)
|
|
731
|
+
const props = className && cssMap[className] ? cssMap[className] : {}
|
|
732
|
+
const style = mergeTextStyles(inheritedStyle, textStyleFromProps(props))
|
|
733
|
+
const spans = []
|
|
734
|
+
for (const c of el.children || []) {
|
|
735
|
+
if (isTextNode(c)) {
|
|
736
|
+
const text = c.value.trim()
|
|
737
|
+
if (!text) continue
|
|
738
|
+
const parts = [`text: ${toDartLiteral(text)}`]
|
|
739
|
+
if (style) parts.push(`style: ${style}`)
|
|
740
|
+
spans.push(`TextSpan(${parts.join(', ')})`)
|
|
741
|
+
continue
|
|
742
|
+
}
|
|
743
|
+
if (c.type !== 'JSXElement') continue
|
|
744
|
+
const childTag = jsxElementName(c).toLowerCase()
|
|
745
|
+
if (childTag === 'br') {
|
|
746
|
+
const parts = [`text: '\\n'`]
|
|
747
|
+
if (style) parts.push(`style: ${style}`)
|
|
748
|
+
spans.push(`TextSpan(${parts.join(', ')})`)
|
|
749
|
+
continue
|
|
750
|
+
}
|
|
751
|
+
if (isInlineTextTag(childTag)) {
|
|
752
|
+
spans.push(...buildInlineTextSpans(c, cssMap, topClassName, skipRootDecoration, style))
|
|
753
|
+
continue
|
|
754
|
+
}
|
|
755
|
+
const child = buildWidgetFromElement(c, cssMap, topClassName, skipRootDecoration)
|
|
756
|
+
spans.push(`WidgetSpan(child: ${child.widget})`)
|
|
757
|
+
}
|
|
758
|
+
return spans
|
|
759
|
+
}
|
|
760
|
+
|
|
748
761
|
function paddingFromProps(props) {
|
|
749
762
|
const p = props['padding']
|
|
750
763
|
const pt = props['padding-top']
|
|
@@ -881,15 +894,9 @@ function usesAppOwnedInputBackground(widget) {
|
|
|
881
894
|
return normalized.startsWith('AppInput(') || normalized.startsWith('_GeneratedDateTimeField(')
|
|
882
895
|
}
|
|
883
896
|
|
|
884
|
-
function usesAppOwnedProgressBarBackground(widget) {
|
|
885
|
-
if (!widget) return false
|
|
886
|
-
const normalized = String(widget).trim()
|
|
887
|
-
return normalized.startsWith('AppProgressBar(')
|
|
888
|
-
}
|
|
889
|
-
|
|
890
897
|
function toDartLiteral(s) {
|
|
891
898
|
const t = String(s || '')
|
|
892
|
-
return `'${t.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
899
|
+
return `'${t.replace(/\\/g, '\\\\').replace(/\$/g, '\\$').replace(/'/g, "\\'")}'`
|
|
893
900
|
}
|
|
894
901
|
|
|
895
902
|
function isTextNode(node) {
|
|
@@ -1176,6 +1183,13 @@ function simplifySvgWidget(widget, size) {
|
|
|
1176
1183
|
return `SizedBox(${width}${height}child: ${svgCall})`
|
|
1177
1184
|
}
|
|
1178
1185
|
|
|
1186
|
+
function applySizeToWidget(widget, size) {
|
|
1187
|
+
if (!size || (size.w == null && size.h == null)) return widget
|
|
1188
|
+
const width = size.w != null ? `width: ${size.w}, ` : ''
|
|
1189
|
+
const height = size.h != null ? `height: ${size.h}, ` : ''
|
|
1190
|
+
return `SizedBox(${width}${height}child: ${widget})`
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1179
1193
|
function shouldStripIconPositioning(size, position, rotate) {
|
|
1180
1194
|
return (position != null || rotate != null) && isSmallIconSize(size)
|
|
1181
1195
|
}
|
|
@@ -1297,92 +1311,6 @@ function buildRadioGroupWidgetFromElement(el, cssMap) {
|
|
|
1297
1311
|
return `_GeneratedRadioGroup(${widgetArgs.join(', ')})`
|
|
1298
1312
|
}
|
|
1299
1313
|
|
|
1300
|
-
function roundNumber(value, digits = 4) {
|
|
1301
|
-
const factor = 10 ** digits
|
|
1302
|
-
return Math.round(value * factor) / factor
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
function parsePaddingValues(props) {
|
|
1306
|
-
const fromPadding = parseBoxValues(props['padding'])
|
|
1307
|
-
if (fromPadding) return fromPadding
|
|
1308
|
-
return {
|
|
1309
|
-
top: cssPxToDouble(props['padding-top']) || 0,
|
|
1310
|
-
right: cssPxToDouble(props['padding-right']) || 0,
|
|
1311
|
-
bottom: cssPxToDouble(props['padding-bottom']) || 0,
|
|
1312
|
-
left: cssPxToDouble(props['padding-left']) || 0,
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
function solidColorFromBackground(props) {
|
|
1317
|
-
const bg = props['background-color'] || props['background']
|
|
1318
|
-
if (!bg || /linear-gradient/i.test(String(bg))) return null
|
|
1319
|
-
return cssColorToAppColor(bg)
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
function gradientOrSolidFromBackground(props) {
|
|
1323
|
-
const bgSource = props['background-image'] || props['background']
|
|
1324
|
-
if (bgSource && /linear-gradient/i.test(String(bgSource))) {
|
|
1325
|
-
const gradient = parseLinearGradient(bgSource)
|
|
1326
|
-
if (gradient) return gradient
|
|
1327
|
-
}
|
|
1328
|
-
const solid = solidColorFromBackground(props)
|
|
1329
|
-
if (solid) return `LinearGradient(colors: [${solid}, ${solid}])`
|
|
1330
|
-
return null
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
function buildProgressBarWidgetFromElement(el, cssMap) {
|
|
1334
|
-
if (!el || !Array.isArray(el.children)) return null
|
|
1335
|
-
const directElements = el.children.filter(c => c.type === 'JSXElement')
|
|
1336
|
-
if (directElements.length !== 1) return null
|
|
1337
|
-
|
|
1338
|
-
const parentClass = getClassNameAttr(el)
|
|
1339
|
-
const child = directElements[0]
|
|
1340
|
-
const childClass = getClassNameAttr(child)
|
|
1341
|
-
if (!parentClass || !childClass) return null
|
|
1342
|
-
|
|
1343
|
-
const parentProps = (cssMap && cssMap[parentClass]) ? cssMap[parentClass] : {}
|
|
1344
|
-
const childProps = (cssMap && cssMap[childClass]) ? cssMap[childClass] : {}
|
|
1345
|
-
|
|
1346
|
-
const parentSize = normalizeGeneratedSize(sizeFromProps(parentProps))
|
|
1347
|
-
const childSize = normalizeGeneratedSize(sizeFromProps(childProps))
|
|
1348
|
-
if (childSize.w == null || childSize.h == null) return null
|
|
1349
|
-
|
|
1350
|
-
const hasProgressLikeBackground = !!solidColorFromBackground(parentProps)
|
|
1351
|
-
const fillGradient = gradientOrSolidFromBackground(childProps)
|
|
1352
|
-
if (!hasProgressLikeBackground || !fillGradient) return null
|
|
1353
|
-
|
|
1354
|
-
const padding = parsePaddingValues(parentProps)
|
|
1355
|
-
let totalWidth = parentSize.w
|
|
1356
|
-
if (totalWidth == null) {
|
|
1357
|
-
const inferred = childSize.w + (padding.left || 0) + (padding.right || 0)
|
|
1358
|
-
if (inferred > childSize.w) totalWidth = inferred
|
|
1359
|
-
}
|
|
1360
|
-
if (totalWidth == null || totalWidth <= 0) return null
|
|
1361
|
-
|
|
1362
|
-
let value = childSize.w / totalWidth
|
|
1363
|
-
if (!Number.isFinite(value)) return null
|
|
1364
|
-
if (value <= 0 || value > 1.05) return null
|
|
1365
|
-
value = Math.max(0, Math.min(1, value))
|
|
1366
|
-
|
|
1367
|
-
const height = parentSize.h ?? childSize.h
|
|
1368
|
-
if (height == null || height <= 0) return null
|
|
1369
|
-
|
|
1370
|
-
const backgroundColor = solidColorFromBackground(parentProps)
|
|
1371
|
-
const radius = borderRadiusFromProps(parentProps) || borderRadiusFromProps(childProps)
|
|
1372
|
-
const args = [
|
|
1373
|
-
`value: ${roundNumber(value)}`,
|
|
1374
|
-
`height: ${roundNumber(height, 2)}`,
|
|
1375
|
-
`width: ${roundNumber(totalWidth, 2)}`,
|
|
1376
|
-
`progressGradient: ${fillGradient}`,
|
|
1377
|
-
'padding: 0',
|
|
1378
|
-
'showDot: false',
|
|
1379
|
-
]
|
|
1380
|
-
if (backgroundColor) args.push(`backgroundColor: ${backgroundColor}`)
|
|
1381
|
-
if (radius) args.push(`borderRadius: ${radius}`)
|
|
1382
|
-
|
|
1383
|
-
return `AppProgressBar(${args.join(', ')})`
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
1314
|
function wrapOverflowIfSized(body, size) {
|
|
1387
1315
|
if (process.env.JSX2FLUTTER_ENABLE_OVERFLOW !== '1') return body
|
|
1388
1316
|
if (!size || (!size.w && !size.h)) return body
|
|
@@ -1413,8 +1341,20 @@ function wrapOverflowIfSized(body, size) {
|
|
|
1413
1341
|
return `OverflowBox(${overflowArgs.join(', ')}, child: ${body})`
|
|
1414
1342
|
}
|
|
1415
1343
|
|
|
1344
|
+
function unwrapDirectFlexWidget(widgetExpr) {
|
|
1345
|
+
let current = String(widgetExpr || '').trim()
|
|
1346
|
+
while (/^(?:const\s+)?(?:Expanded|Flexible)\s*\(/.test(current)) {
|
|
1347
|
+
const wrapperMatch = current.match(/^(?:const\s+)?(?:Expanded|Flexible)\s*\(([\s\S]*)\)$/)
|
|
1348
|
+
if (!wrapperMatch) break
|
|
1349
|
+
const childMatch = wrapperMatch[1].match(/\bchild:\s*([\s\S]+)$/)
|
|
1350
|
+
if (!childMatch) break
|
|
1351
|
+
current = childMatch[1].trim()
|
|
1352
|
+
}
|
|
1353
|
+
return current
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1416
1356
|
function buildGeneratedRippleButton(titleExpr) {
|
|
1417
|
-
return `RippleButton(title: ${titleExpr}, backgroundColor: Colors.transparent,
|
|
1357
|
+
return `RippleButton(title: ${titleExpr}, backgroundColor: Colors.transparent,minWidth : 0, padding: EdgeInsets.zero, onTap: () {})`
|
|
1418
1358
|
}
|
|
1419
1359
|
|
|
1420
1360
|
function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
@@ -1469,12 +1409,6 @@ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
|
1469
1409
|
semanticWidget = radioGroupWidget
|
|
1470
1410
|
}
|
|
1471
1411
|
}
|
|
1472
|
-
if (!semanticWidget) {
|
|
1473
|
-
const progressBarWidget = buildProgressBarWidgetFromElement(el, cssMap)
|
|
1474
|
-
if (progressBarWidget) {
|
|
1475
|
-
semanticWidget = progressBarWidget
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
1412
|
if (!semanticWidget && looksLikeButtonClass(className)) {
|
|
1479
1413
|
const buttonLabel = uniqueTexts(extractTextsFromElement(el, [])).find(Boolean)
|
|
1480
1414
|
if (buttonLabel) {
|
|
@@ -1525,7 +1459,8 @@ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
|
1525
1459
|
const src = getAttr(el, 'src') || ''
|
|
1526
1460
|
if (/^https?:\/\//.test(src)) {
|
|
1527
1461
|
const widget = `Image.network(${toDartLiteral(src)}, fit: BoxFit.cover)`
|
|
1528
|
-
|
|
1462
|
+
const sized = applySizeToWidget(widget, size)
|
|
1463
|
+
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${sized})` : sized, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1529
1464
|
}
|
|
1530
1465
|
const local = resolveLocalAsset(src, className)
|
|
1531
1466
|
if (local) {
|
|
@@ -1538,7 +1473,8 @@ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
|
1538
1473
|
const finalWidget = (rotate != null && !stripPlacement) ? `Transform.rotate(angle: ${rotate}, child: ${simplified})` : simplified
|
|
1539
1474
|
return { widget: finalWidget, isAbsolute: stripPlacement ? false : !!position, position: stripPlacement ? null : position, zIndex, flexGrow, size }
|
|
1540
1475
|
}
|
|
1541
|
-
|
|
1476
|
+
const sized = applySizeToWidget(widget, size)
|
|
1477
|
+
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${sized})` : sized, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1542
1478
|
}
|
|
1543
1479
|
const asset = assetForClassName(className)
|
|
1544
1480
|
if (asset) {
|
|
@@ -1561,12 +1497,10 @@ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
|
1561
1497
|
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1562
1498
|
}
|
|
1563
1499
|
if (tag === 'span' || tag === 'p') {
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
const widget = children.length
|
|
1569
|
-
? `Row(children: [${children.map(c => c.widget).join(', ')}])`
|
|
1500
|
+
const align = textAlignFromProps(props)
|
|
1501
|
+
const spans = buildInlineTextSpans(el, cssMap, topClassName, skipRootDecoration)
|
|
1502
|
+
const widget = spans.length
|
|
1503
|
+
? `RichText(text: TextSpan(children: [${spans.join(', ')}])${align ? `, textAlign: ${align}` : ''})`
|
|
1570
1504
|
: 'SizedBox.shrink()'
|
|
1571
1505
|
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1572
1506
|
}
|
|
@@ -1575,27 +1509,24 @@ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
|
1575
1509
|
const flowKids = semanticWidget ? [] : children.filter(c => !c.isAbsolute)
|
|
1576
1510
|
const absKids = semanticWidget ? [] : children.filter(c => c.isAbsolute).sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0))
|
|
1577
1511
|
const inputOwnedWidget = usesAppOwnedInputBackground(semanticWidget)
|
|
1578
|
-
const progressBarOwnedWidget = usesAppOwnedProgressBarBackground(semanticWidget)
|
|
1579
1512
|
const wrappedInputOwnedWidget = !semanticWidget && flowKids.length === 1 && usesAppOwnedInputBackground(flowKids[0].widget)
|
|
1580
|
-
const wrappedProgressBarOwnedWidget = !semanticWidget && flowKids.length === 1 && usesAppOwnedProgressBarBackground(flowKids[0].widget)
|
|
1581
1513
|
const appOwnedInputWidget = inputOwnedWidget || wrappedInputOwnedWidget
|
|
1582
|
-
const
|
|
1583
|
-
const appOwnedStyledWidget = appOwnedInputWidget || appOwnedProgressBarWidget
|
|
1584
|
-
const omitBackgroundColor = appOwnedStyledWidget
|
|
1514
|
+
const omitBackgroundColor = appOwnedInputWidget
|
|
1585
1515
|
const decoration = (skipRootDecoration && className === topClassName)
|
|
1586
1516
|
? null
|
|
1587
1517
|
: boxDecorationFromProps(props, { omitBackgroundColor })
|
|
1588
|
-
const effectivePadding =
|
|
1589
|
-
const effectiveDecoration =
|
|
1518
|
+
const effectivePadding = appOwnedInputWidget ? null : padding
|
|
1519
|
+
const effectiveDecoration = appOwnedInputWidget ? null : decoration
|
|
1590
1520
|
const layoutSize = (appOwnedInputWidget && size) ? { ...size, h: null } : size
|
|
1591
1521
|
const flex = flexConfigFromProps(props)
|
|
1592
1522
|
let flowBody = semanticWidget
|
|
1593
1523
|
if (!flowBody && flowKids.length) {
|
|
1594
1524
|
if (flex) {
|
|
1525
|
+
const isScrollableRoot = className === topClassName && process.env.JSX2FLUTTER_MODE !== 'classic'
|
|
1595
1526
|
const alignSelf = String(props['align-self'] || '').toLowerCase()
|
|
1596
|
-
const hasMainAxisSize = flex.isRow
|
|
1527
|
+
const hasMainAxisSize = !isScrollableRoot && (flex.isRow
|
|
1597
1528
|
? (layoutSize.w != null || alignSelf === 'stretch')
|
|
1598
|
-
: (layoutSize.h != null)
|
|
1529
|
+
: (layoutSize.h != null))
|
|
1599
1530
|
const gapLiteral = flex.gap ? (flex.isRow ? `${flex.gap}.width` : `${flex.gap}.height`) : null
|
|
1600
1531
|
const baseKids = flowKids.map(c => {
|
|
1601
1532
|
if (flex && c.flexGrow > 0 && hasMainAxisSize) {
|
|
@@ -1727,6 +1658,7 @@ function generateDart(ast, cssMap, outClassName, outPath) {
|
|
|
1727
1658
|
const rootDeco = boxDecorationFromProps(rootProps)
|
|
1728
1659
|
const mode = process.env.JSX2FLUTTER_MODE
|
|
1729
1660
|
let inner = rootWidget?.widget || 'const SizedBox.shrink()'
|
|
1661
|
+
inner = unwrapDirectFlexWidget(inner)
|
|
1730
1662
|
let finalRoot
|
|
1731
1663
|
if (mode === 'classic') {
|
|
1732
1664
|
finalRoot = inner
|
|
@@ -1752,35 +1684,31 @@ function generateDart(ast, cssMap, outClassName, outPath) {
|
|
|
1752
1684
|
const usesCheckbox = finalRoot.includes('_GeneratedCheckbox(') || finalRoot.includes('AppCheckbox(')
|
|
1753
1685
|
const usesRippleButton = finalRoot.includes('RippleButton(')
|
|
1754
1686
|
const usesTextGradient = finalRoot.includes('AppTextGradient(')
|
|
1755
|
-
const usesProgressBar = finalRoot.includes('AppProgressBar(')
|
|
1756
1687
|
const usesGetWidth = finalRoot.includes('Get.width')
|
|
1757
1688
|
const imports = [
|
|
1758
1689
|
"import 'package:flutter/material.dart';",
|
|
1759
|
-
|
|
1760
|
-
|
|
1690
|
+
"import 'package:link_home/src/utils/app_colors.dart';",
|
|
1691
|
+
"import 'package:link_home/src/extensions/int_extensions.dart';",
|
|
1761
1692
|
"import 'package:flutter_svg/flutter_svg.dart';",
|
|
1762
|
-
|
|
1693
|
+
"import 'package:link_home/src/utils/app_assets.dart';",
|
|
1763
1694
|
]
|
|
1764
1695
|
if (usesInput) {
|
|
1765
|
-
imports.push(
|
|
1696
|
+
imports.push("import 'package:link_home/src/ui/widgets/app_input.dart';")
|
|
1766
1697
|
}
|
|
1767
1698
|
if (usesDateTimeField) {
|
|
1768
|
-
imports.push(
|
|
1699
|
+
imports.push("import 'package:link_home/src/ui/widgets/app_input_full_time.dart';")
|
|
1769
1700
|
}
|
|
1770
1701
|
if (usesRadio) {
|
|
1771
|
-
imports.push(
|
|
1702
|
+
imports.push("import 'package:link_home/src/ui/widgets/app_radio_button.dart';")
|
|
1772
1703
|
}
|
|
1773
1704
|
if (usesCheckbox) {
|
|
1774
|
-
imports.push(
|
|
1705
|
+
imports.push("import 'package:link_home/src/ui/widgets/base/checkbox/app_checkbox.dart';")
|
|
1775
1706
|
}
|
|
1776
1707
|
if (usesRippleButton) {
|
|
1777
|
-
imports.push(
|
|
1708
|
+
imports.push("import 'package:link_home/src/ui/widgets/base/ripple_button.dart';")
|
|
1778
1709
|
}
|
|
1779
1710
|
if (usesTextGradient) {
|
|
1780
|
-
imports.push(
|
|
1781
|
-
}
|
|
1782
|
-
if (usesProgressBar) {
|
|
1783
|
-
imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_progressbar.dart';`)
|
|
1711
|
+
imports.push("import 'package:link_home/src/ui/widgets/app_text_gradient.dart';")
|
|
1784
1712
|
}
|
|
1785
1713
|
if (usesGetWidth) {
|
|
1786
1714
|
imports.push("import 'package:get/get.dart';")
|