@omnifyjp/ts 6.7.18 → 6.8.1
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 +16 -2
- package/dist/php/controller-generator.js +2 -0
- package/dist/php/index.d.ts +10 -1
- package/dist/php/index.js +29 -6
- package/dist/php/orphan-cleanup.d.ts +17 -4
- package/dist/php/orphan-cleanup.js +252 -6
- package/dist/php/policy-generator.js +2 -0
- package/dist/php/request-generator.js +2 -0
- package/dist/php/resource-generator.js +2 -0
- package/dist/php/route-generator.js +2 -0
- package/dist/php/types.d.ts +30 -9
- package/dist/php/types.js +58 -5
- package/package.json +1 -1
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 { pruneOrphanServiceFilesIfEnabled } from './php/orphan-cleanup.js';
|
|
33
|
+
import { hasEditableSubclassForBase, pruneDisabledScaffoldLayerBases, 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
36
|
import { runPrettierFormat } from './prettier-format.js';
|
|
@@ -246,7 +246,12 @@ program
|
|
|
246
246
|
// ---- PHP/Laravel generation ----
|
|
247
247
|
if (laravelEnabled) {
|
|
248
248
|
console.log(`\n[Laravel] Generating PHP files`);
|
|
249
|
-
|
|
249
|
+
// #255: a disabled scaffold layer still regenerates the bases that
|
|
250
|
+
// hand-written subclasses extend, so they track the schema.
|
|
251
|
+
const phpConfigForGen = derivePhpConfig(laravelOverrides);
|
|
252
|
+
const phpFiles = generatePhp(input, laravelOverrides, {
|
|
253
|
+
keepDisabledBase: (file) => hasEditableSubclassForBase(configDir, phpConfigForGen, file),
|
|
254
|
+
});
|
|
250
255
|
let phpCreated = 0;
|
|
251
256
|
let phpOverwritten = 0;
|
|
252
257
|
let phpSkipped = 0;
|
|
@@ -391,6 +396,15 @@ program
|
|
|
391
396
|
// pollutes Composer autoload. Prune base files automatically; warn
|
|
392
397
|
// about editable `*Service.php` (may contain user code).
|
|
393
398
|
const phpConfig = derivePhpConfig(laravelOverrides);
|
|
399
|
+
for (const summary of pruneDisabledScaffoldLayerBases(configDir, phpConfig, phpFiles)) {
|
|
400
|
+
if (summary.prunedCount > 0) {
|
|
401
|
+
console.log(` pruned ${summary.prunedCount} stale ${summary.label}(s) — layer disabled`);
|
|
402
|
+
}
|
|
403
|
+
if (summary.keptCount > 0) {
|
|
404
|
+
console.warn(` kept ${summary.keptCount} ${summary.layer} base(s) still extended by hand-written subclasses — ` +
|
|
405
|
+
`set ${summary.layer}.enable: true to keep generating them, or delete the subclasses`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
394
408
|
const cleanup = pruneOrphanServiceFilesIfEnabled(configDir, phpConfig, phpFiles);
|
|
395
409
|
if (cleanup.prunedBases.length > 0) {
|
|
396
410
|
console.log(` ${cleanup.prunedBases.length} orphan ServiceBase pruned (schema removed/renamed/opted-out)`);
|
|
@@ -7,6 +7,8 @@ import { buildControllerMethodAttributes, openApiControllerImports } from './ope
|
|
|
7
7
|
const DEFAULT_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
|
|
8
8
|
/** Generate controller classes for all schemas with api config. */
|
|
9
9
|
export function generateControllers(reader, config) {
|
|
10
|
+
if (!config.controllers.enabled)
|
|
11
|
+
return [];
|
|
10
12
|
const files = [];
|
|
11
13
|
for (const [name, schema] of Object.entries(reader.getSchemasWithApi())) {
|
|
12
14
|
files.push(...generateForSchema(name, schema, reader, config));
|
package/dist/php/index.d.ts
CHANGED
|
@@ -14,5 +14,14 @@ import type { SchemasJson } from '../types.js';
|
|
|
14
14
|
import type { GeneratedFile, LaravelCodegenOverrides } from './types.js';
|
|
15
15
|
export type { GeneratedFile, PhpConfig, LaravelCodegenOverrides } from './types.js';
|
|
16
16
|
export { derivePhpConfig } from './types.js';
|
|
17
|
+
export interface GeneratePhpOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Issue #255: a disabled scaffold layer still regenerates the BASE files a
|
|
20
|
+
* hand-written subclass extends, so they never freeze out of step with the
|
|
21
|
+
* schema. The caller answers "does a subclass exist for this base?" (the CLI
|
|
22
|
+
* checks the disk); omitted, a disabled layer emits nothing, as before.
|
|
23
|
+
*/
|
|
24
|
+
readonly keepDisabledBase?: (file: GeneratedFile) => boolean;
|
|
25
|
+
}
|
|
17
26
|
/** Generate all PHP files from schemas.json data. */
|
|
18
|
-
export declare function generatePhp(data: SchemasJson, overrides?: LaravelCodegenOverrides): GeneratedFile[];
|
|
27
|
+
export declare function generatePhp(data: SchemasJson, overrides?: LaravelCodegenOverrides, options?: GeneratePhpOptions): GeneratedFile[];
|
package/dist/php/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import { generateOpenApi } from './openapi-generator.js';
|
|
|
40
40
|
import { generateArchitectureDoc } from './architecture-doc-generator.js';
|
|
41
41
|
export { derivePhpConfig } from './types.js';
|
|
42
42
|
/** Generate all PHP files from schemas.json data. */
|
|
43
|
-
export function generatePhp(data, overrides) {
|
|
43
|
+
export function generatePhp(data, overrides, options = {}) {
|
|
44
44
|
const reader = SchemaReader.fromData(data);
|
|
45
45
|
const config = derivePhpConfig(overrides);
|
|
46
46
|
const files = [];
|
|
@@ -84,11 +84,8 @@ export function generatePhp(data, overrides) {
|
|
|
84
84
|
// we add the standalone Common / Info / Schemas files.
|
|
85
85
|
files.push(...generateOpenApi(reader, config));
|
|
86
86
|
}
|
|
87
|
-
// Service layer
|
|
88
|
-
//
|
|
89
|
-
// unless explicitly opted out via `options.service: false`. The old
|
|
90
|
-
// opt-in gate here is gone because `generateServices` does its own
|
|
91
|
-
// filter internally; always call it and let it decide.
|
|
87
|
+
// Service layer (#81): per-schema opt-out via `options.service: false`.
|
|
88
|
+
// Layer-level opt-in via `codegen.laravel.service.enable` (issue #252).
|
|
92
89
|
files.push(...generateServices(reader, config));
|
|
93
90
|
// Issue #34: every `kind: enum` schema becomes a global PHP enum class.
|
|
94
91
|
files.push(...generateEnums(reader, config));
|
|
@@ -112,6 +109,9 @@ export function generatePhp(data, overrides) {
|
|
|
112
109
|
// no behavior change for existing consumers, and the doc updates
|
|
113
110
|
// automatically as schemas / config change so it never drifts.
|
|
114
111
|
files.push(...generateArchitectureDoc(reader, config));
|
|
112
|
+
if (options.keepDisabledBase) {
|
|
113
|
+
files.push(...extendedBasesOfDisabledLayers(reader, config, options.keepDisabledBase));
|
|
114
|
+
}
|
|
115
115
|
// #174: every generated PHP file leaves here in Pint's import shape —
|
|
116
116
|
// unused imports dropped, the rest ordered. Doing it once, centrally, is why
|
|
117
117
|
// no template has to remember: the two rules fired on almost every layer.
|
|
@@ -125,3 +125,26 @@ export function generatePhp(data, overrides) {
|
|
|
125
125
|
}
|
|
126
126
|
: file);
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Bases of disabled scaffold layers that a hand-written subclass still extends
|
|
130
|
+
* (#255). Generated exactly as when the layer is on; editable stubs are never
|
|
131
|
+
* emitted, so a disabled layer creates no new scaffold.
|
|
132
|
+
*/
|
|
133
|
+
function extendedBasesOfDisabledLayers(reader, config, keep) {
|
|
134
|
+
const layers = [
|
|
135
|
+
['requests', generateRequests],
|
|
136
|
+
['resources', generateResources],
|
|
137
|
+
['policies', generatePolicies],
|
|
138
|
+
['controllers', generateControllers],
|
|
139
|
+
['services', generateServices],
|
|
140
|
+
];
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const [key, generate] of layers) {
|
|
143
|
+
const layer = config[key];
|
|
144
|
+
if (layer.enabled)
|
|
145
|
+
continue;
|
|
146
|
+
const enabled = { ...config, [key]: { ...layer, enabled: true } };
|
|
147
|
+
out.push(...generate(reader, enabled).filter((f) => f.type === 'base' && keep(f)));
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
@@ -33,12 +33,25 @@ export interface ServiceCleanupResult {
|
|
|
33
33
|
* preserved (may contain user code). Caller should print a warning. */
|
|
34
34
|
warnedEditables: string[];
|
|
35
35
|
}
|
|
36
|
+
/** Apply service orphan cleanup only while the service layer is enabled. */
|
|
37
|
+
export declare function pruneOrphanServiceFilesIfEnabled(configDir: string, config: PhpConfig, generated: readonly GeneratedFile[]): ServiceCleanupResult;
|
|
38
|
+
export interface DisabledLayerPruneSummary {
|
|
39
|
+
layer: string;
|
|
40
|
+
label: string;
|
|
41
|
+
prunedCount: number;
|
|
42
|
+
keptCount: number;
|
|
43
|
+
}
|
|
36
44
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* must remain available until the host application removes its inheritance.
|
|
45
|
+
* When a scaffold layer is disabled (issue #252), remove stale auto-generated
|
|
46
|
+
* BASE files that have no matching user-editable subclass on disk.
|
|
40
47
|
*/
|
|
41
|
-
export declare function
|
|
48
|
+
export declare function pruneDisabledScaffoldLayerBases(configDir: string, config: PhpConfig, generated: readonly GeneratedFile[]): DisabledLayerPruneSummary[];
|
|
49
|
+
/**
|
|
50
|
+
* Issue #255: does a hand-written subclass exist on disk for this generated
|
|
51
|
+
* base of a disabled scaffold layer? Same lookup the prune above uses to keep
|
|
52
|
+
* a base, so a base is regenerated exactly when it would otherwise be kept.
|
|
53
|
+
*/
|
|
54
|
+
export declare function hasEditableSubclassForBase(configDir: string, config: PhpConfig, file: GeneratedFile): boolean;
|
|
42
55
|
/**
|
|
43
56
|
* Delete orphan `*ServiceBase.php` files; flag orphan editable services
|
|
44
57
|
* 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 } from 'node:path';
|
|
22
|
+
import { join, resolve, basename, relative, sep } 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/;
|
|
@@ -43,17 +43,263 @@ function readEditableServiceHead(filePath) {
|
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Apply service cleanup only while this target is managed by Omnify.
|
|
48
|
-
* A globally disabled service layer is a migration boundary: existing bases
|
|
49
|
-
* must remain available until the host application removes its inheritance.
|
|
50
|
-
*/
|
|
46
|
+
/** Apply service orphan cleanup only while the service layer is enabled. */
|
|
51
47
|
export function pruneOrphanServiceFilesIfEnabled(configDir, config, generated) {
|
|
52
48
|
if (!config.services.enabled) {
|
|
53
49
|
return { prunedBases: [], warnedEditables: [] };
|
|
54
50
|
}
|
|
55
51
|
return pruneOrphanServiceFiles(configDir, config, generated);
|
|
56
52
|
}
|
|
53
|
+
function collectModularLayerDirs(configDir, config, category) {
|
|
54
|
+
const dirs = [];
|
|
55
|
+
if (config.structure !== 'modular')
|
|
56
|
+
return dirs;
|
|
57
|
+
const modulesAbs = resolve(configDir, config.modules.path);
|
|
58
|
+
if (!existsSync(modulesAbs) || !statSync(modulesAbs).isDirectory())
|
|
59
|
+
return dirs;
|
|
60
|
+
for (const entry of readdirSync(modulesAbs)) {
|
|
61
|
+
const layerDir = join(modulesAbs, entry, category);
|
|
62
|
+
if (existsSync(layerDir) && statSync(layerDir).isDirectory()) {
|
|
63
|
+
dirs.push(layerDir);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return dirs;
|
|
67
|
+
}
|
|
68
|
+
const BASE_SUBDIRS_SKIP = new Set(['Base', 'OmnifyBase']);
|
|
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) {
|
|
142
|
+
let pruned = 0;
|
|
143
|
+
let kept = 0;
|
|
144
|
+
for (const root of roots) {
|
|
145
|
+
if (!existsSync(root))
|
|
146
|
+
continue;
|
|
147
|
+
for (const entry of readdirSync(root)) {
|
|
148
|
+
if (!spec.matchesBaseFile(entry))
|
|
149
|
+
continue;
|
|
150
|
+
const fullPath = join(root, entry);
|
|
151
|
+
if (expectedAbs.has(fullPath))
|
|
152
|
+
continue;
|
|
153
|
+
const editableName = spec.toEditableFileName(entry);
|
|
154
|
+
if (editableName) {
|
|
155
|
+
const editableRoots = collectEditableSearchRoots(configDir, config, spec.layerConfig, spec.modularCategory, fullPath);
|
|
156
|
+
if (findEditableSubclassFile(editableRoots, editableName, fullPath)) {
|
|
157
|
+
kept += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
unlinkSync(fullPath);
|
|
163
|
+
pruned += 1;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// skip uncleanable files
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { pruned, kept };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* When a scaffold layer is disabled (issue #252), remove stale auto-generated
|
|
174
|
+
* BASE files that have no matching user-editable subclass on disk.
|
|
175
|
+
*/
|
|
176
|
+
export function pruneDisabledScaffoldLayerBases(configDir, config, generated) {
|
|
177
|
+
const expectedAbs = new Set();
|
|
178
|
+
for (const f of generated) {
|
|
179
|
+
expectedAbs.add(resolve(configDir, f.path));
|
|
180
|
+
}
|
|
181
|
+
const summaries = [];
|
|
182
|
+
for (const spec of scaffoldLayerSpecs(config)) {
|
|
183
|
+
if (spec.enabled)
|
|
184
|
+
continue;
|
|
185
|
+
const roots = [
|
|
186
|
+
...collectModularLayerDirs(configDir, config, spec.modularCategory),
|
|
187
|
+
resolve(configDir, spec.legacyBasePath),
|
|
188
|
+
];
|
|
189
|
+
const { pruned, kept } = pruneDisabledLayerBasesInRoots(configDir, config, spec, roots, expectedAbs);
|
|
190
|
+
if (pruned > 0 || kept > 0) {
|
|
191
|
+
summaries.push({
|
|
192
|
+
layer: spec.layer,
|
|
193
|
+
label: spec.label,
|
|
194
|
+
prunedCount: pruned,
|
|
195
|
+
keptCount: kept,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return summaries;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Issue #255: does a hand-written subclass exist on disk for this generated
|
|
203
|
+
* base of a disabled scaffold layer? Same lookup the prune above uses to keep
|
|
204
|
+
* a base, so a base is regenerated exactly when it would otherwise be kept.
|
|
205
|
+
*/
|
|
206
|
+
export function hasEditableSubclassForBase(configDir, config, file) {
|
|
207
|
+
const name = basename(file.path);
|
|
208
|
+
for (const spec of scaffoldLayerSpecs(config)) {
|
|
209
|
+
if (spec.enabled || !spec.matchesBaseFile(name))
|
|
210
|
+
continue;
|
|
211
|
+
const editableName = spec.toEditableFileName(name);
|
|
212
|
+
if (!editableName)
|
|
213
|
+
return false;
|
|
214
|
+
const roots = collectEditableSearchRoots(configDir, config, spec.layerConfig, spec.modularCategory, resolve(configDir, file.path));
|
|
215
|
+
return findEditableSubclassFile(roots, editableName, resolve(configDir, file.path));
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
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
|
+
}
|
|
253
|
+
function scaffoldLayerSpecs(config) {
|
|
254
|
+
const layers = [
|
|
255
|
+
{
|
|
256
|
+
layer: 'request',
|
|
257
|
+
label: 'request base',
|
|
258
|
+
enabled: config.requests.enabled,
|
|
259
|
+
layerConfig: config.requests,
|
|
260
|
+
legacyBasePath: config.requests.basePath,
|
|
261
|
+
modularCategory: 'Requests',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
layer: 'resource',
|
|
265
|
+
label: 'resource base',
|
|
266
|
+
enabled: config.resources.enabled,
|
|
267
|
+
layerConfig: config.resources,
|
|
268
|
+
legacyBasePath: config.resources.basePath,
|
|
269
|
+
modularCategory: 'Resources',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
layer: 'policy',
|
|
273
|
+
label: 'policy base',
|
|
274
|
+
enabled: config.policies.enabled,
|
|
275
|
+
layerConfig: config.policies,
|
|
276
|
+
legacyBasePath: config.policies.basePath,
|
|
277
|
+
modularCategory: 'Policies',
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
layer: 'controller',
|
|
281
|
+
label: 'controller base',
|
|
282
|
+
enabled: config.controllers.enabled,
|
|
283
|
+
layerConfig: config.controllers,
|
|
284
|
+
legacyBasePath: config.controllers.basePath,
|
|
285
|
+
modularCategory: 'Controllers',
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
layer: 'service',
|
|
289
|
+
label: 'service base',
|
|
290
|
+
enabled: config.services.enabled,
|
|
291
|
+
layerConfig: config.services,
|
|
292
|
+
legacyBasePath: config.services.basePath,
|
|
293
|
+
modularCategory: 'Services',
|
|
294
|
+
},
|
|
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
|
+
});
|
|
302
|
+
}
|
|
57
303
|
/**
|
|
58
304
|
* Delete orphan `*ServiceBase.php` files; flag orphan editable services
|
|
59
305
|
* for warning. Pure-ish: takes the generated file list (= source of
|
|
@@ -46,6 +46,8 @@ const RECORD_ACTIONS = new Set(['view', 'edit', 'delete']);
|
|
|
46
46
|
* overriding the method in the editable child.
|
|
47
47
|
*/
|
|
48
48
|
export function generatePolicies(reader, config) {
|
|
49
|
+
if (!config.policies.enabled)
|
|
50
|
+
return [];
|
|
49
51
|
const files = [];
|
|
50
52
|
for (const [name, schema] of Object.entries(reader.getProjectVisibleObjectSchemas())) {
|
|
51
53
|
if (schema.kind === 'pivot')
|
|
@@ -6,6 +6,8 @@ import { toStoreRules, toUpdateRules, formatRules, resolveFkRuleType } from './t
|
|
|
6
6
|
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup } from './types.js';
|
|
7
7
|
/** Generate Store/Update request classes for all project-owned visible object schemas. */
|
|
8
8
|
export function generateRequests(reader, config) {
|
|
9
|
+
if (!config.requests.enabled)
|
|
10
|
+
return [];
|
|
9
11
|
const files = [];
|
|
10
12
|
for (const [name, schema] of Object.entries(reader.getProjectVisibleObjectSchemas())) {
|
|
11
13
|
if (schema.kind === 'extend')
|
|
@@ -17,6 +17,8 @@ import { createClassImporter, renderUseBlock } from './php-style.js';
|
|
|
17
17
|
* cross-resource reference always resolves.
|
|
18
18
|
*/
|
|
19
19
|
export function generateResources(reader, config) {
|
|
20
|
+
if (!config.resources.enabled)
|
|
21
|
+
return [];
|
|
20
22
|
const files = [];
|
|
21
23
|
for (const [name, schema] of Object.entries(reader.getProjectVisibleObjectSchemas())) {
|
|
22
24
|
if (!generatesOwnResource(schema))
|
|
@@ -9,6 +9,8 @@ import { baseFile } from './types.js';
|
|
|
9
9
|
const DEFAULT_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
|
|
10
10
|
/** Generate route files for all schemas with api config. */
|
|
11
11
|
export function generateRoutes(reader, config) {
|
|
12
|
+
if (!config.routes.enabled || !config.controllers.enabled)
|
|
13
|
+
return [];
|
|
12
14
|
const files = [];
|
|
13
15
|
for (const [name, schema] of Object.entries(reader.getSchemasWithApi())) {
|
|
14
16
|
files.push(generateRouteFile(name, schema, config));
|
package/dist/php/types.d.ts
CHANGED
|
@@ -134,7 +134,13 @@ export declare function resolveGlobalTraitNamespace(config: PhpConfig, legacyNam
|
|
|
134
134
|
* working.
|
|
135
135
|
*/
|
|
136
136
|
export interface LaravelPathOverride {
|
|
137
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Opt in/out of generating this layer. Defaults vary by layer (issue #252):
|
|
139
|
+
* `request`, `resource`, `policy`, `controller`, and `service` default to
|
|
140
|
+
* off; model, factory, provider, route, config, and other infrastructure
|
|
141
|
+
* layers default to on. Omitted `enable` uses the layer default — not
|
|
142
|
+
* “enabled unless false”.
|
|
143
|
+
*/
|
|
138
144
|
enable?: boolean;
|
|
139
145
|
/** Path for BASE (auto-generated, regenerated) classes. */
|
|
140
146
|
path?: string;
|
|
@@ -304,6 +310,11 @@ export interface BaseEditableLayer {
|
|
|
304
310
|
*/
|
|
305
311
|
userEditableGroupByFolder: boolean;
|
|
306
312
|
}
|
|
313
|
+
/** Base + editable layer with an on/off toggle (issue #252). */
|
|
314
|
+
export type EnabledBaseEditableLayer = BaseEditableLayer & {
|
|
315
|
+
/** When false, this layer is not generated and stale base files are pruned. */
|
|
316
|
+
enabled: boolean;
|
|
317
|
+
};
|
|
307
318
|
/** PHP codegen configuration (resolved with defaults). */
|
|
308
319
|
export interface PhpConfig {
|
|
309
320
|
/** Filesystem prefix applied to all generated paths. Empty when not set. */
|
|
@@ -340,8 +351,8 @@ export interface PhpConfig {
|
|
|
340
351
|
*/
|
|
341
352
|
modulesPath: string;
|
|
342
353
|
models: BaseEditableLayer;
|
|
343
|
-
requests:
|
|
344
|
-
resources:
|
|
354
|
+
requests: EnabledBaseEditableLayer;
|
|
355
|
+
resources: EnabledBaseEditableLayer;
|
|
345
356
|
factories: {
|
|
346
357
|
namespace: string;
|
|
347
358
|
path: string;
|
|
@@ -350,14 +361,13 @@ export interface PhpConfig {
|
|
|
350
361
|
namespace: string;
|
|
351
362
|
path: string;
|
|
352
363
|
};
|
|
353
|
-
policies:
|
|
354
|
-
controllers:
|
|
355
|
-
services:
|
|
356
|
-
/** False disables Laravel service emission and cleanup for this target. */
|
|
357
|
-
enabled: boolean;
|
|
358
|
-
};
|
|
364
|
+
policies: EnabledBaseEditableLayer;
|
|
365
|
+
controllers: EnabledBaseEditableLayer;
|
|
366
|
+
services: EnabledBaseEditableLayer;
|
|
359
367
|
routes: {
|
|
360
368
|
path: string;
|
|
369
|
+
/** Default true. Route files reference controllers; generation is skipped when `controllers.enabled` is false. */
|
|
370
|
+
enabled: boolean;
|
|
361
371
|
};
|
|
362
372
|
/** Path to the omnify-schemas.php config file (full path including filename). */
|
|
363
373
|
configFile: {
|
|
@@ -443,6 +453,17 @@ export declare function nestEditableByGroup(layer: BaseEditableLayer, loc: {
|
|
|
443
453
|
path: string;
|
|
444
454
|
namespace: string;
|
|
445
455
|
};
|
|
456
|
+
/**
|
|
457
|
+
* Cross-layer validation for explicit `enable: true` combinations that would
|
|
458
|
+
* reference classes from a disabled layer. Surfaces at CLI exit 1 via
|
|
459
|
+
* `derivePhpConfig` (same path as relation-builder throws, issue #242).
|
|
460
|
+
*/
|
|
461
|
+
export declare function validateLaravelCodegenLayerDependencies(overrides?: LaravelCodegenOverrides): void;
|
|
462
|
+
/**
|
|
463
|
+
* Overrides that restore pre-v6.8 “all scaffold layers on” behaviour for
|
|
464
|
+
* projects upgrading from 6.7.x. Merge into `derivePhpConfig({ ... })`.
|
|
465
|
+
*/
|
|
466
|
+
export declare const SCAFFOLD_LAYERS_6_7_ENABLED: Pick<LaravelCodegenOverrides, 'request' | 'resource' | 'policy' | 'controller' | 'service'>;
|
|
446
467
|
/**
|
|
447
468
|
* Derive full PHP config from optional overrides.
|
|
448
469
|
* All paths and namespaces fall back to sensible defaults.
|
package/dist/php/types.js
CHANGED
|
@@ -302,6 +302,45 @@ const DEFAULT_SHARED_PATH = 'app/Omnify/Shared';
|
|
|
302
302
|
const DEFAULT_GLOBAL_ENUMS_PATH = 'app/Omnify/Enums';
|
|
303
303
|
const DEFAULT_GLOBAL_TRAITS_PATH = 'app/Omnify/Traits';
|
|
304
304
|
const DEFAULT_OPENAPI_PATH = 'app/Omnify/OpenApi';
|
|
305
|
+
/** Default `enable` for opt-in scaffold layers (issue #252). */
|
|
306
|
+
const SCAFFOLD_LAYER_DEFAULT_ENABLED = false;
|
|
307
|
+
/** Default `enable` for layers that stay on unless explicitly disabled. */
|
|
308
|
+
const INFRA_LAYER_DEFAULT_ENABLED = true;
|
|
309
|
+
function resolveLayerEnable(override, defaultOn = INFRA_LAYER_DEFAULT_ENABLED) {
|
|
310
|
+
return override?.enable ?? defaultOn;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Cross-layer validation for explicit `enable: true` combinations that would
|
|
314
|
+
* reference classes from a disabled layer. Surfaces at CLI exit 1 via
|
|
315
|
+
* `derivePhpConfig` (same path as relation-builder throws, issue #242).
|
|
316
|
+
*/
|
|
317
|
+
export function validateLaravelCodegenLayerDependencies(overrides) {
|
|
318
|
+
const requestOn = resolveLayerEnable(overrides?.request, SCAFFOLD_LAYER_DEFAULT_ENABLED);
|
|
319
|
+
const resourceOn = resolveLayerEnable(overrides?.resource, SCAFFOLD_LAYER_DEFAULT_ENABLED);
|
|
320
|
+
const controllerOn = resolveLayerEnable(overrides?.controller, SCAFFOLD_LAYER_DEFAULT_ENABLED);
|
|
321
|
+
if (controllerOn) {
|
|
322
|
+
if (!requestOn) {
|
|
323
|
+
throw new Error('codegen.laravel.controller.enable: true requires codegen.laravel.request.enable: true');
|
|
324
|
+
}
|
|
325
|
+
if (!resourceOn) {
|
|
326
|
+
throw new Error('codegen.laravel.controller.enable: true requires codegen.laravel.resource.enable: true');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (overrides?.route?.enable === true && !controllerOn) {
|
|
330
|
+
throw new Error('codegen.laravel.route.enable: true requires codegen.laravel.controller.enable: true');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Overrides that restore pre-v6.8 “all scaffold layers on” behaviour for
|
|
335
|
+
* projects upgrading from 6.7.x. Merge into `derivePhpConfig({ ... })`.
|
|
336
|
+
*/
|
|
337
|
+
export const SCAFFOLD_LAYERS_6_7_ENABLED = {
|
|
338
|
+
request: { enable: true },
|
|
339
|
+
resource: { enable: true },
|
|
340
|
+
policy: { enable: true },
|
|
341
|
+
controller: { enable: true },
|
|
342
|
+
service: { enable: true },
|
|
343
|
+
};
|
|
305
344
|
/**
|
|
306
345
|
* Apply a rootPath prefix to a path. Absolute paths and paths that already
|
|
307
346
|
* begin with `${rootPath}/` are returned as-is, so users can mix and match
|
|
@@ -321,6 +360,7 @@ function withRoot(rootPath, p) {
|
|
|
321
360
|
* All paths and namespaces fall back to sensible defaults.
|
|
322
361
|
*/
|
|
323
362
|
export function derivePhpConfig(overrides) {
|
|
363
|
+
validateLaravelCodegenLayerDependencies(overrides);
|
|
324
364
|
// Strip trailing slash for stable join behavior.
|
|
325
365
|
const rootPath = (overrides?.rootPath ?? '').replace(/\/+$/, '');
|
|
326
366
|
// Namespace generation should NOT include the rootPath prefix — it's a
|
|
@@ -371,8 +411,14 @@ export function derivePhpConfig(overrides) {
|
|
|
371
411
|
globalTraits: { namespace: globalTraitsNs, path: globalTraitsPath },
|
|
372
412
|
modulesPath,
|
|
373
413
|
models: modelsLayer,
|
|
374
|
-
requests:
|
|
375
|
-
|
|
414
|
+
requests: {
|
|
415
|
+
...requestsLayer,
|
|
416
|
+
enabled: resolveLayerEnable(overrides?.request, SCAFFOLD_LAYER_DEFAULT_ENABLED),
|
|
417
|
+
},
|
|
418
|
+
resources: {
|
|
419
|
+
...resourcesLayer,
|
|
420
|
+
enabled: resolveLayerEnable(overrides?.resource, SCAFFOLD_LAYER_DEFAULT_ENABLED),
|
|
421
|
+
},
|
|
376
422
|
factories: {
|
|
377
423
|
namespace: factoryNs,
|
|
378
424
|
path: factoryPath,
|
|
@@ -381,14 +427,21 @@ export function derivePhpConfig(overrides) {
|
|
|
381
427
|
namespace: providerNs,
|
|
382
428
|
path: providerPath,
|
|
383
429
|
},
|
|
384
|
-
policies:
|
|
385
|
-
|
|
430
|
+
policies: {
|
|
431
|
+
...policiesLayer,
|
|
432
|
+
enabled: resolveLayerEnable(overrides?.policy, SCAFFOLD_LAYER_DEFAULT_ENABLED),
|
|
433
|
+
},
|
|
434
|
+
controllers: {
|
|
435
|
+
...controllersLayer,
|
|
436
|
+
enabled: resolveLayerEnable(overrides?.controller, SCAFFOLD_LAYER_DEFAULT_ENABLED),
|
|
437
|
+
},
|
|
386
438
|
services: {
|
|
387
439
|
...servicesLayer,
|
|
388
|
-
enabled: overrides?.service
|
|
440
|
+
enabled: resolveLayerEnable(overrides?.service, SCAFFOLD_LAYER_DEFAULT_ENABLED),
|
|
389
441
|
},
|
|
390
442
|
routes: {
|
|
391
443
|
path: routePath,
|
|
444
|
+
enabled: resolveLayerEnable(overrides?.route, INFRA_LAYER_DEFAULT_ENABLED),
|
|
392
445
|
},
|
|
393
446
|
configFile: {
|
|
394
447
|
path: configFilePath,
|