@omnifyjp/omnify 5.8.17 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/omnify",
3
- "version": "5.8.17",
3
+ "version": "5.8.18",
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": "5.8.17",
40
- "@omnifyjp/omnify-darwin-x64": "5.8.17",
41
- "@omnifyjp/omnify-linux-x64": "5.8.17",
42
- "@omnifyjp/omnify-linux-arm64": "5.8.17",
43
- "@omnifyjp/omnify-win32-x64": "5.8.17"
39
+ "@omnifyjp/omnify-darwin-arm64": "5.8.18",
40
+ "@omnifyjp/omnify-darwin-x64": "5.8.18",
41
+ "@omnifyjp/omnify-linux-x64": "5.8.18",
42
+ "@omnifyjp/omnify-linux-arm64": "5.8.18",
43
+ "@omnifyjp/omnify-win32-x64": "5.8.18"
44
44
  }
45
45
  }
package/ts-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
@@ -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
+ }