@omnifyjp/ts 6.8.0 → 7.0.0
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 -57
- package/dist/php/controller-generator.js +11 -1
- package/dist/php/orphan-cleanup.js +55 -23
- package/dist/php/relation-builder.js +2 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -32,8 +32,6 @@ import { generatePhp, derivePhpConfig } from './php/index.js';
|
|
|
32
32
|
import { evaluateShadowingConflicts } from './php/shadowing-conflicts.js';
|
|
33
33
|
import { hasEditableSubclassForBase, pruneDisabledScaffoldLayerBases, pruneOrphanServiceFilesIfEnabled } from './php/orphan-cleanup.js';
|
|
34
34
|
import { findSiblingWithSameBasename, findAppRootAncestor } from './php/sibling-skip.js';
|
|
35
|
-
import { runPintFormat } from './php/pint-format.js';
|
|
36
|
-
import { runPrettierFormat } from './prettier-format.js';
|
|
37
35
|
import { resolveInput, sniffInputKind } from './input-resolver.js';
|
|
38
36
|
function resolveFromConfig(configPath) {
|
|
39
37
|
const raw = readFileSync(configPath, 'utf-8');
|
|
@@ -195,7 +193,6 @@ program
|
|
|
195
193
|
let tsCreated = 0;
|
|
196
194
|
let tsOverwritten = 0;
|
|
197
195
|
let tsSkipped = 0;
|
|
198
|
-
const tsWrittenPaths = [];
|
|
199
196
|
for (const file of files) {
|
|
200
197
|
let filePath;
|
|
201
198
|
if (file.category === 'enum' || file.category === 'plugin-enum') {
|
|
@@ -210,7 +207,6 @@ program
|
|
|
210
207
|
continue;
|
|
211
208
|
}
|
|
212
209
|
writeFileSync(filePath, file.content, 'utf-8');
|
|
213
|
-
tsWrittenPaths.push(filePath);
|
|
214
210
|
if (file.overwrite || !existsSync(filePath)) {
|
|
215
211
|
tsOverwritten++;
|
|
216
212
|
}
|
|
@@ -223,22 +219,6 @@ program
|
|
|
223
219
|
console.log(` ${tsCreated} files created (user-editable)`);
|
|
224
220
|
if (tsSkipped > 0)
|
|
225
221
|
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
|
-
}
|
|
242
222
|
}
|
|
243
223
|
else {
|
|
244
224
|
console.log(`\n[TypeScript] Skipped (disabled)`);
|
|
@@ -259,7 +239,6 @@ program
|
|
|
259
239
|
let phpShadowedOutOfSync = 0;
|
|
260
240
|
let phpShadowedAtRisk = 0;
|
|
261
241
|
const skippedSiblingExamples = [];
|
|
262
|
-
const writtenPaths = [];
|
|
263
242
|
for (const file of phpFiles) {
|
|
264
243
|
const filePath = resolve(configDir, file.path);
|
|
265
244
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
@@ -323,7 +302,6 @@ program
|
|
|
323
302
|
}
|
|
324
303
|
}
|
|
325
304
|
writeFileSync(filePath, file.content, 'utf-8');
|
|
326
|
-
writtenPaths.push(filePath);
|
|
327
305
|
if (file.overwrite || !existsSync(filePath)) {
|
|
328
306
|
phpOverwritten++;
|
|
329
307
|
}
|
|
@@ -355,41 +333,6 @@ program
|
|
|
355
333
|
console.log(` ... and ${phpSkippedSibling - skippedSiblingExamples.length} more`);
|
|
356
334
|
}
|
|
357
335
|
}
|
|
358
|
-
// Issue #100 v5.8.20: auto-format every written PHP file via
|
|
359
|
-
// Laravel Pint when it's installed in the project. Same preset
|
|
360
|
-
// the project's CI already uses, so codegen output stops failing
|
|
361
|
-
// `pint --test` and devs no longer need a manual sweep after
|
|
362
|
-
// every `omnify generate`.
|
|
363
|
-
//
|
|
364
|
-
// NOT a silent no-op when Pint is missing any more. It used to be, and that cost a
|
|
365
|
-
// consumer several investigation sessions: their CI regenerated without Pint on PATH,
|
|
366
|
-
// wrote 927 files differing from the committed (Pint-formatted) tree, and the only
|
|
367
|
-
// evidence was one ABSENT log line. It was read as a Linux/Darwin build divergence and
|
|
368
|
-
// chased as far as "pick a canonical platform" before anyone ran the two commands that
|
|
369
|
-
// settle it (godx-jp/godx-corebooks#108, reproduced exactly: hide `vendor/bin/pint`,
|
|
370
|
-
// regenerate, 927 files; restore it, 0).
|
|
371
|
-
//
|
|
372
|
-
// The opt-out (`OMNIFY_NO_PINT=1`) stays quiet — that one is a choice, not a surprise.
|
|
373
|
-
if (writtenPaths.length > 0) {
|
|
374
|
-
const pintResult = runPintFormat(writtenPaths);
|
|
375
|
-
if (pintResult.ran) {
|
|
376
|
-
if (pintResult.exitStatus === 0) {
|
|
377
|
-
console.log(` Formatted ${pintResult.formattedCount} PHP file(s) via Laravel Pint`);
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
console.warn(` [omnify] Laravel Pint exited with status ${pintResult.exitStatus}; generated files may not match project formatting preset.`);
|
|
381
|
-
if (pintResult.errorMessage) {
|
|
382
|
-
const trimmed = pintResult.errorMessage.trim().split('\n').slice(0, 5).join('\n');
|
|
383
|
-
console.warn(` ${trimmed}`);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
else if (pintResult.skipped === 'pint-not-found') {
|
|
388
|
-
console.warn(` [omnify] Laravel Pint not found — ${writtenPaths.length} PHP file(s) written UNFORMATTED.`);
|
|
389
|
-
console.warn(' If this project commits generated PHP, the output will differ from a tree formatted by Pint.');
|
|
390
|
-
console.warn(' Run `composer install` before `omnify generate`, or set OMNIFY_NO_PINT=1 to silence this deliberately.');
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
336
|
// Orphan cleanup: when a schema is removed, renamed, flipped from
|
|
394
337
|
// kind:object → kind:pivot, or opted out via `options.service: false`,
|
|
395
338
|
// its previously-emitted `*ServiceBase.php` becomes orphan and
|
|
@@ -208,7 +208,7 @@ function generateUserController(name, schema, config) {
|
|
|
208
208
|
className: `${modelName}ControllerBase`,
|
|
209
209
|
fqn: `${resolveModularBaseNamespace(config, name, 'Controllers', config.controllers.baseNamespace)}\\${modelName}ControllerBase`,
|
|
210
210
|
}
|
|
211
|
-
:
|
|
211
|
+
: legacyGroupedBase(config, `${modelName}Controller`, schema.group);
|
|
212
212
|
const editable = config.structure === 'modular'
|
|
213
213
|
? { className: `${modelName}Controller`, fileName: `${modelName}Controller.php`, namespace: config.controllers.namespace, path: config.controllers.path }
|
|
214
214
|
: resolveEditableClass(config.controllers, modelName, 'Controller');
|
|
@@ -244,3 +244,13 @@ class ${editable.className} extends ${baseAlias}
|
|
|
244
244
|
`;
|
|
245
245
|
return userFile(`${userPath}/${editable.fileName}`, content);
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Legacy layout nests the base controller under its schema group (see
|
|
249
|
+
* `generateBaseController`), so the stub must import it from the same nested
|
|
250
|
+
* namespace — a flat import names a class that does not exist.
|
|
251
|
+
*/
|
|
252
|
+
function legacyGroupedBase(config, className, group) {
|
|
253
|
+
const raw = resolveBaseClass(config.controllers, className, 'Base');
|
|
254
|
+
const { namespace } = nestByGroup({ path: '', namespace: raw.namespace }, group);
|
|
255
|
+
return { className: raw.className, fqn: `${namespace}\\${raw.className}` };
|
|
256
|
+
}
|
|
@@ -66,25 +66,26 @@ function collectModularLayerDirs(configDir, config, category) {
|
|
|
66
66
|
return dirs;
|
|
67
67
|
}
|
|
68
68
|
const BASE_SUBDIRS_SKIP = new Set(['Base', 'OmnifyBase']);
|
|
69
|
-
function findEditableSubclassFile(roots, fileName) {
|
|
69
|
+
function findEditableSubclassFile(roots, fileName, excludeAbsPath) {
|
|
70
70
|
for (const root of roots) {
|
|
71
|
-
|
|
71
|
+
const found = findFileAbsolutePath(root, fileName);
|
|
72
|
+
if (found && found !== excludeAbsPath)
|
|
72
73
|
return true;
|
|
73
74
|
}
|
|
74
75
|
return false;
|
|
75
76
|
}
|
|
76
|
-
function
|
|
77
|
+
function findFileAbsolutePath(dir, fileName) {
|
|
77
78
|
if (!existsSync(dir))
|
|
78
|
-
return
|
|
79
|
+
return null;
|
|
79
80
|
let info;
|
|
80
81
|
try {
|
|
81
82
|
info = statSync(dir);
|
|
82
83
|
}
|
|
83
84
|
catch {
|
|
84
|
-
return
|
|
85
|
+
return null;
|
|
85
86
|
}
|
|
86
87
|
if (!info.isDirectory()) {
|
|
87
|
-
return basename(dir) === fileName;
|
|
88
|
+
return basename(dir) === fileName ? resolve(dir) : null;
|
|
88
89
|
}
|
|
89
90
|
for (const entry of readdirSync(dir)) {
|
|
90
91
|
if (BASE_SUBDIRS_SKIP.has(entry))
|
|
@@ -97,14 +98,15 @@ function findFileByName(dir, fileName) {
|
|
|
97
98
|
continue;
|
|
98
99
|
}
|
|
99
100
|
if (info.isDirectory()) {
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
const nested = findFileAbsolutePath(full, fileName);
|
|
102
|
+
if (nested)
|
|
103
|
+
return nested;
|
|
102
104
|
}
|
|
103
105
|
else if (entry === fileName) {
|
|
104
|
-
return
|
|
106
|
+
return resolve(full);
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
|
-
return
|
|
109
|
+
return null;
|
|
108
110
|
}
|
|
109
111
|
function collectEditableSearchRoots(configDir, config, layer, modularCategory, baseAbsPath) {
|
|
110
112
|
const roots = [];
|
|
@@ -151,7 +153,7 @@ function pruneDisabledLayerBasesInRoots(configDir, config, spec, roots, expected
|
|
|
151
153
|
const editableName = spec.toEditableFileName(entry);
|
|
152
154
|
if (editableName) {
|
|
153
155
|
const editableRoots = collectEditableSearchRoots(configDir, config, spec.layerConfig, spec.modularCategory, fullPath);
|
|
154
|
-
if (findEditableSubclassFile(editableRoots, editableName)) {
|
|
156
|
+
if (findEditableSubclassFile(editableRoots, editableName, fullPath)) {
|
|
155
157
|
kept += 1;
|
|
156
158
|
continue;
|
|
157
159
|
}
|
|
@@ -210,12 +212,46 @@ export function hasEditableSubclassForBase(configDir, config, file) {
|
|
|
210
212
|
if (!editableName)
|
|
211
213
|
return false;
|
|
212
214
|
const roots = collectEditableSearchRoots(configDir, config, spec.layerConfig, spec.modularCategory, resolve(configDir, file.path));
|
|
213
|
-
return findEditableSubclassFile(roots, editableName);
|
|
215
|
+
return findEditableSubclassFile(roots, editableName, resolve(configDir, file.path));
|
|
214
216
|
}
|
|
215
217
|
return false;
|
|
216
218
|
}
|
|
219
|
+
const SCAFFOLD_LAYER_BASE_SUFFIX = {
|
|
220
|
+
request: 'RequestBase.php',
|
|
221
|
+
resource: 'ResourceBase.php',
|
|
222
|
+
policy: 'PolicyBase.php',
|
|
223
|
+
controller: 'ControllerBase.php',
|
|
224
|
+
service: 'ServiceBase.php',
|
|
225
|
+
};
|
|
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
|
+
/**
|
|
234
|
+
* Legacy `flatBase` puts regenerated bases directly at `layer.basePath` with
|
|
235
|
+
* the same filename as the user-editable stub (different directory). That
|
|
236
|
+
* directory is generated-only: resolveBaseClass() always targets basePath and
|
|
237
|
+
* only per-schema base classes are written there (issue #96).
|
|
238
|
+
*/
|
|
239
|
+
function disabledLayerFileMatchers(config, layerConfig, baseSuffix, editableSuffix) {
|
|
240
|
+
const modularMatches = (name) => name.endsWith(baseSuffix);
|
|
241
|
+
const modularToEditable = (name) => baseToEditableFileName(name, baseSuffix, editableSuffix);
|
|
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
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return { matchesBaseFile: modularMatches, toEditableFileName: modularToEditable };
|
|
252
|
+
}
|
|
217
253
|
function scaffoldLayerSpecs(config) {
|
|
218
|
-
|
|
254
|
+
const layers = [
|
|
219
255
|
{
|
|
220
256
|
layer: 'request',
|
|
221
257
|
label: 'request base',
|
|
@@ -223,8 +259,6 @@ function scaffoldLayerSpecs(config) {
|
|
|
223
259
|
layerConfig: config.requests,
|
|
224
260
|
legacyBasePath: config.requests.basePath,
|
|
225
261
|
modularCategory: 'Requests',
|
|
226
|
-
matchesBaseFile: (name) => name.endsWith('RequestBase.php'),
|
|
227
|
-
toEditableFileName: (name) => baseToEditableFileName(name, 'RequestBase.php', 'Request.php'),
|
|
228
262
|
},
|
|
229
263
|
{
|
|
230
264
|
layer: 'resource',
|
|
@@ -233,8 +267,6 @@ function scaffoldLayerSpecs(config) {
|
|
|
233
267
|
layerConfig: config.resources,
|
|
234
268
|
legacyBasePath: config.resources.basePath,
|
|
235
269
|
modularCategory: 'Resources',
|
|
236
|
-
matchesBaseFile: (name) => name.endsWith('ResourceBase.php'),
|
|
237
|
-
toEditableFileName: (name) => baseToEditableFileName(name, 'ResourceBase.php', 'Resource.php'),
|
|
238
270
|
},
|
|
239
271
|
{
|
|
240
272
|
layer: 'policy',
|
|
@@ -243,8 +275,6 @@ function scaffoldLayerSpecs(config) {
|
|
|
243
275
|
layerConfig: config.policies,
|
|
244
276
|
legacyBasePath: config.policies.basePath,
|
|
245
277
|
modularCategory: 'Policies',
|
|
246
|
-
matchesBaseFile: (name) => name.endsWith('PolicyBase.php'),
|
|
247
|
-
toEditableFileName: (name) => baseToEditableFileName(name, 'PolicyBase.php', 'Policy.php'),
|
|
248
278
|
},
|
|
249
279
|
{
|
|
250
280
|
layer: 'controller',
|
|
@@ -253,8 +283,6 @@ function scaffoldLayerSpecs(config) {
|
|
|
253
283
|
layerConfig: config.controllers,
|
|
254
284
|
legacyBasePath: config.controllers.basePath,
|
|
255
285
|
modularCategory: 'Controllers',
|
|
256
|
-
matchesBaseFile: (name) => name.endsWith('ControllerBase.php'),
|
|
257
|
-
toEditableFileName: (name) => baseToEditableFileName(name, 'ControllerBase.php', 'Controller.php'),
|
|
258
286
|
},
|
|
259
287
|
{
|
|
260
288
|
layer: 'service',
|
|
@@ -263,10 +291,14 @@ function scaffoldLayerSpecs(config) {
|
|
|
263
291
|
layerConfig: config.services,
|
|
264
292
|
legacyBasePath: config.services.basePath,
|
|
265
293
|
modularCategory: 'Services',
|
|
266
|
-
matchesBaseFile: (name) => name.endsWith('ServiceBase.php'),
|
|
267
|
-
toEditableFileName: (name) => baseToEditableFileName(name, 'ServiceBase.php', 'Service.php'),
|
|
268
294
|
},
|
|
269
295
|
];
|
|
296
|
+
return layers.map((entry) => {
|
|
297
|
+
const baseSuffix = SCAFFOLD_LAYER_BASE_SUFFIX[entry.layer];
|
|
298
|
+
const editableSuffix = SCAFFOLD_LAYER_EDITABLE_SUFFIX[entry.layer];
|
|
299
|
+
const matchers = disabledLayerFileMatchers(config, entry.layerConfig, baseSuffix, editableSuffix);
|
|
300
|
+
return { ...entry, ...matchers };
|
|
301
|
+
});
|
|
270
302
|
}
|
|
271
303
|
/**
|
|
272
304
|
* Delete orphan `*ServiceBase.php` files; flag orphan editable services
|
|
@@ -43,7 +43,8 @@ export function buildRelation(propName, property, modelNamespace, context) {
|
|
|
43
43
|
const related = relation === 'MorphTo'
|
|
44
44
|
? '\\Illuminate\\Database\\Eloquent\\Model'
|
|
45
45
|
: '\\' + modelNamespace + '\\' + target;
|
|
46
|
-
|
|
46
|
+
// Blank line before the tag: Pint's `phpdoc_separation` (laravel preset).
|
|
47
|
+
method = method.replace(' */', ` *\n * @return ${type}<${related}, $this>\n */`);
|
|
47
48
|
return { method, imports };
|
|
48
49
|
}
|
|
49
50
|
/** Generate the table name Laravel's joiningTable() infers from model basenames. */
|