@omnifyjp/ts 5.8.17 → 5.8.19

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
@@ -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, findAppRootAncestor } 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,37 @@ program
226
229
  phpSkipped++;
227
230
  continue;
228
231
  }
232
+ // Issue #98 v5.8.18 / #99 v5.8.19: skip user-editable stub
233
+ // when a project-owned sibling with the same basename exists
234
+ // elsewhere under the project's `app/` root. Two patterns
235
+ // covered:
236
+ // 1. Domain subfolder organization (#98 v5.8.18): project
237
+ // hand-writes `app/Http/Requests/Admin/BrandStoreRequest.php`;
238
+ // omnify wants to emit the flat default
239
+ // `app/Http/Requests/BrandStoreRequest.php`. The flat
240
+ // stub is unused noise.
241
+ // 2. v4 → v5 upgrade (#99 v5.8.19): project has v4 wrapper at
242
+ // `app/Services/Omnify/<Name>Service.php`; v5 wants to
243
+ // emit at the new modular path
244
+ // `app/Omnify/Services/<Name>Service.php`. Two wrappers
245
+ // compete; the v4 one carries the project's actual logic.
246
+ //
247
+ // Scope: scan from the project's `app/` directory (walked up
248
+ // from the target). Falls back to the file's parent dir when
249
+ // no `app/` ancestor is found (e.g. unusual rootPath layout).
250
+ // Only applies to user-editable files (`overwrite=false`); base
251
+ // files always regenerate.
252
+ if (!file.overwrite) {
253
+ const scanRoot = findAppRootAncestor(filePath) ?? dirname(filePath);
254
+ const collision = findSiblingWithSameBasename(scanRoot, basename(filePath), filePath);
255
+ if (collision) {
256
+ phpSkippedSibling++;
257
+ if (skippedSiblingExamples.length < 3) {
258
+ skippedSiblingExamples.push(`${relative(configDir, filePath)} (sibling: ${relative(configDir, collision)})`);
259
+ }
260
+ continue;
261
+ }
262
+ }
229
263
  writeFileSync(filePath, file.content, 'utf-8');
