@omnifyjp/ts 5.8.16 → 5.8.18
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 +36 -1
- package/dist/php/resource-generator.js +29 -18
- package/dist/php/sibling-skip.d.ts +29 -0
- package/dist/php/sibling-skip.js +69 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -24,12 +24,13 @@
|
|
|
24
24
|
* omnify-ts --update # force re-fetch remote input
|
|
25
25
|
*/
|
|
26
26
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
27
|
-
import { resolve, dirname, join } from 'node:path';
|
|
27
|
+
import { resolve, dirname, basename, join, relative } from 'node:path';
|
|
28
28
|
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
32
|
import { pruneOrphanServiceFiles } from './php/orphan-cleanup.js';
|
|
33
|
+
import { findSiblingWithSameBasename } from './php/sibling-skip.js';
|
|
33
34
|
import { resolveInput, sniffInputKind } from './input-resolver.js';
|
|
34
35
|
function resolveFromConfig(configPath) {
|
|
35
36
|
const raw = readFileSync(configPath, 'utf-8');
|
|
@@ -219,6 +220,8 @@ program
|
|
|
219
220
|
let phpCreated = 0;
|
|
220
221
|
let phpOverwritten = 0;
|
|
221
222
|
let phpSkipped = 0;
|
|
223
|
+
let phpSkippedSibling = 0;
|
|
224
|
+
const skippedSiblingExamples = [];
|
|
222
225
|
for (const file of phpFiles) {
|
|
223
226
|
const filePath = resolve(configDir, file.path);
|
|
224
227
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
@@ -226,6 +229,29 @@ program
|
|
|
226
229
|
phpSkipped++;
|
|
227
230
|
continue;
|
|
228
231
|
}
|
|
232
|
+
// Issue #98 v5.8.18: skip user-editable stub when a project-owned
|
|
233
|
+
// sibling with the same basename exists elsewhere in the layer's
|
|
234
|
+
// editable subtree (e.g. project organizes Requests by domain
|
|
235
|
+
// folder `Admin/BrandStoreRequest.php` while omnify wanted to
|
|
236
|
+
// emit the FLAT default stub `BrandStoreRequest.php` at the
|
|
237
|
+
// layer root). The flat stub is unused noise in that layout,
|
|
238
|
+
// and every `omnify generate` would otherwise re-create it,
|
|
239
|
+
// forcing the dev to delete it manually after each run.
|
|
240
|
+
// Only applies to user-editable files (overwrite=false); base
|
|
241
|
+
// files always regenerate. Scope: scan ONLY the file's parent
|
|
242
|
+
// directory recursively — not sibling directories — so a
|
|
243
|
+
// grouped `Cms/<Name>.php` stub doesn't get skipped by an
|
|
244
|
+
// unrelated `Admin/<Name>.php`.
|
|
245
|
+
if (!file.overwrite) {
|
|
246
|
+
const collision = findSiblingWithSameBasename(dirname(filePath), basename(filePath), filePath);
|
|
247
|
+
if (collision) {
|
|
248
|
+
phpSkippedSibling++;
|
|
249
|
+
if (skippedSiblingExamples.length < 3) {
|
|
250
|
+
skippedSiblingExamples.push(`${relative(configDir, filePath)} (sibling: ${relative(configDir, collision)})`);
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
229
255
|
writeFileSync(filePath, file.content, 'utf-8');
|
|
230
256
|
if (file.overwrite || !existsSync(filePath)) {
|
|
231
257
|
phpOverwritten++;
|
|
@@ -239,6 +265,15 @@ program
|
|
|
239
265
|
console.log(` ${phpCreated} files created (user-editable)`);
|
|
240
266
|
if (phpSkipped > 0)
|
|
241
267
|
console.log(` ${phpSkipped} files skipped (already exist)`);
|
|
268
|
+
if (phpSkippedSibling > 0) {
|
|
269
|
+
console.log(` ${phpSkippedSibling} user-editable stub(s) skipped (project sibling with same name found in subfolder)`);
|
|
270
|
+
for (const ex of skippedSiblingExamples) {
|
|
271
|
+
console.log(` e.g. ${ex}`);
|
|
272
|
+
}
|
|
273
|
+
if (phpSkippedSibling > skippedSiblingExamples.length) {
|
|
274
|
+
console.log(` ... and ${phpSkippedSibling - skippedSiblingExamples.length} more`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
242
277
|
// Orphan cleanup: when a schema is removed, renamed, flipped from
|
|
243
278
|
// kind:object → kind:pivot, or opted out via `options.service: false`,
|
|
244
279
|
// its previously-emitted `*ServiceBase.php` becomes orphan and
|
|
@@ -46,7 +46,7 @@ function generateBaseResource(name, schema, reader, config) {
|
|
|
46
46
|
if (isHiddenByDefault(type) || hidden)
|
|
47
47
|
continue;
|
|
48
48
|
if (type === 'Association') {
|
|
49
|
-
addAssociationFields(propName, prop, fields, resourceNamespace, modelNamespace, reader);
|
|
49
|
+
addAssociationFields(propName, prop, fields, resourceNamespace, modelNamespace, reader, config);
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
52
|
if (expandedProperties[propName]) {
|
|
@@ -193,27 +193,38 @@ class ${editable.className} extends ${baseAlias}
|
|
|
193
193
|
`;
|
|
194
194
|
return userFile(`${editable.path}/${editable.fileName}`, content);
|
|
195
195
|
}
|
|
196
|
-
function addAssociationFields(propName, prop, fields, resourceNamespace, modelNamespace, reader) {
|
|
196
|
+
function addAssociationFields(propName, prop, fields, resourceNamespace, modelNamespace, reader, config) {
|
|
197
197
|
const relation = prop['relation'] ?? '';
|
|
198
198
|
const target = prop['target'] ?? '';
|
|
199
199
|
const methodName = toCamelCase(propName);
|
|
200
|
-
// Resolve resource namespace for the target
|
|
201
|
-
// Issue #98 v5.8.12
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
200
|
+
// Resolve resource namespace for the target.
|
|
201
|
+
// Issue #98 v5.8.12 / #99 v5.8.17: cross-resource refs match where
|
|
202
|
+
// the target Resource file actually lives. Same pattern as the
|
|
203
|
+
// v5.8.10/11 model + service / controller fixes — point at the
|
|
204
|
+
// EDITABLE so app code consistently resolves the same class:
|
|
205
|
+
// 1. Package target → keep the package's resource namespace.
|
|
206
|
+
// 2. Project target on modular structure → use the resource
|
|
207
|
+
// namespace as-is (modular has its own per-schema isolation).
|
|
208
|
+
// 3. `userEditableGroupByFolder: true` → nest editable namespace
|
|
209
|
+
// by group so the FQCN matches the grouped editable file.
|
|
210
|
+
// 4. Default (flat editable) → use the editable namespace as-is.
|
|
211
|
+
// v4 projects with `resource.namespace: App\\Http\\Resources`
|
|
212
|
+
// single-tier flat layout (#99) hit this path and stay flat.
|
|
213
|
+
const packageResNs = target ? reader.resolveResourceNamespace(target, resourceNamespace) : resourceNamespace;
|
|
214
|
+
let targetResNs = packageResNs;
|
|
215
|
+
if (packageResNs && packageResNs === resourceNamespace) {
|
|
216
|
+
// Project-owned target: switch to editable namespace + apply
|
|
217
|
+
// optional group nesting. Modular structure aligns base + editable
|
|
218
|
+
// on the same namespace so this is a no-op.
|
|
219
|
+
const editableNs = config.resources.userEditableNamespace || resourceNamespace;
|
|
214
220
|
const targetSchema = target ? reader.getSchema(target) : undefined;
|
|
215
|
-
if (
|
|
216
|
-
|
|
221
|
+
if (config.structure !== 'modular'
|
|
222
|
+
&& config.resources.userEditableGroupByFolder
|
|
223
|
+
&& targetSchema?.group) {
|
|
224
|
+
targetResNs = `${editableNs}\\${toPascalCase(targetSchema.group)}`;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
targetResNs = editableNs;
|
|
217
228
|
}
|
|
218
229
|
}
|
|
219
230
|
// If target is a package schema without a resource namespace, use simple whenLoaded
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-editable stub collision detection. Issue #98 v5.8.18.
|
|
3
|
+
*
|
|
4
|
+
* Many Laravel projects organize Requests / Resources / Services by
|
|
5
|
+
* domain subfolder (`Admin/`, `Cms/`, `Audit/`, etc.) BEFORE adopting
|
|
6
|
+
* omnify. When omnify then emits the FLAT default stub at the layer
|
|
7
|
+
* root (e.g. `app/Http/Requests/BrandStoreRequest.php`), the stub
|
|
8
|
+
* collides ergonomically with the project's hand-written
|
|
9
|
+
* `app/Http/Requests/Admin/BrandStoreRequest.php` — same basename,
|
|
10
|
+
* unused by any caller, gets re-created every `omnify generate`,
|
|
11
|
+
* forcing the dev to delete it manually.
|
|
12
|
+
*
|
|
13
|
+
* Detection: scan the file's PARENT DIRECTORY recursively for any
|
|
14
|
+
* file with the same basename. If found at a path != target, skip
|
|
15
|
+
* emission.
|
|
16
|
+
*
|
|
17
|
+
* Scope rule: scan ONLY the file's parent directory (and its subdirs),
|
|
18
|
+
* NOT sibling directories. So a grouped editable at `Cms/Banner.php`
|
|
19
|
+
* isn't skipped by an unrelated `Admin/Banner.php`.
|
|
20
|
+
*
|
|
21
|
+
* Bounded by `MAX_FILES_SCANNED` so a project root with tens of
|
|
22
|
+
* thousands of files doesn't grind generate to a halt.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Scan `rootDir` recursively for a file whose basename matches
|
|
26
|
+
* `baseName`, excluding `excludePath` itself. Returns the first
|
|
27
|
+
* match's full path or null.
|
|
28
|
+
*/
|
|
29
|
+
export declare function findSiblingWithSameBasename(rootDir: string, baseName: string, excludePath: string): string | null;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-editable stub collision detection. Issue #98 v5.8.18.
|
|
3
|
+
*
|
|
4
|
+
* Many Laravel projects organize Requests / Resources / Services by
|
|
5
|
+
* domain subfolder (`Admin/`, `Cms/`, `Audit/`, etc.) BEFORE adopting
|
|
6
|
+
* omnify. When omnify then emits the FLAT default stub at the layer
|
|
7
|
+
* root (e.g. `app/Http/Requests/BrandStoreRequest.php`), the stub
|
|
8
|
+
* collides ergonomically with the project's hand-written
|
|
9
|
+
* `app/Http/Requests/Admin/BrandStoreRequest.php` — same basename,
|
|
10
|
+
* unused by any caller, gets re-created every `omnify generate`,
|
|
11
|
+
* forcing the dev to delete it manually.
|
|
12
|
+
*
|
|
13
|
+
* Detection: scan the file's PARENT DIRECTORY recursively for any
|
|
14
|
+
* file with the same basename. If found at a path != target, skip
|
|
15
|
+
* emission.
|
|
16
|
+
*
|
|
17
|
+
* Scope rule: scan ONLY the file's parent directory (and its subdirs),
|
|
18
|
+
* NOT sibling directories. So a grouped editable at `Cms/Banner.php`
|
|
19
|
+
* isn't skipped by an unrelated `Admin/Banner.php`.
|
|
20
|
+
*
|
|
21
|
+
* Bounded by `MAX_FILES_SCANNED` so a project root with tens of
|
|
22
|
+
* thousands of files doesn't grind generate to a halt.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
const MAX_FILES_SCANNED = 5000;
|
|
27
|
+
/**
|
|
28
|
+
* Scan `rootDir` recursively for a file whose basename matches
|
|
29
|
+
* `baseName`, excluding `excludePath` itself. Returns the first
|
|
30
|
+
* match's full path or null.
|
|
31
|
+
*/
|
|
32
|
+
export function findSiblingWithSameBasename(rootDir, baseName, excludePath) {
|
|
33
|
+
let scanned = 0;
|
|
34
|
+
const stack = [rootDir];
|
|
35
|
+
while (stack.length > 0) {
|
|
36
|
+
if (scanned >= MAX_FILES_SCANNED)
|
|
37
|
+
return null;
|
|
38
|
+
const dir = stack.pop();
|
|
39
|
+
if (!existsSync(dir))
|
|
40
|
+
continue;
|
|
41
|
+
let entries;
|
|
42
|
+
try {
|
|
43
|
+
entries = readdirSync(dir);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
scanned++;
|
|
50
|
+
if (scanned >= MAX_FILES_SCANNED)
|
|
51
|
+
return null;
|
|
52
|
+
const fullPath = join(dir, entry);
|
|
53
|
+
let stats;
|
|
54
|
+
try {
|
|
55
|
+
stats = statSync(fullPath);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (stats.isDirectory()) {
|
|
61
|
+
stack.push(fullPath);
|
|
62
|
+
}
|
|
63
|
+
else if (entry === baseName && fullPath !== excludePath) {
|
|
64
|
+
return fullPath;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|