ngx-form-designer 0.0.23
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/README.md +279 -0
- package/convert-legacy-rem-to-sass-function.mjs +198 -0
- package/fesm2022/ngx-form-designer.mjs +33548 -0
- package/fesm2022/ngx-form-designer.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/data/data-catalog.d.ts +46 -0
- package/lib/data/data-provider.d.ts +69 -0
- package/lib/data/data-source-client.d.ts +59 -0
- package/lib/data/data-source-parsers.d.ts +7 -0
- package/lib/data/external-data-source.d.ts +29 -0
- package/lib/data/file-upload-client.d.ts +19 -0
- package/lib/data/http-data-source-client.d.ts +31 -0
- package/lib/data/in-memory-data-catalog.service.d.ts +12 -0
- package/lib/data/runtime-field-data-access-registry.service.d.ts +29 -0
- package/lib/data/runtime-field-data-access.d.ts +32 -0
- package/lib/data/tree-utils.d.ts +28 -0
- package/lib/email-renderer/email-renderer.component.d.ts +21 -0
- package/lib/form-core/event-api-reference.d.ts +8 -0
- package/lib/form-core/form-engine.d.ts +55 -0
- package/lib/form-core/form-event-runner.d.ts +28 -0
- package/lib/form-core/models.d.ts +358 -0
- package/lib/form-core/plugin-metadata.d.ts +10 -0
- package/lib/form-core/rule-evaluation.service.d.ts +18 -0
- package/lib/form-core/schema-factory.d.ts +11 -0
- package/lib/form-core/schema-guard.d.ts +18 -0
- package/lib/form-core/schema-utils.d.ts +9 -0
- package/lib/form-designer/data-panel/data-panel.component.d.ts +101 -0
- package/lib/form-designer/designer-context.service.d.ts +29 -0
- package/lib/form-designer/designer-state.service.d.ts +167 -0
- package/lib/form-designer/dynamic-properties/dynamic-properties.component.d.ts +92 -0
- package/lib/form-designer/events-panel/events-panel.component.d.ts +21 -0
- package/lib/form-designer/events-workspace.component.d.ts +125 -0
- package/lib/form-designer/field-palette.component.d.ts +99 -0
- package/lib/form-designer/form-designer-shell.component.d.ts +81 -0
- package/lib/form-designer/form-preview.component.d.ts +36 -0
- package/lib/form-designer/form-settings-inspector.component.d.ts +15 -0
- package/lib/form-designer/global-data-manager.component.d.ts +32 -0
- package/lib/form-designer/inspector-sections/inspector-advanced-section.component.d.ts +17 -0
- package/lib/form-designer/inspector-sections/inspector-backgrounds-section.component.d.ts +14 -0
- package/lib/form-designer/inspector-sections/inspector-borders-section.component.d.ts +45 -0
- package/lib/form-designer/inspector-sections/inspector-effects-section.component.d.ts +22 -0
- package/lib/form-designer/inspector-sections/inspector-layout-section.component.d.ts +33 -0
- package/lib/form-designer/inspector-sections/inspector-position-section.component.d.ts +28 -0
- package/lib/form-designer/inspector-sections/inspector-size-section.component.d.ts +12 -0
- package/lib/form-designer/inspector-sections/inspector-spacing-section.component.d.ts +13 -0
- package/lib/form-designer/inspector-sections/inspector-typography-section.component.d.ts +31 -0
- package/lib/form-designer/json-form-designer.component.d.ts +17 -0
- package/lib/form-designer/layer-tree/layer-tree.component.d.ts +24 -0
- package/lib/form-designer/layout-canvas.component.d.ts +69 -0
- package/lib/form-designer/page-style.d.ts +2 -0
- package/lib/form-designer/properties-panel.component.d.ts +65 -0
- package/lib/form-designer/rules-editor/query-builder/query-builder.component.d.ts +23 -0
- package/lib/form-designer/rules-editor/rules-panel/rules-panel.component.d.ts +15 -0
- package/lib/form-designer/services/widget-definition-resolver.service.d.ts +38 -0
- package/lib/form-designer/template-library.d.ts +9 -0
- package/lib/form-designer/widget-inspector.component.d.ts +30 -0
- package/lib/form-renderer/form-viewer/form-viewer-readonly.component.d.ts +56 -0
- package/lib/form-renderer/form-viewer/form-viewer.component.d.ts +55 -0
- package/lib/form-renderer/json-form-renderer.component.d.ts +98 -0
- package/lib/form-renderer/layout-node.component.d.ts +94 -0
- package/lib/plugins/core-plugins.d.ts +5 -0
- package/lib/plugins/designer-plugin.d.ts +15 -0
- package/lib/plugins/plugin-context.d.ts +18 -0
- package/lib/plugins/plugin-providers.d.ts +3 -0
- package/lib/plugins/section-definition.d.ts +16 -0
- package/lib/theme/theme.service.d.ts +15 -0
- package/lib/ui/json-schema-editor.component.d.ts +27 -0
- package/lib/ui/monaco-editor.component.d.ts +24 -0
- package/lib/ui/ui-accordion.component.d.ts +11 -0
- package/lib/ui/ui-box-model.component.d.ts +55 -0
- package/lib/ui/ui-color-swatch.component.d.ts +12 -0
- package/lib/ui/ui-dimension.component.d.ts +21 -0
- package/lib/ui/ui-edge-box.component.d.ts +20 -0
- package/lib/ui/ui-field-wrapper.component.d.ts +8 -0
- package/lib/ui/ui-icon.module.d.ts +7 -0
- package/lib/ui/ui-input.component.d.ts +17 -0
- package/lib/ui/ui-range-number.component.d.ts +16 -0
- package/lib/ui/ui-select-icon.component.d.ts +18 -0
- package/lib/ui/ui-tabs.component.d.ts +25 -0
- package/lib/website/website-brick-studio.component.d.ts +67 -0
- package/lib/website/website-designer-shell.component.d.ts +53 -0
- package/lib/website/website-preview-shell.component.d.ts +25 -0
- package/lib/website/website-project.models.d.ts +78 -0
- package/lib/website/website-project.service.d.ts +50 -0
- package/lib/website/website-section-library.d.ts +6 -0
- package/lib/widgets/email-widgets/email-button-widget.component.d.ts +15 -0
- package/lib/widgets/email-widgets/email-heading-widget.component.d.ts +15 -0
- package/lib/widgets/email-widgets/email-text-widget.component.d.ts +13 -0
- package/lib/widgets/email-widgets.d.ts +2 -0
- package/lib/widgets/field-widgets/checkbox/checkbox-widget.component.d.ts +28 -0
- package/lib/widgets/field-widgets/checkbox-group/checkbox-group-widget.component.d.ts +40 -0
- package/lib/widgets/field-widgets/file-upload/file-upload-widget.component.d.ts +45 -0
- package/lib/widgets/field-widgets/radio/radio-widget.component.d.ts +39 -0
- package/lib/widgets/field-widgets/repeatable-group/repeatable-group-widget.component.d.ts +69 -0
- package/lib/widgets/field-widgets/rich-text/rich-text-widget.component.d.ts +17 -0
- package/lib/widgets/field-widgets/search/search-widget.component.d.ts +56 -0
- package/lib/widgets/field-widgets/select/select-widget.component.d.ts +53 -0
- package/lib/widgets/field-widgets/text-field/text-field.component.d.ts +39 -0
- package/lib/widgets/field-widgets/tree-select/tree-select-widget.component.d.ts +47 -0
- package/lib/widgets/page-link-context.d.ts +8 -0
- package/lib/widgets/page-widgets/brick-settings.component.d.ts +23 -0
- package/lib/widgets/page-widgets/brick-widget.component.d.ts +47 -0
- package/lib/widgets/page-widgets/button-link-settings.component.d.ts +23 -0
- package/lib/widgets/page-widgets/button-widget.component.d.ts +21 -0
- package/lib/widgets/page-widgets/heading-widget.component.d.ts +22 -0
- package/lib/widgets/page-widgets/inline-quill-editor.component.d.ts +35 -0
- package/lib/widgets/page-widgets/table-inspector.component.d.ts +17 -0
- package/lib/widgets/page-widgets/table-widget.component.d.ts +36 -0
- package/lib/widgets/page-widgets/text-block-widget.component.d.ts +22 -0
- package/lib/widgets/page-widgets.d.ts +2 -0
- package/lib/widgets/static-widgets/image/image-widget.component.d.ts +20 -0
- package/lib/widgets/style-helpers.d.ts +8 -0
- package/lib/widgets/style-properties.d.ts +28 -0
- package/lib/widgets/style-sections.d.ts +1 -0
- package/lib/widgets/table-widget.d.ts +2 -0
- package/lib/widgets/widget-definition.d.ts +81 -0
- package/lib/widgets/widget-editor-context.d.ts +8 -0
- package/lib/widgets/widget-packs.d.ts +4 -0
- package/lib/widgets/widgets.d.ts +2 -0
- package/package.json +47 -0
- package/public-api.d.ts +73 -0
- package/style-test-bug.scss +2562 -0
- package/tailwind.preset.js +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# NgxFormDesigner
|
|
2
|
+
|
|
3
|
+
A visual Form Designer for Angular applications, featuring a drag-and-drop interface, responsive layout management, and an enterprise-grade rules engine.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Drag & Drop**: Intuitive canvas for arranging widgets.
|
|
7
|
+
- **Responsive Layout**: Row/Column system with breakpoint support.
|
|
8
|
+
- **Rules Engine**: Conditional logic (show/hide, validation, disabling) based on field values.
|
|
9
|
+
- **Event Workspace**: Dedicated Events tab for configuring `change/click/blur` actions (`setValue`, `log`, `api`).
|
|
10
|
+
- **Event Datasource Runtime**: API event actions write to `datasourceId`, and mapped widgets auto-refresh.
|
|
11
|
+
- **Search Widget**: Dedicated `core.form:search` widget with autosuggest list + datasource-backed querying.
|
|
12
|
+
- **JSON Schema**: Export/Import forms as simple JSON.
|
|
13
|
+
- **Modern UI**: Built with Tailwind CSS and lucide-angular icons.
|
|
14
|
+
|
|
15
|
+
## Documentation
|
|
16
|
+
|
|
17
|
+
Full beginner-friendly docs live in `projects/ngx-form-designer/docs/README.md`:
|
|
18
|
+
|
|
19
|
+
- `projects/ngx-form-designer/docs/GETTING_STARTED.md`
|
|
20
|
+
- `projects/ngx-form-designer/docs/PUBLIC_API.md`
|
|
21
|
+
- `projects/ngx-form-designer/docs/ARCHITECTURE.md`
|
|
22
|
+
- `projects/ngx-form-designer/docs/SCHEMA_REFERENCE.md`
|
|
23
|
+
- `projects/ngx-form-designer/docs/WIDGETS.md`
|
|
24
|
+
- `projects/ngx-form-designer/docs/PLUGINS.md`
|
|
25
|
+
- `projects/ngx-form-designer/docs/DATA_SOURCES.md`
|
|
26
|
+
- `projects/ngx-form-designer/docs/EVENT_DATASOURCE_RUNTIME.md`
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install ngx-form-designer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Dependencies
|
|
35
|
+
|
|
36
|
+
This library relies on **Tailwind CSS** and **lucide-angular** for UI styling and iconography.
|
|
37
|
+
|
|
38
|
+
1. **Tailwind CSS**: The library uses Tailwind utility classes.
|
|
39
|
+
|
|
40
|
+
**Option A: Using the Preset (Recommended)**
|
|
41
|
+
In your `tailwind.config.js` (or similar), add the preset:
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
module.exports = {
|
|
45
|
+
presets: [
|
|
46
|
+
require('ngx-form-designer/tailwind.preset.js')
|
|
47
|
+
],
|
|
48
|
+
content: [
|
|
49
|
+
"./src/**/*.{html,ts}",
|
|
50
|
+
"./node_modules/ngx-form-designer/**/*.{html,ts,mjs}"
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Option B: Manual Configuration**
|
|
56
|
+
If you prefer to manually copy the theme tokens, see `projects/ngx-form-designer/docs/STYLING_AND_THEME.md`.
|
|
57
|
+
|
|
58
|
+
**Design tokens (recommended):** The demo/theme uses Tailwind tokens like `accent-*`, `ink-*`, `slate-*`, `shadow-card`, and `shadow-popover`. If your app doesn’t define them, the UI will still work but can look different. See `projects/ngx-form-designer/docs/STYLING_AND_THEME.md`.
|
|
59
|
+
|
|
60
|
+
2. **lucide-angular**: Install and configure lucide-angular icons:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install lucide-angular
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
In your `app.config.ts`, import and provide the icons you need:
|
|
67
|
+
```typescript
|
|
68
|
+
import { LucideAngularModule, icons } from 'lucide-angular';
|
|
69
|
+
|
|
70
|
+
export const appConfig: ApplicationConfig = {
|
|
71
|
+
providers: [
|
|
72
|
+
// ... other providers
|
|
73
|
+
importProvidersFrom(LucideAngularModule.pick(icons))
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
Import `JsonFormDesignerComponent` (standalone) into your component or route.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Component } from '@angular/core';
|
|
84
|
+
import { JsonFormDesignerComponent } from 'ngx-form-designer';
|
|
85
|
+
|
|
86
|
+
@Component({
|
|
87
|
+
standalone: true,
|
|
88
|
+
imports: [JsonFormDesignerComponent],
|
|
89
|
+
template: `<app-json-form-designer></app-json-form-designer>`
|
|
90
|
+
})
|
|
91
|
+
export class MyComponent {}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
If you want the full scaffolded UI (top bar + template library) with minimal setup, use the shell component:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { Component } from '@angular/core';
|
|
98
|
+
import { FormDesignerShellComponent, type DesignerEventApiDefinition } from 'ngx-form-designer';
|
|
99
|
+
|
|
100
|
+
@Component({
|
|
101
|
+
standalone: true,
|
|
102
|
+
imports: [FormDesignerShellComponent],
|
|
103
|
+
template: `<app-form-designer-shell [eventApis]="eventApis"></app-form-designer-shell>`
|
|
104
|
+
})
|
|
105
|
+
export class MyDesignerPage {
|
|
106
|
+
eventApis: DesignerEventApiDefinition[] = [];
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Provider setup (required)
|
|
111
|
+
|
|
112
|
+
Widgets are registered via plugins. The simplest setup is to register the built-in plugin pack:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { ApplicationConfig } from '@angular/core';
|
|
116
|
+
import { CORE_DESIGNER_PLUGINS, provideDesignerPlugins } from 'ngx-form-designer';
|
|
117
|
+
|
|
118
|
+
export const appConfig: ApplicationConfig = {
|
|
119
|
+
providers: [
|
|
120
|
+
...provideDesignerPlugins(CORE_DESIGNER_PLUGINS),
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Without this, the palette will be empty and the renderer can’t resolve widgets.
|
|
126
|
+
|
|
127
|
+
## Publishing
|
|
128
|
+
|
|
129
|
+
Build the library:
|
|
130
|
+
```bash
|
|
131
|
+
ng build ngx-form-designer
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Publish from dist:
|
|
135
|
+
```bash
|
|
136
|
+
cd dist/ngx-form-designer
|
|
137
|
+
npm publish
|
|
138
|
+
```
|
|
139
|
+
## Data Sources (API-only)
|
|
140
|
+
|
|
141
|
+
The library uses a "Data Sources over APIs" architecture. Widgets should not store large datasets (CSV/JSON arrays) directly in schema. Instead, they reference a `datasourceId` and delegate data fetching to a registered `DataProvider`.
|
|
142
|
+
|
|
143
|
+
Full guide: `projects/ngx-form-designer/docs/DATA_SOURCES.md`.
|
|
144
|
+
|
|
145
|
+
### Concepts
|
|
146
|
+
|
|
147
|
+
1. **Datasource ID**: A stable string identifier (e.g., `countries`, `users`, `products`) representing a dataset.
|
|
148
|
+
2. **DataProvider Pipeline**: Widgets request data via the `DataProvider` service.
|
|
149
|
+
- `DataProvider` delegates to `DataSourceClient`.
|
|
150
|
+
- `DataSourceClient` is an abstraction you implement (or use the default) to fetch data.
|
|
151
|
+
3. **Designer & Runtime Consistency**: The Form Designer uses the exact same pipeline to render "Live Previews" of data in the configuration panel.
|
|
152
|
+
|
|
153
|
+
### Event datasource
|
|
154
|
+
|
|
155
|
+
Event API actions also write into `datasourceId` (event datasource). Any widget mapped to that datasource (select/search/table/text/date/time) refreshes after the event API call completes.
|
|
156
|
+
|
|
157
|
+
### Backend API Contract
|
|
158
|
+
|
|
159
|
+
If you use the HTTP-based `DataSourceClient`, your backend should implement these endpoints:
|
|
160
|
+
|
|
161
|
+
| Method | Endpoint | Description |
|
|
162
|
+
| :--- | :--- | :--- |
|
|
163
|
+
| `GET` | `/data-sources` | List available data sources (id, label) |
|
|
164
|
+
| `GET` | `/data-sources/{sourceId}` | Get details for a specific source |
|
|
165
|
+
| `GET` | `/data-sources/{sourceId}/columns` | Get column definitions (name, type) |
|
|
166
|
+
| `POST` | `/data-sources/{sourceId}/query` | query rows with filtering/sorting/paging |
|
|
167
|
+
|
|
168
|
+
**Query Payload Example:**
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"page": { "limit": 50, "offset": 0 },
|
|
172
|
+
"search": { "term": "foo", "columns": ["name"] },
|
|
173
|
+
"filters": [
|
|
174
|
+
{ "column": "category", "op": "eq", "value": "active" }
|
|
175
|
+
],
|
|
176
|
+
"sort": [ { "column": "name", "dir": "asc" } ]
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Host App Setup
|
|
181
|
+
|
|
182
|
+
#### 1. In-Memory / Mock Setup
|
|
183
|
+
Use this for rapid prototyping or testing without a real backend.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { DataCatalog, InMemoryDataCatalogService } from 'ngx-form-designer';
|
|
187
|
+
|
|
188
|
+
// in app.config.ts
|
|
189
|
+
providers: [
|
|
190
|
+
{ provide: DataCatalog, useExisting: InMemoryDataCatalogService }
|
|
191
|
+
]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### 2. HTTP Backend Setup
|
|
195
|
+
Use this to connect to your real API.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { provideHttpDataSourceClient } from 'ngx-form-designer';
|
|
199
|
+
|
|
200
|
+
// in app.config.ts
|
|
201
|
+
providers: [
|
|
202
|
+
provideHttpDataSourceClient({
|
|
203
|
+
baseUrl: 'https://api.example.com',
|
|
204
|
+
getHeaders: () => ({ Authorization: 'Bearer ' + myAuthService.getToken() })
|
|
205
|
+
})
|
|
206
|
+
]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Configuration Examples
|
|
210
|
+
|
|
211
|
+
#### Basic Dropdown (Select)
|
|
212
|
+
Fetches data from `countries` source, uses `name` as label and `isoCode` as value.
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"type": "select",
|
|
217
|
+
"dataConfig": {
|
|
218
|
+
"type": "source",
|
|
219
|
+
"datasourceId": "countries",
|
|
220
|
+
"labelKey": "name",
|
|
221
|
+
"valueKey": "isoCode"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Search-Enabled Dropdown
|
|
227
|
+
Enables server-side search to handle large datasets.
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"type": "select",
|
|
232
|
+
"dataConfig": {
|
|
233
|
+
"type": "source",
|
|
234
|
+
"datasourceId": "products",
|
|
235
|
+
"labelKey": "productName",
|
|
236
|
+
"valueKey": "id",
|
|
237
|
+
"searchEnabled": true,
|
|
238
|
+
"optionsLimit": 20,
|
|
239
|
+
"searchColumns": ["productName", "sku"]
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Tree Select (Hierarchical)
|
|
245
|
+
Expects a flat list of nodes where each node has an ID and a Parent ID.
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"type": "tree-select",
|
|
250
|
+
"dataConfig": {
|
|
251
|
+
"type": "source",
|
|
252
|
+
"datasourceId": "departments",
|
|
253
|
+
"labelKey": "title",
|
|
254
|
+
"valueKey": "id",
|
|
255
|
+
"treeIdKey": "id",
|
|
256
|
+
"treeParentKey": "parentId"
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### Data Table
|
|
262
|
+
Paged table view with server-side sorting.
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{
|
|
266
|
+
"type": "table",
|
|
267
|
+
"dataConfig": {
|
|
268
|
+
"type": "source",
|
|
269
|
+
"datasourceId": "orders",
|
|
270
|
+
"pageSize": 10,
|
|
271
|
+
"sort": [{ "column": "createdAt", "dir": "desc" }]
|
|
272
|
+
},
|
|
273
|
+
"columns": [
|
|
274
|
+
{ "key": "id", "label": "Order ID" },
|
|
275
|
+
{ "key": "total", "label": "Amount" },
|
|
276
|
+
{ "key": "status", "label": "Status" }
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
```
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_EXTENSIONS = ['.scss'];
|
|
9
|
+
const DEFAULT_EXCLUDES = ['.git', '.angular', '.next', '.nuxt', 'coverage', 'dist', 'node_modules', 'tmp'];
|
|
10
|
+
|
|
11
|
+
function formatPxIntent(value, precision) {
|
|
12
|
+
const rounded = Number(value.toFixed(precision));
|
|
13
|
+
const normalized = Number.isInteger(rounded) ? `${rounded}` : `${rounded}`;
|
|
14
|
+
return normalized.replace(/(\.\d*?[1-9])0+$/u, '$1').replace(/\.0$/u, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeUseStatement(useStatement) {
|
|
18
|
+
return useStatement?.trim() ?? '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function withUseStatement(text, useStatement) {
|
|
22
|
+
const normalizedUseStatement = normalizeUseStatement(useStatement);
|
|
23
|
+
if (!normalizedUseStatement || text.includes(normalizedUseStatement)) {
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return `${normalizedUseStatement}\n\n${text}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function convertLegacyRemToSassFunctionInText(text, options = {}) {
|
|
31
|
+
const basePx = options.basePx ?? 10;
|
|
32
|
+
const precision = options.precision ?? 6;
|
|
33
|
+
|
|
34
|
+
const convertedText = text.replace(/(-?(?:\d+\.?\d*|\.\d+))rem\b/gu, (match, numericPart) => {
|
|
35
|
+
const originalValue = Number(numericPart);
|
|
36
|
+
if (!Number.isFinite(originalValue) || originalValue === 0) {
|
|
37
|
+
return match;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return `rem(${formatPxIntent(originalValue * basePx, precision)})`;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (convertedText === text) {
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return withUseStatement(convertedText, options.useStatement);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function collectFiles(targetPath, options, collected = []) {
|
|
51
|
+
const stats = await fs.stat(targetPath);
|
|
52
|
+
if (stats.isFile()) {
|
|
53
|
+
if (options.includeExtensions.includes(path.extname(targetPath).toLowerCase())) {
|
|
54
|
+
collected.push(targetPath);
|
|
55
|
+
}
|
|
56
|
+
return collected;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (entry.isDirectory() && options.excludeDirectories.has(entry.name)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await collectFiles(path.join(targetPath, entry.name), options, collected);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return collected;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function processScssTargetPath(targetPath, options = {}) {
|
|
72
|
+
const resolvedOptions = {
|
|
73
|
+
basePx: options.basePx ?? 10,
|
|
74
|
+
precision: options.precision ?? 6,
|
|
75
|
+
useStatement: normalizeUseStatement(options.useStatement),
|
|
76
|
+
write: options.write ?? false,
|
|
77
|
+
includeExtensions: options.includeExtensions ?? DEFAULT_EXTENSIONS,
|
|
78
|
+
excludeDirectories: new Set(options.excludeDirectories ?? DEFAULT_EXCLUDES)
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const absoluteTargetPath = path.resolve(targetPath);
|
|
82
|
+
const files = await collectFiles(absoluteTargetPath, resolvedOptions);
|
|
83
|
+
const changedFiles = [];
|
|
84
|
+
|
|
85
|
+
for (const filePath of files) {
|
|
86
|
+
const currentText = await fs.readFile(filePath, 'utf8');
|
|
87
|
+
const nextText = convertLegacyRemToSassFunctionInText(currentText, resolvedOptions);
|
|
88
|
+
if (nextText === currentText) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const replacements = [...currentText.matchAll(/(-?(?:\d+\.?\d*|\.\d+))rem\b/gu)]
|
|
93
|
+
.filter(([, numericPart]) => Number(numericPart) !== 0)
|
|
94
|
+
.length;
|
|
95
|
+
|
|
96
|
+
changedFiles.push({ filePath, replacements, nextText });
|
|
97
|
+
|
|
98
|
+
if (resolvedOptions.write) {
|
|
99
|
+
await fs.writeFile(filePath, nextText, 'utf8');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
targetPath: absoluteTargetPath,
|
|
105
|
+
changedFiles,
|
|
106
|
+
visitedFiles: files.length
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printUsage() {
|
|
111
|
+
console.log(`Usage:
|
|
112
|
+
node scripts/convert-legacy-rem-to-sass-function.mjs <target-path> [--write]
|
|
113
|
+
[--base=10] [--precision=6] [--use=\"@use 'styles/units' as *;\"]
|
|
114
|
+
[--exclude=node_modules,dist,.git]
|
|
115
|
+
|
|
116
|
+
Only .scss files are scanned. Dry-run is the default.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseCliArgs(argv) {
|
|
120
|
+
const [targetPath, ...flags] = argv;
|
|
121
|
+
if (!targetPath || targetPath === '--help' || targetPath === '-h') {
|
|
122
|
+
return { showHelp: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const options = {
|
|
126
|
+
write: false,
|
|
127
|
+
basePx: 10,
|
|
128
|
+
precision: 6,
|
|
129
|
+
useStatement: '',
|
|
130
|
+
excludeDirectories: DEFAULT_EXCLUDES
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const flag of flags) {
|
|
134
|
+
if (flag === '--write') {
|
|
135
|
+
options.write = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (flag.startsWith('--base=')) {
|
|
139
|
+
options.basePx = Number(flag.slice('--base='.length));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (flag.startsWith('--precision=')) {
|
|
143
|
+
options.precision = Number(flag.slice('--precision='.length));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (flag.startsWith('--use=')) {
|
|
147
|
+
options.useStatement = flag.slice('--use='.length);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (flag.startsWith('--exclude=')) {
|
|
151
|
+
options.excludeDirectories = flag
|
|
152
|
+
.slice('--exclude='.length)
|
|
153
|
+
.split(',')
|
|
154
|
+
.map(value => value.trim())
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
throw new Error(`Unknown argument: ${flag}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (![options.basePx, options.precision].every(Number.isFinite)) {
|
|
163
|
+
throw new Error('Numeric arguments must be finite numbers.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { showHelp: false, targetPath, options };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function main() {
|
|
170
|
+
try {
|
|
171
|
+
const parsed = parseCliArgs(process.argv.slice(2));
|
|
172
|
+
if (parsed.showHelp) {
|
|
173
|
+
printUsage();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = await processScssTargetPath(parsed.targetPath, parsed.options);
|
|
178
|
+
if (!result.changedFiles.length) {
|
|
179
|
+
console.log(`No legacy rem values needed conversion in ${result.targetPath}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const changedFile of result.changedFiles) {
|
|
184
|
+
console.log(`${parsed.options.write ? 'updated' : 'would update'} ${changedFile.filePath} (${changedFile.replacements} replacements)`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log(
|
|
188
|
+
`${parsed.options.write ? 'Updated' : 'Found'} ${result.changedFiles.length} file(s) out of ${result.visitedFiles} scanned in ${result.targetPath}`
|
|
189
|
+
);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error(error instanceof Error ? error.message : error);
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
197
|
+
await main();
|
|
198
|
+
}
|