230
264
  if (file.overwrite || !existsSync(filePath)) {
231
265
  phpOverwritten++;
@@ -239,6 +273,15 @@ program
239
273
  console.log(` ${phpCreated} files created (user-editable)`);
240
274
  if (phpSkipped > 0)
241
275
  console.log(` ${phpSkipped} files skipped (already exist)`);
276
+ if (phpSkippedSibling > 0) {
277
+ console.log(` ${phpSkippedSibling} user-editable stub(s) skipped (project sibling with same name found in subfolder)`);
278
+ for (const ex of skippedSiblingExamples) {
279
+ console.log(` e.g. ${ex}`);
280
+ }
281
+ if (phpSkippedSibling > skippedSiblingExamples.length) {
282
+ console.log(` ... and ${phpSkippedSibling - skippedSiblingExamples.length} more`);
283
+ }
284
+ }
242
285
  // Orphan cleanup: when a schema is removed, renamed, flipped from
243
286
  // kind:object → kind:pivot, or opted out via `options.service: false`,
244
287
  // its previously-emitted `*ServiceBase.php` becomes orphan and
@@ -0,0 +1,42 @@
1
+ /**
2
+ * User-editable stub collision detection. Issue #98 v5.8.18 / #99 v5.8.19.
3
+ *
4
+ * Many Laravel projects organize Requests / Resources / Services by
5
+ * domain subfolder (`Admin/`, `Cms/`, `Audit/`) BEFORE adopting omnify,
6
+ * OR they upgrade from v4 with wrappers at v4 paths
7
+ * (`app/Services/Omnify/<Name>Service.php`) and v5 wants to emit at
8
+ * the new modular path (`app/Omnify/Services/<Name>Service.php`). In
9
+ * both cases the auto-emitted user-editable stub is unused noise that
10
+ * regenerates every `omnify generate`, forcing the dev to delete it.
11
+ *
12
+ * Detection: scan a configurable root directory recursively for any
13
+ * file with the same basename. If found at a path != target, skip
14
+ * emission.
15
+ *
16
+ * Scope decision:
17
+ * - Wide scope (e.g. project's `app/`): finds cross-layer wrappers
18
+ * like the v4 → v5 upgrade case (#99).
19
+ * - Narrow scope (file's parent dir): finds same-layer siblings only,
20
+ * like a domain-organized `Admin/Brand.php` next to the flat
21
+ * default (#98 v5.8.18).
22
+ *
23
+ * The CLI passes whichever scope matches the layout. Bounded by
24
+ * `MAX_FILES_SCANNED` so a project with tens of thousands of files
25
+ * doesn't grind generate to a halt.
26
+ */
27
+ /**
28
+ * Walk up from `filePath` looking for the closest ancestor directory
29
+ * named `app`. Returns its absolute path or null. Used by the CLI to
30
+ * pick a project-wide search root for sibling-skip — covers v4 → v5
31
+ * cross-layer wrappers (e.g. v4 wrapper at `app/Services/Omnify/<X>Service.php`
32
+ * vs v5 modular emit at `app/Omnify/Services/<X>Service.php`). Capped
33
+ * at 12 levels of climbing so we don't traverse the entire FS on a
34
+ * malformed input.
35
+ */
36
+ export declare function findAppRootAncestor(filePath: string): string | null;
37
+ /**
38
+ * Scan `rootDir` recursively for a file whose basename matches
39
+ * `baseName`, excluding `excludePath` itself. Returns the first
40
+ * match's full path or null.
41
+ */
42
+ export declare function findSiblingWithSameBasename(rootDir: string, baseName: string, excludePath: string): string | null;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * User-editable stub collision detection. Issue #98 v5.8.18 / #99 v5.8.19.
3
+ *
4
+ * Many Laravel projects organize Requests / Resources / Services by
5
+ * domain subfolder (`Admin/`, `Cms/`, `Audit/`) BEFORE adopting omnify,
6
+ * OR they upgrade from v4 with wrappers at v4 paths
7
+ * (`app/Services/Omnify/<Name>Service.php`) and v5 wants to emit at
8
+ * the new modular path (`app/Omnify/Services/<Name>Service.php`). In
9
+ * both cases the auto-emitted user-editable stub is unused noise that
10
+ * regenerates every `omnify generate`, forcing the dev to delete it.
11
+ *
12
+ * Detection: scan a configurable root directory recursively for any
13
+ * file with the same basename. If found at a path != target, skip
14
+ * emission.
15
+ *
16
+ * Scope decision:
17
+ * - Wide scope (e.g. project's `app/`): finds cross-layer wrappers
18
+ * like the v4 → v5 upgrade case (#99).
19
+ * - Narrow scope (file's parent dir): finds same-layer siblings only,
20
+ * like a domain-organized `Admin/Brand.php` next to the flat
21
+ * default (#98 v5.8.18).
22
+ *
23
+ * The CLI passes whichever scope matches the layout. Bounded by
24
+ * `MAX_FILES_SCANNED` so a project with tens of thousands of files
25
+ * doesn't grind generate to a halt.
26
+ */
27
+ import { existsSync, readdirSync, statSync } from 'node:fs';
28
+ import { basename, dirname, join } from 'node:path';
29
+ const MAX_FILES_SCANNED = 5000;
30
+ /**
31
+ * Walk up from `filePath` looking for the closest ancestor directory
32
+ * named `app`. Returns its absolute path or null. Used by the CLI to
33
+ * pick a project-wide search root for sibling-skip — covers v4 → v5
34
+ * cross-layer wrappers (e.g. v4 wrapper at `app/Services/Omnify/<X>Service.php`
35
+ * vs v5 modular emit at `app/Omnify/Services/<X>Service.php`). Capped
36
+ * at 12 levels of climbing so we don't traverse the entire FS on a
37
+ * malformed input.
38
+ */
39
+ export function findAppRootAncestor(filePath) {
40
+ let cur = dirname(filePath);
41
+ for (let i = 0; i < 12; i++) {
42
+ if (basename(cur) === 'app')
43
+ return cur;
44
+ const parent = dirname(cur);
45
+ if (parent === cur)
46
+ break;
47
+ cur = parent;
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * Scan `rootDir` recursively for a file whose basename matches
53
+ * `baseName`, excluding `excludePath` itself. Returns the first
54
+ * match's full path or null.
55
+ */
56
+ export function findSiblingWithSameBasename(rootDir, baseName, excludePath) {
57
+ let scanned = 0;
58
+ const stack = [rootDir];
59
+ while (stack.length > 0) {
60
+ if (scanned >= MAX_FILES_SCANNED)
61
+ return null;
62
+ const dir = stack.pop();
63
+ if (!existsSync(dir))
64
+ continue;
65
+ let entries;
66
+ try {
67
+ entries = readdirSync(dir);
68
+ }
69
+ catch {
70
+ continue;
71
+ }
72
+ for (const entry of entries) {
73
+ scanned++;
74
+ if (scanned >= MAX_FILES_SCANNED)
75
+ return null;
76
+ const fullPath = join(dir, entry);
77
+ let stats;
78
+ try {
79
+ stats = statSync(fullPath);
80
+ }
81
+ catch {
82
+ continue;
83
+ }
84
+ if (stats.isDirectory()) {
85
+ stack.push(fullPath);
86
+ }
87
+ else if (entry === baseName && fullPath !== excludePath) {
88
+ return fullPath;
89
+ }
90
+ }
91
+ }
92
+ return null;
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "5.8.17",
3
+ "version": "5.8.19",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",