@omnifyjp/ts 5.2.2 → 5.2.3

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
@@ -28,7 +28,8 @@ import { resolve, dirname, join } 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
- import { generatePhp } from './php/index.js';
31
+ import { generatePhp, derivePhpConfig } from './php/index.js';
32
+ import { pruneOrphanServiceFiles } from './php/orphan-cleanup.js';
32
33
  import { resolveInput, sniffInputKind } from './input-resolver.js';
33
34
  function resolveFromConfig(configPath) {
34
35
  const raw = readFileSync(configPath, 'utf-8');
@@ -238,6 +239,25 @@ program
238
239
  console.log(` ${phpCreated} files created (user-editable)`);
239
240
  if (phpSkipped > 0)
240
241
  console.log(` ${phpSkipped} files skipped (already exist)`);
242
+ // Orphan cleanup: when a schema is removed, renamed, flipped from
243
+ // kind:object → kind:pivot, or opted out via `options.service: false`,
244
+ // its previously-emitted `*ServiceBase.php` becomes orphan and
245
+ // pollutes Composer autoload. Prune base files automatically; warn
246
+ // about editable `*Service.php` (may contain user code).
247
+ const phpConfig = derivePhpConfig(laravelOverrides);
248
+ const cleanup = pruneOrphanServiceFiles(configDir, phpConfig, phpFiles);
249
+ if (cleanup.prunedBases.length > 0) {
250
+ console.log(` ${cleanup.prunedBases.length} orphan ServiceBase pruned (schema removed/renamed/opted-out)`);
251
+ for (const p of cleanup.prunedBases) {
252
+ console.log(` pruned: ${p}`);
253
+ }
254
+ }
255
+ for (const p of cleanup.warnedEditables) {
256
+ console.warn(`[omnify-ts] orphan editable service ${p} — backing schema is no ` +
257
+ `longer eligible for service codegen (removed / renamed / opted out / ` +
258
+ `flipped to non-object kind). Safe to delete by hand if it has no ` +
259
+ `custom code inside; preserved for now to protect any hand-edits.`);
260
+ }
241
261
  }
242
262
  console.log(`\nGeneration complete.`);
243
263
  });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Orphan cleanup for service codegen.
