@omnifyjp/ts 7.0.1 → 7.0.3
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 +0 -0
- package/dist/php/faker-mapper.js +58 -20
- package/dist/php/orphan-cleanup.js +71 -21
- package/dist/php/request-generator.js +41 -2
- package/package.json +2 -2
- package/dist/php/pint-format.d.ts +0 -57
- package/dist/php/pint-format.js +0 -102
- package/dist/prettier-format.d.ts +0 -23
- package/dist/prettier-format.js +0 -102
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/php/faker-mapper.js
CHANGED
|
@@ -18,8 +18,8 @@ export function toFaker(propName, property, schemas = {}) {
|
|
|
18
18
|
case 'Int':
|
|
19
19
|
case 'BigInt': return intFaker(snakeName, property);
|
|
20
20
|
case 'TinyInt': return `'${snakeName}' => fake()->numberBetween(0, 127),`;
|
|
21
|
-
case 'Float':
|
|
22
|
-
case 'Decimal': return
|
|
21
|
+
case 'Float': return `'${snakeName}' => fake()->randomFloat(2, 1, 10000),`;
|
|
22
|
+
case 'Decimal': return decimalFaker(snakeName, property);
|
|
23
23
|
case 'Boolean': return `'${snakeName}' => fake()->boolean(),`;
|
|
24
24
|
case 'Text': return `'${snakeName}' => fake()->paragraphs(3, true),`;
|
|
25
25
|
case 'MediumText': return `'${snakeName}' => fake()->paragraphs(3, true),`;
|
|
@@ -82,43 +82,81 @@ export function associationFaker(propName, property, modelNamespace, currentSche
|
|
|
82
82
|
return { fake, import: importLine };
|
|
83
83
|
}
|
|
84
84
|
function stringFaker(propName, snakeName, property) {
|
|
85
|
+
const expr = stringFakerExpr(propName, snakeName, property);
|
|
86
|
+
const length = property['length'];
|
|
87
|
+
// Clamp to the column's actual length regardless of which branch above
|
|
88
|
+
// picked the generator: a wrong/over-eager name heuristic (or a caller
|
|
89
|
+
// constraining `length` below what the heuristic normally produces) must
|
|
90
|
+
// stay inside the column instead of overflowing it — an SQL 1406 on
|
|
91
|
+
// MySQL/MariaDB weeks after the schema shipped (sqlite enforces no such
|
|
92
|
+
// limit, so this was invisible locally). Applied uniformly to every
|
|
93
|
+
// generator here rather than cherry-picked per branch (#264).
|
|
94
|
+
const value = length != null ? `mb_substr(${expr}, 0, ${length})` : expr;
|
|
95
|
+
return `'${snakeName}' => ${value},`;
|
|
96
|
+
}
|
|
97
|
+
function stringFakerExpr(propName, snakeName, property) {
|
|
85
98
|
const lower = propName.toLowerCase();
|
|
99
|
+
// Name-heuristic tokens, split on the already-normalized snake_case column
|
|
100
|
+
// name so a single word boundary decides a match — `shipAddress` must not
|
|
101
|
+
// trip the bare `ip` check meant for `ipAddress` (#264: more-specific
|
|
102
|
+
// patterns must win, deterministically, not by accidental substring order).
|
|
103
|
+
const tokens = snakeName.split('_');
|
|
86
104
|
if (propName === 'slug')
|
|
87
|
-
return `
|
|
105
|
+
return `fake()->unique()->slug(2)`;
|
|
88
106
|
if (propName === 'uuid' || propName === 'uid')
|
|
89
|
-
return `
|
|
107
|
+
return `(string) \\Illuminate\\Support\\Str::uuid()`;
|
|
108
|
+
if (tokens.includes('ip'))
|
|
109
|
+
return `fake()->ipv4()`;
|
|
90
110
|
if (lower.includes('email'))
|
|
91
|
-
return `
|
|
111
|
+
return `fake()->unique()->safeEmail()`;
|
|
92
112
|
if (lower.includes('phone'))
|
|
93
|
-
return `
|
|
113
|
+
return `fake()->phoneNumber()`;
|
|
94
114
|
if (lower.includes('image') || lower.includes('photo') || lower.includes('avatar'))
|
|
95
|
-
return `
|
|
115
|
+
return `fake()->imageUrl()`;
|
|
96
116
|
if (lower.includes('url') || lower.includes('website'))
|
|
97
|
-
return `
|
|
117
|
+
return `fake()->url()`;
|
|
98
118
|
if (lower.includes('path') || lower.includes('file'))
|
|
99
|
-
return `'
|
|
119
|
+
return `'uploads/'.fake()->uuid().'.jpg'`;
|
|
100
120
|
if (propName === 'name' || propName === 'title')
|
|
101
|
-
return `
|
|
121
|
+
return `fake()->sentence(3)`;
|
|
102
122
|
if (lower.includes('name'))
|
|
103
|
-
return `
|
|
123
|
+
return `fake()->name()`;
|
|
104
124
|
if (lower.includes('address'))
|
|
105
|
-
return `
|
|
125
|
+
return `fake()->address()`;
|
|
106
126
|
if (lower.includes('city'))
|
|
107
|
-
return `
|
|
127
|
+
return `fake()->city()`;
|
|
108
128
|
if (lower.includes('country'))
|
|
109
|
-
return `
|
|
129
|
+
return `fake()->country()`;
|
|
110
130
|
if (lower.includes('zip') || lower.includes('postal'))
|
|
111
|
-
return `
|
|
131
|
+
return `fake()->postcode()`;
|
|
112
132
|
if (lower.includes('color'))
|
|
113
|
-
return `
|
|
133
|
+
return `fake()->hexColor()`;
|
|
114
134
|
if (lower.includes('token') || lower.includes('secret') || lower.includes('key'))
|
|
115
|
-
return
|
|
135
|
+
return `\\Illuminate\\Support\\Str::random(32)`;
|
|
116
136
|
if (lower.includes('code'))
|
|
117
|
-
return `
|
|
137
|
+
return `fake()->unique()->regexify('[A-Z0-9]{8}')`;
|
|
118
138
|
const length = property['length'];
|
|
119
139
|
if (length != null && length <= 50)
|
|
120
|
-
return `
|
|
121
|
-
return `
|
|
140
|
+
return `fake()->words(3, true)`;
|
|
141
|
+
return `fake()->sentence()`;
|
|
142
|
+
}
|
|
143
|
+
/** Largest value a `Decimal(precision, scale)` column accepts (all-9s at that precision/scale). */
|
|
144
|
+
function decimalMax(precision, scale) {
|
|
145
|
+
const digits = '9'.repeat(Math.max(precision, 1));
|
|
146
|
+
if (scale <= 0)
|
|
147
|
+
return digits;
|
|
148
|
+
const intPart = digits.slice(0, digits.length - scale) || '0';
|
|
149
|
+
const fracPart = digits.slice(digits.length - scale);
|
|
150
|
+
return `${intPart}.${fracPart}`;
|
|
151
|
+
}
|
|
152
|
+
function decimalFaker(snakeName, property) {
|
|
153
|
+
// Laravel's default Decimal migration is `decimal(8, 2)` (blueprint.go),
|
|
154
|
+
// matched here so an unspecified precision/scale keeps generating the same
|
|
155
|
+
// range as before.
|
|
156
|
+
const precision = property['precision'] ?? 8;
|
|
157
|
+
const scale = property['scale'] ?? 2;
|
|
158
|
+
const max = decimalMax(precision, scale);
|
|
159
|
+
return `'${snakeName}' => fake()->randomFloat(${scale}, 0, ${max}),`;
|
|
122
160
|
}
|
|
123
161
|
function intFaker(snakeName, property) {
|
|
124
162
|
const isUnique = property['unique'] ?? false;
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* warning is printed so the user can decide whether to clean up by hand.
|
|
20
20
|
*/
|
|
21
21
|
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
22
|
-
import { join, resolve, basename } from 'node:path';
|
|
22
|
+
import { join, relative, resolve, basename } from 'node:path';
|
|
23
|
+
import { resolveModularBaseNamespace } from './types.js';
|
|
23
24
|
/** Matches the header emitted by `generateEditableService` in service-generator.ts */
|
|
24
25
|
export const OMNIFY_EDITABLE_SERVICE_MARKER = 'SAFE TO EDIT - This file is never overwritten by Omnify.';
|
|
25
26
|
const EXTENDS_SERVICE_BASE_RE = /extends\s+[\w\\]+ServiceBase\b/;
|
|
@@ -60,34 +61,83 @@ function collectModularLayerDirs(configDir, config, category) {
|
|
|
60
61
|
for (const entry of readdirSync(modulesAbs)) {
|
|
61
62
|
const layerDir = join(modulesAbs, entry, category);
|
|
62
63
|
if (existsSync(layerDir) && statSync(layerDir).isDirectory()) {
|
|
63
|
-
dirs.push(
|
|
64
|
+
dirs.push({
|
|
65
|
+
dir: layerDir,
|
|
66
|
+
namespace: resolveModularBaseNamespace(config, entry, category, ''),
|
|
67
|
+
});
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
return dirs;
|
|
67
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* FQCN a base file claims from its LOCATION: the layer root's namespace,
|
|
74
|
+
* one segment per group subdirectory, then the class name from the file
|
|
75
|
+
* name (`Base/Crm/FooRequestBase.php` under `App\...\Base` →
|
|
76
|
+
* `App\...\Base\Crm\FooRequestBase`). Derived relative to the layer
|
|
77
|
+
* root, never from the bare basename — a grouped file's group segment
|
|
78
|
+
* is part of its identity, and it is also what hand-written `use`
|
|
79
|
+
* imports name (#263).
|
|
80
|
+
*/
|
|
81
|
+
function fqcnFromLayerRelativePath(rootNamespace, relPath) {
|
|
82
|
+
const segments = relPath.split(/[\\/]/).filter((s) => s.length > 0);
|
|
83
|
+
const fileName = segments.pop();
|
|
84
|
+
if (!fileName)
|
|
85
|
+
return rootNamespace;
|
|
86
|
+
return [rootNamespace, ...segments, fileName.replace(/\.php$/i, '')]
|
|
87
|
+
.filter((s) => s.length > 0)
|
|
88
|
+
.join('\\');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Recursively visit every file under `root`. Directories are entered
|
|
92
|
+
* but never removed — the surrounding cleanup (pruneOrphanServiceFiles,
|
|
93
|
+
* walkPhpFiles) deletes files only and leaves emptied folders behind,
|
|
94
|
+
* and this prune matches that behaviour.
|
|
95
|
+
*/
|
|
96
|
+
function walkLayerFiles(root, visit) {
|
|
97
|
+
if (!existsSync(root))
|
|
98
|
+
return;
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = readdirSync(root);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const full = join(root, entry);
|
|
108
|
+
let info;
|
|
109
|
+
try {
|
|
110
|
+
info = statSync(full);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (info.isDirectory()) {
|
|
116
|
+
walkLayerFiles(full, visit);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
visit(full);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
68
123
|
function pruneDisabledLayerBasesInRoots(spec, roots, expectedAbs, referenced) {
|
|
69
124
|
let pruned = 0;
|
|
70
125
|
let kept = 0;
|
|
126
|
+
// Grouped schemas nest their bases one directory per group
|
|
127
|
+
// (`app/Omnify/Requests/OmnifyBase/Crm/FooRequestBase.php`), so the
|
|
128
|
+
// walk must recurse — a one-level `readdirSync` leaves every grouped
|
|
129
|
+
// stale base on disk (#263).
|
|
71
130
|
for (const root of roots) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
continue;
|
|
77
|
-
const fullPath = join(root, entry);
|
|
131
|
+
walkLayerFiles(root.dir, (fullPath) => {
|
|
132
|
+
const name = basename(fullPath);
|
|
133
|
+
if (!spec.matchesBaseFile(name))
|
|
134
|
+
return;
|
|
78
135
|
if (expectedAbs.has(fullPath))
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
content = readFileSync(fullPath, 'utf-8');
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
const fqcn = declaredClassName(content);
|
|
88
|
-
if (fqcn && referenced.has(fqcn)) {
|
|
136
|
+
return;
|
|
137
|
+
const fqcn = fqcnFromLayerRelativePath(root.namespace, relative(root.dir, fullPath));
|
|
138
|
+
if (referenced.has(fqcn)) {
|
|
89
139
|
kept += 1;
|
|
90
|
-
|
|
140
|
+
return;
|
|
91
141
|
}
|
|
92
142
|
try {
|
|
93
143
|
unlinkSync(fullPath);
|
|
@@ -96,7 +146,7 @@ function pruneDisabledLayerBasesInRoots(spec, roots, expectedAbs, referenced) {
|
|
|
96
146
|
catch {
|
|
97
147
|
// skip uncleanable files
|
|
98
148
|
}
|
|
99
|
-
}
|
|
149
|
+
});
|
|
100
150
|
}
|
|
101
151
|
return { pruned, kept };
|
|
102
152
|
}
|
|
@@ -115,7 +165,7 @@ export function pruneDisabledScaffoldLayerBases(configDir, config, generated, re
|
|
|
115
165
|
continue;
|
|
116
166
|
const roots = [
|
|
117
167
|
...collectModularLayerDirs(configDir, config, spec.modularCategory),
|
|
118
|
-
resolve(configDir, spec.legacyBasePath),
|
|
168
|
+
{ dir: resolve(configDir, spec.legacyBasePath), namespace: spec.layerConfig.baseNamespace },
|
|
119
169
|
];
|
|
120
170
|
const { pruned, kept } = pruneDisabledLayerBasesInRoots(spec, roots, expectedAbs, referenced);
|
|
121
171
|
if (pruned > 0 || kept > 0) {
|
|
@@ -3,7 +3,32 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase, toSnakeCase, toCamelCase, toFkColumnName } from './naming-helper.js';
|
|
5
5
|
import { toStoreRules, toUpdateRules, formatRules, resolveFkRuleType } from './type-mapper.js';
|
|
6
|
-
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup } from './types.js';
|
|
6
|
+
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup, resolveGlobalEnumNamespace } from './types.js';
|
|
7
|
+
import { enumClassName } from './enum-generator.js';
|
|
8
|
+
/**
|
|
9
|
+
* Format rules for an EnumRef property, replacing the generic `'string'` rule
|
|
10
|
+
* `toStoreRules`/`toUpdateRules` emit for EnumRef with `Rule::enum(<Class>::class)`
|
|
11
|
+
* — bare `'string'` lets any value through validation while the model cast
|
|
12
|
+
* and migration column both enforce the enum's set (#265). Duplicates just
|
|
13
|
+
* enough of `formatRules`' string/rule_unique_ignore branches, since the
|
|
14
|
+
* `Rule::enum(...)` case can't be added to type-mapper.ts's `Rule` union from
|
|
15
|
+
* here without editing a file this job does not own.
|
|
16
|
+
*/
|
|
17
|
+
function formatEnumRefRules(rules, enumFqcn) {
|
|
18
|
+
const parts = [];
|
|
19
|
+
for (const rule of rules) {
|
|
20
|
+
if (rule === 'string') {
|
|
21
|
+
parts.push(`Rule::enum(${enumFqcn}::class)`);
|
|
22
|
+
}
|
|
23
|
+
else if (typeof rule === 'string') {
|
|
24
|
+
parts.push(`'${rule}'`);
|
|
25
|
+
}
|
|
26
|
+
else if (Array.isArray(rule) && rule[0] === 'rule_unique_ignore') {
|
|
27
|
+
parts.push(`Rule::unique('${rule[1]}')->ignore($this->route('${rule[2]}'))`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return `[${parts.join(', ')}]`;
|
|
31
|
+
}
|
|
7
32
|
/** Generate Store/Update request classes for all project-owned visible object schemas. */
|
|
8
33
|
export function generateRequests(reader, config) {
|
|
9
34
|
if (!config.requests.enabled)
|
|
@@ -142,7 +167,21 @@ function generateBaseRequest(name, schema, reader, config, action) {
|
|
|
142
167
|
const rules = isUpdate
|
|
143
168
|
? toUpdateRules(prop, tableName, modelRouteParam)
|
|
144
169
|
: toStoreRules(prop, tableName);
|
|
145
|
-
|
|
170
|
+
// EnumRef: validate against the enum's actual value set via Rule::enum()
|
|
171
|
+
// instead of the bare 'string' toStoreRules/toUpdateRules emit for it —
|
|
172
|
+
// otherwise an out-of-set string passes validation, hits the DB, and only
|
|
173
|
+
// explodes later when the model cast reads it back (#265). Use the exact
|
|
174
|
+
// FQCN the model cast/doc-block already use (model-generator.ts's
|
|
175
|
+
// buildCasts), so validation and the cast never disagree. Falls back to
|
|
176
|
+
// the plain rules when the referenced enum schema doesn't exist, matching
|
|
177
|
+
// toStoreRules' own fallback for that case.
|
|
178
|
+
const enumName = type === 'EnumRef' ? prop['enum'] : undefined;
|
|
179
|
+
const enumSchema = typeof enumName === 'string' ? reader.getSchema(enumName) : undefined;
|
|
180
|
+
const globalEnumNs = type === 'EnumRef' ? resolveGlobalEnumNamespace(config, config.models.namespace) : '';
|
|
181
|
+
const formattedRules = typeof enumName === 'string' && enumSchema?.kind === 'enum' && globalEnumNs
|
|
182
|
+
? formatEnumRefRules(rules, `\\${globalEnumNs}\\${enumClassName(enumName)}`)
|
|
183
|
+
: formatRules(rules);
|
|
184
|
+
rulesLines.push(` '${snakeName}' => ${formattedRules},`);
|
|
146
185
|
attributeKeys.push(snakeName);
|
|
147
186
|
// Translatable: add locale-nested rules (Astrotomic expects {locale}.{field})
|
|
148
187
|
const isTranslatable = prop['translatable'] ?? false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnifyjp/ts",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.3",
|
|
4
4
|
"description": "TypeScript model type generator from Omnify schemas.json",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"omnify-ts": "dist/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "tsc",
|
|
12
|
+
"build": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|
|
13
13
|
"dev": "tsc --watch",
|
|
14
14
|
"generate-l12": "node dist/cli.js --config ../../examples/l12/omnify.yaml",
|
|
15
15
|
"test": "vitest run"
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-format generated PHP files via Laravel Pint. Issue #100 v5.8.20.
|
|
3
|
-
*
|
|
4
|
-
* Reporter scenario (godx-jp/godx-kintai 2026-04-26 audit): every
|
|
5
|
-
* `omnify generate --force` produces 150+ PHP files that fail Laravel
|
|
6
|
-
* Pint's standard preset (`vendor/bin/pint --test`):
|
|
7
|
-
*
|
|
8
|
-
* - ordered_imports — `use` statements not alphabetised
|
|
9
|
-
* - fully_qualified_strict_types — exception types referenced via FQN
|
|
10
|
-
* - class_attributes_separation — missing blank lines between members
|
|
11
|
-
* - function_declaration — function-keyword spacing
|
|
12
|
-
* - unary_operator_spaces — `! \$x` vs `!\$x`
|
|
13
|
-
* - no_unused_imports — codegen leaves the occasional dangling import
|
|
14
|
-
* - single_line_empty_body — empty class bodies on multiple lines
|
|
15
|
-
* - self_static_accessor — `self::` vs `static::` on enums
|
|
16
|
-
* - blank_line_before_statement — enum `case` separators
|
|
17
|
-
*
|
|
18
|
-
* Auto-detect: when `<rootPath>/vendor/bin/pint` exists, run it on the
|
|
19
|
-
* files we just wrote. Same Pint preset the project's CI already uses,
|
|
20
|
-
* so no risk of formatter drift between codegen and CI checks.
|
|
21
|
-
*
|
|
22
|
-
* Opt-out: future config field; for now `chmod -x vendor/bin/pint` or
|
|
23
|
-
* deleting it disables the auto-run.
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* Resolve the `vendor/bin/pint` path for the project. Walks up from
|
|
27
|
-
* the first written file until we find a sibling `vendor/bin/pint`,
|
|
28
|
-
* stopping at filesystem root or after 12 levels (defensive against
|
|
29
|
-
* pathological inputs). Returns the absolute path or null.
|
|
30
|
-
*/
|
|
31
|
-
export declare function findPintBinary(anyWrittenPhpPath: string): string | null;
|
|
32
|
-
export interface PintFormatResult {
|
|
33
|
-
ran: boolean;
|
|
34
|
-
pintPath: string | null;
|
|
35
|
-
/** Files passed to pint. */
|
|
36
|
-
formattedCount: number;
|
|
37
|
-
/** Pint's exit status if it ran. 0 = success. */
|
|
38
|
-
exitStatus: number | null;
|
|
39
|
-
/** Captured stderr when pint failed (status != 0). */
|
|
40
|
-
errorMessage?: string;
|
|
41
|
-
/**
|
|
42
|
-
* Why the run did not happen, when `ran` is false. The caller needs the three apart: only
|
|
43
|
-
* `pint-not-found` is worth telling a human about, and it is the one that silently changes what
|
|
44
|
-
* gets written to disk (godx-jp/godx-corebooks#108 — 927 files, reproduced).
|
|
45
|
-
*/
|
|
46
|
-
skipped?: 'no-php-files' | 'opted-out' | 'pint-not-found';
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Run Laravel Pint on the given PHP files (absolute paths). No-op if
|
|
50
|
-
* Pint isn't installed in the project. Pint is invoked with `--quiet`
|
|
51
|
-
* so it stays out of the way unless something breaks.
|
|
52
|
-
*
|
|
53
|
-
* Pint takes a list of files (or directories) and formats them in
|
|
54
|
-
* place. It uses the project's `pint.json` config if present, else the
|
|
55
|
-
* Laravel preset.
|
|
56
|
-
*/
|
|
57
|
-
export declare function runPintFormat(writtenPhpPaths: string[]): PintFormatResult;
|
package/dist/php/pint-format.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-format generated PHP files via Laravel Pint. Issue #100 v5.8.20.
|
|
3
|
-
*
|
|
4
|
-
* Reporter scenario (godx-jp/godx-kintai 2026-04-26 audit): every
|
|
5
|
-
* `omnify generate --force` produces 150+ PHP files that fail Laravel
|
|
6
|
-
* Pint's standard preset (`vendor/bin/pint --test`):
|
|
7
|
-
*
|
|
8
|
-
* - ordered_imports — `use` statements not alphabetised
|
|
9
|
-
* - fully_qualified_strict_types — exception types referenced via FQN
|
|
10
|
-
* - class_attributes_separation — missing blank lines between members
|
|
11
|
-
* - function_declaration — function-keyword spacing
|
|
12
|
-
* - unary_operator_spaces — `! \$x` vs `!\$x`
|
|
13
|
-
* - no_unused_imports — codegen leaves the occasional dangling import
|
|
14
|
-
* - single_line_empty_body — empty class bodies on multiple lines
|
|
15
|
-
* - self_static_accessor — `self::` vs `static::` on enums
|
|
16
|
-
* - blank_line_before_statement — enum `case` separators
|
|
17
|
-
*
|
|
18
|
-
* Auto-detect: when `<rootPath>/vendor/bin/pint` exists, run it on the
|
|
19
|
-
* files we just wrote. Same Pint preset the project's CI already uses,
|
|
20
|
-
* so no risk of formatter drift between codegen and CI checks.
|
|
21
|
-
*
|
|
22
|
-
* Opt-out: future config field; for now `chmod -x vendor/bin/pint` or
|
|
23
|
-
* deleting it disables the auto-run.
|
|
24
|
-
*/
|
|
25
|
-
import { existsSync, statSync } from 'node:fs';
|
|
26
|
-
import { spawnSync } from 'node:child_process';
|
|
27
|
-
import { dirname, join } from 'node:path';
|
|
28
|
-
/**
|
|
29
|
-
* Resolve the `vendor/bin/pint` path for the project. Walks up from
|
|
30
|
-
* the first written file until we find a sibling `vendor/bin/pint`,
|
|
31
|
-
* stopping at filesystem root or after 12 levels (defensive against
|
|
32
|
-
* pathological inputs). Returns the absolute path or null.
|
|
33
|
-
*/
|
|
34
|
-
export function findPintBinary(anyWrittenPhpPath) {
|
|
35
|
-
let cur = dirname(anyWrittenPhpPath);
|
|
36
|
-
for (let i = 0; i < 12; i++) {
|
|
37
|
-
const candidate = join(cur, 'vendor', 'bin', 'pint');
|
|
38
|
-
if (existsSync(candidate)) {
|
|
39
|
-
try {
|
|
40
|
-
const st = statSync(candidate);
|
|
41
|
-
if (st.isFile())
|
|
42
|
-
return candidate;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
// ignore stat errors and keep climbing
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const parent = dirname(cur);
|
|
49
|
-
if (parent === cur)
|
|
50
|
-
break;
|
|
51
|
-
cur = parent;
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Run Laravel Pint on the given PHP files (absolute paths). No-op if
|
|
57
|
-
* Pint isn't installed in the project. Pint is invoked with `--quiet`
|
|
58
|
-
* so it stays out of the way unless something breaks.
|
|
59
|
-
*
|
|
60
|
-
* Pint takes a list of files (or directories) and formats them in
|
|
61
|
-
* place. It uses the project's `pint.json` config if present, else the
|
|
62
|
-
* Laravel preset.
|
|
63
|
-
*/
|
|
64
|
-
export function runPintFormat(writtenPhpPaths) {
|
|
65
|
-
if (writtenPhpPaths.length === 0) {
|
|
66
|
-
return { ran: false, pintPath: null, formattedCount: 0, exitStatus: null, skipped: 'no-php-files' };
|
|
67
|
-
}
|
|
68
|
-
// Opt-out (#174). The auto-run hides what the generators actually emit: a
|
|
69
|
-
// template that Pint rewrites looks fine here and produces churn in every
|
|
70
|
-
// project where Pint is absent or does not run. `OMNIFY_NO_PINT=1` is how
|
|
71
|
-
// the repo's own fixture check measures the raw output.
|
|
72
|
-
if (process.env.OMNIFY_NO_PINT === '1') {
|
|
73
|
-
return { ran: false, pintPath: null, formattedCount: 0, exitStatus: null, skipped: 'opted-out' };
|
|
74
|
-
}
|
|
75
|
-
// Resolution walks up from a written file. Trying only the first one makes
|
|
76
|
-
// it depend on emission order — a single file outside the Laravel root and
|
|
77
|
-
// the whole run goes unformatted, silently. Try each until one resolves.
|
|
78
|
-
const pintPath = writtenPhpPaths.reduce((found, path) => found ?? findPintBinary(path), null);
|
|
79
|
-
if (!pintPath) {
|
|
80
|
-
return { ran: false, pintPath: null, formattedCount: 0, exitStatus: null, skipped: 'pint-not-found' };
|
|
81
|
-
}
|
|
82
|
-
// Pint's working dir should be the Laravel project root (where
|
|
83
|
-
// composer.json + pint.json live). The pint binary is at
|
|
84
|
-
// `<projectRoot>/vendor/bin/pint`, so projectRoot is two parents up.
|
|
85
|
-
const projectRoot = dirname(dirname(dirname(pintPath)));
|
|
86
|
-
// Pass each file as its own arg. Pint accepts multiple file/dir
|
|
87
|
-
// arguments; capping the batch is unnecessary up to a few hundred
|
|
88
|
-
// files (well under the OS arg limit).
|
|
89
|
-
const result = spawnSync(pintPath, ['--quiet', ...writtenPhpPaths], {
|
|
90
|
-
cwd: projectRoot,
|
|
91
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
92
|
-
encoding: 'utf-8',
|
|
93
|
-
});
|
|
94
|
-
const exitStatus = result.status ?? null;
|
|
95
|
-
return {
|
|
96
|
-
ran: true,
|
|
97
|
-
pintPath,
|
|
98
|
-
formattedCount: writtenPhpPaths.length,
|
|
99
|
-
exitStatus,
|
|
100
|
-
errorMessage: exitStatus !== 0 ? (result.stderr || result.stdout || undefined) : undefined,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
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;
|
package/dist/prettier-format.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
}
|