@unifylib/ui-lib 1.1.64 → 1.1.67

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.
Files changed (38) hide show
  1. package/cli/generators/dashboard.js +147 -0
  2. package/cli/generators/form-screen.js +95 -0
  3. package/cli/generators/line-editor.js +110 -0
  4. package/cli/generators/permission-matrix.js +115 -0
  5. package/cli/generators/section-form.js +103 -0
  6. package/cli/generators/table-screen.js +181 -0
  7. package/cli/index.js +185 -0
  8. package/cli/utils.js +228 -0
  9. package/esm2022/lib/components/action-comment/action-comment.component.mjs +6 -5
  10. package/esm2022/lib/components/advanced-filter/filter-builder/filter-builder.component.mjs +8 -3
  11. package/esm2022/lib/components/audit-log-list/audit-log.component.mjs +3 -3
  12. package/esm2022/lib/components/auto-complete/auto-complete.component.mjs +11 -3
  13. package/esm2022/lib/components/base-form-canvas/base-form-canvas.component.mjs +50 -4
  14. package/esm2022/lib/components/base-table/base-table.component.mjs +73 -9
  15. package/esm2022/lib/components/button-actions/button-actions.component.mjs +3 -3
  16. package/esm2022/lib/components/editable-base-table/editable-base-table.component.mjs +3 -3
  17. package/esm2022/lib/components/equation-builder/equation-builder.component.mjs +9 -4
  18. package/esm2022/lib/components/multi-auto-complete/multi-auto-complete.component.mjs +20 -4
  19. package/esm2022/lib/components/paginator/paginator.component.mjs +26 -6
  20. package/esm2022/lib/components/report-details-dialog/report-details-dialog.component.mjs +3 -3
  21. package/esm2022/lib/components/report-form/report-form.component.mjs +3 -3
  22. package/esm2022/lib/components/search-bar/search-bar.component.mjs +5 -4
  23. package/esm2022/lib/components/section-form-canvas/section-form-canvas.component.mjs +3 -3
  24. package/esm2022/lib/components/shared/attachment-uploader/attachment-uploader.component.mjs +5 -3
  25. package/esm2022/lib/components/snackbar-static/snackbar-static.component.mjs +3 -3
  26. package/esm2022/lib/components/title-bar/title-bar.component.mjs +3 -3
  27. package/fesm2022/iq-ui-lib.mjs +233 -72
  28. package/fesm2022/iq-ui-lib.mjs.map +1 -1
  29. package/fesm2022/unifylib-ui-lib.mjs +233 -72
  30. package/fesm2022/unifylib-ui-lib.mjs.map +1 -1
  31. package/lib/components/advanced-filter/filter-builder/filter-builder.component.d.ts +2 -0
  32. package/lib/components/auto-complete/auto-complete.component.d.ts +2 -1
  33. package/lib/components/base-form-canvas/base-form-canvas.component.d.ts +4 -0
  34. package/lib/components/base-table/base-table.component.d.ts +10 -1
  35. package/lib/components/multi-auto-complete/multi-auto-complete.component.d.ts +3 -1
  36. package/lib/components/paginator/paginator.component.d.ts +6 -1
  37. package/lib/components/shared/attachment-uploader/attachment-uploader.component.d.ts +2 -1
  38. package/package.json +1 -1
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @param {{
5
+ * kebab: string,
6
+ * pascal: string,
7
+ * labelsSection: string,
8
+ * apiUri: string,
9
+ * importPath: string,
10
+ * }} ctx
11
+ */
12
+ function generate(ctx) {
13
+ const fileBase = ctx.kebab;
14
+
15
+ const settings = `import { Validators } from "@angular/forms";
16
+ import {
17
+ ButtonActionSettings,
18
+ FieldInfo,
19
+ FilterRequest,
20
+ PageInfo,
21
+ ReportRequest,
22
+ TableColumn,
23
+ } from "${ctx.importPath}";
24
+
25
+ export const PAGE_INFO: PageInfo = {
26
+ id: "${ctx.kebab}",
27
+ labelsSection: "${ctx.labelsSection}",
28
+ hasAddButton: true,
29
+ apiUri: "${ctx.apiUri}",
30
+ selfRouting: true,
31
+ listRoutePath: "/${ctx.kebab}",
32
+ draftSupported: true,
33
+ pageSize: 10,
34
+ };
35
+
36
+ export const COLUMNS: TableColumn<Record<string, unknown>>[] = [
37
+ { property: "id", type: "text", enableSorting: true, visible: true, label: "id" },
38
+ { property: "name", type: "text", enableSorting: true, visible: true, label: "name" },
39
+ ];
40
+
41
+ export const FILTER_FIELDS: FieldInfo[] = [
42
+ new FieldInfo({
43
+ property: "name",
44
+ row: 1,
45
+ type: "text",
46
+ label: "name",
47
+ advancedFilter: true,
48
+ filterOperator: "LIKE",
49
+ validators: [Validators.maxLength(30)],
50
+ maxLength: 30,
51
+ } as FieldInfo),
52
+ ];
53
+
54
+ function buildReportRequest(): ReportRequest {
55
+ const filterRequest = new FilterRequest(PAGE_INFO.pageSize ?? 10);
56
+ filterRequest.columns = COLUMNS.filter((c) => c.visible !== false).map((c) => c.property);
57
+ return new ReportRequest({ filterRequest });
58
+ }
59
+
60
+ export const REPORT_REQUEST: ReportRequest = buildReportRequest();
61
+
62
+ export const LIST_ACTIONS: ButtonActionSettings[] = [
63
+ new ButtonActionSettings({
64
+ key: 1,
65
+ id: "view",
66
+ label: "VIEW",
67
+ linkHref: (row: unknown) => {
68
+ const id = (row as { id?: number }).id;
69
+ return id != null ? \`/${ctx.kebab}/view/\${id}\` : undefined;
70
+ },
71
+ } as ButtonActionSettings),
72
+ ];
73
+
74
+ export const TOOLBAR_ACTIONS: ButtonActionSettings[] = [
75
+ new ButtonActionSettings({
76
+ key: 1,
77
+ id: "create",
78
+ label: "ADD_NEW",
79
+ linkHref: "/${ctx.kebab}/new",
80
+ } as ButtonActionSettings),
81
+ ];
82
+ `;
83
+
84
+ const componentTs = `import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
85
+ import { Router } from "@angular/router";
86
+ import { TranslateModule } from "@ngx-translate/core";
87
+ import {
88
+ BaseTableComponent,
89
+ ButtonActionSettings,
90
+ Filter,
91
+ } from "${ctx.importPath}";
92
+ import {
93
+ COLUMNS,
94
+ FILTER_FIELDS,
95
+ LIST_ACTIONS,
96
+ PAGE_INFO,
97
+ REPORT_REQUEST,
98
+ TOOLBAR_ACTIONS,
99
+ } from "./${fileBase}.settings";
100
+
101
+ @Component({
102
+ selector: "app-${fileBase}",
103
+ standalone: true,
104
+ imports: [BaseTableComponent, TranslateModule],
105
+ templateUrl: "./${fileBase}.component.html",
106
+ styleUrls: ["./${fileBase}.component.scss"],
107
+ changeDetection: ChangeDetectionStrategy.OnPush,
108
+ })
109
+ export class ${ctx.pascal}Component {
110
+ private readonly router = inject(Router);
111
+
112
+ readonly pageInfo = PAGE_INFO;
113
+ readonly columnsDefinition = COLUMNS;
114
+ readonly filterFieldDefinitions = FILTER_FIELDS;
115
+ readonly reportRequest = REPORT_REQUEST;
116
+ readonly filters: Filter[] = [];
117
+ readonly listActions = LIST_ACTIONS;
118
+ readonly toolbarActions = TOOLBAR_ACTIONS;
119
+ readonly isPending = false;
120
+
121
+ onListActionClicked(event: {
122
+ action: ButtonActionSettings;
123
+ row: unknown;
124
+ isPending: boolean;
125
+ }): void {
126
+ const href =
127
+ typeof event.action.linkHref === "function"
128
+ ? event.action.linkHref(event.row)
129
+ : event.action.linkHref;
130
+ if (href) {
131
+ void this.router.navigateByUrl(href);
132
+ }
133
+ }
134
+
135
+ onToolbarButton(action: ButtonActionSettings): void {
136
+ if (typeof action.linkHref === "string") {
137
+ void this.router.navigateByUrl(action.linkHref);
138
+ }
139
+ }
140
+ }
141
+ `;
142
+
143
+ const componentHtml = `<app-base-table
144
+ [pageInfo]="pageInfo"
145
+ [columns]="columnsDefinition"
146
+ [reportRequest]="reportRequest"
147
+ [filterFields]="filterFieldDefinitions"
148
+ [isPending]="isPending"
149
+ [filters]="filters"
150
+ [listAction]="listActions"
151
+ [extraActions]="toolbarActions"
152
+ [buttonsDisplayMode]="'list'"
153
+ (listActionClicked)="onListActionClicked($event)"
154
+ (buttonClicked)="onToolbarButton($event)">
155
+ </app-base-table>
156
+ `;
157
+
158
+ const componentScss = `:host {
159
+ display: block;
160
+ }
161
+ `;
162
+
163
+ return [
164
+ { relative: `${fileBase}.settings.ts`, content: settings },
165
+ { relative: `${fileBase}.component.ts`, content: componentTs },
166
+ { relative: `${fileBase}.component.html`, content: componentHtml },
167
+ { relative: `${fileBase}.component.scss`, content: componentScss },
168
+ ];
169
+ }
170
+
171
+ function getTranslations(ctx) {
172
+ return {
173
+ [`${ctx.labelsSection}.list_title`]: `${ctx.pascal} List`,
174
+ [`${ctx.labelsSection}.id`]: "ID",
175
+ [`${ctx.labelsSection}.name`]: "Name",
176
+ VIEW: "View",
177
+ ADD_NEW: "Add new",
178
+ };
179
+ }
180
+
181
+ module.exports = { generate, getTranslations };
package/cli/index.js ADDED
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const path = require("path");
5
+ const {
6
+ kebabCase,
7
+ pascalCase,
8
+ upperSnakeFromKebab,
9
+ lastPathSegment,
10
+ writeFiles,
11
+ upsertI18nEntries,
12
+ } = require("./utils");
13
+
14
+ const GENERATORS = {
15
+ "form-screen": require("./generators/form-screen.js"),
16
+ "table-screen": require("./generators/table-screen.js"),
17
+ "section-form": require("./generators/section-form.js"),
18
+ "line-editor": require("./generators/line-editor.js"),
19
+ dashboard: require("./generators/dashboard.js"),
20
+ "permission-matrix": require("./generators/permission-matrix.js"),
21
+ };
22
+
23
+ function printHelp() {
24
+ console.log(`
25
+ iq-ui — scaffold Angular screens that use @unifylib/ui-lib
26
+
27
+ Usage:
28
+ iq-ui -g <type> <destination> [options]
29
+
30
+ Types:
31
+ form-screen app-base-form-canvas + settings
32
+ table-screen app-base-table + settings
33
+ section-form lib-section-form-canvas + settings
34
+ line-editor app-item-line-editor + settings
35
+ dashboard lib-action-card grid (no settings file)
36
+ permission-matrix lib-permission-matrix + settings
37
+
38
+ Options:
39
+ --name <n> File/class base name (default: last segment of destination)
40
+ --labels-section <s> i18n labelsSection (default: UPPER_SNAKE from name)
41
+ --api-uri <u> API path for PageInfo.apiUri (default: /<name>)
42
+ --fields-count <n> Placeholder fields (default: 6; form-screen, section-form, line-editor)
43
+ --import-path <p> Library import (default: @iq/ui-lib)
44
+ --i18n-path <p> Optional translation file/folder (example: src/assets/en.json or src/assets)
45
+ --dry-run Print files that would be written
46
+ -h, --help Show this help
47
+
48
+ Examples (after npm install @unifylib/ui-lib, use the package binary; do not run \`npx iq-ui\`, which looks up a wrong package on npm):
49
+ npx -p @unifylib/ui-lib iq-ui -g form-screen src/app/registration --i18n-path src/assets/en.json
50
+ npx -p @unifylib/ui-lib iq-ui -g table-screen src/app/devices --labels-section DEVICE --api-uri /devices/list
51
+ npx -p @unifylib/ui-lib iq-ui -g dashboard src/app/home --import-path @unifylib/ui-lib
52
+
53
+ Local dev / linked build:
54
+ node node_modules/@unifylib/ui-lib/cli/index.js -g form-screen src/app/registration
55
+ `);
56
+ }
57
+
58
+ /**
59
+ * @returns {{
60
+ * type?: string,
61
+ * destination?: string,
62
+ * name?: string,
63
+ * labelsSection?: string,
64
+ * apiUri?: string,
65
+ * fieldsCount: number,
66
+ * importPath: string,
67
+ * i18nPath?: string,
68
+ * dryRun: boolean,
69
+ * help: boolean,
70
+ * }}
71
+ */
72
+ function parseArgs(argv) {
73
+ const out = {
74
+ fieldsCount: 6,
75
+ importPath: "@iq/ui-lib",
76
+ dryRun: false,
77
+ help: false,
78
+ };
79
+
80
+ for (let i = 2; i < argv.length; i++) {
81
+ const a = argv[i];
82
+ if (a === "-h" || a === "--help") {
83
+ out.help = true;
84
+ continue;
85
+ }
86
+ if (a === "--dry-run") {
87
+ out.dryRun = true;
88
+ continue;
89
+ }
90
+ if (a === "-g" || a === "--generate") {
91
+ out.type = argv[++i];
92
+ continue;
93
+ }
94
+ if (a === "--name") {
95
+ out.name = argv[++i];
96
+ continue;
97
+ }
98
+ if (a === "--labels-section") {
99
+ out.labelsSection = argv[++i];
100
+ continue;
101
+ }
102
+ if (a === "--api-uri") {
103
+ out.apiUri = argv[++i];
104
+ continue;
105
+ }
106
+ if (a === "--fields-count") {
107
+ out.fieldsCount = parseInt(argv[++i], 10) || 6;
108
+ continue;
109
+ }
110
+ if (a === "--import-path") {
111
+ out.importPath = argv[++i];
112
+ continue;
113
+ }
114
+ if (a === "--i18n-path") {
115
+ out.i18nPath = argv[++i];
116
+ continue;
117
+ }
118
+ if (a.startsWith("-")) {
119
+ console.error("Unknown option:", a);
120
+ process.exitCode = 1;
121
+ out.help = true;
122
+ return out;
123
+ }
124
+ if (!out.destination) {
125
+ out.destination = a;
126
+ } else {
127
+ console.error("Unexpected argument:", a);
128
+ process.exitCode = 1;
129
+ out.help = true;
130
+ return out;
131
+ }
132
+ }
133
+ return out;
134
+ }
135
+
136
+ function main() {
137
+ const args = parseArgs(process.argv);
138
+ if (args.help) {
139
+ printHelp();
140
+ process.exit(process.exitCode || 0);
141
+ }
142
+ if (!args.type || !args.destination) {
143
+ console.error("Missing required: -g <type> and <destination>\n");
144
+ printHelp();
145
+ process.exit(1);
146
+ }
147
+
148
+ const gen = GENERATORS[args.type];
149
+ if (!gen || typeof gen.generate !== "function") {
150
+ console.error("Unknown generator type:", args.type);
151
+ printHelp();
152
+ process.exit(1);
153
+ }
154
+
155
+ const rawName = args.name || lastPathSegment(args.destination);
156
+ const kebab = kebabCase(rawName);
157
+ const pascal = pascalCase(rawName);
158
+ const labelsSection = args.labelsSection || upperSnakeFromKebab(kebab);
159
+ const apiUri = args.apiUri != null ? args.apiUri : `/${kebab}`;
160
+
161
+ const destDir = path.isAbsolute(args.destination)
162
+ ? args.destination
163
+ : path.join(process.cwd(), args.destination);
164
+
165
+ const ctx = {
166
+ kebab,
167
+ pascal,
168
+ labelsSection,
169
+ apiUri,
170
+ importPath: args.importPath,
171
+ fieldsCount: args.fieldsCount,
172
+ };
173
+
174
+ const files = gen.generate(ctx);
175
+ writeFiles({ destDir, dryRun: args.dryRun }, files);
176
+
177
+ if (args.i18nPath && typeof gen.getTranslations === "function") {
178
+ const translations = gen.getTranslations(ctx);
179
+ if (translations && Object.keys(translations).length > 0) {
180
+ upsertI18nEntries(args.i18nPath, translations, args.dryRun);
181
+ }
182
+ }
183
+ }
184
+
185
+ main();
package/cli/utils.js ADDED
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ /**
7
+ * @param {string} destPath
8
+ * @returns {string}
9
+ */
10
+ function lastPathSegment(destPath) {
11
+ const normalized = destPath.replace(/\\/g, "/").replace(/\/+$/, "");
12
+ const parts = normalized.split("/").filter(Boolean);
13
+ return parts[parts.length - 1] || "feature";
14
+ }
15
+
16
+ /**
17
+ * @param {string} str
18
+ * @returns {string}
19
+ */
20
+ function kebabCase(str) {
21
+ return String(str)
22
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
23
+ .replace(/[\s_]+/g, "-")
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9-]/g, "")
26
+ .replace(/-+/g, "-")
27
+ .replace(/^-|-$/g, "") || "feature";
28
+ }
29
+
30
+ /**
31
+ * @param {string} str
32
+ * @returns {string}
33
+ */
34
+ function pascalCase(str) {
35
+ return kebabCase(str)
36
+ .split("-")
37
+ .filter(Boolean)
38
+ .map((s) => s[0].toUpperCase() + s.slice(1))
39
+ .join("");
40
+ }
41
+
42
+ /**
43
+ * @param {string} kebab
44
+ * @returns {string}
45
+ */
46
+ function upperSnakeFromKebab(kebab) {
47
+ return kebabCase(kebab)
48
+ .split("-")
49
+ .filter(Boolean)
50
+ .map((s) => s.toUpperCase())
51
+ .join("_");
52
+ }
53
+
54
+ /**
55
+ * @param {string} destDir
56
+ * @param {boolean} dryRun
57
+ */
58
+ function ensureDir(destDir, dryRun) {
59
+ if (dryRun) return;
60
+ fs.mkdirSync(destDir, { recursive: true });
61
+ }
62
+
63
+ /**
64
+ * @param {string} filePath
65
+ * @param {string} content
66
+ * @param {boolean} dryRun
67
+ */
68
+ function writeFile(filePath, content, dryRun) {
69
+ if (dryRun) {
70
+ console.log(`[dry-run] would write ${filePath} (${content.length} bytes)`);
71
+ return;
72
+ }
73
+ fs.writeFileSync(filePath, content, "utf8");
74
+ console.log(`Wrote ${filePath}`);
75
+ }
76
+
77
+ /**
78
+ * @param {{ destDir: string, dryRun: boolean }} opts
79
+ * @param {Array<{ relative: string, content: string }>} files
80
+ */
81
+ function writeFiles(opts, files) {
82
+ ensureDir(opts.destDir, opts.dryRun);
83
+ for (const f of files) {
84
+ const filePath = path.join(opts.destDir, f.relative);
85
+ writeFile(filePath, f.content, opts.dryRun);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {Record<string, unknown>} target
91
+ * @param {string[]} pathParts
92
+ * @param {string} value
93
+ */
94
+ function setNested(target, pathParts, value) {
95
+ let cursor = target;
96
+ for (let i = 0; i < pathParts.length; i++) {
97
+ const part = pathParts[i];
98
+ const isLeaf = i === pathParts.length - 1;
99
+ if (isLeaf) {
100
+ cursor[part] = value;
101
+ return;
102
+ }
103
+ if (typeof cursor[part] !== "object" || cursor[part] === null || Array.isArray(cursor[part])) {
104
+ cursor[part] = {};
105
+ }
106
+ cursor = /** @type {Record<string, unknown>} */ (cursor[part]);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * @param {string} targetPath
112
+ * @returns {string[]}
113
+ */
114
+ function resolveJsonTargets(targetPath) {
115
+ const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(process.cwd(), targetPath);
116
+ if (!fs.existsSync(fullPath)) {
117
+ return [fullPath];
118
+ }
119
+
120
+ const stat = fs.statSync(fullPath);
121
+ if (stat.isFile()) {
122
+ return [fullPath];
123
+ }
124
+
125
+ if (!stat.isDirectory()) {
126
+ return [fullPath];
127
+ }
128
+
129
+ /** @type {string[]} */
130
+ const out = [];
131
+ /** @param {string} fileName */
132
+ function isTranslationFileName(fileName) {
133
+ const lower = fileName.toLowerCase();
134
+ // Common translation file patterns
135
+ // - en.json / ar.json
136
+ // - en-us.json / ar-QA.json
137
+ // - messages.en.json / messages-ar.json
138
+ return (
139
+ /^(en|ar)([-_][a-z0-9]+)*\.json$/i.test(lower) ||
140
+ /\.((en|ar)([-_][a-z0-9]+)*)\.json$/i.test(lower) ||
141
+ lower.includes("i18n")
142
+ );
143
+ }
144
+ /** @param {string} dir */
145
+ function walk(dir) {
146
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
147
+ for (const entry of entries) {
148
+ const entryPath = path.join(dir, entry.name);
149
+ if (entry.isDirectory()) {
150
+ walk(entryPath);
151
+ } else if (entry.isFile() && isTranslationFileName(entry.name)) {
152
+ out.push(entryPath);
153
+ }
154
+ }
155
+ }
156
+
157
+ walk(fullPath);
158
+ return out;
159
+ }
160
+
161
+ /**
162
+ * @param {string} i18nPath
163
+ * @param {Record<string, string>} entries
164
+ * @param {boolean} dryRun
165
+ */
166
+ function upsertI18nEntries(i18nPath, entries, dryRun) {
167
+ const targets = resolveJsonTargets(i18nPath);
168
+ if (targets.length === 0) {
169
+ console.log(`No JSON files found in i18n target: ${i18nPath}`);
170
+ return;
171
+ }
172
+
173
+ for (const fullPath of targets) {
174
+ if (dryRun) {
175
+ console.log(`[dry-run] would update i18n file ${fullPath} with ${Object.keys(entries).length} keys`);
176
+ continue;
177
+ }
178
+
179
+ let data = {};
180
+ if (fs.existsSync(fullPath)) {
181
+ const raw = fs.readFileSync(fullPath, "utf8").trim();
182
+ data = raw ? JSON.parse(raw) : {};
183
+ } else {
184
+ const parent = path.dirname(fullPath);
185
+ fs.mkdirSync(parent, { recursive: true });
186
+ }
187
+
188
+ for (const [key, value] of Object.entries(entries)) {
189
+ const parts = key.split(".").filter(Boolean);
190
+ if (parts.length === 0) continue;
191
+ setNested(data, parts, value);
192
+ }
193
+
194
+ fs.writeFileSync(fullPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
195
+ console.log(`Updated i18n ${fullPath}`);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Generate placeholder FieldInfo rows (2 per row by default).
201
+ * @param {number} count
202
+ * @param {number} perRow
203
+ * @param {string} indent
204
+ * @returns {string}
205
+ */
206
+ function buildFieldInfoLines(count, perRow, indent) {
207
+ const lines = [];
208
+ for (let i = 1; i <= count; i++) {
209
+ const row = Math.ceil(i / perRow);
210
+ lines.push(
211
+ `${indent}new FieldInfo({ row: ${row}, property: "field${i}", label: "field${i}", required: false, type: "text", validators: [Validators.maxLength(100)], validationMessages: { maxlength: "VALIDATION_MAX_100" } })`
212
+ );
213
+ }
214
+ return lines.join(",\n");
215
+ }
216
+
217
+ module.exports = {
218
+ lastPathSegment,
219
+ kebabCase,
220
+ pascalCase,
221
+ upperSnakeFromKebab,
222
+ ensureDir,
223
+ writeFile,
224
+ writeFiles,
225
+ buildFieldInfoLines,
226
+ resolveJsonTargets,
227
+ upsertI18nEntries,
228
+ };