@omnifyjp/ts 7.0.0 → 7.0.2
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 +6 -3
- package/dist/php/factory-generator.js +2 -2
- package/dist/php/faker-mapper.js +3 -3
- package/dist/php/file-cleanup-generator.js +6 -2
- package/dist/php/orphan-cleanup.d.ts +16 -5
- package/dist/php/orphan-cleanup.js +104 -117
- 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
|
@@ -30,7 +30,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
30
30
|
import { generateTypeScript } from './generator.js';
|
|
31
31
|
import { generatePhp, derivePhpConfig } from './php/index.js';
|
|
32
32
|
import { evaluateShadowingConflicts } from './php/shadowing-conflicts.js';
|
|
33
|
-
import { hasEditableSubclassForBase, pruneDisabledScaffoldLayerBases, pruneOrphanServiceFilesIfEnabled } from './php/orphan-cleanup.js';
|
|
33
|
+
import { collectReferencedClasses, hasEditableSubclassForBase, pruneDisabledScaffoldLayerBases, pruneOrphanServiceFilesIfEnabled } from './php/orphan-cleanup.js';
|
|
34
34
|
import { findSiblingWithSameBasename, findAppRootAncestor } from './php/sibling-skip.js';
|
|
35
35
|
import { resolveInput, sniffInputKind } from './input-resolver.js';
|
|
36
36
|
function resolveFromConfig(configPath) {
|
|
@@ -228,9 +228,12 @@ program
|
|
|
228
228
|
console.log(`\n[Laravel] Generating PHP files`);
|
|
229
229
|
// #255: a disabled scaffold layer still regenerates the bases that
|
|
230
230
|
// hand-written subclasses extend, so they track the schema.
|
|
231
|
+
// Read before anything is written, so the answer reflects the project's
|
|
232
|
+
// own code, not this run's output.
|
|
231
233
|
const phpConfigForGen = derivePhpConfig(laravelOverrides);
|
|
234
|
+
const referencedClasses = collectReferencedClasses(configDir, phpConfigForGen);
|
|
232
235
|
const phpFiles = generatePhp(input, laravelOverrides, {
|
|
233
|
-
keepDisabledBase: (file) => hasEditableSubclassForBase(
|
|
236
|
+
keepDisabledBase: (file) => hasEditableSubclassForBase(phpConfigForGen, referencedClasses, file),
|
|
234
237
|
});
|
|
235
238
|
let phpCreated = 0;
|
|
236
239
|
let phpOverwritten = 0;
|
|
@@ -339,7 +342,7 @@ program
|
|
|
339
342
|
// pollutes Composer autoload. Prune base files automatically; warn
|
|
340
343
|
// about editable `*Service.php` (may contain user code).
|
|
341
344
|
const phpConfig = derivePhpConfig(laravelOverrides);
|
|
342
|
-
for (const summary of pruneDisabledScaffoldLayerBases(configDir, phpConfig, phpFiles)) {
|
|
345
|
+
for (const summary of pruneDisabledScaffoldLayerBases(configDir, phpConfig, phpFiles, referencedClasses)) {
|
|
343
346
|
if (summary.prunedCount > 0) {
|
|
344
347
|
console.log(` pruned ${summary.prunedCount} stale ${summary.label}(s) — layer disabled`);
|
|
345
348
|
}
|
|
@@ -178,7 +178,7 @@ class FileFactory extends Factory
|
|
|
178
178
|
public function definition(): array
|
|
179
179
|
{
|
|
180
180
|
$extension = fake()->randomElement(['jpg', 'png', 'pdf', 'docx']);
|
|
181
|
-
$originalName = fake()->word()
|
|
181
|
+
$originalName = fake()->word().'.'.$extension;
|
|
182
182
|
$mimeTypes = [
|
|
183
183
|
'jpg' => 'image/jpeg',
|
|
184
184
|
'png' => 'image/png',
|
|
@@ -189,7 +189,7 @@ class FileFactory extends Factory
|
|
|
189
189
|
return [
|
|
190
190
|
'collection' => 'default',
|
|
191
191
|
'disk' => 'public',
|
|
192
|
-
'path' => 'files/'
|
|
192
|
+
'path' => 'files/'.now()->format('Y/m').'/'.Str::uuid().'.'.$extension,
|
|
193
193
|
'original_name' => $originalName,
|
|
194
194
|
'mime_type' => $mimeTypes[$extension] ?? 'application/octet-stream',
|
|
195
195
|
'size' => fake()->numberBetween(1024, 10485760),
|
package/dist/php/faker-mapper.js
CHANGED
|
@@ -57,7 +57,7 @@ export function compoundFaker(propName, expansion, sourceType) {
|
|
|
57
57
|
`'${snakeName}_branch_code' => ${jaFaker}->regexify('[0-9]{3}'),`,
|
|
58
58
|
`'${snakeName}_account_type' => ${jaFaker}->randomElement(['1', '2', '4']),`,
|
|
59
59
|
`'${snakeName}_account_number' => ${jaFaker}->regexify('[0-9]{7}'),`,
|
|
60
|
-
`'${snakeName}_account_holder' => ${jaFaker}->lastKanaName()
|
|
60
|
+
`'${snakeName}_account_holder' => ${jaFaker}->lastKanaName().' '.${jaFaker}->firstKanaName(),`,
|
|
61
61
|
];
|
|
62
62
|
default:
|
|
63
63
|
return genericCompoundFaker(snakeName, expansion);
|
|
@@ -96,7 +96,7 @@ function stringFaker(propName, snakeName, property) {
|
|
|
96
96
|
if (lower.includes('url') || lower.includes('website'))
|
|
97
97
|
return `'${snakeName}' => fake()->url(),`;
|
|
98
98
|
if (lower.includes('path') || lower.includes('file'))
|
|
99
|
-
return `'${snakeName}' => 'uploads/'
|
|
99
|
+
return `'${snakeName}' => 'uploads/'.fake()->uuid().'.jpg',`;
|
|
100
100
|
if (propName === 'name' || propName === 'title')
|
|
101
101
|
return `'${snakeName}' => fake()->sentence(3),`;
|
|
102
102
|
if (lower.includes('name'))
|
|
@@ -183,7 +183,7 @@ function japaneseAddressFaker(snakeName, expansion) {
|
|
|
183
183
|
else {
|
|
184
184
|
lines.push(`'${snakeName}_prefecture' => fake()->randomElement(['hokkaido', 'aomori', 'iwate', 'miyagi', 'akita', 'yamagata', 'fukushima', 'ibaraki', 'tochigi', 'gunma', 'saitama', 'chiba', 'tokyo', 'kanagawa', 'niigata', 'toyama', 'ishikawa', 'fukui', 'yamanashi', 'nagano', 'gifu', 'shizuoka', 'aichi', 'mie', 'shiga', 'kyoto', 'osaka', 'hyogo', 'nara', 'wakayama', 'tottori', 'shimane', 'okayama', 'hiroshima', 'yamaguchi', 'tokushima', 'kagawa', 'ehime', 'kochi', 'fukuoka', 'saga', 'nagasaki', 'kumamoto', 'oita', 'miyazaki', 'kagoshima', 'okinawa']),`);
|
|
185
185
|
}
|
|
186
|
-
lines.push(`'${snakeName}_address1' => ${jaFaker}->city()
|
|
186
|
+
lines.push(`'${snakeName}_address1' => ${jaFaker}->city().${jaFaker}->ward(),`);
|
|
187
187
|
lines.push(`'${snakeName}_address2' => ${jaFaker}->streetAddress(),`);
|
|
188
188
|
lines.push(`'${snakeName}_address3' => ${jaFaker}->optional()->secondaryAddress(),`);
|
|
189
189
|
return lines;
|
|
@@ -68,12 +68,16 @@ class CleanupFiles extends Command
|
|
|
68
68
|
|
|
69
69
|
if ($phase === null || $phase === '1') {
|
|
70
70
|
$r = $this->phase1SoftDelete($dryRun);
|
|
71
|
-
if ($r !== self::SUCCESS)
|
|
71
|
+
if ($r !== self::SUCCESS) {
|
|
72
|
+
$result = $r;
|
|
73
|
+
}
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
if ($phase === null || $phase === '2') {
|
|
75
77
|
$r = $this->phase2Purge($dryRun);
|
|
76
|
-
if ($r !== self::SUCCESS)
|
|
78
|
+
if ($r !== self::SUCCESS) {
|
|
79
|
+
$result = $r;
|
|
80
|
+
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
return $result;
|
|
@@ -45,13 +45,24 @@ export interface DisabledLayerPruneSummary {
|
|
|
45
45
|
* When a scaffold layer is disabled (issue #252), remove stale auto-generated
|
|
46
46
|
* BASE files that have no matching user-editable subclass on disk.
|
|
47
47
|
*/
|
|
48
|
-
export declare function pruneDisabledScaffoldLayerBases(configDir: string, config: PhpConfig, generated: readonly GeneratedFile[]): DisabledLayerPruneSummary[];
|
|
48
|
+
export declare function pruneDisabledScaffoldLayerBases(configDir: string, config: PhpConfig, generated: readonly GeneratedFile[], referenced?: ReadonlySet<string>): DisabledLayerPruneSummary[];
|
|
49
49
|
/**
|
|
50
|
-
* Issue #255:
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* Issue #255: is this generated base of a disabled scaffold layer still used by
|
|
51
|
+
* hand-written code? Same question the prune above asks before deleting a base,
|
|
52
|
+
* so a base is regenerated exactly when it would otherwise be kept.
|
|
53
53
|
*/
|
|
54
|
-
export declare function hasEditableSubclassForBase(
|
|
54
|
+
export declare function hasEditableSubclassForBase(config: PhpConfig, referenced: ReadonlySet<string>, file: GeneratedFile): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Every class hand-written PHP under the Laravel root names in a `use` import
|
|
57
|
+
* or a fully-qualified `extends` (#260).
|
|
58
|
+
*
|
|
59
|
+
* "Is this base still used?" used to be answered by looking for a file named
|
|
60
|
+
* after it in the layer's configured directories. Real projects keep their
|
|
61
|
+
* subclasses wherever they like — `app/Policies/`, `app/Services/Promotion/` —
|
|
62
|
+
* so a used base looked unused and was deleted under running code. The import
|
|
63
|
+
* is what PHP resolves, so the import is what we check.
|
|
64
|
+
*/
|
|
65
|
+
export declare function collectReferencedClasses(configDir: string, config: PhpConfig): Set<string>;
|
|
55
66
|
/**
|
|
56
67
|
* Delete orphan `*ServiceBase.php` files; flag orphan editable services
|
|
57
68
|
* for warning. Pure-ish: takes the generated file list (= source of
|
|
@@ -19,7 +19,7 @@
|
|
|
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
|
|
22
|
+
import { join, resolve, basename } from 'node:path';
|
|
23
23
|
/** Matches the header emitted by `generateEditableService` in service-generator.ts */
|
|
24
24
|
export const OMNIFY_EDITABLE_SERVICE_MARKER = 'SAFE TO EDIT - This file is never overwritten by Omnify.';
|
|
25
25
|
const EXTENDS_SERVICE_BASE_RE = /extends\s+[\w\\]+ServiceBase\b/;
|
|
@@ -65,80 +65,7 @@ function collectModularLayerDirs(configDir, config, category) {
|
|
|
65
65
|
}
|
|
66
66
|
return dirs;
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
function findEditableSubclassFile(roots, fileName, excludeAbsPath) {
|
|
70
|
-
for (const root of roots) {
|
|
71
|
-
const found = findFileAbsolutePath(root, fileName);
|
|
72
|
-
if (found && found !== excludeAbsPath)
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
function findFileAbsolutePath(dir, fileName) {
|
|
78
|
-
if (!existsSync(dir))
|
|
79
|
-
return null;
|
|
80
|
-
let info;
|
|
81
|
-
try {
|
|
82
|
-
info = statSync(dir);
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
if (!info.isDirectory()) {
|
|
88
|
-
return basename(dir) === fileName ? resolve(dir) : null;
|
|
89
|
-
}
|
|
90
|
-
for (const entry of readdirSync(dir)) {
|
|
91
|
-
if (BASE_SUBDIRS_SKIP.has(entry))
|
|
92
|
-
continue;
|
|
93
|
-
const full = join(dir, entry);
|
|
94
|
-
try {
|
|
95
|
-
info = statSync(full);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (info.isDirectory()) {
|
|
101
|
-
const nested = findFileAbsolutePath(full, fileName);
|
|
102
|
-
if (nested)
|
|
103
|
-
return nested;
|
|
104
|
-
}
|
|
105
|
-
else if (entry === fileName) {
|
|
106
|
-
return resolve(full);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
function collectEditableSearchRoots(configDir, config, layer, modularCategory, baseAbsPath) {
|
|
112
|
-
const roots = [];
|
|
113
|
-
const add = (p) => {
|
|
114
|
-
const abs = resolve(configDir, p);
|
|
115
|
-
if (!roots.includes(abs))
|
|
116
|
-
roots.push(abs);
|
|
117
|
-
};
|
|
118
|
-
add(layer.userEditablePath);
|
|
119
|
-
if (layer.path !== layer.userEditablePath) {
|
|
120
|
-
add(layer.path);
|
|
121
|
-
}
|
|
122
|
-
if (config.structure === 'modular') {
|
|
123
|
-
const modulesAbs = resolve(configDir, config.modules.path);
|
|
124
|
-
const rel = relative(modulesAbs, baseAbsPath);
|
|
125
|
-
if (rel && !rel.startsWith('..') && !rel.startsWith(`..${sep}`)) {
|
|
126
|
-
const segments = rel.split(/[/\\]/);
|
|
127
|
-
if (segments.length >= 2 && segments[1] === modularCategory) {
|
|
128
|
-
const schemaFolder = segments[0];
|
|
129
|
-
add(join(layer.userEditablePath, schemaFolder));
|
|
130
|
-
add(join(layer.path, schemaFolder));
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return roots;
|
|
135
|
-
}
|
|
136
|
-
function baseToEditableFileName(baseFileName, baseSuffix, editableSuffix) {
|
|
137
|
-
if (!baseFileName.endsWith(baseSuffix))
|
|
138
|
-
return null;
|
|
139
|
-
return `${baseFileName.slice(0, -baseSuffix.length)}${editableSuffix}`;
|
|
140
|
-
}
|
|
141
|
-
function pruneDisabledLayerBasesInRoots(configDir, config, spec, roots, expectedAbs) {
|
|
68
|
+
function pruneDisabledLayerBasesInRoots(spec, roots, expectedAbs, referenced) {
|
|
142
69
|
let pruned = 0;
|
|
143
70
|
let kept = 0;
|
|
144
71
|
for (const root of roots) {
|
|
@@ -150,13 +77,17 @@ function pruneDisabledLayerBasesInRoots(configDir, config, spec, roots, expected
|
|
|
150
77
|
const fullPath = join(root, entry);
|
|
151
78
|
if (expectedAbs.has(fullPath))
|
|
152
79
|
continue;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
80
|
+
let content = '';
|
|
81
|
+
try {
|
|
82
|
+
content = readFileSync(fullPath, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const fqcn = declaredClassName(content);
|
|
88
|
+
if (fqcn && referenced.has(fqcn)) {
|
|
89
|
+
kept += 1;
|
|
90
|
+
continue;
|
|
160
91
|
}
|
|
161
92
|
try {
|
|
162
93
|
unlinkSync(fullPath);
|
|
@@ -173,7 +104,7 @@ function pruneDisabledLayerBasesInRoots(configDir, config, spec, roots, expected
|
|
|
173
104
|
* When a scaffold layer is disabled (issue #252), remove stale auto-generated
|
|
174
105
|
* BASE files that have no matching user-editable subclass on disk.
|
|
175
106
|
*/
|
|
176
|
-
export function pruneDisabledScaffoldLayerBases(configDir, config, generated) {
|
|
107
|
+
export function pruneDisabledScaffoldLayerBases(configDir, config, generated, referenced = collectReferencedClasses(configDir, config)) {
|
|
177
108
|
const expectedAbs = new Set();
|
|
178
109
|
for (const f of generated) {
|
|
179
110
|
expectedAbs.add(resolve(configDir, f.path));
|
|
@@ -186,7 +117,7 @@ export function pruneDisabledScaffoldLayerBases(configDir, config, generated) {
|
|
|
186
117
|
...collectModularLayerDirs(configDir, config, spec.modularCategory),
|
|
187
118
|
resolve(configDir, spec.legacyBasePath),
|
|
188
119
|
];
|
|
189
|
-
const { pruned, kept } = pruneDisabledLayerBasesInRoots(
|
|
120
|
+
const { pruned, kept } = pruneDisabledLayerBasesInRoots(spec, roots, expectedAbs, referenced);
|
|
190
121
|
if (pruned > 0 || kept > 0) {
|
|
191
122
|
summaries.push({
|
|
192
123
|
layer: spec.layer,
|
|
@@ -199,22 +130,94 @@ export function pruneDisabledScaffoldLayerBases(configDir, config, generated) {
|
|
|
199
130
|
return summaries;
|
|
200
131
|
}
|
|
201
132
|
/**
|
|
202
|
-
* Issue #255:
|
|
203
|
-
*
|
|
204
|
-
*
|
|
133
|
+
* Issue #255: is this generated base of a disabled scaffold layer still used by
|
|
134
|
+
* hand-written code? Same question the prune above asks before deleting a base,
|
|
135
|
+
* so a base is regenerated exactly when it would otherwise be kept.
|
|
205
136
|
*/
|
|
206
|
-
export function hasEditableSubclassForBase(
|
|
137
|
+
export function hasEditableSubclassForBase(config, referenced, file) {
|
|
207
138
|
const name = basename(file.path);
|
|
208
|
-
|
|
209
|
-
|
|
139
|
+
const isDisabledScaffoldBase = scaffoldLayerSpecs(config).some((spec) => !spec.enabled && spec.matchesBaseFile(name));
|
|
140
|
+
if (!isDisabledScaffoldBase)
|
|
141
|
+
return false;
|
|
142
|
+
const fqcn = declaredClassName(file.content);
|
|
143
|
+
return fqcn !== null && referenced.has(fqcn);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Every class hand-written PHP under the Laravel root names in a `use` import
|
|
147
|
+
* or a fully-qualified `extends` (#260).
|
|
148
|
+
*
|
|
149
|
+
* "Is this base still used?" used to be answered by looking for a file named
|
|
150
|
+
* after it in the layer's configured directories. Real projects keep their
|
|
151
|
+
* subclasses wherever they like — `app/Policies/`, `app/Services/Promotion/` —
|
|
152
|
+
* so a used base looked unused and was deleted under running code. The import
|
|
153
|
+
* is what PHP resolves, so the import is what we check.
|
|
154
|
+
*/
|
|
155
|
+
export function collectReferencedClasses(configDir, config) {
|
|
156
|
+
// Paths resolve against configDir, which for a workspace run is a cache
|
|
157
|
+
// directory; the Laravel app itself sits at rootPath.
|
|
158
|
+
const laravelRoot = resolve(configDir, config.rootPath);
|
|
159
|
+
const referenced = new Set();
|
|
160
|
+
for (const dir of REFERENCE_SCAN_DIRS) {
|
|
161
|
+
walkPhpFiles(resolve(laravelRoot, dir), (content) => {
|
|
162
|
+
if (GENERATED_MARKER.test(content))
|
|
163
|
+
return;
|
|
164
|
+
for (const m of content.matchAll(/^use\s+\\?([A-Za-z_][\w\\]*)(?:\s+as\s+\w+)?\s*;/gm)) {
|
|
165
|
+
referenced.add(m[1]);
|
|
166
|
+
}
|
|
167
|
+
for (const m of content.matchAll(/\bextends\s+\\([A-Za-z_][\w\\]*)/g)) {
|
|
168
|
+
referenced.add(m[1]);
|
|
169
|
+
}
|
|
170
|
+
// An unqualified parent with no import resolves in the file's own namespace.
|
|
171
|
+
const ns = content.match(/^namespace\s+([^;\s]+)\s*;/m)?.[1];
|
|
172
|
+
for (const m of content.matchAll(/\bextends\s+([A-Za-z_]\w*)\b/g)) {
|
|
173
|
+
referenced.add(ns ? `${ns}\\${m[1]}` : m[1]);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return referenced;
|
|
178
|
+
}
|
|
179
|
+
const REFERENCE_SCAN_DIRS = ['app', 'routes', 'tests', 'database', 'config'];
|
|
180
|
+
const GENERATED_MARKER = /auto-generated by Omnify/i;
|
|
181
|
+
const SKIP_DIRS = new Set(['vendor', 'node_modules', 'storage', '.git']);
|
|
182
|
+
function walkPhpFiles(dir, visit) {
|
|
183
|
+
let entries;
|
|
184
|
+
try {
|
|
185
|
+
entries = readdirSync(dir);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
if (SKIP_DIRS.has(entry))
|
|
210
192
|
continue;
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
193
|
+
const full = join(dir, entry);
|
|
194
|
+
let info;
|
|
195
|
+
try {
|
|
196
|
+
info = statSync(full);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (info.isDirectory()) {
|
|
202
|
+
walkPhpFiles(full, visit);
|
|
203
|
+
}
|
|
204
|
+
else if (entry.endsWith('.php')) {
|
|
205
|
+
try {
|
|
206
|
+
visit(readFileSync(full, 'utf-8'));
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// unreadable file: nothing to learn from it
|
|
210
|
+
}
|
|
211
|
+
}
|
|
216
212
|
}
|
|
217
|
-
|
|
213
|
+
}
|
|
214
|
+
/** `Namespace\Class` a PHP file declares, or null. */
|
|
215
|
+
function declaredClassName(content) {
|
|
216
|
+
const ns = content.match(/^namespace\s+([^;\s]+)\s*;/m);
|
|
217
|
+
const cls = content.match(/^(?:(?:abstract|final|readonly)\s+)*(?:class|interface|trait|enum)\s+(\w+)/m);
|
|
218
|
+
if (!cls)
|
|
219
|
+
return null;
|
|
220
|
+
return ns ? `${ns[1]}\\${cls[1]}` : cls[1];
|
|
218
221
|
}
|
|
219
222
|
const SCAFFOLD_LAYER_BASE_SUFFIX = {
|
|
220
223
|
request: 'RequestBase.php',
|
|
@@ -223,32 +226,17 @@ const SCAFFOLD_LAYER_BASE_SUFFIX = {
|
|
|
223
226
|
controller: 'ControllerBase.php',
|
|
224
227
|
service: 'ServiceBase.php',
|
|
225
228
|
};
|
|
226
|
-
const SCAFFOLD_LAYER_EDITABLE_SUFFIX = {
|
|
227
|
-
request: 'Request.php',
|
|
228
|
-
resource: 'Resource.php',
|
|
229
|
-
policy: 'Policy.php',
|
|
230
|
-
controller: 'Controller.php',
|
|
231
|
-
service: 'Service.php',
|
|
232
|
-
};
|
|
233
229
|
/**
|
|
234
230
|
* Legacy `flatBase` puts regenerated bases directly at `layer.basePath` with
|
|
235
231
|
* the same filename as the user-editable stub (different directory). That
|
|
236
232
|
* directory is generated-only: resolveBaseClass() always targets basePath and
|
|
237
233
|
* only per-schema base classes are written there (issue #96).
|
|
238
234
|
*/
|
|
239
|
-
function disabledLayerFileMatchers(config, layerConfig, baseSuffix
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (config.structure === 'modular') {
|
|
243
|
-
return { matchesBaseFile: modularMatches, toEditableFileName: modularToEditable };
|
|
244
|
-
}
|
|
245
|
-
if (layerConfig.flatBase) {
|
|
246
|
-
return {
|
|
247
|
-
matchesBaseFile: (name) => name.endsWith('.php'),
|
|
248
|
-
toEditableFileName: (name) => (name.endsWith('.php') ? name : null),
|
|
249
|
-
};
|
|
235
|
+
function disabledLayerFileMatchers(config, layerConfig, baseSuffix) {
|
|
236
|
+
if (config.structure !== 'modular' && layerConfig.flatBase) {
|
|
237
|
+
return { matchesBaseFile: (name) => name.endsWith('.php') };
|
|
250
238
|
}
|
|
251
|
-
return { matchesBaseFile:
|
|
239
|
+
return { matchesBaseFile: (name) => name.endsWith(baseSuffix) };
|
|
252
240
|
}
|
|
253
241
|
function scaffoldLayerSpecs(config) {
|
|
254
242
|
const layers = [
|
|
@@ -295,8 +283,7 @@ function scaffoldLayerSpecs(config) {
|
|
|
295
283
|
];
|
|
296
284
|
return layers.map((entry) => {
|
|
297
285
|
const baseSuffix = SCAFFOLD_LAYER_BASE_SUFFIX[entry.layer];
|
|
298
|
-
const
|
|
299
|
-
const matchers = disabledLayerFileMatchers(config, entry.layerConfig, baseSuffix, editableSuffix);
|
|
286
|
+
const matchers = disabledLayerFileMatchers(config, entry.layerConfig, baseSuffix);
|
|
300
287
|
return { ...entry, ...matchers };
|
|
301
288
|
});
|
|
302
289
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnifyjp/ts",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.2",
|
|
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
|
-
}
|