@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 +4 -6
- package/dist/php/model-generator.js +28 -8
- package/dist/php/shadowing-conflicts.d.ts +17 -0
- package/dist/php/shadowing-conflicts.js +119 -0
- package/dist/php/types.d.ts +9 -19
- package/dist/php/types.js +0 -21
- package/package.json +1 -1
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 {
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/dist/php/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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}
|