3
+ *
4
+ * Service generation is opt-out (#81): every project `kind: object` schema
5
+ * gets a `<Name>ServiceBase.php` (regenerated wholesale, no user content)
6
+ * and a `<Name>Service.php` (sinh-1-lần userFile, may contain hand code).
7
+ *
8
+ * When a schema flips from `kind: object` → `kind: pivot`, gets renamed,
9
+ * removed, opted out via `options.service: false`, or hidden, the
10
+ * previously-emitted base service stays on disk forever and pollutes
11
+ * Composer autoload (a class file with no backing schema = mystery code
12
+ * for the next dev to inherit).
13
+ *
14
+ * This module mirrors the existing pivot YAML cleanup pattern
15
+ * (`RemoveOrphanAutoPivots` in the Go layer): scan known service
16
+ * directories, diff against the current generation's expected output set,
17
+ * delete the orphan base files. Editable `*Service.php` files are NEVER
18
+ * deleted — they may contain hand-written user code — instead a console
19
+ * warning is printed so the user can decide whether to clean up by hand.
20
+ */
21
+ import type { GeneratedFile, PhpConfig } from './types.js';
22
+ export interface ServiceCleanupResult {
23
+ /** Absolute paths to `*ServiceBase.php` files that were deleted. */
24
+ prunedBases: string[];
25
+ /** Absolute paths to `*Service.php` files that look orphan but are
26
+ * preserved (may contain user code). Caller should print a warning. */
27
+ warnedEditables: string[];
28
+ }
29
+ /**
30
+ * Delete orphan `*ServiceBase.php` files; flag orphan editable services
31
+ * for warning. Pure-ish: takes the generated file list (= source of
32
+ * truth for "what should exist") and the resolved PhpConfig (= where to
33
+ * look). Filesystem mutations are limited to `unlinkSync` on base files
34
+ * that are demonstrably absent from the generated set.
35
+ */
36
+ export declare function pruneOrphanServiceFiles(configDir: string, config: PhpConfig, generated: readonly GeneratedFile[]): ServiceCleanupResult;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Orphan cleanup for service codegen.
3
+ *
4
+ * Service generation is opt-out (#81): every project `kind: object` schema
5
+ * gets a `<Name>ServiceBase.php` (regenerated wholesale, no user content)
6
+ * and a `<Name>Service.php` (sinh-1-lần userFile, may contain hand code).
7
+ *
8
+ * When a schema flips from `kind: object` → `kind: pivot`, gets renamed,
9
+ * removed, opted out via `options.service: false`, or hidden, the
10
+ * previously-emitted base service stays on disk forever and pollutes
11
+ * Composer autoload (a class file with no backing schema = mystery code
12
+ * for the next dev to inherit).
13
+ *
14
+ * This module mirrors the existing pivot YAML cleanup pattern
15
+ * (`RemoveOrphanAutoPivots` in the Go layer): scan known service
16
+ * directories, diff against the current generation's expected output set,
17
+ * delete the orphan base files. Editable `*Service.php` files are NEVER
18
+ * deleted — they may contain hand-written user code — instead a console
19
+ * warning is printed so the user can decide whether to clean up by hand.
20
+ */
21
+ import { existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
22
+ import { join, resolve, basename } from 'node:path';
23
+ /**
24
+ * Delete orphan `*ServiceBase.php` files; flag orphan editable services
25
+ * for warning. Pure-ish: takes the generated file list (= source of
26
+ * truth for "what should exist") and the resolved PhpConfig (= where to
27
+ * look). Filesystem mutations are limited to `unlinkSync` on base files
28
+ * that are demonstrably absent from the generated set.
29
+ */
30
+ export function pruneOrphanServiceFiles(configDir, config, generated) {
31
+ // Build the absolute-path set of files THIS generation pass produced.
32
+ // Anything matching `*ServiceBase.php` outside this set is orphan.
33
+ const expectedAbs = new Set();
34
+ for (const f of generated) {
35
+ expectedAbs.add(resolve(configDir, f.path));
36
+ }
37
+ const result = { prunedBases: [], warnedEditables: [] };
38
+ // ---- Base service cleanup -----------------------------------------------
39
+ //
40
+ // Search roots cover both layout modes:
41
+ // - Modular: `<modules.path>/<Schema>/Services/<Schema>ServiceBase.php`
42
+ // (one folder per schema; orphan schemas have orphan folders)
43
+ // - Legacy: `<services.basePath>/<Schema>ServiceBase.php`
44
+ // (flat folder; orphan schemas leave loose files)
45
+ //
46
+ // We always scan BOTH so a project that switched modes mid-flight gets
47
+ // the leftover-from-the-other-mode files cleaned too.
48
+ const baseSearchRoots = [];
49
+ if (config.structure === 'modular') {
50
+ const modulesAbs = resolve(configDir, config.modules.path);
51
+ if (existsSync(modulesAbs) && statSync(modulesAbs).isDirectory()) {
52
+ for (const entry of readdirSync(modulesAbs)) {
53
+ const svcDir = join(modulesAbs, entry, 'Services');
54
+ if (existsSync(svcDir) && statSync(svcDir).isDirectory()) {
55
+ baseSearchRoots.push(svcDir);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ baseSearchRoots.push(resolve(configDir, config.services.basePath));
61
+ for (const root of baseSearchRoots) {
62
+ if (!existsSync(root))
63
+ continue;
64
+ for (const entry of readdirSync(root)) {
65
+ if (!entry.endsWith('ServiceBase.php'))
66
+ continue;
67
+ const fullPath = join(root, entry);
68
+ if (expectedAbs.has(fullPath))
69
+ continue;
70
+ try {
71
+ unlinkSync(fullPath);
72
+ result.prunedBases.push(fullPath);
73
+ }
74
+ catch {
75
+ // EPERM / EBUSY / ENOENT — skip. The next generate pass will
76
+ // retry; meanwhile we don't crash the whole codegen pipeline
77
+ // on a single uncleanable file.
78
+ }
79
+ }
80
+ }
81
+ // ---- Editable service warning -------------------------------------------
82
+ //
83
+ // Editable services live at `<services.path>/<Group>/<Schema>Service.php`
84
+ // (group-nested) or `<services.path>/<Schema>Service.php` (no group).
85
+ // The recursive walk skips the `OmnifyBase` subfolder (already covered
86
+ // by the base cleanup above) so we don't double-process or warn on
87
+ // base files masquerading as editable.
88
+ const editableRoot = resolve(configDir, config.services.path);
89
+ walkServiceFiles(editableRoot, (path) => {
90
+ const name = basename(path);
91
+ if (name.endsWith('ServiceBase.php'))
92
+ return;
93
+ if (!name.endsWith('Service.php'))
94
+ return;
95
+ if (expectedAbs.has(path))
96
+ return;
97
+ result.warnedEditables.push(path);
98
+ });
99
+ return result;
100
+ }
101
+ function walkServiceFiles(root, visit) {
102
+ if (!existsSync(root))
103
+ return;
104
+ for (const entry of readdirSync(root)) {
105
+ const full = join(root, entry);
106
+ let info;
107
+ try {
108
+ info = statSync(full);
109
+ }
110
+ catch {
111
+ continue;
112
+ }
113
+ if (info.isDirectory()) {
114
+ // OmnifyBase holds the base files we already process above —
115
+ // skip to avoid the editable scan re-flagging them.
116
+ if (entry === 'OmnifyBase')
117
+ continue;
118
+ walkServiceFiles(full, visit);
119
+ }
120
+ else {
121
+ visit(full);
122
+ }
123
+ }
124
+ }
@@ -27,3 +27,18 @@ import type { GeneratedFile, PhpConfig } from './types.js';
27
27
  * - package-owned schemas (those get services in their owning package)
28
28
  */
29
29
  export declare function generateServices(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
30
+ /**
31
+ * Returns the set of `*ServiceBase.php` filenames the current schema set
32
+ * is expected to produce. The cli.ts orphan-cleanup compares this set
33
+ * against the actual files in the base-services directory and deletes
34
+ * any mismatches — handles the case where a schema is renamed, removed,
35
+ * flipped from `kind: object` to `kind: pivot`, or opted out via
36
+ * `options.service: false`.
37
+ *
38
+ * Filename format mirrors `generateBaseService`: `{PascalSchemaName}ServiceBase.php`.
39
+ *
40
+ * Editable `*Service.php` files are NOT in scope — those are user-owned
41
+ * (sinh-1-lần userFile semantics) and may contain hand-written code; the
42
+ * cli prints a warning instead of deleting them.
43
+ */
44
+ export declare function expectedBaseServiceFileNames(reader: SchemaReader): Set<string>;
@@ -31,17 +31,40 @@ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace
31
31
  */
32
32
  export function generateServices(reader, config) {
33
33
  const files = [];
34
+ const candidates = collectServiceCandidates(reader);
35
+ // Emit deprecation warnings once per schema for legacy service-block keys
36
+ // that now duplicate property-level metadata.
37
+ for (const [name, schema] of Object.entries(candidates)) {
38
+ warnLegacyServiceKeys(name, schema);
39
+ }
40
+ for (const [name, schema] of Object.entries(candidates)) {
41
+ files.push(...generateForSchema(name, schema, reader, config));
42
+ }
43
+ return files;
44
+ }
45
+ /**
46
+ * Collect the set of schemas that should produce a service (base + editable
47
+ * pair) on this generation pass. Shared by `generateServices` (which emits
48
+ * the files) and `expectedBaseServiceFileNames` (which drives the orphan
49
+ * cleanup in cli.ts). Keeping a single source of truth for the candidate
50
+ * filter means the cleanup can never disagree with the emitter — when a
51
+ * schema flips from `kind: object` to `kind: pivot`, the previously-emitted
52
+ * `*ServiceBase.php` becomes orphan and the cleanup deletes it on the next
53
+ * regeneration.
54
+ *
55
+ * #81 + #80 interaction (v3.23.1): iterate every project-owned schema
56
+ * directly instead of going through `getProjectObjectSchemas()`, which
57
+ * filters out KindPartial. Phantom-upstream extends (#80 self-reference
58
+ * fallback — e.g. `kind: extend, target: Product` with no upstream
59
+ * package loaded) are kept as KindPartial so downstream generators
60
+ * skip emitting tables for them, but their SERVICES still belong in
61
+ * the consumer project and must be generated here. Kind=object stays
62
+ * the opt-out default; Kind=partial/extend is included when the user
63
+ * has signalled service intent (legacy options.service block or any
64
+ * property-level service flag).
65
+ */
66
+ function collectServiceCandidates(reader) {
34
67
  const candidates = {};
35
- // #81 + #80 interaction (v3.23.1): iterate every project-owned schema
36
- // directly instead of going through `getProjectObjectSchemas()`, which
37
- // filters out KindPartial. Phantom-upstream extends (#80 self-reference
38
- // fallback — e.g. `kind: extend, target: Product` with no upstream
39
- // package loaded) are kept as KindPartial so downstream generators
40
- // skip emitting tables for them, but their SERVICES still belong in
41
- // the consumer project and must be generated here. Kind=object stays
42
- // the opt-out default; Kind=partial/extend is included when the user
43
- // has signalled service intent (legacy options.service block or any
44
- // property-level service flag).
45
68
  const allSchemas = reader.getSchemas();
46
69
  for (const [name, schema] of Object.entries(allSchemas)) {
47
70
  // Project-owned only — packages emit their own services in the
@@ -89,15 +112,29 @@ export function generateServices(reader, config) {
89
112
  candidates[name] = schema;
90
113
  }
91
114
  }
92
- // Emit deprecation warnings once per schema for legacy service-block keys
93
- // that now duplicate property-level metadata.
94
- for (const [name, schema] of Object.entries(candidates)) {
95
- warnLegacyServiceKeys(name, schema);
96
- }
97
- for (const [name, schema] of Object.entries(candidates)) {
98
- files.push(...generateForSchema(name, schema, reader, config));
115
+ return candidates;
116
+ }
117
+ /**
118
+ * Returns the set of `*ServiceBase.php` filenames the current schema set
119
+ * is expected to produce. The cli.ts orphan-cleanup compares this set
120
+ * against the actual files in the base-services directory and deletes
121
+ * any mismatches — handles the case where a schema is renamed, removed,
122
+ * flipped from `kind: object` to `kind: pivot`, or opted out via
123
+ * `options.service: false`.
124
+ *
125
+ * Filename format mirrors `generateBaseService`: `{PascalSchemaName}ServiceBase.php`.
126
+ *
127
+ * Editable `*Service.php` files are NOT in scope — those are user-owned
128
+ * (sinh-1-lần userFile semantics) and may contain hand-written code; the
129
+ * cli prints a warning instead of deleting them.
130
+ */
131
+ export function expectedBaseServiceFileNames(reader) {
132
+ const candidates = collectServiceCandidates(reader);
133
+ const out = new Set();
134
+ for (const name of Object.keys(candidates)) {
135
+ out.add(`${toPascalCase(name)}ServiceBase.php`);
99
136
  }
100
- return files;
137
+ return out;
101
138
  }
102
139
  /**
103
140
  * #81 + #80 (v3.23.1): detect "user wants a service for this schema" signal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "5.2.2",
3
+ "version": "5.2.3",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",