@omnifyjp/ts 6.7.18 → 6.8.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 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
- const phpFiles = generatePhp(input, laravelOverrides);
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));
@@ -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. #81 (v3.23.0): service generation is now opt-out by
88
- // default — every project `kind: object` schema gets a base service
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
- * Apply service cleanup only while this target is managed by Omnify.
38
- * A globally disabled service layer is a migration boundary: existing bases
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 pruneOrphanServiceFilesIfEnabled(configDir: string, config: PhpConfig, generated: readonly GeneratedFile[]): ServiceCleanupResult;
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,231 @@ 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) {
70
+ for (const root of roots) {
71
+ if (findFileByName(root, fileName))
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+ function findFileByName(dir, fileName) {
77
+ if (!existsSync(dir))
78
+ return false;
79
+ let info;
80
+ try {
81
+ info = statSync(dir);
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ if (!info.isDirectory()) {
87
+ return basename(dir) === fileName;
88
+ }
89
+ for (const entry of readdirSync(dir)) {
90
+ if (BASE_SUBDIRS_SKIP.has(entry))
91
+ continue;
92
+ const full = join(dir, entry);
93
+ try {
94
+ info = statSync(full);
95
+ }
96
+ catch {
97
+ continue;
98
+ }
99
+ if (info.isDirectory()) {
100
+ if (findFileByName(full, fileName))
101
+ return true;
102
+ }
103
+ else if (entry === fileName) {
104
+ return true;
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+ function collectEditableSearchRoots(configDir, config, layer, modularCategory, baseAbsPath) {
110
+ const roots = [];
111
+ const add = (p) => {
112
+ const abs = resolve(configDir, p);
113
+ if (!roots.includes(abs))
114
+ roots.push(abs);
115
+ };
116
+ add(layer.userEditablePath);
117
+ if (layer.path !== layer.userEditablePath) {
118
+ add(layer.path);
119
+ }
120
+ if (config.structure === 'modular') {
121
+ const modulesAbs = resolve(configDir, config.modules.path);
122
+ const rel = relative(modulesAbs, baseAbsPath);
123
+ if (rel && !rel.startsWith('..') && !rel.startsWith(`..${sep}`)) {
124
+ const segments = rel.split(/[/\\]/);
125
+ if (segments.length >= 2 && segments[1] === modularCategory) {
126
+ const schemaFolder = segments[0];
127
+ add(join(layer.userEditablePath, schemaFolder));
128
+ add(join(layer.path, schemaFolder));
129
+ }
130
+ }
131
+ }
132
+ return roots;
133
+ }
134
+ function baseToEditableFileName(baseFileName, baseSuffix, editableSuffix) {
135
+ if (!baseFileName.endsWith(baseSuffix))
136
+ return null;
137
+ return `${baseFileName.slice(0, -baseSuffix.length)}${editableSuffix}`;
138
+ }
139
+ function pruneDisabledLayerBasesInRoots(configDir, config, spec, roots, expectedAbs) {
140
+ let pruned = 0;
141
+ let kept = 0;
142
+ for (const root of roots) {
143
+ if (!existsSync(root))
144
+ continue;
145
+ for (const entry of readdirSync(root)) {
146
+ if (!spec.matchesBaseFile(entry))
147
+ continue;
148
+ const fullPath = join(root, entry);
149
+ if (expectedAbs.has(fullPath))
150
+ continue;
151
+ const editableName = spec.toEditableFileName(entry);
152
+ if (editableName) {
153
+ const editableRoots = collectEditableSearchRoots(configDir, config, spec.layerConfig, spec.modularCategory, fullPath);
154
+ if (findEditableSubclassFile(editableRoots, editableName)) {
155
+ kept += 1;
156
+ continue;
157
+ }
158
+ }
159
+ try {
160
+ unlinkSync(fullPath);
161
+ pruned += 1;
162
+ }
163
+ catch {
164
+ // skip uncleanable files
165
+ }
166
+ }
167
+ }
168
+ return { pruned, kept };
169
+ }
170
+ /**
171
+ * When a scaffold layer is disabled (issue #252), remove stale auto-generated
172
+ * BASE files that have no matching user-editable subclass on disk.
173
+ */
174
+ export function pruneDisabledScaffoldLayerBases(configDir, config, generated) {
175
+ const expectedAbs = new Set();
176
+ for (const f of generated) {
177
+ expectedAbs.add(resolve(configDir, f.path));
178
+ }
179
+ const summaries = [];
180
+ for (const spec of scaffoldLayerSpecs(config)) {
181
+ if (spec.enabled)
182
+ continue;
183
+ const roots = [
184
+ ...collectModularLayerDirs(configDir, config, spec.modularCategory),
185
+ resolve(configDir, spec.legacyBasePath),
186
+ ];
187
+ const { pruned, kept } = pruneDisabledLayerBasesInRoots(configDir, config, spec, roots, expectedAbs);
188
+ if (pruned > 0 || kept > 0) {
189
+ summaries.push({
190
+ layer: spec.layer,
191
+ label: spec.label,
192
+ prunedCount: pruned,
193
+ keptCount: kept,
194
+ });
195
+ }
196
+ }
197
+ return summaries;
198
+ }
199
+ /**
200
+ * Issue #255: does a hand-written subclass exist on disk for this generated
201
+ * base of a disabled scaffold layer? Same lookup the prune above uses to keep
202
+ * a base, so a base is regenerated exactly when it would otherwise be kept.
203
+ */
204
+ export function hasEditableSubclassForBase(configDir, config, file) {
205
+ const name = basename(file.path);
206
+ for (const spec of scaffoldLayerSpecs(config)) {
207
+ if (spec.enabled || !spec.matchesBaseFile(name))
208
+ continue;
209
+ const editableName = spec.toEditableFileName(name);
210
+ if (!editableName)
211
+ return false;
212
+ const roots = collectEditableSearchRoots(configDir, config, spec.layerConfig, spec.modularCategory, resolve(configDir, file.path));
213
+ return findEditableSubclassFile(roots, editableName);
214
+ }
215
+ return false;
216
+ }
217
+ function scaffoldLayerSpecs(config) {
218
+ return [
219
+ {
220
+ layer: 'request',
221
+ label: 'request base',
222
+ enabled: config.requests.enabled,
223
+ layerConfig: config.requests,
224
+ legacyBasePath: config.requests.basePath,
225
+ modularCategory: 'Requests',
226
+ matchesBaseFile: (name) => name.endsWith('RequestBase.php'),
227
+ toEditableFileName: (name) => baseToEditableFileName(name, 'RequestBase.php', 'Request.php'),
228
+ },
229
+ {
230
+ layer: 'resource',
231
+ label: 'resource base',
232
+ enabled: config.resources.enabled,
233
+ layerConfig: config.resources,
234
+ legacyBasePath: config.resources.basePath,
235
+ modularCategory: 'Resources',
236
+ matchesBaseFile: (name) => name.endsWith('ResourceBase.php'),
237
+ toEditableFileName: (name) => baseToEditableFileName(name, 'ResourceBase.php', 'Resource.php'),
238
+ },
239
+ {
240
+ layer: 'policy',
241
+ label: 'policy base',
242
+ enabled: config.policies.enabled,
243
+ layerConfig: config.policies,
244
+ legacyBasePath: config.policies.basePath,
245
+ modularCategory: 'Policies',
246
+ matchesBaseFile: (name) => name.endsWith('PolicyBase.php'),
247
+ toEditableFileName: (name) => baseToEditableFileName(name, 'PolicyBase.php', 'Policy.php'),
248
+ },
249
+ {
250
+ layer: 'controller',
251
+ label: 'controller base',
252
+ enabled: config.controllers.enabled,
253
+ layerConfig: config.controllers,
254
+ legacyBasePath: config.controllers.basePath,
255
+ modularCategory: 'Controllers',
256
+ matchesBaseFile: (name) => name.endsWith('ControllerBase.php'),
257
+ toEditableFileName: (name) => baseToEditableFileName(name, 'ControllerBase.php', 'Controller.php'),
258
+ },
259
+ {
260
+ layer: 'service',
261
+ label: 'service base',
262
+ enabled: config.services.enabled,
263
+ layerConfig: config.services,
264
+ legacyBasePath: config.services.basePath,
265
+ modularCategory: 'Services',
266
+ matchesBaseFile: (name) => name.endsWith('ServiceBase.php'),
267
+ toEditableFileName: (name) => baseToEditableFileName(name, 'ServiceBase.php', 'Service.php'),
268
+ },
269
+ ];
270
+ }
57
271
  /**
58
272
  * Delete orphan `*ServiceBase.php` files; flag orphan editable services
59
273
  * 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));
@@ -134,7 +134,13 @@ export declare function resolveGlobalTraitNamespace(config: PhpConfig, legacyNam
134
134
  * working.
135
135
  */
136
136
  export interface LaravelPathOverride {
137
- /** Disable generation for layers that support it (currently `service`). */
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: BaseEditableLayer;
344
- resources: BaseEditableLayer;
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: BaseEditableLayer;
354
- controllers: BaseEditableLayer;
355
- services: BaseEditableLayer & {
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: requestsLayer,
375
- resources: resourcesLayer,
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: policiesLayer,
385
- controllers: controllersLayer,
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?.enable ?? true,
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "6.7.18",
3
+ "version": "6.8.0",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",