@omnifyjp/ts 6.7.4 → 6.7.6
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/dist/cli.js +24 -2
- package/dist/enum-generator.js +3 -2
- package/dist/metadata-generator.js +12 -46
- package/dist/php/orphan-cleanup.d.ts +7 -0
- package/dist/php/orphan-cleanup.js +50 -1
- package/dist/prettier-format.d.ts +23 -0
- package/dist/prettier-format.js +102 -0
- package/dist/ts-literal.d.ts +8 -0
- package/dist/ts-literal.js +39 -0
- package/dist/zod-generator.js +4 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,7 @@ import { stripPhpComments } from './php/types.js';
|
|
|
33
33
|
import { pruneOrphanServiceFilesIfEnabled } from './php/orphan-cleanup.js';
|
|
34
34
|
import { findSiblingWithSameBasename, findAppRootAncestor } from './php/sibling-skip.js';
|
|
35
35
|
import { runPintFormat } from './php/pint-format.js';
|
|
36
|
+
import { runPrettierFormat } from './prettier-format.js';
|
|
36
37
|
import { resolveInput, sniffInputKind } from './input-resolver.js';
|
|
37
38
|
function resolveFromConfig(configPath) {
|
|
38
39
|
const raw = readFileSync(configPath, 'utf-8');
|
|
@@ -194,6 +195,7 @@ program
|
|
|
194
195
|
let tsCreated = 0;
|
|
195
196
|
let tsOverwritten = 0;
|
|
196
197
|
let tsSkipped = 0;
|
|
198
|
+
const tsWrittenPaths = [];
|
|
197
199
|
for (const file of files) {
|
|
198
200
|
let filePath;
|
|
199
201
|
if (file.category === 'enum' || file.category === 'plugin-enum') {
|
|
@@ -208,6 +210,7 @@ program
|
|
|
208
210
|
continue;
|
|
209
211
|
}
|
|
210
212
|
writeFileSync(filePath, file.content, 'utf-8');
|
|
213
|
+
tsWrittenPaths.push(filePath);
|
|
211
214
|
if (file.overwrite || !existsSync(filePath)) {
|
|
212
215
|
tsOverwritten++;
|
|
213
216
|
}
|
|
@@ -220,6 +223,22 @@ program
|
|
|
220
223
|
console.log(` ${tsCreated} files created (user-editable)`);
|
|
221
224
|
if (tsSkipped > 0)
|
|
222
225
|
console.log(` ${tsSkipped} files skipped (already exist)`);
|
|
226
|
+
// Issue #211: format via the consumer project's Prettier (same idea as Pint for PHP).
|
|
227
|
+
if (tsWrittenPaths.length > 0) {
|
|
228
|
+
const prettierResult = runPrettierFormat(tsWrittenPaths);
|
|
229
|
+
if (prettierResult.ran) {
|
|
230
|
+
if (prettierResult.exitStatus === 0) {
|
|
231
|
+
console.log(` Formatted ${prettierResult.formattedCount} TS file(s) via Prettier`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
console.warn(` [omnify] Prettier exited with status ${prettierResult.exitStatus}; generated files may not match project formatting preset.`);
|
|
235
|
+
if (prettierResult.errorMessage) {
|
|
236
|
+
const trimmed = prettierResult.errorMessage.trim().split('\n').slice(0, 5).join('\n');
|
|
237
|
+
console.warn(` ${trimmed}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
223
242
|
}
|
|
224
243
|
else {
|
|
225
244
|
console.log(`\n[TypeScript] Skipped (disabled)`);
|
|
@@ -366,11 +385,14 @@ program
|
|
|
366
385
|
console.log(` pruned: ${p}`);
|
|
367
386
|
}
|
|
368
387
|
}
|
|
369
|
-
|
|
370
|
-
console.warn(`[omnify-ts] orphan editable service
|
|
388
|
+
if (cleanup.warnedEditables.length > 0) {
|
|
389
|
+
console.warn(`[omnify-ts] ${cleanup.warnedEditables.length} orphan editable service(s) — backing schema is no ` +
|
|
371
390
|
`longer eligible for service codegen (removed / renamed / opted out / ` +
|
|
372
391
|
`flipped to non-object kind). Safe to delete by hand if it has no ` +
|
|
373
392
|
`custom code inside; preserved for now to protect any hand-edits.`);
|
|
393
|
+
for (const p of cleanup.warnedEditables) {
|
|
394
|
+
console.warn(` orphan editable service: ${p}`);
|
|
395
|
+
}
|
|
374
396
|
}
|
|
375
397
|
}
|
|
376
398
|
console.log(`\nGeneration complete.`);
|
package/dist/enum-generator.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Generates TypeScript enums with helper methods from schema enum definitions,
|
|
5
5
|
* plugin enums (customTypes.enums), and inline property enums.
|
|
6
6
|
*/
|
|
7
|
+
import { formatTsObjectKey, tsObjectLiteral } from './ts-literal.js';
|
|
7
8
|
/** Convert a string to PascalCase. */
|
|
8
9
|
export function toPascalCase(value) {
|
|
9
10
|
const normalized = value.replace(/([a-z])([A-Z])/g, '$1_$2');
|
|
@@ -154,7 +155,7 @@ export function formatEnum(enumDef, style = 'enum') {
|
|
|
154
155
|
.map(v => {
|
|
155
156
|
if (isMultiLocaleLabel(v.label)) {
|
|
156
157
|
const locales = Object.entries(v.label)
|
|
157
|
-
.map(([locale, text]) =>
|
|
158
|
+
.map(([locale, text]) => `${formatTsObjectKey(locale)}: '${escapeString(text)}'`)
|
|
158
159
|
.join(', ');
|
|
159
160
|
return ` [${name}.${v.name}]: { ${locales} },`;
|
|
160
161
|
}
|
|
@@ -193,7 +194,7 @@ export function formatEnum(enumDef, style = 'enum') {
|
|
|
193
194
|
if (hasExtra) {
|
|
194
195
|
const extraEntries = values
|
|
195
196
|
.filter(v => v.extra !== undefined)
|
|
196
|
-
.map(v => ` [${name}.${v.name}]: ${
|
|
197
|
+
.map(v => ` [${name}.${v.name}]: ${tsObjectLiteral(v.extra, 2)},`)
|
|
197
198
|
.join('\n');
|
|
198
199
|
parts.push(`const ${lowerFirst(name)}Extra: Partial<Record<${name}, Record<string, unknown>>> = {\n${extraEntries}\n};\n\n`);
|
|
199
200
|
parts.push(`/** Get extra metadata for ${name} value */\n`);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* interface, zod schemas, and i18n object — none of those are modified.
|
|
15
15
|
*/
|
|
16
16
|
import { toSnakeCase, toFkColumnName } from './interface-generator.js';
|
|
17
|
+
import { formatTsObjectKey, tsObjectLiteral } from './ts-literal.js';
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Type classification — single source of truth so #55 (payload builder)
|
|
19
20
|
// can reuse the same field-type decisions.
|
|
@@ -240,41 +241,6 @@ export function buildSchemaMetadata(schema, allSchemas, options, displayNames) {
|
|
|
240
241
|
// ---------------------------------------------------------------------------
|
|
241
242
|
// Formatter — turns the IR into a TypeScript const literal.
|
|
242
243
|
// ---------------------------------------------------------------------------
|
|
243
|
-
function jsLiteral(value, indent = 0) {
|
|
244
|
-
if (value === null)
|
|
245
|
-
return 'null';
|
|
246
|
-
if (value === undefined)
|
|
247
|
-
return 'undefined';
|
|
248
|
-
if (typeof value === 'string')
|
|
249
|
-
return JSON.stringify(value);
|
|
250
|
-
if (typeof value === 'number' || typeof value === 'boolean')
|
|
251
|
-
return String(value);
|
|
252
|
-
if (Array.isArray(value)) {
|
|
253
|
-
if (value.length === 0)
|
|
254
|
-
return '[]';
|
|
255
|
-
const inner = value.map((v) => jsLiteral(v, indent + 2)).join(', ');
|
|
256
|
-
return `[${inner}]`;
|
|
257
|
-
}
|
|
258
|
-
if (typeof value === 'object') {
|
|
259
|
-
const entries = Object.entries(value);
|
|
260
|
-
if (entries.length === 0)
|
|
261
|
-
return '{}';
|
|
262
|
-
const pad = ' '.repeat(indent + 2);
|
|
263
|
-
const closingPad = ' '.repeat(indent);
|
|
264
|
-
const inner = entries
|
|
265
|
-
.map(([k, v]) => `${pad}${formatKey(k)}: ${jsLiteral(v, indent + 2)}`)
|
|
266
|
-
.join(',\n');
|
|
267
|
-
return `{\n${inner},\n${closingPad}}`;
|
|
268
|
-
}
|
|
269
|
-
return JSON.stringify(value);
|
|
270
|
-
}
|
|
271
|
-
function formatKey(key) {
|
|
272
|
-
// Use bare identifier when possible (preserves nice JS look); fall back
|
|
273
|
-
// to quoted string for keys with hyphens, dots, or starting with a digit.
|
|
274
|
-
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key))
|
|
275
|
-
return key;
|
|
276
|
-
return JSON.stringify(key);
|
|
277
|
-
}
|
|
278
244
|
/** Render the SchemaMetadata IR to a TypeScript const literal block. */
|
|
279
245
|
export function formatMetadataConst(meta) {
|
|
280
246
|
const lowerName = meta.modelName.charAt(0).toLowerCase() + meta.modelName.slice(1);
|
|
@@ -299,39 +265,39 @@ export function formatMetadataConst(meta) {
|
|
|
299
265
|
parts.push(`\n`);
|
|
300
266
|
parts.push(` fields: {\n`);
|
|
301
267
|
for (const [fieldName, field] of Object.entries(meta.fields)) {
|
|
302
|
-
parts.push(` ${
|
|
268
|
+
parts.push(` ${formatTsObjectKey(fieldName)}: {\n`);
|
|
303
269
|
parts.push(` type: ${JSON.stringify(field.type)},\n`);
|
|
304
270
|
parts.push(` required: ${field.required},\n`);
|
|
305
271
|
parts.push(` nullable: ${field.nullable},\n`);
|
|
306
272
|
parts.push(` translatable: ${field.translatable},\n`);
|
|
307
|
-
parts.push(` validation: ${
|
|
273
|
+
parts.push(` validation: ${tsObjectLiteral(field.validation, 6)},\n`);
|
|
308
274
|
if (field.enum) {
|
|
309
275
|
parts.push(` enum: ${JSON.stringify(field.enum)},\n`);
|
|
310
276
|
}
|
|
311
277
|
if (field.enumValues) {
|
|
312
|
-
parts.push(` enumValues: ${
|
|
278
|
+
parts.push(` enumValues: ${tsObjectLiteral([...field.enumValues], 6)} as const,\n`);
|
|
313
279
|
}
|
|
314
280
|
if (field.relation) {
|
|
315
|
-
parts.push(` relation: ${
|
|
281
|
+
parts.push(` relation: ${tsObjectLiteral(field.relation, 6)},\n`);
|
|
316
282
|
}
|
|
317
283
|
if (field.compoundType) {
|
|
318
284
|
parts.push(` compoundType: ${JSON.stringify(field.compoundType)},\n`);
|
|
319
285
|
}
|
|
320
|
-
parts.push(` label: ${
|
|
286
|
+
parts.push(` label: ${tsObjectLiteral(field.label, 6)},\n`);
|
|
321
287
|
if (field.placeholder) {
|
|
322
|
-
parts.push(` placeholder: ${
|
|
288
|
+
parts.push(` placeholder: ${tsObjectLiteral(field.placeholder, 6)},\n`);
|
|
323
289
|
}
|
|
324
290
|
if (field.description) {
|
|
325
|
-
parts.push(` description: ${
|
|
291
|
+
parts.push(` description: ${tsObjectLiteral(field.description, 6)},\n`);
|
|
326
292
|
}
|
|
327
293
|
parts.push(` },\n`);
|
|
328
294
|
}
|
|
329
295
|
parts.push(` },\n\n`);
|
|
330
296
|
// Aggregations — emit as `as const` arrays so the literal types narrow.
|
|
331
|
-
parts.push(` translatableFields: ${
|
|
332
|
-
parts.push(` requiredFields: ${
|
|
333
|
-
parts.push(` enumFields: ${
|
|
334
|
-
parts.push(` relationFields: ${
|
|
297
|
+
parts.push(` translatableFields: ${tsObjectLiteral(meta.aggregations.translatableFields, 2)} as const,\n`);
|
|
298
|
+
parts.push(` requiredFields: ${tsObjectLiteral(meta.aggregations.requiredFields, 2)} as const,\n`);
|
|
299
|
+
parts.push(` enumFields: ${tsObjectLiteral(meta.aggregations.enumFields, 2)} as const,\n`);
|
|
300
|
+
parts.push(` relationFields: ${tsObjectLiteral(meta.aggregations.relationFields, 2)} as const,\n`);
|
|
335
301
|
parts.push(`} as const;\n\n`);
|
|
336
302
|
// Field-name type alias for ergonomic keying in downstream consumers.
|
|
337
303
|
parts.push(`export type ${meta.modelName}FieldName = keyof typeof ${constName}.fields;\n\n`);
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
* warning is printed so the user can decide whether to clean up by hand.
|
|
20
20
|
*/
|
|
21
21
|
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
22
|
+
/** Matches the header emitted by `generateEditableService` in service-generator.ts */
|
|
23
|
+
export declare const OMNIFY_EDITABLE_SERVICE_MARKER = "SAFE TO EDIT - This file is never overwritten by Omnify.";
|
|
24
|
+
/**
|
|
25
|
+
* True when the file looks like an Omnify sinh-1-lần editable stub (not a
|
|
26
|
+
* hand-written app service that merely ends in `Service.php`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function looksLikeOmnifyEditableService(fileContent: string): boolean;
|
|
22
29
|
export interface ServiceCleanupResult {
|
|
23
30
|
/** Absolute paths to `*ServiceBase.php` files that were deleted. */
|
|
24
31
|
prunedBases: string[];
|
|
@@ -18,8 +18,31 @@
|
|
|
18
18
|
* deleted — they may contain hand-written user code — instead a console
|
|
19
19
|
* warning is printed so the user can decide whether to clean up by hand.
|
|
20
20
|
*/
|
|
21
|
-
import { existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
21
|
+
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
22
22
|
import { join, resolve, basename } from 'node:path';
|
|
23
|
+
/** Matches the header emitted by `generateEditableService` in service-generator.ts */
|
|
24
|
+
export const OMNIFY_EDITABLE_SERVICE_MARKER = 'SAFE TO EDIT - This file is never overwritten by Omnify.';
|
|
25
|
+
const EXTENDS_SERVICE_BASE_RE = /extends\s+[\w\\]+ServiceBase\b/;
|
|
26
|
+
/**
|
|
27
|
+
* True when the file looks like an Omnify sinh-1-lần editable stub (not a
|
|
28
|
+
* hand-written app service that merely ends in `Service.php`).
|
|
29
|
+
*/
|
|
30
|
+
export function looksLikeOmnifyEditableService(fileContent) {
|
|
31
|
+
if (fileContent.includes(OMNIFY_EDITABLE_SERVICE_MARKER)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return EXTENDS_SERVICE_BASE_RE.test(fileContent);
|
|
35
|
+
}
|
|
36
|
+
function readEditableServiceHead(filePath) {
|
|
37
|
+
try {
|
|
38
|
+
const buf = readFileSync(filePath);
|
|
39
|
+
const max = Math.min(buf.length, 8192);
|
|
40
|
+
return buf.toString('utf8', 0, max);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
23
46
|
/**
|
|
24
47
|
* Apply service cleanup only while this target is managed by Omnify.
|
|
25
48
|
* A globally disabled service layer is a migration boundary: existing bases
|
|
@@ -96,6 +119,25 @@ export function pruneOrphanServiceFiles(configDir, config, generated) {
|
|
|
96
119
|
// The recursive walk skips the `OmnifyBase` subfolder (already covered
|
|
97
120
|
// by the base cleanup above) so we don't double-process or warn on
|
|
98
121
|
// base files masquerading as editable.
|
|
122
|
+
// Membership in `expectedAbs` is the WRONG test for an editable service, and
|
|
123
|
+
// was the whole defect: an editable service is sinh-1-lần, so once it exists
|
|
124
|
+
// this pass deliberately skips it and it is never in the generated set. Every
|
|
125
|
+
// editable service that had ever been created therefore reported itself
|
|
126
|
+
// orphan on every run — 28 of them here, each with a live `kind: object`
|
|
127
|
+
// schema that omnify was regenerating a base for in the very same pass, and
|
|
128
|
+
// each advertised as "safe to delete by hand" while holding the app's
|
|
129
|
+
// business logic.
|
|
130
|
+
//
|
|
131
|
+
// What actually marks a schema ineligible is the absence of its BASE: bases
|
|
132
|
+
// are regenerated wholesale every pass, so `<Name>ServiceBase.php` is in the
|
|
133
|
+
// generated set exactly while the schema still opts into service codegen.
|
|
134
|
+
const expectedBaseNames = new Set();
|
|
135
|
+
for (const abs of expectedAbs) {
|
|
136
|
+
const name = basename(abs);
|
|
137
|
+
if (name.endsWith('ServiceBase.php')) {
|
|
138
|
+
expectedBaseNames.add(name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
99
141
|
const editableRoot = resolve(configDir, config.services.path);
|
|
100
142
|
walkServiceFiles(editableRoot, (path) => {
|
|
101
143
|
const name = basename(path);
|
|
@@ -105,6 +147,13 @@ export function pruneOrphanServiceFiles(configDir, config, generated) {
|
|
|
105
147
|
return;
|
|
106
148
|
if (expectedAbs.has(path))
|
|
107
149
|
return;
|
|
150
|
+
// `AbsenceService.php` → `AbsenceServiceBase.php`
|
|
151
|
+
const baseName = `${name.slice(0, -'Service.php'.length)}ServiceBase.php`;
|
|
152
|
+
if (expectedBaseNames.has(baseName))
|
|
153
|
+
return;
|
|
154
|
+
const head = readEditableServiceHead(path);
|
|
155
|
+
if (head === null || !looksLikeOmnifyEditableService(head))
|
|
156
|
+
return;
|
|
108
157
|
result.warnedEditables.push(path);
|
|
109
158
|
});
|
|
110
159
|
return result;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-format generated TypeScript files via the consuming project's Prettier.
|
|
3
|
+
* Issue omnify-jp/omnify-go#211 (mirrors php/pint-format.ts).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Resolve `node_modules/.bin/prettier` by walking up from a written `.ts` file.
|
|
7
|
+
*/
|
|
8
|
+
export declare function findPrettierBinary(anyWrittenTsPath: string): string | null;
|
|
9
|
+
/** Directory containing the `node_modules` that owns this Prettier binary. */
|
|
10
|
+
export declare function prettierProjectRoot(prettierPath: string): string;
|
|
11
|
+
export interface PrettierFormatResult {
|
|
12
|
+
ran: boolean;
|
|
13
|
+
prettierPath: string | null;
|
|
14
|
+
formattedCount: number;
|
|
15
|
+
exitStatus: number | null;
|
|
16
|
+
errorMessage?: string;
|
|
17
|
+
skipped?: 'no-ts-files' | 'opted-out' | 'prettier-not-found';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Run the project's Prettier on the given TypeScript files (absolute paths).
|
|
21
|
+
* Silent no-op when Prettier is not installed; never throws.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runPrettierFormat(writtenTsPaths: string[]): PrettierFormatResult;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-format generated TypeScript files via the consuming project's Prettier.
|
|
3
|
+
* Issue omnify-jp/omnify-go#211 (mirrors php/pint-format.ts).
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, statSync } from 'node:fs';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
/** Max file paths per Prettier invocation (stay under typical ARG_MAX). */
|
|
9
|
+
const PRETTIER_BATCH_SIZE = 100;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve `node_modules/.bin/prettier` by walking up from a written `.ts` file.
|
|
12
|
+
*/
|
|
13
|
+
export function findPrettierBinary(anyWrittenTsPath) {
|
|
14
|
+
let cur = dirname(anyWrittenTsPath);
|
|
15
|
+
for (let i = 0; i < 12; i++) {
|
|
16
|
+
const candidate = join(cur, 'node_modules', '.bin', 'prettier');
|
|
17
|
+
if (existsSync(candidate)) {
|
|
18
|
+
try {
|
|
19
|
+
const st = statSync(candidate);
|
|
20
|
+
if (st.isFile())
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// ignore stat errors and keep climbing
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const parent = dirname(cur);
|
|
28
|
+
if (parent === cur)
|
|
29
|
+
break;
|
|
30
|
+
cur = parent;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** Directory containing the `node_modules` that owns this Prettier binary. */
|
|
35
|
+
export function prettierProjectRoot(prettierPath) {
|
|
36
|
+
return dirname(dirname(dirname(prettierPath)));
|
|
37
|
+
}
|
|
38
|
+
function runPrettierBatch(prettierPath, projectRoot, files) {
|
|
39
|
+
const result = spawnSync(prettierPath, ['--write', ...files], {
|
|
40
|
+
cwd: projectRoot,
|
|
41
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
42
|
+
encoding: 'utf-8',
|
|
43
|
+
});
|
|
44
|
+
const exitStatus = result.status ?? null;
|
|
45
|
+
return {
|
|
46
|
+
exitStatus,
|
|
47
|
+
errorMessage: exitStatus !== 0 ? (result.stderr || result.stdout || undefined) : undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Run the project's Prettier on the given TypeScript files (absolute paths).
|
|
52
|
+
* Silent no-op when Prettier is not installed; never throws.
|
|
53
|
+
*/
|
|
54
|
+
export function runPrettierFormat(writtenTsPaths) {
|
|
55
|
+
if (writtenTsPaths.length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
ran: false,
|
|
58
|
+
prettierPath: null,
|
|
59
|
+
formattedCount: 0,
|
|
60
|
+
exitStatus: null,
|
|
61
|
+
skipped: 'no-ts-files',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (process.env.OMNIFY_NO_PRETTIER === '1') {
|
|
65
|
+
return {
|
|
66
|
+
ran: false,
|
|
67
|
+
prettierPath: null,
|
|
68
|
+
formattedCount: 0,
|
|
69
|
+
exitStatus: null,
|
|
70
|
+
skipped: 'opted-out',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const prettierPath = writtenTsPaths.reduce((found, path) => found ?? findPrettierBinary(path), null);
|
|
74
|
+
if (!prettierPath) {
|
|
75
|
+
return {
|
|
76
|
+
ran: false,
|
|
77
|
+
prettierPath: null,
|
|
78
|
+
formattedCount: 0,
|
|
79
|
+
exitStatus: null,
|
|
80
|
+
skipped: 'prettier-not-found',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const projectRoot = prettierProjectRoot(prettierPath);
|
|
84
|
+
let exitStatus = 0;
|
|
85
|
+
let errorMessage;
|
|
86
|
+
for (let i = 0; i < writtenTsPaths.length; i += PRETTIER_BATCH_SIZE) {
|
|
87
|
+
const batch = writtenTsPaths.slice(i, i + PRETTIER_BATCH_SIZE);
|
|
88
|
+
const batchResult = runPrettierBatch(prettierPath, projectRoot, batch);
|
|
89
|
+
if (batchResult.exitStatus !== 0) {
|
|
90
|
+
exitStatus = batchResult.exitStatus;
|
|
91
|
+
errorMessage = batchResult.errorMessage;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
ran: true,
|
|
97
|
+
prettierPath,
|
|
98
|
+
formattedCount: writtenTsPaths.length,
|
|
99
|
+
exitStatus,
|
|
100
|
+
errorMessage,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit TypeScript object/array literals (not JSON.stringify blobs).
|
|
3
|
+
* Issue omnify-jp/omnify-go#211 — Prettier-stable i18n label shapes.
|
|
4
|
+
*/
|
|
5
|
+
/** Object key as a bare identifier when valid, else a quoted string. */
|
|
6
|
+
export declare function formatTsObjectKey(key: string): string;
|
|
7
|
+
/** Render a value as a TypeScript literal expression. */
|
|
8
|
+
export declare function tsObjectLiteral(value: unknown, indent?: number): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit TypeScript object/array literals (not JSON.stringify blobs).
|
|
3
|
+
* Issue omnify-jp/omnify-go#211 — Prettier-stable i18n label shapes.
|
|
4
|
+
*/
|
|
5
|
+
/** Object key as a bare identifier when valid, else a quoted string. */
|
|
6
|
+
export function formatTsObjectKey(key) {
|
|
7
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key))
|
|
8
|
+
return key;
|
|
9
|
+
return JSON.stringify(key);
|
|
10
|
+
}
|
|
11
|
+
/** Render a value as a TypeScript literal expression. */
|
|
12
|
+
export function tsObjectLiteral(value, indent = 0) {
|
|
13
|
+
if (value === null)
|
|
14
|
+
return 'null';
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return 'undefined';
|
|
17
|
+
if (typeof value === 'string')
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
20
|
+
return String(value);
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
if (value.length === 0)
|
|
23
|
+
return '[]';
|
|
24
|
+
const inner = value.map((v) => tsObjectLiteral(v, indent + 2)).join(', ');
|
|
25
|
+
return `[${inner}]`;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === 'object') {
|
|
28
|
+
const entries = Object.entries(value);
|
|
29
|
+
if (entries.length === 0)
|
|
30
|
+
return '{}';
|
|
31
|
+
const pad = ' '.repeat(indent + 2);
|
|
32
|
+
const closingPad = ' '.repeat(indent);
|
|
33
|
+
const inner = entries
|
|
34
|
+
.map(([k, v]) => `${pad}${formatTsObjectKey(k)}: ${tsObjectLiteral(v, indent + 2)}`)
|
|
35
|
+
.join(',\n');
|
|
36
|
+
return `{\n${inner},\n${closingPad}}`;
|
|
37
|
+
}
|
|
38
|
+
return JSON.stringify(value);
|
|
39
|
+
}
|
package/dist/zod-generator.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Generates Zod schemas alongside TypeScript interfaces for form validation.
|
|
5
5
|
*/
|
|
6
6
|
import { toSnakeCase } from './interface-generator.js';
|
|
7
|
+
import { tsObjectLiteral } from './ts-literal.js';
|
|
7
8
|
/**
|
|
8
9
|
* Get localized display name as multi-locale object.
|
|
9
10
|
*/
|
|
@@ -517,14 +518,14 @@ export function formatZodSchemasSection(schemaName, zodSchemas, displayNames, ex
|
|
|
517
518
|
parts.push(`// ============================================================================\n\n`);
|
|
518
519
|
parts.push(`/**\n * Unified i18n object for ${schemaName}\n */\n`);
|
|
519
520
|
parts.push(`export const ${lowerName}I18n = {\n`);
|
|
520
|
-
parts.push(` label: ${
|
|
521
|
+
parts.push(` label: ${tsObjectLiteral(displayNames.displayName, 2)},\n`);
|
|
521
522
|
parts.push(` fields: {\n`);
|
|
522
523
|
for (const [propName, labelMap] of Object.entries(displayNames.propertyDisplayNames)) {
|
|
523
524
|
const placeholderMap = displayNames.propertyPlaceholders[propName];
|
|
524
525
|
parts.push(` ${propName}: {\n`);
|
|
525
|
-
parts.push(` label: ${
|
|
526
|
+
parts.push(` label: ${tsObjectLiteral(labelMap, 6)},\n`);
|
|
526
527
|
if (placeholderMap) {
|
|
527
|
-
parts.push(` placeholder: ${
|
|
528
|
+
parts.push(` placeholder: ${tsObjectLiteral(placeholderMap, 6)},\n`);
|
|
528
529
|
}
|
|
529
530
|
parts.push(` },\n`);
|
|
530
531
|
}
|