@omnifyjp/ts 6.7.6 → 6.7.9

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
@@ -29,7 +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
+ import { evaluateShadowingConflicts } from './php/shadowing-conflicts.js';
33
33
  import { 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';
@@ -271,11 +271,9 @@ program
271
271
  // the base generates. #171 — a redeclared $fillable shadows the
272
272
  // parent's, so every property added afterwards is stripped on mass
273
273
  // assignment with no error anywhere.
274
- for (const conflict of file.conflicts ?? []) {
275
- if (new RegExp(conflict.pattern).test(stripPhpComments(existing))) {
276
- phpShadowed++;
277
- console.warn(`[omnify-ts] ${relative(configDir, filePath)}: ${conflict.hint}`);
278
- }
274
+ for (const hint of evaluateShadowingConflicts(existing, file.conflicts ?? [])) {
275
+ phpShadowed++;
276
+ console.warn(`[omnify-ts] ${relative(configDir, filePath)}: ${hint}`);
279
277
  }
280
278
  continue;
281
279
  }
@@ -5,7 +5,8 @@ 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, shadowingPropertyConflicts, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup, } from './types.js';
8
+ import { shadowingPropertyConflicts } from './shadowing-conflicts.js';
9
+ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup, } from './types.js';
9
10
  import { enumClassName } from './enum-generator.js';
