@omnifyjp/omnify 6.2.0 → 6.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/omnify",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "description": "Schema-driven code generation for Laravel, TypeScript, and SQL",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,10 +36,10 @@
36
36
  "zod": "^3.24.0"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@omnifyjp/omnify-darwin-arm64": "6.2.0",
40
- "@omnifyjp/omnify-darwin-x64": "6.2.0",
41
- "@omnifyjp/omnify-linux-x64": "6.2.0",
42
- "@omnifyjp/omnify-linux-arm64": "6.2.0",
43
- "@omnifyjp/omnify-win32-x64": "6.2.0"
39
+ "@omnifyjp/omnify-darwin-arm64": "6.3.0",
40
+ "@omnifyjp/omnify-darwin-x64": "6.3.0",
41
+ "@omnifyjp/omnify-linux-x64": "6.3.0",
42
+ "@omnifyjp/omnify-linux-arm64": "6.3.0",
43
+ "@omnifyjp/omnify-win32-x64": "6.3.0"
44
44
  }
45
45
  }
package/ts-dist/cli.js CHANGED
@@ -29,6 +29,7 @@ import { Command } from 'commander';
29
29
  import { parse as parseYaml } from 'yaml';
30
30
  import { generateTypeScript } from './generator.js';
31
31
  import { generatePhp, derivePhpConfig } from './php/index.js';
32
+ import { stripPhpComments } from './php/types.js';
32
33
  import { pruneOrphanServiceFilesIfEnabled } from './php/orphan-cleanup.js';
33
34
  import { findSiblingWithSameBasename, findAppRootAncestor } from './php/sibling-skip.js';
34
35
  import { runPintFormat } from './php/pint-format.js';
@@ -100,6 +101,11 @@ function resolveFromConfig(configPath) {
100
101
  route: laravelConfig?.route,
101
102
  config: laravelConfig?.config,
102
103
  nestedset: laravelConfig?.nestedset,
104
+ modules: laravelConfig?.modules,
105
+ shared: laravelConfig?.shared,
106
+ enums: laravelConfig?.enums,
107
+ traits: laravelConfig?.traits,
108
+ openapi: laravelConfig?.openapi,
103
109
  } : undefined;
