@akanjs/devkit 2.3.9-rc.3 → 2.3.9-rc.5
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/akanContext.ts +8 -2
- package/devkitUtils.test.ts +2 -2
- package/executors.ts +17 -13
- package/getModelFileData.ts +5 -2
- package/package.json +2 -2
- package/workflow/artifacts.ts +415 -0
- package/workflow/executor.ts +266 -0
- package/workflow/index.ts +9 -1399
- package/workflow/plan.ts +375 -0
- package/workflow/primitive.ts +59 -0
- package/workflow/render.ts +252 -0
- package/workflow/source.ts +426 -0
- package/workflow/types.ts +296 -0
- package/workflow/uiPolicy.ts +45 -0
- package/workflow/utils.ts +23 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import type { Sys } from "../commandDecorators";
|
|
2
|
+
import { generatedFilePathsForTarget } from "./artifacts";
|
|
3
|
+
import { createPrimitiveWriteReport } from "./primitive";
|
|
4
|
+
import type {
|
|
5
|
+
PrimitiveChangedFile,
|
|
6
|
+
PrimitiveFileMap,
|
|
7
|
+
PrimitiveGeneratedFile,
|
|
8
|
+
PrimitiveNextAction,
|
|
9
|
+
PrimitiveValidationCommand,
|
|
10
|
+
WorkflowDiagnostic,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
export const getSysRoot = (sys: Sys) => `${sys.type}s/${sys.name}`;
|
|
14
|
+
|
|
15
|
+
export const sourceFile = (sys: Sys, path: string, action: PrimitiveChangedFile["action"], reason: string) => ({
|
|
16
|
+
path: `${getSysRoot(sys)}/${path}`,
|
|
17
|
+
action,
|
|
18
|
+
reason,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const moduleComponentName = (moduleName: string) =>
|
|
22
|
+
`${moduleName.slice(0, 1).toUpperCase()}${moduleName.slice(1)}`;
|
|
23
|
+
|
|
24
|
+
export const moduleSourcePaths = (moduleName: string) => {
|
|
25
|
+
const componentName = moduleComponentName(moduleName);
|
|
26
|
+
return {
|
|
27
|
+
abstract: `lib/${moduleName}/${moduleName}.abstract.md`,
|
|
28
|
+
constant: `lib/${moduleName}/${moduleName}.constant.ts`,
|
|
29
|
+
dictionary: `lib/${moduleName}/${moduleName}.dictionary.ts`,
|
|
30
|
+
service: `lib/${moduleName}/${moduleName}.service.ts`,
|
|
31
|
+
signal: `lib/${moduleName}/${moduleName}.signal.ts`,
|
|
32
|
+
store: `lib/${moduleName}/${moduleName}.store.ts`,
|
|
33
|
+
template: `lib/${moduleName}/${componentName}.Template.tsx`,
|
|
34
|
+
unit: `lib/${moduleName}/${componentName}.Unit.tsx`,
|
|
35
|
+
util: `lib/${moduleName}/${componentName}.Util.tsx`,
|
|
36
|
+
view: `lib/${moduleName}/${componentName}.View.tsx`,
|
|
37
|
+
zone: `lib/${moduleName}/${componentName}.Zone.tsx`,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const generatedFilesForSync = (sys: Sys, reason = "Generated files may change after sync.") =>
|
|
42
|
+
generatedFilePathsForTarget(getSysRoot(sys), reason);
|
|
43
|
+
|
|
44
|
+
export const validationCommandsForTarget = (target: string) =>
|
|
45
|
+
[
|
|
46
|
+
{ command: `akan sync ${target}`, reason: "Refresh generated Akan files from source conventions." },
|
|
47
|
+
{ command: `akan lint ${target}`, reason: "Validate formatting, imports, and static lint rules." },
|
|
48
|
+
] satisfies PrimitiveValidationCommand[];
|
|
49
|
+
|
|
50
|
+
export const nextActionsForTarget = (target: string) =>
|
|
51
|
+
[
|
|
52
|
+
{ command: `akan sync ${target}`, reason: "Refresh generated Akan files after source changes." },
|
|
53
|
+
{ command: `akan lint ${target}`, reason: "Validate the target after generated files are refreshed." },
|
|
54
|
+
] satisfies PrimitiveNextAction[];
|
|
55
|
+
|
|
56
|
+
export const createPassedPrimitiveReport = ({
|
|
57
|
+
command,
|
|
58
|
+
changedFiles,
|
|
59
|
+
generatedFiles,
|
|
60
|
+
target,
|
|
61
|
+
nextActions,
|
|
62
|
+
}: {
|
|
63
|
+
command: string;
|
|
64
|
+
changedFiles: PrimitiveChangedFile[];
|
|
65
|
+
generatedFiles?: PrimitiveGeneratedFile[];
|
|
66
|
+
target: string;
|
|
67
|
+
nextActions?: PrimitiveNextAction[];
|
|
68
|
+
}) =>
|
|
69
|
+
createPrimitiveWriteReport({
|
|
70
|
+
command,
|
|
71
|
+
changedFiles,
|
|
72
|
+
generatedFiles: generatedFiles ?? [],
|
|
73
|
+
validationCommands: validationCommandsForTarget(target),
|
|
74
|
+
diagnostics: [],
|
|
75
|
+
nextActions: nextActions ?? nextActionsForTarget(target),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const scalarChangedFiles = (sys: Sys, scalarName: string, files: PrimitiveFileMap) =>
|
|
79
|
+
Object.values(files).map((file) =>
|
|
80
|
+
sourceFile(sys, `lib/__scalar/${scalarName}/${file.filename}`, "create", "Scalar source file was created."),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
export const titleize = (value: string) =>
|
|
84
|
+
value
|
|
85
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
86
|
+
.replace(/[-_]+/g, " ")
|
|
87
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
88
|
+
|
|
89
|
+
export const lowerlize = (value: string) => `${value.slice(0, 1).toLowerCase()}${value.slice(1)}`;
|
|
90
|
+
|
|
91
|
+
const koLabels: Record<string, string> = {
|
|
92
|
+
amount: "금액",
|
|
93
|
+
budget: "예산",
|
|
94
|
+
category: "카테고리",
|
|
95
|
+
content: "내용",
|
|
96
|
+
count: "개수",
|
|
97
|
+
createdAt: "생성일",
|
|
98
|
+
date: "날짜",
|
|
99
|
+
description: "설명",
|
|
100
|
+
due: "마감일",
|
|
101
|
+
dueAt: "마감일",
|
|
102
|
+
email: "이메일",
|
|
103
|
+
enabled: "활성화",
|
|
104
|
+
endAt: "종료일",
|
|
105
|
+
id: "ID",
|
|
106
|
+
name: "이름",
|
|
107
|
+
owner: "담당자",
|
|
108
|
+
priority: "우선순위",
|
|
109
|
+
project: "프로젝트",
|
|
110
|
+
rating: "평점",
|
|
111
|
+
startAt: "시작일",
|
|
112
|
+
status: "상태",
|
|
113
|
+
title: "제목",
|
|
114
|
+
updatedAt: "수정일",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const splitFieldWords = (fieldName: string) =>
|
|
118
|
+
fieldName
|
|
119
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
120
|
+
.replace(/[-_]+/g, " ")
|
|
121
|
+
.split(/\s+/)
|
|
122
|
+
.map((word) => word.trim())
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
|
|
125
|
+
const koLabelForField = (fieldName: string) => {
|
|
126
|
+
if (koLabels[fieldName]) return koLabels[fieldName];
|
|
127
|
+
const words = splitFieldWords(fieldName);
|
|
128
|
+
const translated = words.map((word) => koLabels[word] ?? koLabels[lowerlize(word)] ?? null);
|
|
129
|
+
return translated.every(Boolean) ? translated.join(" ") : null;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const bilingualLabelForField = (fieldName: string) => {
|
|
133
|
+
const en = titleize(fieldName);
|
|
134
|
+
return { en, ko: koLabelForField(fieldName) ?? en };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const bilingualDescriptionForField = (fieldName: string) => {
|
|
138
|
+
const label = bilingualLabelForField(fieldName);
|
|
139
|
+
return {
|
|
140
|
+
en: `Enter ${label.en.toLowerCase()}.`,
|
|
141
|
+
ko: `${label.ko} 값을 입력합니다.`,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const normalizeFieldType = (typeName: string) => {
|
|
146
|
+
const normalizedTypes: Record<string, string> = {
|
|
147
|
+
string: "String",
|
|
148
|
+
boolean: "Boolean",
|
|
149
|
+
date: "Date",
|
|
150
|
+
int: "Int",
|
|
151
|
+
integer: "Int",
|
|
152
|
+
float: "Float",
|
|
153
|
+
double: "Float",
|
|
154
|
+
decimal: "Float",
|
|
155
|
+
};
|
|
156
|
+
return normalizedTypes[typeName.toLowerCase()] ?? typeName;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const ensureBaseTypeImport = (content: string, typeName: string) => {
|
|
160
|
+
if (typeName !== "Int" && typeName !== "Float") return content;
|
|
161
|
+
if (new RegExp(`import \\{[^}]*\\b${typeName}\\b[^}]*\\} from "akanjs/base";`).test(content)) return content;
|
|
162
|
+
const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
|
|
163
|
+
if (baseImport) {
|
|
164
|
+
const names = baseImport[1]
|
|
165
|
+
.split(",")
|
|
166
|
+
.map((name) => name.trim())
|
|
167
|
+
.filter(Boolean);
|
|
168
|
+
return content.replace(baseImport[0], `import { ${[...names, typeName].sort().join(", ")} } from "akanjs/base";`);
|
|
169
|
+
}
|
|
170
|
+
return `import { ${typeName} } from "akanjs/base";\n${content}`;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export type FieldDefaultValue = string | number | boolean | null;
|
|
174
|
+
|
|
175
|
+
const numericDefault = (typeName: "Int" | "Float", rawDefault: FieldDefaultValue): string | null => {
|
|
176
|
+
if (typeof rawDefault === "number") {
|
|
177
|
+
if (!Number.isFinite(rawDefault)) return null;
|
|
178
|
+
if (typeName === "Int" && !Number.isInteger(rawDefault)) return null;
|
|
179
|
+
return String(rawDefault);
|
|
180
|
+
}
|
|
181
|
+
if (typeof rawDefault !== "string") return null;
|
|
182
|
+
const trimmed = rawDefault.trim();
|
|
183
|
+
if (!trimmed || !Number.isFinite(Number(trimmed))) return null;
|
|
184
|
+
if (typeName === "Int" && !/^-?\d+$/.test(trimmed)) return null;
|
|
185
|
+
return trimmed;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const booleanDefault = (rawDefault: FieldDefaultValue): string | null => {
|
|
189
|
+
if (typeof rawDefault === "boolean") return String(rawDefault);
|
|
190
|
+
if (typeof rawDefault !== "string") return null;
|
|
191
|
+
const lowered = rawDefault.trim().toLowerCase();
|
|
192
|
+
return lowered === "true" || lowered === "false" ? lowered : null;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const dateDefault = (rawDefault: FieldDefaultValue): string | null => {
|
|
196
|
+
if (typeof rawDefault === "number" && Number.isFinite(rawDefault)) return `new Date(${rawDefault})`;
|
|
197
|
+
if (typeof rawDefault !== "string") return null;
|
|
198
|
+
const trimmed = rawDefault.trim();
|
|
199
|
+
if (!trimmed) return null;
|
|
200
|
+
if (trimmed === "now") return "new Date()";
|
|
201
|
+
if (!Number.isNaN(Date.parse(trimmed))) return `new Date(${JSON.stringify(trimmed)})`;
|
|
202
|
+
return null;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const coerceFieldDefault = (
|
|
206
|
+
typeName: string,
|
|
207
|
+
defaultValue?: FieldDefaultValue,
|
|
208
|
+
options: { enumValues?: readonly string[] | null } = {},
|
|
209
|
+
): { expression: string | null; diagnostic?: WorkflowDiagnostic; normalized: boolean; normalizedType: string } => {
|
|
210
|
+
const normalizedType = typeName.toLowerCase() === "enum" ? "enum" : normalizeFieldType(typeName);
|
|
211
|
+
if (defaultValue === undefined || defaultValue === null || defaultValue === "") {
|
|
212
|
+
return { expression: null, normalized: false, normalizedType };
|
|
213
|
+
}
|
|
214
|
+
if (normalizedType === "Int" || normalizedType === "Float") {
|
|
215
|
+
const expression = numericDefault(normalizedType, defaultValue);
|
|
216
|
+
if (expression !== null) return { expression, normalized: typeof defaultValue === "string", normalizedType };
|
|
217
|
+
return {
|
|
218
|
+
expression: null,
|
|
219
|
+
normalized: false,
|
|
220
|
+
normalizedType,
|
|
221
|
+
diagnostic: {
|
|
222
|
+
severity: "error",
|
|
223
|
+
code: "primitive-default-value-invalid",
|
|
224
|
+
input: "default",
|
|
225
|
+
failureScope: "source-change",
|
|
226
|
+
message: `Default value for ${normalizedType} must be a numeric literal. Received: ${JSON.stringify(defaultValue)}.`,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (normalizedType === "Boolean") {
|
|
231
|
+
const expression = booleanDefault(defaultValue);
|
|
232
|
+
if (expression !== null) return { expression, normalized: typeof defaultValue === "string", normalizedType };
|
|
233
|
+
return {
|
|
234
|
+
expression: null,
|
|
235
|
+
normalized: false,
|
|
236
|
+
normalizedType,
|
|
237
|
+
diagnostic: {
|
|
238
|
+
severity: "error",
|
|
239
|
+
code: "primitive-default-value-invalid",
|
|
240
|
+
input: "default",
|
|
241
|
+
failureScope: "source-change",
|
|
242
|
+
message: `Default value for Boolean must be true or false. Received: ${JSON.stringify(defaultValue)}.`,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (normalizedType === "Date") {
|
|
247
|
+
const expression = dateDefault(defaultValue);
|
|
248
|
+
if (expression !== null) return { expression, normalized: true, normalizedType };
|
|
249
|
+
return {
|
|
250
|
+
expression: null,
|
|
251
|
+
normalized: false,
|
|
252
|
+
normalizedType,
|
|
253
|
+
diagnostic: {
|
|
254
|
+
severity: "error",
|
|
255
|
+
code: "primitive-default-value-invalid",
|
|
256
|
+
input: "default",
|
|
257
|
+
failureScope: "source-change",
|
|
258
|
+
message: `Default value for Date must be "now", a timestamp, or a parseable date string. Received: ${JSON.stringify(
|
|
259
|
+
defaultValue,
|
|
260
|
+
)}.`,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (normalizedType === "enum") {
|
|
265
|
+
if (typeof defaultValue === "string" && options.enumValues?.includes(defaultValue)) {
|
|
266
|
+
return { expression: JSON.stringify(defaultValue), normalized: false, normalizedType };
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
expression: null,
|
|
270
|
+
normalized: false,
|
|
271
|
+
normalizedType,
|
|
272
|
+
diagnostic: {
|
|
273
|
+
severity: "error",
|
|
274
|
+
code: "primitive-default-value-invalid",
|
|
275
|
+
input: "default",
|
|
276
|
+
failureScope: "source-change",
|
|
277
|
+
message: `Default value for enum must be one of: ${(options.enumValues ?? []).join(", ")}. Received: ${JSON.stringify(
|
|
278
|
+
defaultValue,
|
|
279
|
+
)}.`,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
expression: JSON.stringify(String(defaultValue)),
|
|
285
|
+
normalized: typeof defaultValue !== "string",
|
|
286
|
+
normalizedType,
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const fieldExpression = (
|
|
291
|
+
typeName: string,
|
|
292
|
+
defaultValue?: FieldDefaultValue,
|
|
293
|
+
options: { enumValues?: readonly string[] | null } = {},
|
|
294
|
+
) => {
|
|
295
|
+
const typeExpression = normalizeFieldType(typeName);
|
|
296
|
+
const defaultExpression = coerceFieldDefault(
|
|
297
|
+
options.enumValues ? "enum" : typeExpression,
|
|
298
|
+
defaultValue,
|
|
299
|
+
options,
|
|
300
|
+
).expression;
|
|
301
|
+
const defaultOption = defaultExpression ? `, { default: ${defaultExpression} }` : "";
|
|
302
|
+
return `field(${typeExpression}${defaultOption})`;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export const insertIntoObject = (content: string, className: string, line: string) => {
|
|
306
|
+
const classIndex = content.indexOf(`export class ${className} extends via`);
|
|
307
|
+
if (classIndex < 0) return null;
|
|
308
|
+
const objectEndIndex = content.indexOf("}))", classIndex);
|
|
309
|
+
if (objectEndIndex < 0) return null;
|
|
310
|
+
const prefix = content.slice(0, objectEndIndex);
|
|
311
|
+
const suffix = content.slice(objectEndIndex);
|
|
312
|
+
const insertion = prefix.endsWith("\n") ? ` ${line}\n` : `\n ${line}\n`;
|
|
313
|
+
return `${prefix}${insertion}${suffix}`;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const insertLightProjectionField = (content: string, moduleClassName: string, fieldName: string) => {
|
|
317
|
+
const classIndex = content.indexOf(`export class Light${moduleClassName} extends via`);
|
|
318
|
+
if (classIndex < 0) return null;
|
|
319
|
+
const arrayMatch = /\[([\s\S]*?)\]\s+as const/.exec(content.slice(classIndex));
|
|
320
|
+
if (!arrayMatch || arrayMatch.index === undefined) return null;
|
|
321
|
+
const arrayStart = classIndex + arrayMatch.index;
|
|
322
|
+
const arrayEnd = arrayStart + arrayMatch[0].length;
|
|
323
|
+
const fields = [...arrayMatch[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]).filter(Boolean);
|
|
324
|
+
if (fields.includes(fieldName)) return content;
|
|
325
|
+
const nextFields = [...fields, fieldName];
|
|
326
|
+
const nextArray =
|
|
327
|
+
nextFields.length === 0
|
|
328
|
+
? "[] as const"
|
|
329
|
+
: `[\n${nextFields.map((field) => ` ${JSON.stringify(field)},`).join("\n")}\n] as const`;
|
|
330
|
+
return `${content.slice(0, arrayStart)}${nextArray}${content.slice(arrayEnd)}`;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export const insertTemplateField = ({
|
|
334
|
+
content,
|
|
335
|
+
moduleName,
|
|
336
|
+
moduleClassName,
|
|
337
|
+
fieldName,
|
|
338
|
+
component,
|
|
339
|
+
}: {
|
|
340
|
+
content: string;
|
|
341
|
+
moduleName: string;
|
|
342
|
+
moduleClassName: string;
|
|
343
|
+
fieldName: string;
|
|
344
|
+
component: "Field.Text" | "Field.Number" | "Field.Date";
|
|
345
|
+
}) => {
|
|
346
|
+
if (content.includes(`l("${moduleName}.${fieldName}")`) || content.includes(`.${fieldName}`)) return content;
|
|
347
|
+
const layoutEndIndex = content.indexOf(" </Layout.Template>");
|
|
348
|
+
if (layoutEndIndex < 0) return null;
|
|
349
|
+
const formName = `${moduleName}Form`;
|
|
350
|
+
if (!content.includes(`const ${formName} = st.use.${moduleName}Form();`)) return null;
|
|
351
|
+
const fieldSetter = `st.do.set${moduleComponentName(fieldName)}On${moduleClassName}`;
|
|
352
|
+
const fieldBlock = ` <${component}
|
|
353
|
+
label={l("${moduleName}.${fieldName}")}
|
|
354
|
+
desc={l("${moduleName}.${fieldName}.desc")}
|
|
355
|
+
value={${formName}.${fieldName}}
|
|
356
|
+
onChange={${fieldSetter}}
|
|
357
|
+
/>
|
|
358
|
+
`;
|
|
359
|
+
return `${content.slice(0, layoutEndIndex)}${fieldBlock}${content.slice(layoutEndIndex)}`;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
export const ensureEnumImport = (content: string) => {
|
|
363
|
+
if (content.includes("enumOf")) return content;
|
|
364
|
+
const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
|
|
365
|
+
if (baseImport) {
|
|
366
|
+
const names = baseImport[1]?.split(",").map((name) => name.trim()) ?? [];
|
|
367
|
+
return content.replace(baseImport[0], `import { ${[...names, "enumOf"].sort().join(", ")} } from "akanjs/base";`);
|
|
368
|
+
}
|
|
369
|
+
return `import { enumOf } from "akanjs/base";\n${content}`;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export const insertEnumClass = (content: string, enumClassName: string, enumName: string, values: string[]) => {
|
|
373
|
+
if (content.includes(`export class ${enumClassName} extends enumOf`)) return content;
|
|
374
|
+
const enumClass = `export class ${enumClassName} extends enumOf("${enumName}", [\n${values
|
|
375
|
+
.map((value) => ` ${JSON.stringify(value)},`)
|
|
376
|
+
.join("\n")}\n] as const) {}\n\n`;
|
|
377
|
+
const firstClassIndex = content.indexOf("export class ");
|
|
378
|
+
if (firstClassIndex < 0) return `${content}\n${enumClass}`;
|
|
379
|
+
return `${content.slice(0, firstClassIndex)}${enumClass}${content.slice(firstClassIndex)}`;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
export const insertDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
|
|
383
|
+
if (new RegExp(`\\b${fieldName}\\s*:`).test(content)) return content;
|
|
384
|
+
const label = bilingualLabelForField(fieldName);
|
|
385
|
+
const desc = bilingualDescriptionForField(fieldName);
|
|
386
|
+
const modelIndex = content.indexOf(`.model<${moduleClassName}>((t) => ({`);
|
|
387
|
+
if (modelIndex < 0) return null;
|
|
388
|
+
const objectEndIndex = content.indexOf(" }))", modelIndex);
|
|
389
|
+
if (objectEndIndex < 0) return null;
|
|
390
|
+
return `${content.slice(0, objectEndIndex)} ${fieldName}: t([${JSON.stringify(label.en)}, ${JSON.stringify(
|
|
391
|
+
label.ko,
|
|
392
|
+
)}]).desc([${JSON.stringify(desc.en)}, ${JSON.stringify(desc.ko)}]),\n${content.slice(objectEndIndex)}`;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
export const ensureConstantTypeImport = (content: string, constantPath: string, typeName: string) => {
|
|
396
|
+
if (new RegExp(`import type \\{[^}]*\\b${typeName}\\b[^}]*\\} from "${constantPath}";`).test(content)) return content;
|
|
397
|
+
const importPattern = new RegExp(`import type \\{ ([^}]+) \\} from "${constantPath}";`);
|
|
398
|
+
const existingImport = content.match(importPattern);
|
|
399
|
+
if (existingImport !== null) {
|
|
400
|
+
const names = existingImport[1]?.split(",").map((name) => name.trim()) ?? [];
|
|
401
|
+
return content.replace(
|
|
402
|
+
existingImport[0],
|
|
403
|
+
`import type { ${[...names, typeName].sort().join(", ")} } from "${constantPath}";`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
return `import type { ${typeName} } from "${constantPath}";\n${content}`;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
export const insertDictionaryEnum = (content: string, enumClassName: string, enumName: string, values: string[]) => {
|
|
410
|
+
if (content.includes(`.enum<${enumClassName}>("${enumName}"`)) return content;
|
|
411
|
+
const enumBlock = ` .enum<${enumClassName}>("${enumName}", (t) => ({\n${values
|
|
412
|
+
.map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${JSON.stringify(titleize(value))}]),`)
|
|
413
|
+
.join("\n")}\n }))\n`;
|
|
414
|
+
const chainEndIndex = content.lastIndexOf(";");
|
|
415
|
+
const insertBeforeIndex = [content.indexOf(".error("), content.indexOf(".translate("), chainEndIndex]
|
|
416
|
+
.filter((index) => index >= 0)
|
|
417
|
+
.sort((a, b) => a - b)[0];
|
|
418
|
+
if (insertBeforeIndex === undefined) return null;
|
|
419
|
+
return `${content.slice(0, insertBeforeIndex)}${enumBlock}${content.slice(insertBeforeIndex)}`;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
export const parseValues = (value: string | null) =>
|
|
423
|
+
value
|
|
424
|
+
?.split(",")
|
|
425
|
+
.map((item) => item.trim())
|
|
426
|
+
.filter(Boolean) ?? [];
|