10
11
  /** Generate base model and user model for all project-owned object schemas,
11
12
  * plus, for each package schema the project extends, a user model that
@@ -130,7 +131,7 @@ function generateForSchema(name, schema, reader, config) {
130
131
  const hasNestedSet = detectNestedSet(options, properties);
131
132
  return [
132
133
  generateBaseModel(name, schema, reader, config),
133
- generateUserModel(name, schema, config, hasNestedSet),
134
+ generateUserModel(name, schema, reader, config, hasNestedSet),
134
135
  ];
135
136
  }
136
137
  function generateBaseModel(name, schema, reader, config) {
@@ -402,7 +403,7 @@ ${auditLogProperties}${relations}${accessors}${fileAccessors}${auditSection}${ne
402
403
  : `${baseDescriptor.path}/${baseDescriptor.className}.php`;
403
404
  return baseFile(filePath, finalContent);
404
405
  }
405
- function generateUserModel(name, schema, config, hasNestedSet = false) {
406
+ function generateUserModel(name, schema, reader, config, hasNestedSet = false) {
406
407
  const modelName = toPascalCase(name);
407
408
  // Issue #96: editable stub may live at a different path/namespace
408
409
  // than the base (e.g. `app/Models/<Name>.php` while base is at
@@ -454,6 +455,14 @@ function generateUserModel(name, schema, config, hasNestedSet = false) {
454
455
  if (hasNestedSet) {
455
456
  traits.push(' use NodeTrait;');
456
457
  }
458
+ const properties = (schema.properties ?? {});
459
+ const expandedProperties = reader.getExpandedProperties(name);
460
+ const propertyOrder = reader.getPropertyOrder(name);
461
+ const baseArrayValues = {
462
+ fillable: fillableFieldNames(properties, expandedProperties, propertyOrder),
463
+ hidden: hiddenFieldNames(properties, expandedProperties, propertyOrder),
464
+ appends: appendsFieldNames(expandedProperties),
465
+ };
457
466
  const content = `<?php
458
467
 
459
468
  /**
@@ -488,7 +497,7 @@ ${traits.join('\n')}
488
497
  // most common edit to it — redeclaring $fillable — silently disables every
489
498
  // property omnify adds to the base afterwards. We cannot prevent the edit,
490
499
  // but we can stop it from being invisible.
491
- return userFile(`${editable.path}/${editable.fileName}`, content, undefined, shadowingPropertyConflicts(baseAlias));
500
+ return userFile(`${editable.path}/${editable.fileName}`, content, undefined, shadowingPropertyConflicts(baseAlias, baseArrayValues));
492
501
  }
493
502
  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) {
494
503
  const lines = [];
@@ -669,7 +678,7 @@ function buildTraits(hasSoftDelete, isAuthenticatable, hasTranslatable, needsUui
669
678
  names.sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0));
670
679
  return names.map((n) => ` use ${n};`).join('\n') + '\n';
671
680
  }
672
- function buildFillable(properties, expandedProperties, propertyOrder) {
681
+ function fillableFieldNames(properties, expandedProperties, propertyOrder) {
673
682
  const fields = [];
674
683
  for (const propName of propertyOrder) {
675
684
  const prop = properties[propName];
@@ -703,11 +712,15 @@ function buildFillable(properties, expandedProperties, propertyOrder) {
703
712
  }
704
713
  fields.push(toSnakeCase(propName));
705
714
  }
715
+ return fields;
716
+ }
717
+ function buildFillable(properties, expandedProperties, propertyOrder) {
718
+ const fields = fillableFieldNames(properties, expandedProperties, propertyOrder);
706
719
  if (fields.length === 0)
707
720
  return '';
708
721
  return fields.map(f => ` '${f}',`).join('\n') + '\n';
709
722
  }
710
- function buildHidden(properties, expandedProperties, propertyOrder) {
723
+ function hiddenFieldNames(properties, expandedProperties, propertyOrder) {
711
724
  const fields = [];
712
725
  for (const propName of propertyOrder) {
713
726
  const prop = properties[propName];
@@ -727,7 +740,6 @@ function buildHidden(properties, expandedProperties, propertyOrder) {
727
740
  }
728
741
  continue;
729
742
  }
730
- // Check individual field hidden overrides for compound types
731
743
  if (expandedProperties[propName]) {
732
744
  const fieldOverrides = prop['fields'] ?? {};
733
745
  for (const col of expandedProperties[propName].columns ?? []) {
@@ -738,11 +750,15 @@ function buildHidden(properties, expandedProperties, propertyOrder) {
738
750
  }
739
751
  }
740
752
  }
753
+ return fields;
754
+ }
755
+ function buildHidden(properties, expandedProperties, propertyOrder) {
756
+ const fields = hiddenFieldNames(properties, expandedProperties, propertyOrder);
741
757
  if (fields.length === 0)
742
758
  return '';
743
759
  return fields.map(f => ` '${f}',`).join('\n') + '\n';
744
760
  }
745
- function buildAppends(expandedProperties) {
761
+ function appendsFieldNames(expandedProperties) {
746
762
  const appends = [];
747
763
  for (const [propName, expansion] of Object.entries(expandedProperties)) {
748
764
  const sourceType = expansion.sourceType ?? '';
@@ -757,6 +773,10 @@ function buildAppends(expandedProperties) {
757
773
  break;
758
774
  }
759
775
  }
776
+ return appends;
777
+ }
778
+ function buildAppends(expandedProperties) {
779
+ const appends = appendsFieldNames(expandedProperties);
760
780
  if (appends.length === 0)
761
781
  return '';
762
782
  return appends.map(a => ` '${a}',`).join('\n') + '\n';
@@ -0,0 +1,17 @@
1
+ import { type ShadowingEloquentProperty, type ShadowingPropertyConflict } from './types.js';
2
+ /** Case-sensitive intent marker checked against raw file content (issue #222). */
3
+ export declare function hasIntentionalShadowMarker(rawContent: string, prop: string): boolean;
4
+ /**
5
+ * Build the `conflicts` entries for a generated model's editable subclass.
6
+ */
7
+ export declare function shadowingPropertyConflicts(baseClass: string, baseArrayValues?: Partial<Record<ShadowingEloquentProperty, readonly string[]>>): ShadowingPropertyConflict[];
8
+ /**
9
+ * Parse a redeclared PHP string array property (first `];` closes the array).
10
+ * Operates on comment-stripped source so commented-out earlier declarations are ignored.
11
+ */
12
+ export declare function parseRedeclaredStringArray(source: string, prop: string): string[] | null;
13
+ export declare function formatShadowingDrift(baseValues: readonly string[], childValues: string[] | null): string;
14
+ /**
15
+ * Evaluate shadowing conflicts for a skipped user-editable model file.
16
+ */
17
+ export declare function evaluateShadowingConflicts(existingRaw: string, conflicts: readonly ShadowingPropertyConflict[]): string[];
@@ -0,0 +1,119 @@
1
+ import { SHADOWING_ELOQUENT_PROPERTIES, stripPhpComments, } from './types.js';
2
+ /** Case-sensitive intent marker checked against raw file content (issue #222). */
3
+ export function hasIntentionalShadowMarker(rawContent, prop) {
4
+ return rawContent.includes(`@omnify-${prop} intentional`);
5
+ }
6
+ function shadowingConsequence(prop) {
7
+ switch (prop) {
8
+ case 'fillable':
9
+ return (`Anything omnify adds to $fillable from now on is silently dropped: ` +
10
+ `mass assignment strips it, save() still returns true, and the column stays NULL.`);
11
+ case 'guarded':
12
+ return (`Anything omnify adds to $guarded from now on is silently unguarded: ` +
13
+ `mass assignment can write it.`);
14
+ case 'hidden':
15
+ return (`Attributes omnify adds to $hidden from now on would no longer be hidden ` +
16
+ `and may appear in JSON serialization.`);
17
+ case 'visible':
18
+ return (`Attributes omnify adds to $visible from now on would no longer be exposed ` +
19
+ `in JSON serialization.`);
20
+ case 'appends':
21
+ return (`Accessors omnify adds to $appends from now on would not be appended to ` +
22
+ `the model's array or JSON form.`);
23
+ default:
24
+ return `Configuration omnify adds to $${prop} from now on stops applying on this model.`;
25
+ }
26
+ }
27
+ function shadowingFixAdvice(prop, baseClass) {
28
+ return (`If this narrowing is intentional, add \`/** @omnify-${prop} intentional */\` ` +
29
+ `above the property to silence this warning. If not, remove the redeclaration ` +
30
+ `and let ${baseClass}::$${prop} apply, or merge from the parent explicitly.`);
31
+ }
32
+ /**
33
+ * Build the `conflicts` entries for a generated model's editable subclass.
34
+ */
35
+ export function shadowingPropertyConflicts(baseClass, baseArrayValues) {
36
+ return SHADOWING_ELOQUENT_PROPERTIES.map((prop) => {
37
+ const values = baseArrayValues?.[prop];
38
+ const baseValues = values && values.length > 0 && values.every((v) => typeof v === 'string')
39
+ ? values
40
+ : undefined;
41
+ return {
42
+ property: prop,
43
+ pattern: String.raw `(?:public|protected|private)\s+(?:static\s+)?(?:array\s+)?\$` +
44
+ prop +
45
+ String.raw `\s*=`,
46
+ hint: `redeclares $${prop}, which REPLACES ${baseClass}::$${prop} rather than ` +
47
+ `merging with it (PHP does not merge redeclared properties). ` +
48
+ `${shadowingConsequence(prop)} ${shadowingFixAdvice(prop, baseClass)}`,
49
+ ...(baseValues ? { baseValues } : {}),
50
+ };
51
+ });
52
+ }
53
+ /**
54
+ * Parse a redeclared PHP string array property (first `];` closes the array).
55
+ * Operates on comment-stripped source so commented-out earlier declarations are ignored.
56
+ */
57
+ export function parseRedeclaredStringArray(source, prop) {
58
+ const stripped = stripPhpComments(source);
59
+ const match = stripped.match(new RegExp(String.raw `\$` + prop + String.raw `\s*=\s*\[[\s\S]*?\];`));
60
+ if (!match) {
61
+ return null;
62
+ }
63
+ const segment = match[0];
64
+ const values = [];
65
+ const singleQuoted = /'([^'\\]*(?:\\.[^'\\]*)*)'/g;
66
+ for (const m of segment.matchAll(singleQuoted)) {
67
+ values.push(m[1].replace(/\\'/g, "'"));
68
+ }
69
+ const doubleQuoted = /"([^"\\]*(?:\\.[^"\\]*)*)"/g;
70
+ for (const m of segment.matchAll(doubleQuoted)) {
71
+ values.push(m[1].replace(/\\"/g, '"'));
72
+ }
73
+ return values;
74
+ }
75
+ export function formatShadowingDrift(baseValues, childValues) {
76
+ if (childValues === null) {
77
+ return '';
78
+ }
79
+ const baseSet = new Set(baseValues);
80
+ const childSet = new Set(childValues);
81
+ const missing = baseValues.filter((v) => !childSet.has(v));
82
+ const extra = childValues.filter((v) => !baseSet.has(v));
83
+ if (missing.length === 0 && extra.length === 0) {
84
+ return 'The copy is currently identical and will drift when the schema changes.';
85
+ }
86
+ const parts = [];
87
+ if (missing.length > 0) {
88
+ parts.push(`missing from child: ${missing.join(', ')}`);
89
+ }
90
+ if (extra.length > 0) {
91
+ parts.push(`extra in child: ${extra.join(', ')}`);
92
+ }
93
+ return parts.join('; ') + '.';
94
+ }
95
+ /**
96
+ * Evaluate shadowing conflicts for a skipped user-editable model file.
97
+ */
98
+ export function evaluateShadowingConflicts(existingRaw, conflicts) {
99
+ const stripped = stripPhpComments(existingRaw);
100
+ const warnings = [];
101
+ for (const conflict of conflicts) {
102
+ if (hasIntentionalShadowMarker(existingRaw, conflict.property)) {
103
+ continue;
104
+ }
105
+ if (!new RegExp(conflict.pattern).test(stripped)) {
106
+ continue;
107
+ }
108
+ let message = conflict.hint;
109
+ if (conflict.baseValues && conflict.baseValues.length > 0) {
110
+ const childValues = parseRedeclaredStringArray(existingRaw, conflict.property);
111
+ const drift = formatShadowingDrift(conflict.baseValues, childValues);
112
+ if (drift) {
113
+ message += ` ${drift}`;
114
+ }
115
+ }
116
+ warnings.push(message);
117
+ }
118
+ return warnings;
119
+ }
@@ -34,10 +34,7 @@ export interface GeneratedFile {
34
34
  * Each entry is matched against the content of the file we skipped, and
35
35
  * `hint` is printed when it matches.
36
36
  */
37
- readonly conflicts?: readonly {
38
- readonly pattern: string;
39
- readonly hint: string;
40
- }[];
37
+ readonly conflicts?: readonly ShadowingPropertyConflict[];
41
38
  }
42
39
  /** Create a base file (always overwritten). */
43
40
  export declare function baseFile(path: string, content: string): GeneratedFile;
@@ -45,10 +42,7 @@ export declare function baseFile(path: string, content: string): GeneratedFile;
45
42
  export declare function userFile(path: string, content: string, expects?: {
46
43
  needle: string;
47
44
  hint: string;
48
- }, conflicts?: readonly {
49
- pattern: string;
50
- hint: string;
51
- }[]): GeneratedFile;
45
+ }, conflicts?: readonly ShadowingPropertyConflict[]): GeneratedFile;
52
46
  /**
53
47
  * Eloquent array properties that REPLACE the parent's when redeclared in a
54
48
  * subclass, rather than merging. Redeclaring any of these in a user-editable
@@ -56,17 +50,13 @@ export declare function userFile(path: string, content: string, expects?: {
56
50
  * completely silent. Issue #171.
57
51
  */
58
52
  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
- }[];
53
+ export type ShadowingEloquentProperty = (typeof SHADOWING_ELOQUENT_PROPERTIES)[number];
54
+ export type ShadowingPropertyConflict = {
55
+ readonly property: ShadowingEloquentProperty;
56
+ readonly pattern: string;
57
+ readonly hint: string;
58
+ readonly baseValues?: readonly string[];
59
+ };
70
60
  /** Category of base file for modular path resolution. */
71
61
  export type BaseCategory = 'Models' | 'Controllers' | 'Requests' | 'Resources' | 'Services' | 'Enums' | 'Traits' | 'Locales' | 'Policies';
72
62
  /**
package/dist/php/types.js CHANGED
@@ -33,27 +33,6 @@ export const SHADOWING_ELOQUENT_PROPERTIES = [
33
33
  'touches',
34
34
  'with',
35
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
- }));
56
- }
57
36
  /**
58
37
  * Resolve base file path for a schema based on structure.
59
38
  * Modular: {modules.path}/{SchemaName}/{category}/{fileName}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "6.7.6",
3
+ "version": "6.7.9",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",