104
110
  return {
105
111
  inputSpec,
@@ -226,6 +232,7 @@ program
226
232
  let phpOverwritten = 0;
227
233
  let phpSkipped = 0;
228
234
  let phpSkippedSibling = 0;
235
+ let phpShadowed = 0;
229
236
  const skippedSiblingExamples = [];
230
237
  const writtenPaths = [];
231
238
  for (const file of phpFiles) {
@@ -233,11 +240,24 @@ program
233
240
  mkdirSync(dirname(filePath), { recursive: true });
234
241
  if (!file.overwrite && existsSync(filePath)) {
235
242
  phpSkipped++;
243
+ const existing = file.expects || (file.conflicts && file.conflicts.length > 0)
244
+ ? readFileSync(filePath, 'utf-8')
245
+ : '';
236
246
  // The file predates something the template now needs. Say so once,
237
247
  // naming the file and the line, rather than rewriting user code.
238
- if (file.expects && !readFileSync(filePath, 'utf-8').includes(file.expects.needle)) {
248
+ if (file.expects && !existing.includes(file.expects.needle)) {
239
249
  console.warn(`[omnify-ts] ${relative(configDir, filePath)}: ${file.expects.hint}`);
240
250
  }
251
+ // The mirror: the file contains an edit that silently disables what
252
+ // the base generates. #171 — a redeclared $fillable shadows the
253
+ // parent's, so every property added afterwards is stripped on mass
254
+ // assignment with no error anywhere.
255
+ for (const conflict of file.conflicts ?? []) {
256
+ if (new RegExp(conflict.pattern).test(stripPhpComments(existing))) {
257
+ phpShadowed++;
258
+ console.warn(`[omnify-ts] ${relative(configDir, filePath)}: ${conflict.hint}`);
259
+ }
260
+ }
241
261
  continue;
242
262
  }
243
263
  // Issue #98 v5.8.18 / #99 v5.8.19: skip user-editable stub
@@ -285,6 +305,10 @@ program
285
305
  console.log(` ${phpCreated} files created (user-editable)`);
286
306
  if (phpSkipped > 0)
287
307
  console.log(` ${phpSkipped} files skipped (already exist)`);
308
+ if (phpShadowed > 0) {
309
+ console.warn(` ${phpShadowed} editable file(s) shadow a generated property — ` +
310
+ `schema changes will NOT reach them until that is fixed (see warnings above).`);
311
+ }
288
312
  if (phpSkippedSibling > 0) {
289
313
  console.log(` ${phpSkippedSibling} user-editable stub(s) skipped (project sibling with same name found in subfolder)`);
290
314
  for (const ex of skippedSiblingExamples) {
@@ -5,7 +5,7 @@ import { toPascalCase, toSnakeCase, toCamelCase, toFkColumnName, pluralize } fro
5
5
  import { resolveTimestamps } from './timestamps.js';
6
6
  import { toCast, toPhpDocType, isHiddenByDefault } from './type-mapper.js';
7
7
  import { buildRelation } from './relation-builder.js';
8
- import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup, } from './types.js';
8
+ import { baseFile, userFile, shadowingPropertyConflicts, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup, } from './types.js';
9
9
  import { enumClassName } from './enum-generator.js';
10
10
  /** Generate base model and user model for all project-owned object schemas,
11
11
  * plus user models for package schemas (extending the package model). */
@@ -474,7 +474,11 @@ ${traits.join('\n')}
474
474
  //
475
475
  }
476
476
  `;
477
- return userFile(`${editable.path}/${editable.fileName}`, content);
477
+ // #171: this subclass is created once and then owned by the project. The
478
+ // most common edit to it — redeclaring $fillable — silently disables every
479
+ // property omnify adds to the base afterwards. We cannot prevent the edit,
480
+ // but we can stop it from being invisible.
481
+ return userFile(`${editable.path}/${editable.fileName}`, content, undefined, shadowingPropertyConflicts(baseAlias));
478
482
  }
479
483
  function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, hasTranslatable, properties, needsUuidTrait = false, needsUlidTrait = false, hasNestedSet = false, nestedSetNamespace = 'Aimeos\\Nestedset', hasFiles = false, modelNamespace = '', localesNamespace = '', traitsNamespace = '', sharedModelsNamespace = '', config, hasAuditLog = false) {
480
484
  const lines = [];
@@ -21,6 +21,23 @@ export interface GeneratedFile {
21
21
  readonly needle: string;
22
22
  readonly hint: string;
23
23
  };
24
+ /**
25
+ * For a user file only: things the file must NOT contain, and why.
26
+ *
27
+ * The mirror of `expects`. Some edits to a user-editable subclass silently
28
+ * disable what the base class generates — the worst being a redeclared
29
+ * `protected $fillable`, which in PHP REPLACES the parent's rather than
30
+ * merging, so every property added to the schema afterwards is stripped by
31
+ * Eloquent on mass assignment. Nothing throws; `save()` returns true and the
32
+ * column stays NULL (issue #171).
33
+ *
34
+ * Each entry is matched against the content of the file we skipped, and
35
+ * `hint` is printed when it matches.
36
+ */
37
+ readonly conflicts?: readonly {
38
+ readonly pattern: string;
39
+ readonly hint: string;
40
+ }[];
24
41
  }
25
42
  /** Create a base file (always overwritten). */
26
43
  export declare function baseFile(path: string, content: string): GeneratedFile;
@@ -28,7 +45,28 @@ export declare function baseFile(path: string, content: string): GeneratedFile;
28
45
  export declare function userFile(path: string, content: string, expects?: {
29
46
  needle: string;
30
47
  hint: string;
31
- }): GeneratedFile;
48
+ }, conflicts?: readonly {
49
+ pattern: string;
50
+ hint: string;
51
+ }[]): GeneratedFile;
52
+ /**
53
+ * Eloquent array properties that REPLACE the parent's when redeclared in a
54
+ * subclass, rather than merging. Redeclaring any of these in a user-editable
55
+ * model shadows whatever the generated base puts there, and the failure is
56
+ * completely silent. Issue #171.
57
+ */
58
+ export declare const SHADOWING_ELOQUENT_PROPERTIES: readonly ["fillable", "guarded", "casts", "hidden", "visible", "appends", "dates", "touches", "with"];
59
+ /**
60
+ * Build the `conflicts` entries for a generated model's editable subclass.
61
+ *
62
+ * `baseClass` is named in the hint because the whole point is that the base is
63
+ * still correct — the generator did emit the property — and the reader needs to
64
+ * know which file they are shadowing.
65
+ */
66
+ export declare function shadowingPropertyConflicts(baseClass: string): {
67
+ pattern: string;
68
+ hint: string;
69
+ }[];
32
70
  /** Category of base file for modular path resolution. */
33
71
  export type BaseCategory = 'Models' | 'Controllers' | 'Requests' | 'Resources' | 'Services' | 'Enums' | 'Traits' | 'Locales' | 'Policies';
34
72
  /**
@@ -418,3 +456,15 @@ export declare function nestEditableByGroup(layer: BaseEditableLayer, loc: {
418
456
  * All paths and namespaces fall back to sensible defaults.
419
457
  */
420
458
  export declare function derivePhpConfig(overrides?: LaravelCodegenOverrides): PhpConfig;
459
+ /**
460
+ * Strip PHP comments so a commented-out declaration does not read as a real
461
+ * one. Without this, the very common
462
+ *
463
+ * // protected $fillable = ['name']; // left from a refactor
464
+ *
465
+ * would be reported as shadowing the base — and a warning that fires on code
466
+ * that is not running is worse than no warning, because people learn to ignore
467
+ * it. Naive on purpose: it can strip a `//` inside a string literal, which only
468
+ * ever costs us a warning we would otherwise have printed, never a false one.
469
+ */
470
+ export declare function stripPhpComments(source: string): string;
@@ -6,8 +6,53 @@ export function baseFile(path, content) {
6
6
  return { path, content, overwrite: true, type: 'base' };
7
7
  }
8
8
  /** Create a user file (created once, skip if exists). */
9
- export function userFile(path, content, expects) {
10
- return { path, content, overwrite: false, type: 'user', ...(expects ? { expects } : {}) };
9
+ export function userFile(path, content, expects, conflicts) {
10
+ return {
11
+ path,
12
+ content,
13
+ overwrite: false,
14
+ type: 'user',
15
+ ...(expects ? { expects } : {}),
16
+ ...(conflicts && conflicts.length > 0 ? { conflicts } : {}),
17
+ };
18
+ }
19
+ /**
20
+ * Eloquent array properties that REPLACE the parent's when redeclared in a
21
+ * subclass, rather than merging. Redeclaring any of these in a user-editable
22
+ * model shadows whatever the generated base puts there, and the failure is
23
+ * completely silent. Issue #171.
24
+ */
25
+ export const SHADOWING_ELOQUENT_PROPERTIES = [
26
+ 'fillable',
27
+ 'guarded',
28
+ 'casts',
29
+ 'hidden',
30
+ 'visible',
31
+ 'appends',
32
+ 'dates',
33
+ 'touches',
34
+ 'with',
35
+ ];
36
+ /**
37
+ * Build the `conflicts` entries for a generated model's editable subclass.
38
+ *
39
+ * `baseClass` is named in the hint because the whole point is that the base is
40
+ * still correct — the generator did emit the property — and the reader needs to
41
+ * know which file they are shadowing.
42
+ */
43
+ export function shadowingPropertyConflicts(baseClass) {
44
+ return SHADOWING_ELOQUENT_PROPERTIES.map((prop) => ({
45
+ // Matches `protected $fillable`, `public array $casts`, `protected static
46
+ // $with` — any redeclaration, whatever the visibility or type hint.
47
+ pattern: String.raw `(?:public|protected|private)\s+(?:static\s+)?(?:array\s+)?\$` + prop + String.raw `\s*=`,
48
+ hint: `redeclares $${prop}, which REPLACES ` +
49
+ `${baseClass}::$${prop} rather than merging with it (PHP does not merge ` +
50
+ `redeclared properties). Anything omnify adds to $${prop} from now on is ` +
51
+ `silently dropped: mass assignment strips it, save() still returns true, ` +
52
+ `and the column stays NULL. Fix: delete the redeclaration and let the base ` +
53
+ `provide it, or build it from the parent, e.g. ` +
54
+ `__construct() { $this->${prop} = array_merge(parent::$${prop} ?? [], [...]); }`,
55
+ }));
11
56
  }
12
57
  /**
13
58
  * Resolve base file path for a schema based on structure.
@@ -382,3 +427,20 @@ export function derivePhpConfig(overrides) {
382
427
  },
383
428
  };
384
429
  }
430
+ /**
431
+ * Strip PHP comments so a commented-out declaration does not read as a real
432
+ * one. Without this, the very common
433
+ *
434
+ * // protected $fillable = ['name']; // left from a refactor
435
+ *
436
+ * would be reported as shadowing the base — and a warning that fires on code
437
+ * that is not running is worse than no warning, because people learn to ignore
438
+ * it. Naive on purpose: it can strip a `//` inside a string literal, which only
439
+ * ever costs us a warning we would otherwise have printed, never a false one.
440
+ */
441
+ export function stripPhpComments(source) {
442
+ return source
443
+ .replace(/\/\*[\s\S]*?\*\//g, '')
444
+ .replace(/(^|\s)\/\/[^\n]*/g, '$1')
445
+ .replace(/(^|\s)#[^\n]*/g, '$1');
446
+ }