@omnifyjp/ts 6.7.4 → 6.7.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/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
- for (const p of cleanup.warnedEditables) {
370
- console.warn(`[omnify-ts] orphan editable service ${p} — backing schema is no ` +
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.`);
@@ -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]) => `'${locale}': '${escapeString(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}]: ${JSON.stringify(v.extra)},`)
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(` ${formatKey(fieldName)}: {\n`);
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: ${jsLiteral(field.validation, 6)},\n`);
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: ${jsLiteral([...field.enumValues], 6)} as const,\n`);
278
+ parts.push(` enumValues: ${tsObjectLiteral([...field.enumValues], 6)} as const,\n`);
313
279
  }
314
280
  if (field.relation) {
315
- parts.push(` relation: ${jsLiteral(field.relation, 6)},\n`);
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: ${jsLiteral(field.label, 6)},\n`);
286
+ parts.push(` label: ${tsObjectLiteral(field.label, 6)},\n`);
321
287
  if (field.placeholder) {
322
- parts.push(` placeholder: ${jsLiteral(field.placeholder, 6)},\n`);
288
+ parts.push(` placeholder: ${tsObjectLiteral(field.placeholder, 6)},\n`);
323
289
  }
324
290
  if (field.description) {
325
- parts.push(` description: ${jsLiteral(field.description, 6)},\n`);
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: ${jsLiteral(meta.aggregations.translatableFields, 2)} as const,\n`);
332
- parts.push(` requiredFields: ${jsLiteral(meta.aggregations.requiredFields, 2)} as const,\n`);
333
- parts.push(` enumFields: ${jsLiteral(meta.aggregations.enumFields, 2)} as const,\n`);
334
- parts.push(` relationFields: ${jsLiteral(meta.aggregations.relationFields, 2)} as const,\n`);
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
@@ -105,6 +128,9 @@ export function pruneOrphanServiceFiles(configDir, config, generated) {
105
128
  return;
106
129
  if (expectedAbs.has(path))
107
130
  return;
131
+ const head = readEditableServiceHead(path);
132
+ if (head === null || !looksLikeOmnifyEditableService(head))
133
+ return;
108
134
  result.warnedEditables.push(path);
109
135
  });
110
136
  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
+ }
@@ -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: ${JSON.stringify(displayNames.displayName)},\n`);
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: ${JSON.stringify(labelMap)},\n`);
526
+ parts.push(` label: ${tsObjectLiteral(labelMap, 6)},\n`);
526
527
  if (placeholderMap) {
527
- parts.push(` placeholder: ${JSON.stringify(placeholderMap)},\n`);
528
+ parts.push(` placeholder: ${tsObjectLiteral(placeholderMap, 6)},\n`);
528
529
  }
529
530
  parts.push(` },\n`);
530
531
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "6.7.4",
3
+ "version": "6.7.5",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",