@akanjs/devkit 2.3.9-rc.0 → 2.3.9-rc.10
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/aiEditor.test.ts +4 -12
- package/akanContext.ts +409 -15
- package/akanMcpContract.test.ts +37 -0
- package/akanMcpContract.ts +701 -0
- package/capacitorApp.test.ts +14 -8
- package/commandDecorators/command.ts +2 -1
- package/commandDecorators/commandDecorators.test.ts +13 -0
- package/devkitUtils.test.ts +2 -2
- package/executors.test.ts +21 -0
- package/executors.ts +22 -17
- package/getModelFileData.ts +5 -2
- package/index.ts +2 -0
- package/linter.ts +27 -74
- package/package.json +2 -2
- package/typecheck/typecheck.proc.ts +1 -2
- package/workflow/artifacts.ts +571 -0
- package/workflow/executor.ts +590 -0
- package/workflow/index.ts +11 -0
- package/workflow/moduleIndex.test.ts +296 -0
- package/workflow/moduleIndex.ts +504 -0
- package/workflow/plan.ts +429 -0
- package/workflow/primitive.ts +59 -0
- package/workflow/render.ts +335 -0
- package/workflow/rolloutGate.test.ts +109 -0
- package/workflow/rolloutGate.ts +141 -0
- package/workflow/source.test.ts +62 -0
- package/workflow/source.ts +864 -0
- package/workflow/types.ts +475 -0
- package/workflow/uiPolicy.ts +45 -0
- package/workflow/utils.ts +23 -0
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { Sys } from "../commandDecorators";
|
|
3
|
+
import { generatedFilePathsForTarget } from "./artifacts";
|
|
4
|
+
import { createPrimitiveWriteReport } from "./primitive";
|
|
5
|
+
import type {
|
|
6
|
+
PrimitiveChangedFile,
|
|
7
|
+
PrimitiveFileMap,
|
|
8
|
+
PrimitiveGeneratedFile,
|
|
9
|
+
PrimitiveNextAction,
|
|
10
|
+
PrimitiveValidationCommand,
|
|
11
|
+
WorkflowDiagnostic,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
export const getSysRoot = (sys: Sys) => `${sys.type}s/${sys.name}`;
|
|
15
|
+
|
|
16
|
+
export const sourceFile = (sys: Sys, path: string, action: PrimitiveChangedFile["action"], reason: string) => ({
|
|
17
|
+
path: `${getSysRoot(sys)}/${path}`,
|
|
18
|
+
action,
|
|
19
|
+
reason,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const moduleComponentName = (moduleName: string) =>
|
|
23
|
+
moduleName
|
|
24
|
+
.replace(/[-_]+/g, " ")
|
|
25
|
+
.replace(/(?:^|\s+)([a-zA-Z0-9])/g, (_, char: string) => char.toUpperCase())
|
|
26
|
+
.replace(/\s+/g, "");
|
|
27
|
+
|
|
28
|
+
export const moduleSourcePaths = (moduleName: string) => {
|
|
29
|
+
const componentName = moduleComponentName(moduleName);
|
|
30
|
+
return {
|
|
31
|
+
abstract: `lib/${moduleName}/${moduleName}.abstract.md`,
|
|
32
|
+
constant: `lib/${moduleName}/${moduleName}.constant.ts`,
|
|
33
|
+
dictionary: `lib/${moduleName}/${moduleName}.dictionary.ts`,
|
|
34
|
+
service: `lib/${moduleName}/${moduleName}.service.ts`,
|
|
35
|
+
signal: `lib/${moduleName}/${moduleName}.signal.ts`,
|
|
36
|
+
store: `lib/${moduleName}/${moduleName}.store.ts`,
|
|
37
|
+
template: `lib/${moduleName}/${componentName}.Template.tsx`,
|
|
38
|
+
unit: `lib/${moduleName}/${componentName}.Unit.tsx`,
|
|
39
|
+
util: `lib/${moduleName}/${componentName}.Util.tsx`,
|
|
40
|
+
view: `lib/${moduleName}/${componentName}.View.tsx`,
|
|
41
|
+
zone: `lib/${moduleName}/${componentName}.Zone.tsx`,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const generatedFilesForSync = (sys: Sys, reason = "Generated files may change after sync.") =>
|
|
46
|
+
generatedFilePathsForTarget(getSysRoot(sys), reason);
|
|
47
|
+
|
|
48
|
+
export const validationCommandsForTarget = (target: string) =>
|
|
49
|
+
[
|
|
50
|
+
{ command: `akan sync ${target}`, reason: "Refresh generated Akan files from source conventions." },
|
|
51
|
+
{ command: `akan lint ${target}`, reason: "Validate formatting, imports, and static lint rules." },
|
|
52
|
+
] satisfies PrimitiveValidationCommand[];
|
|
53
|
+
|
|
54
|
+
export const nextActionsForTarget = (target: string) =>
|
|
55
|
+
[
|
|
56
|
+
{ command: `akan sync ${target}`, reason: "Refresh generated Akan files after source changes." },
|
|
57
|
+
{ command: `akan lint ${target}`, reason: "Validate the target after generated files are refreshed." },
|
|
58
|
+
] satisfies PrimitiveNextAction[];
|
|
59
|
+
|
|
60
|
+
export const createPassedPrimitiveReport = ({
|
|
61
|
+
command,
|
|
62
|
+
changedFiles,
|
|
63
|
+
generatedFiles,
|
|
64
|
+
target,
|
|
65
|
+
nextActions,
|
|
66
|
+
}: {
|
|
67
|
+
command: string;
|
|
68
|
+
changedFiles: PrimitiveChangedFile[];
|
|
69
|
+
generatedFiles?: PrimitiveGeneratedFile[];
|
|
70
|
+
target: string;
|
|
71
|
+
nextActions?: PrimitiveNextAction[];
|
|
72
|
+
}) =>
|
|
73
|
+
createPrimitiveWriteReport({
|
|
74
|
+
command,
|
|
75
|
+
changedFiles,
|
|
76
|
+
generatedFiles: generatedFiles ?? [],
|
|
77
|
+
validationCommands: validationCommandsForTarget(target),
|
|
78
|
+
diagnostics: [],
|
|
79
|
+
nextActions: nextActions ?? nextActionsForTarget(target),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const scalarChangedFiles = (sys: Sys, scalarName: string, files: PrimitiveFileMap) =>
|
|
83
|
+
Object.values(files).map((file) =>
|
|
84
|
+
sourceFile(sys, `lib/__scalar/${scalarName}/${file.filename}`, "create", "Scalar source file was created."),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
export const titleize = (value: string) =>
|
|
88
|
+
value
|
|
89
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
90
|
+
.replace(/[-_]+/g, " ")
|
|
91
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
92
|
+
|
|
93
|
+
export const lowerlize = (value: string) => `${value.slice(0, 1).toLowerCase()}${value.slice(1)}`;
|
|
94
|
+
|
|
95
|
+
const koLabels: Record<string, string> = {
|
|
96
|
+
amount: "금액",
|
|
97
|
+
budget: "예산",
|
|
98
|
+
category: "카테고리",
|
|
99
|
+
content: "내용",
|
|
100
|
+
count: "개수",
|
|
101
|
+
createdAt: "생성일",
|
|
102
|
+
date: "날짜",
|
|
103
|
+
description: "설명",
|
|
104
|
+
due: "마감일",
|
|
105
|
+
dueAt: "마감일",
|
|
106
|
+
email: "이메일",
|
|
107
|
+
enabled: "활성화",
|
|
108
|
+
endAt: "종료일",
|
|
109
|
+
id: "ID",
|
|
110
|
+
name: "이름",
|
|
111
|
+
owner: "담당자",
|
|
112
|
+
priority: "우선순위",
|
|
113
|
+
project: "프로젝트",
|
|
114
|
+
rating: "평점",
|
|
115
|
+
startAt: "시작일",
|
|
116
|
+
status: "상태",
|
|
117
|
+
title: "제목",
|
|
118
|
+
updatedAt: "수정일",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const splitFieldWords = (fieldName: string) =>
|
|
122
|
+
fieldName
|
|
123
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
124
|
+
.replace(/[-_]+/g, " ")
|
|
125
|
+
.split(/\s+/)
|
|
126
|
+
.map((word) => word.trim())
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
|
|
129
|
+
const koLabelForField = (fieldName: string) => {
|
|
130
|
+
if (koLabels[fieldName]) return koLabels[fieldName];
|
|
131
|
+
const words = splitFieldWords(fieldName);
|
|
132
|
+
const translated = words.map((word) => koLabels[word] ?? koLabels[lowerlize(word)] ?? null);
|
|
133
|
+
return translated.every(Boolean) ? translated.join(" ") : null;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const bilingualLabelForField = (fieldName: string) => {
|
|
137
|
+
const en = titleize(fieldName);
|
|
138
|
+
return { en, ko: koLabelForField(fieldName) ?? en };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const bilingualDescriptionForField = (fieldName: string) => {
|
|
142
|
+
const label = bilingualLabelForField(fieldName);
|
|
143
|
+
return {
|
|
144
|
+
en: `Enter ${label.en.toLowerCase()}.`,
|
|
145
|
+
ko: `${label.ko} 값을 입력합니다.`,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const normalizeFieldType = (typeName: string) => {
|
|
150
|
+
const normalizedTypes: Record<string, string> = {
|
|
151
|
+
string: "String",
|
|
152
|
+
boolean: "Boolean",
|
|
153
|
+
date: "Date",
|
|
154
|
+
int: "Int",
|
|
155
|
+
integer: "Int",
|
|
156
|
+
float: "Float",
|
|
157
|
+
double: "Float",
|
|
158
|
+
decimal: "Float",
|
|
159
|
+
};
|
|
160
|
+
return normalizedTypes[typeName.toLowerCase()] ?? typeName;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const ensureBaseTypeImport = (content: string, typeName: string) => {
|
|
164
|
+
if (typeName !== "Int" && typeName !== "Float") return content;
|
|
165
|
+
const source = sourceFileFor("constant.ts", content);
|
|
166
|
+
const baseImport = findNamedImport(source, "akanjs/base");
|
|
167
|
+
if (baseImport) {
|
|
168
|
+
if (baseImport.names.includes(typeName)) return content;
|
|
169
|
+
const nextNames = [...baseImport.names, typeName].sort();
|
|
170
|
+
return spliceText(
|
|
171
|
+
content,
|
|
172
|
+
baseImport.namedBindingsStart,
|
|
173
|
+
baseImport.namedBindingsEnd,
|
|
174
|
+
`{ ${nextNames.join(", ")} }`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return `import { ${typeName} } from "akanjs/base";\n${content}`;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export type FieldDefaultValue = string | number | boolean | null;
|
|
181
|
+
|
|
182
|
+
const numericDefault = (typeName: "Int" | "Float", rawDefault: FieldDefaultValue): string | null => {
|
|
183
|
+
if (typeof rawDefault === "number") {
|
|
184
|
+
if (!Number.isFinite(rawDefault)) return null;
|
|
185
|
+
if (typeName === "Int" && !Number.isInteger(rawDefault)) return null;
|
|
186
|
+
return String(rawDefault);
|
|
187
|
+
}
|
|
188
|
+
if (typeof rawDefault !== "string") return null;
|
|
189
|
+
const trimmed = rawDefault.trim();
|
|
190
|
+
if (!trimmed || !Number.isFinite(Number(trimmed))) return null;
|
|
191
|
+
if (typeName === "Int" && !/^-?\d+$/.test(trimmed)) return null;
|
|
192
|
+
return trimmed;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const booleanDefault = (rawDefault: FieldDefaultValue): string | null => {
|
|
196
|
+
if (typeof rawDefault === "boolean") return String(rawDefault);
|
|
197
|
+
if (typeof rawDefault !== "string") return null;
|
|
198
|
+
const lowered = rawDefault.trim().toLowerCase();
|
|
199
|
+
return lowered === "true" || lowered === "false" ? lowered : null;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const dateDefault = (rawDefault: FieldDefaultValue): string | null => {
|
|
203
|
+
if (typeof rawDefault === "number" && Number.isFinite(rawDefault)) return `new Date(${rawDefault})`;
|
|
204
|
+
if (typeof rawDefault !== "string") return null;
|
|
205
|
+
const trimmed = rawDefault.trim();
|
|
206
|
+
if (!trimmed) return null;
|
|
207
|
+
if (trimmed === "now") return "new Date()";
|
|
208
|
+
if (!Number.isNaN(Date.parse(trimmed))) return `new Date(${JSON.stringify(trimmed)})`;
|
|
209
|
+
return null;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const coerceFieldDefault = (
|
|
213
|
+
typeName: string,
|
|
214
|
+
defaultValue?: FieldDefaultValue,
|
|
215
|
+
options: { enumValues?: readonly string[] | null } = {},
|
|
216
|
+
): { expression: string | null; diagnostic?: WorkflowDiagnostic; normalized: boolean; normalizedType: string } => {
|
|
217
|
+
const normalizedType = typeName.toLowerCase() === "enum" ? "enum" : normalizeFieldType(typeName);
|
|
218
|
+
if (defaultValue === undefined || defaultValue === null || defaultValue === "") {
|
|
219
|
+
return { expression: null, normalized: false, normalizedType };
|
|
220
|
+
}
|
|
221
|
+
if (normalizedType === "Int" || normalizedType === "Float") {
|
|
222
|
+
const expression = numericDefault(normalizedType, defaultValue);
|
|
223
|
+
if (expression !== null) return { expression, normalized: typeof defaultValue === "string", normalizedType };
|
|
224
|
+
return {
|
|
225
|
+
expression: null,
|
|
226
|
+
normalized: false,
|
|
227
|
+
normalizedType,
|
|
228
|
+
diagnostic: {
|
|
229
|
+
severity: "error",
|
|
230
|
+
code: "primitive-default-value-invalid",
|
|
231
|
+
input: "default",
|
|
232
|
+
failureScope: "source-change",
|
|
233
|
+
message: `Default value for ${normalizedType} must be a numeric literal. Received: ${JSON.stringify(defaultValue)}.`,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (normalizedType === "Boolean") {
|
|
238
|
+
const expression = booleanDefault(defaultValue);
|
|
239
|
+
if (expression !== null) return { expression, normalized: typeof defaultValue === "string", normalizedType };
|
|
240
|
+
return {
|
|
241
|
+
expression: null,
|
|
242
|
+
normalized: false,
|
|
243
|
+
normalizedType,
|
|
244
|
+
diagnostic: {
|
|
245
|
+
severity: "error",
|
|
246
|
+
code: "primitive-default-value-invalid",
|
|
247
|
+
input: "default",
|
|
248
|
+
failureScope: "source-change",
|
|
249
|
+
message: `Default value for Boolean must be true or false. Received: ${JSON.stringify(defaultValue)}.`,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (normalizedType === "Date") {
|
|
254
|
+
const expression = dateDefault(defaultValue);
|
|
255
|
+
if (expression !== null) return { expression, normalized: true, normalizedType };
|
|
256
|
+
return {
|
|
257
|
+
expression: null,
|
|
258
|
+
normalized: false,
|
|
259
|
+
normalizedType,
|
|
260
|
+
diagnostic: {
|
|
261
|
+
severity: "error",
|
|
262
|
+
code: "primitive-default-value-invalid",
|
|
263
|
+
input: "default",
|
|
264
|
+
failureScope: "source-change",
|
|
265
|
+
message: `Default value for Date must be "now", a timestamp, or a parseable date string. Received: ${JSON.stringify(
|
|
266
|
+
defaultValue,
|
|
267
|
+
)}.`,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (normalizedType === "enum") {
|
|
272
|
+
if (typeof defaultValue === "string" && options.enumValues?.includes(defaultValue)) {
|
|
273
|
+
return { expression: JSON.stringify(defaultValue), normalized: false, normalizedType };
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
expression: null,
|
|
277
|
+
normalized: false,
|
|
278
|
+
normalizedType,
|
|
279
|
+
diagnostic: {
|
|
280
|
+
severity: "error",
|
|
281
|
+
code: "primitive-default-value-invalid",
|
|
282
|
+
input: "default",
|
|
283
|
+
failureScope: "source-change",
|
|
284
|
+
message: `Default value for enum must be one of: ${(options.enumValues ?? []).join(", ")}. Received: ${JSON.stringify(
|
|
285
|
+
defaultValue,
|
|
286
|
+
)}.`,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
expression: JSON.stringify(String(defaultValue)),
|
|
292
|
+
normalized: typeof defaultValue !== "string",
|
|
293
|
+
normalizedType,
|
|
294
|
+
};
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export const fieldExpression = (
|
|
298
|
+
typeName: string,
|
|
299
|
+
defaultValue?: FieldDefaultValue,
|
|
300
|
+
options: { enumValues?: readonly string[] | null; builderName?: string } = {},
|
|
301
|
+
) => {
|
|
302
|
+
const typeExpression = normalizeFieldType(typeName);
|
|
303
|
+
const defaultExpression = coerceFieldDefault(
|
|
304
|
+
options.enumValues ? "enum" : typeExpression,
|
|
305
|
+
defaultValue,
|
|
306
|
+
options,
|
|
307
|
+
).expression;
|
|
308
|
+
const defaultOption = defaultExpression ? `, { default: ${defaultExpression} }` : "";
|
|
309
|
+
return `${options.builderName ?? "field"}(${typeExpression}${defaultOption})`;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const fieldOrderingPriority = [
|
|
313
|
+
"id",
|
|
314
|
+
"name",
|
|
315
|
+
"title",
|
|
316
|
+
"status",
|
|
317
|
+
"category",
|
|
318
|
+
"description",
|
|
319
|
+
"content",
|
|
320
|
+
"startAt",
|
|
321
|
+
"dueAt",
|
|
322
|
+
"endAt",
|
|
323
|
+
"createdAt",
|
|
324
|
+
"updatedAt",
|
|
325
|
+
] as const;
|
|
326
|
+
|
|
327
|
+
type FieldOrderingName = string;
|
|
328
|
+
|
|
329
|
+
interface LocatedField {
|
|
330
|
+
name: FieldOrderingName;
|
|
331
|
+
start: number;
|
|
332
|
+
fullStart: number;
|
|
333
|
+
end: number;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
interface ObjectInsertionLocator {
|
|
337
|
+
objectStart: number;
|
|
338
|
+
objectEnd: number;
|
|
339
|
+
fields: LocatedField[];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
interface ArrayInsertionLocator {
|
|
343
|
+
projectionStart: number;
|
|
344
|
+
projectionEnd: number;
|
|
345
|
+
fields: string[];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
interface NamedImportLocator {
|
|
349
|
+
names: string[];
|
|
350
|
+
namedBindingsStart: number;
|
|
351
|
+
namedBindingsEnd: number;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export interface AkanConstantFieldStructure {
|
|
355
|
+
name: string;
|
|
356
|
+
expressionBuilder: string | null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export interface AkanConstantStructure {
|
|
360
|
+
parseValid: boolean;
|
|
361
|
+
inputObjectFound: boolean;
|
|
362
|
+
builderName: string | null;
|
|
363
|
+
fields: AkanConstantFieldStructure[];
|
|
364
|
+
lightProjectionFields: string[];
|
|
365
|
+
baseImports: string[];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface AkanDictionaryStructure {
|
|
369
|
+
parseValid: boolean;
|
|
370
|
+
modelObjectFound: boolean;
|
|
371
|
+
chainOrderValid: boolean;
|
|
372
|
+
chainMethods: string[];
|
|
373
|
+
fields: string[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const sourceFileFor = (fileName: string, content: string, scriptKind = ts.ScriptKind.TS) =>
|
|
377
|
+
ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
378
|
+
|
|
379
|
+
const hasParseDiagnostics = (source: ts.SourceFile) =>
|
|
380
|
+
((source as ts.SourceFile & { parseDiagnostics?: readonly ts.Diagnostic[] }).parseDiagnostics ?? []).length > 0;
|
|
381
|
+
|
|
382
|
+
const spliceText = (content: string, start: number, end: number, replacement: string) =>
|
|
383
|
+
`${content.slice(0, start)}${replacement}${content.slice(end)}`;
|
|
384
|
+
|
|
385
|
+
const lineStartAt = (content: string, position: number) => content.lastIndexOf("\n", Math.max(0, position - 1)) + 1;
|
|
386
|
+
|
|
387
|
+
const lineEndAt = (content: string, position: number) => {
|
|
388
|
+
const end = content.indexOf("\n", position);
|
|
389
|
+
return end < 0 ? content.length : end + 1;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const lineIndentAt = (content: string, position: number) =>
|
|
393
|
+
/^[ \t]*/.exec(content.slice(lineStartAt(content, position)))?.[0] ?? "";
|
|
394
|
+
|
|
395
|
+
const nodeName = (node: ts.PropertyName | ts.BindingName | undefined) => {
|
|
396
|
+
if (!node) return null;
|
|
397
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
|
|
398
|
+
return null;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const propertyName = (node: ts.ObjectLiteralElementLike) =>
|
|
402
|
+
ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) || ts.isMethodDeclaration(node)
|
|
403
|
+
? nodeName(node.name)
|
|
404
|
+
: null;
|
|
405
|
+
|
|
406
|
+
const expressionName = (expression: ts.Expression): string | null => {
|
|
407
|
+
if (ts.isIdentifier(expression)) return expression.text;
|
|
408
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
|
|
409
|
+
if (ts.isCallExpression(expression)) return expressionName(expression.expression);
|
|
410
|
+
if (ts.isAsExpression(expression)) return expressionName(expression.expression);
|
|
411
|
+
return null;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const firstObjectReturnedByArrow = (node: ts.Node): ts.ObjectLiteralExpression | null => {
|
|
415
|
+
if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return null;
|
|
416
|
+
if (ts.isObjectLiteralExpression(node.body)) return node.body;
|
|
417
|
+
if (ts.isParenthesizedExpression(node.body) && ts.isObjectLiteralExpression(node.body.expression)) {
|
|
418
|
+
return node.body.expression;
|
|
419
|
+
}
|
|
420
|
+
if (!ts.isBlock(node.body)) return null;
|
|
421
|
+
for (const statement of node.body.statements) {
|
|
422
|
+
if (ts.isReturnStatement(statement) && statement.expression && ts.isObjectLiteralExpression(statement.expression)) {
|
|
423
|
+
return statement.expression;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const isViaCall = (expression: ts.Expression) =>
|
|
430
|
+
ts.isCallExpression(expression) && expressionName(expression.expression) === "via";
|
|
431
|
+
|
|
432
|
+
const heritageCall = (node: ts.ClassDeclaration) => {
|
|
433
|
+
const heritage = node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [];
|
|
434
|
+
const expression = heritage.find((clause) => isViaCall(clause.expression))?.expression;
|
|
435
|
+
return expression && ts.isCallExpression(expression) ? expression : null;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const callExpressionName = (node: ts.CallExpression) =>
|
|
439
|
+
ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : expressionName(node.expression);
|
|
440
|
+
|
|
441
|
+
const locatedObject = (source: ts.SourceFile, objectLiteral: ts.ObjectLiteralExpression): ObjectInsertionLocator => ({
|
|
442
|
+
objectStart: objectLiteral.getStart(source),
|
|
443
|
+
objectEnd: objectLiteral.getEnd(),
|
|
444
|
+
fields: objectLiteral.properties
|
|
445
|
+
.map((property): LocatedField | null => {
|
|
446
|
+
const name = propertyName(property);
|
|
447
|
+
if (!name) return null;
|
|
448
|
+
return {
|
|
449
|
+
name,
|
|
450
|
+
start: property.getStart(source),
|
|
451
|
+
fullStart: property.getFullStart(),
|
|
452
|
+
end: property.getEnd(),
|
|
453
|
+
};
|
|
454
|
+
})
|
|
455
|
+
.filter((field): field is LocatedField => field !== null),
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const findConstantInputObject = (source: ts.SourceFile, className: string): ObjectInsertionLocator | null => {
|
|
459
|
+
let locator: ObjectInsertionLocator | null = null;
|
|
460
|
+
const visit = (node: ts.Node) => {
|
|
461
|
+
if (locator) return;
|
|
462
|
+
if (!ts.isClassDeclaration(node) || node.name?.text !== className) {
|
|
463
|
+
ts.forEachChild(node, visit);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const viaCall = heritageCall(node);
|
|
467
|
+
const callback = viaCall?.arguments.find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
|
|
468
|
+
const objectLiteral = callback ? firstObjectReturnedByArrow(callback) : null;
|
|
469
|
+
if (objectLiteral) locator = locatedObject(source, objectLiteral);
|
|
470
|
+
};
|
|
471
|
+
ts.forEachChild(source, visit);
|
|
472
|
+
return locator;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
interface DictionaryModelLocator extends ObjectInsertionLocator {
|
|
476
|
+
chainMethods: string[];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const chainMethodsForCall = (node: ts.Expression): string[] => {
|
|
480
|
+
if (ts.isAsExpression(node) || ts.isParenthesizedExpression(node)) return chainMethodsForCall(node.expression);
|
|
481
|
+
if (!ts.isCallExpression(node)) return [];
|
|
482
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
483
|
+
return [...chainMethodsForCall(node.expression.expression), node.expression.name.text];
|
|
484
|
+
}
|
|
485
|
+
const name = expressionName(node.expression);
|
|
486
|
+
return name ? [name] : [];
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const outermostFluentCall = (node: ts.CallExpression) => {
|
|
490
|
+
let current: ts.CallExpression = node;
|
|
491
|
+
while (
|
|
492
|
+
ts.isPropertyAccessExpression(current.parent) &&
|
|
493
|
+
current.parent.expression === current &&
|
|
494
|
+
ts.isCallExpression(current.parent.parent) &&
|
|
495
|
+
current.parent.parent.expression === current.parent
|
|
496
|
+
) {
|
|
497
|
+
current = current.parent.parent;
|
|
498
|
+
}
|
|
499
|
+
return current;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const protectedDictionaryChainOrder = ["model", "slice", "enum", "error", "translate"] as const;
|
|
503
|
+
|
|
504
|
+
const dictionaryChainOrderValid = (chainMethods: readonly string[]) => {
|
|
505
|
+
const protectedOrder = new Map(protectedDictionaryChainOrder.map((method, index) => [method, index]));
|
|
506
|
+
let lastOrder = -1;
|
|
507
|
+
for (const method of chainMethods) {
|
|
508
|
+
const order = protectedOrder.get(method as (typeof protectedDictionaryChainOrder)[number]);
|
|
509
|
+
if (order === undefined) continue;
|
|
510
|
+
if (order < lastOrder) return false;
|
|
511
|
+
lastOrder = order;
|
|
512
|
+
}
|
|
513
|
+
return true;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const findDictionaryModelObject = (source: ts.SourceFile, moduleClassName: string): DictionaryModelLocator | null => {
|
|
517
|
+
let locator: DictionaryModelLocator | null = null;
|
|
518
|
+
const visit = (node: ts.Node) => {
|
|
519
|
+
if (locator) return;
|
|
520
|
+
if (!ts.isCallExpression(node) || callExpressionName(node) !== "model") {
|
|
521
|
+
ts.forEachChild(node, visit);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const typeArgument = node.typeArguments?.[0];
|
|
525
|
+
if (!typeArgument || typeArgument.getText(source) !== moduleClassName) {
|
|
526
|
+
ts.forEachChild(node, visit);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const callback = node.arguments.find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
|
|
530
|
+
const objectLiteral = callback ? firstObjectReturnedByArrow(callback) : null;
|
|
531
|
+
if (objectLiteral) {
|
|
532
|
+
locator = {
|
|
533
|
+
...locatedObject(source, objectLiteral),
|
|
534
|
+
chainMethods: chainMethodsForCall(outermostFluentCall(node)),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
ts.forEachChild(source, visit);
|
|
539
|
+
return locator;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const findLightProjectionArray = (source: ts.SourceFile, moduleClassName: string): ArrayInsertionLocator | null => {
|
|
543
|
+
let locator: ArrayInsertionLocator | null = null;
|
|
544
|
+
const visit = (node: ts.Node) => {
|
|
545
|
+
if (locator) return;
|
|
546
|
+
if (!ts.isClassDeclaration(node) || node.name?.text !== `Light${moduleClassName}`) {
|
|
547
|
+
ts.forEachChild(node, visit);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const viaCall = heritageCall(node);
|
|
551
|
+
const projectionArg = viaCall?.arguments.find((arg) => {
|
|
552
|
+
const expression = ts.isAsExpression(arg) ? arg.expression : arg;
|
|
553
|
+
return ts.isArrayLiteralExpression(expression);
|
|
554
|
+
});
|
|
555
|
+
const arrayLiteral = projectionArg
|
|
556
|
+
? ts.isAsExpression(projectionArg)
|
|
557
|
+
? projectionArg.expression
|
|
558
|
+
: projectionArg
|
|
559
|
+
: null;
|
|
560
|
+
if (!projectionArg || !arrayLiteral || !ts.isArrayLiteralExpression(arrayLiteral)) return;
|
|
561
|
+
locator = {
|
|
562
|
+
projectionStart: projectionArg.getStart(source),
|
|
563
|
+
projectionEnd: projectionArg.getEnd(),
|
|
564
|
+
fields: arrayLiteral.elements
|
|
565
|
+
.map((element) => (ts.isStringLiteralLike(element) ? element.text : null))
|
|
566
|
+
.filter((field): field is string => field !== null),
|
|
567
|
+
};
|
|
568
|
+
};
|
|
569
|
+
ts.forEachChild(source, visit);
|
|
570
|
+
return locator;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const findNamedImport = (source: ts.SourceFile, moduleSpecifier: string): NamedImportLocator | null => {
|
|
574
|
+
for (const statement of source.statements) {
|
|
575
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
576
|
+
if (statement.moduleSpecifier.text !== moduleSpecifier) continue;
|
|
577
|
+
const namedBindings = statement.importClause?.namedBindings;
|
|
578
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
|
|
579
|
+
return {
|
|
580
|
+
names: namedBindings.elements.map((element) => element.name.text),
|
|
581
|
+
namedBindingsStart: namedBindings.getStart(source),
|
|
582
|
+
namedBindingsEnd: namedBindings.getEnd(),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const fieldExpressionBuilder = (property: ts.ObjectLiteralElementLike) => {
|
|
589
|
+
if (!ts.isPropertyAssignment(property)) return null;
|
|
590
|
+
const initializer = ts.isAsExpression(property.initializer) ? property.initializer.expression : property.initializer;
|
|
591
|
+
if (!ts.isCallExpression(initializer)) return null;
|
|
592
|
+
return expressionName(initializer.expression);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const priorityOf = (fieldName: string) => {
|
|
596
|
+
const priority = (fieldOrderingPriority as readonly string[]).indexOf(fieldName);
|
|
597
|
+
return priority < 0 ? null : priority;
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
export const insertionIndexForFieldOrder = (fieldNames: readonly string[], newFieldName: string) => {
|
|
601
|
+
const newPriority = priorityOf(newFieldName);
|
|
602
|
+
if (newPriority !== null) {
|
|
603
|
+
const greaterPriorityIndex = fieldNames.findIndex((name) => {
|
|
604
|
+
const existingPriority = priorityOf(name);
|
|
605
|
+
return existingPriority !== null && existingPriority > newPriority;
|
|
606
|
+
});
|
|
607
|
+
if (greaterPriorityIndex >= 0) return greaterPriorityIndex;
|
|
608
|
+
|
|
609
|
+
const lastPriorityIndex = fieldNames.reduce((lastIndex, name, index) => {
|
|
610
|
+
const existingPriority = priorityOf(name);
|
|
611
|
+
return existingPriority !== null ? index : lastIndex;
|
|
612
|
+
}, -1);
|
|
613
|
+
return lastPriorityIndex + 1;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const lastNonPriorityIndex = fieldNames.reduce(
|
|
617
|
+
(lastIndex, name, index) => (priorityOf(name) === null ? index : lastIndex),
|
|
618
|
+
-1,
|
|
619
|
+
);
|
|
620
|
+
return lastNonPriorityIndex >= 0 ? lastNonPriorityIndex + 1 : fieldNames.length;
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const insertOrderedFieldLine = (
|
|
624
|
+
content: string,
|
|
625
|
+
locator: ObjectInsertionLocator,
|
|
626
|
+
fieldName: string,
|
|
627
|
+
line: string,
|
|
628
|
+
options: { fieldIndent: string; closingIndent: string },
|
|
629
|
+
) => {
|
|
630
|
+
if (locator.fields.some((field) => field.name === fieldName)) return content;
|
|
631
|
+
const insertIndex = insertionIndexForFieldOrder(
|
|
632
|
+
locator.fields.map((field) => field.name),
|
|
633
|
+
fieldName,
|
|
634
|
+
);
|
|
635
|
+
const formattedLine = line.trim();
|
|
636
|
+
if (locator.fields.length === 0) {
|
|
637
|
+
return spliceText(
|
|
638
|
+
content,
|
|
639
|
+
locator.objectStart,
|
|
640
|
+
locator.objectEnd,
|
|
641
|
+
`{\n${options.fieldIndent}${formattedLine}\n${options.closingIndent}}`,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const beforeField = locator.fields[insertIndex];
|
|
646
|
+
if (beforeField) {
|
|
647
|
+
const leadingComments = ts.getLeadingCommentRanges(content, beforeField.fullStart) ?? [];
|
|
648
|
+
const insertAt = lineStartAt(content, leadingComments[0]?.pos ?? beforeField.start);
|
|
649
|
+
const indent = lineIndentAt(content, beforeField.start) || options.fieldIndent;
|
|
650
|
+
return spliceText(content, insertAt, insertAt, `${indent}${formattedLine}\n`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const afterField = locator.fields[locator.fields.length - 1];
|
|
654
|
+
const insertAt = lineEndAt(content, afterField.end);
|
|
655
|
+
const indent = lineIndentAt(content, afterField.start) || options.fieldIndent;
|
|
656
|
+
return spliceText(content, insertAt, insertAt, `${indent}${formattedLine}\n`);
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
export const viaBuilderParameterName = (content: string, className: string) => {
|
|
660
|
+
const source = sourceFileFor("constant.ts", content);
|
|
661
|
+
let builderName: string | null = null;
|
|
662
|
+
const visit = (node: ts.Node) => {
|
|
663
|
+
if (builderName !== null) return;
|
|
664
|
+
if (!ts.isClassDeclaration(node) || node.name?.text !== className) {
|
|
665
|
+
ts.forEachChild(node, visit);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const viaCall = heritageCall(node);
|
|
669
|
+
const callback = viaCall?.arguments.find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
|
|
670
|
+
if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
|
|
671
|
+
builderName = nodeName(callback.parameters[0]?.name);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
ts.forEachChild(source, visit);
|
|
675
|
+
return builderName;
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
export const inspectConstantStructure = (
|
|
679
|
+
content: string,
|
|
680
|
+
className: string,
|
|
681
|
+
moduleClassName: string,
|
|
682
|
+
): AkanConstantStructure => {
|
|
683
|
+
const source = sourceFileFor("constant.ts", content);
|
|
684
|
+
const inputObject = findConstantInputObject(source, className);
|
|
685
|
+
const lightProjection = findLightProjectionArray(source, moduleClassName);
|
|
686
|
+
const baseImport = findNamedImport(source, "akanjs/base");
|
|
687
|
+
const fields: AkanConstantFieldStructure[] = [];
|
|
688
|
+
if (inputObject) {
|
|
689
|
+
const visit = (node: ts.Node) => {
|
|
690
|
+
if (!ts.isClassDeclaration(node) || node.name?.text !== className) {
|
|
691
|
+
ts.forEachChild(node, visit);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const viaCall = heritageCall(node);
|
|
695
|
+
const callback = viaCall?.arguments.find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
|
|
696
|
+
const objectLiteral = callback ? firstObjectReturnedByArrow(callback) : null;
|
|
697
|
+
if (!objectLiteral) return;
|
|
698
|
+
fields.push(
|
|
699
|
+
...objectLiteral.properties
|
|
700
|
+
.map((property): AkanConstantFieldStructure | null => {
|
|
701
|
+
const name = propertyName(property);
|
|
702
|
+
if (!name) return null;
|
|
703
|
+
return { name, expressionBuilder: fieldExpressionBuilder(property) };
|
|
704
|
+
})
|
|
705
|
+
.filter((field): field is AkanConstantFieldStructure => field !== null),
|
|
706
|
+
);
|
|
707
|
+
};
|
|
708
|
+
ts.forEachChild(source, visit);
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
parseValid: !hasParseDiagnostics(source),
|
|
712
|
+
inputObjectFound: inputObject !== null,
|
|
713
|
+
builderName: viaBuilderParameterName(content, className),
|
|
714
|
+
fields,
|
|
715
|
+
lightProjectionFields: lightProjection?.fields ?? [],
|
|
716
|
+
baseImports: baseImport?.names ?? [],
|
|
717
|
+
};
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
export const inspectDictionaryStructure = (content: string, moduleClassName: string): AkanDictionaryStructure => {
|
|
721
|
+
const source = sourceFileFor("dictionary.ts", content);
|
|
722
|
+
const modelObject = findDictionaryModelObject(source, moduleClassName);
|
|
723
|
+
return {
|
|
724
|
+
parseValid: !hasParseDiagnostics(source),
|
|
725
|
+
modelObjectFound: modelObject !== null,
|
|
726
|
+
chainOrderValid: modelObject ? dictionaryChainOrderValid(modelObject.chainMethods) : false,
|
|
727
|
+
chainMethods: modelObject?.chainMethods ?? [],
|
|
728
|
+
fields: modelObject?.fields.map((field) => field.name) ?? [],
|
|
729
|
+
};
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
export const insertIntoObject = (content: string, className: string, line: string) => {
|
|
733
|
+
const fieldName = /^([A-Za-z_$][\w$]*)\s*:/.exec(line.trim())?.[1];
|
|
734
|
+
if (!fieldName) return null;
|
|
735
|
+
const source = sourceFileFor("constant.ts", content);
|
|
736
|
+
const locator = findConstantInputObject(source, className);
|
|
737
|
+
if (!locator) return null;
|
|
738
|
+
return insertOrderedFieldLine(content, locator, fieldName, line, { fieldIndent: " ", closingIndent: "" });
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
export const insertLightProjectionField = (content: string, moduleClassName: string, fieldName: string) => {
|
|
742
|
+
const source = sourceFileFor("constant.ts", content);
|
|
743
|
+
const locator = findLightProjectionArray(source, moduleClassName);
|
|
744
|
+
if (!locator) return null;
|
|
745
|
+
if (locator.fields.includes(fieldName)) return content;
|
|
746
|
+
const nextFields = [...locator.fields, fieldName];
|
|
747
|
+
const nextArray =
|
|
748
|
+
nextFields.length === 0
|
|
749
|
+
? "[] as const"
|
|
750
|
+
: `[\n${nextFields.map((field) => ` ${JSON.stringify(field)},`).join("\n")}\n] as const`;
|
|
751
|
+
return spliceText(content, locator.projectionStart, locator.projectionEnd, nextArray);
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
export const insertTemplateField = ({
|
|
755
|
+
content,
|
|
756
|
+
moduleName,
|
|
757
|
+
moduleClassName,
|
|
758
|
+
fieldName,
|
|
759
|
+
component,
|
|
760
|
+
}: {
|
|
761
|
+
content: string;
|
|
762
|
+
moduleName: string;
|
|
763
|
+
moduleClassName: string;
|
|
764
|
+
fieldName: string;
|
|
765
|
+
component: "Field.Text" | "Field.Number" | "Field.Date";
|
|
766
|
+
}) => {
|
|
767
|
+
if (content.includes(`l("${moduleName}.${fieldName}")`) || content.includes(`.${fieldName}`)) return content;
|
|
768
|
+
const layoutEndIndex = content.indexOf(" </Layout.Template>");
|
|
769
|
+
if (layoutEndIndex < 0) return null;
|
|
770
|
+
const formName = `${moduleName}Form`;
|
|
771
|
+
if (!content.includes(`const ${formName} = st.use.${moduleName}Form();`)) return null;
|
|
772
|
+
const fieldSetter = `st.do.set${moduleComponentName(fieldName)}On${moduleClassName}`;
|
|
773
|
+
const fieldBlock = ` <${component}
|
|
774
|
+
label={l("${moduleName}.${fieldName}")}
|
|
775
|
+
desc={l("${moduleName}.${fieldName}.desc")}
|
|
776
|
+
value={${formName}.${fieldName}}
|
|
777
|
+
onChange={${fieldSetter}}
|
|
778
|
+
/>
|
|
779
|
+
`;
|
|
780
|
+
return `${content.slice(0, layoutEndIndex)}${fieldBlock}${content.slice(layoutEndIndex)}`;
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
export const ensureEnumImport = (content: string) => {
|
|
784
|
+
if (content.includes("enumOf")) return content;
|
|
785
|
+
const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
|
|
786
|
+
if (baseImport) {
|
|
787
|
+
const names = baseImport[1]?.split(",").map((name) => name.trim()) ?? [];
|
|
788
|
+
return content.replace(baseImport[0], `import { ${[...names, "enumOf"].sort().join(", ")} } from "akanjs/base";`);
|
|
789
|
+
}
|
|
790
|
+
return `import { enumOf } from "akanjs/base";\n${content}`;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
export const insertEnumClass = (content: string, enumClassName: string, enumName: string, values: string[]) => {
|
|
794
|
+
if (content.includes(`export class ${enumClassName} extends enumOf`)) return content;
|
|
795
|
+
const enumClass = `export class ${enumClassName} extends enumOf("${enumName}", [\n${values
|
|
796
|
+
.map((value) => ` ${JSON.stringify(value)},`)
|
|
797
|
+
.join("\n")}\n] as const) {}\n\n`;
|
|
798
|
+
const firstClassIndex = content.indexOf("export class ");
|
|
799
|
+
if (firstClassIndex < 0) return `${content}\n${enumClass}`;
|
|
800
|
+
return `${content.slice(0, firstClassIndex)}${enumClass}${content.slice(firstClassIndex)}`;
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const dictionaryModelFieldLine = (fieldName: string) => {
|
|
804
|
+
const label = bilingualLabelForField(fieldName);
|
|
805
|
+
const desc = bilingualDescriptionForField(fieldName);
|
|
806
|
+
return `${fieldName}: t([${JSON.stringify(label.en)}, ${JSON.stringify(label.ko)}]).desc([${JSON.stringify(
|
|
807
|
+
desc.en,
|
|
808
|
+
)}, ${JSON.stringify(desc.ko)}]),`;
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
export const insertDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
|
|
812
|
+
const source = sourceFileFor("dictionary.ts", content);
|
|
813
|
+
const locator = findDictionaryModelObject(source, moduleClassName);
|
|
814
|
+
if (!locator) return null;
|
|
815
|
+
return insertOrderedFieldLine(content, locator, fieldName, dictionaryModelFieldLine(fieldName), {
|
|
816
|
+
fieldIndent: " ",
|
|
817
|
+
closingIndent: " ",
|
|
818
|
+
});
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
export const hasConstantInputField = (content: string, className: string, fieldName: string) => {
|
|
822
|
+
const source = sourceFileFor("constant.ts", content);
|
|
823
|
+
if (hasParseDiagnostics(source)) return false;
|
|
824
|
+
return findConstantInputObject(source, className)?.fields.some((field) => field.name === fieldName) ?? false;
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
export const hasDictionaryModelField = (content: string, moduleClassName: string, fieldName: string) => {
|
|
828
|
+
const source = sourceFileFor("dictionary.ts", content);
|
|
829
|
+
if (hasParseDiagnostics(source)) return false;
|
|
830
|
+
return findDictionaryModelObject(source, moduleClassName)?.fields.some((field) => field.name === fieldName) ?? false;
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
export const ensureConstantTypeImport = (content: string, constantPath: string, typeName: string) => {
|
|
834
|
+
if (new RegExp(`import type \\{[^}]*\\b${typeName}\\b[^}]*\\} from "${constantPath}";`).test(content)) return content;
|
|
835
|
+
const importPattern = new RegExp(`import type \\{ ([^}]+) \\} from "${constantPath}";`);
|
|
836
|
+
const existingImport = content.match(importPattern);
|
|
837
|
+
if (existingImport !== null) {
|
|
838
|
+
const names = existingImport[1]?.split(",").map((name) => name.trim()) ?? [];
|
|
839
|
+
return content.replace(
|
|
840
|
+
existingImport[0],
|
|
841
|
+
`import type { ${[...names, typeName].sort().join(", ")} } from "${constantPath}";`,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
return `import type { ${typeName} } from "${constantPath}";\n${content}`;
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
export const insertDictionaryEnum = (content: string, enumClassName: string, enumName: string, values: string[]) => {
|
|
848
|
+
if (content.includes(`.enum<${enumClassName}>("${enumName}"`)) return content;
|
|
849
|
+
const enumBlock = ` .enum<${enumClassName}>("${enumName}", (t) => ({\n${values
|
|
850
|
+
.map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${JSON.stringify(titleize(value))}]),`)
|
|
851
|
+
.join("\n")}\n }))\n`;
|
|
852
|
+
const chainEndIndex = content.lastIndexOf(";");
|
|
853
|
+
const insertBeforeIndex = [content.indexOf(".error("), content.indexOf(".translate("), chainEndIndex]
|
|
854
|
+
.filter((index) => index >= 0)
|
|
855
|
+
.sort((a, b) => a - b)[0];
|
|
856
|
+
if (insertBeforeIndex === undefined) return null;
|
|
857
|
+
return `${content.slice(0, insertBeforeIndex)}${enumBlock}${content.slice(insertBeforeIndex)}`;
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
export const parseValues = (value: string | null) =>
|
|
861
|
+
value
|
|
862
|
+
?.split(",")
|
|
863
|
+
.map((item) => item.trim())
|
|
864
|
+
.filter(Boolean) ?? [];
|