@postxl/cli 1.7.1 → 1.8.1

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.
@@ -40,6 +40,7 @@ const generator_1 = require("@postxl/generator");
40
40
  const generators_1 = require("@postxl/generators");
41
41
  const schema_1 = require("@postxl/schema");
42
42
  const utils_1 = require("@postxl/utils");
43
+ const load_project_schema_1 = require("./helpers/load-project-schema");
43
44
  const log_schema_error_1 = require("./helpers/log-schema-error");
44
45
  function register(program) {
45
46
  program
@@ -56,12 +57,14 @@ This reads the schema and runs only the types generator. The generated type file
56
57
  if (!path.isAbsolute(inputPath)) {
57
58
  inputPath = path.join(process.cwd(), inputPath);
58
59
  }
59
- // Resolve schema path
60
+ // Resolve schema path / project path
60
61
  let schemaPath = '';
62
+ let projectPath;
61
63
  try {
62
64
  const stats = await fs.stat(inputPath);
63
65
  if (stats.isDirectory()) {
64
66
  schemaPath = path.join(inputPath, 'postxl-schema.json');
67
+ projectPath = inputPath;
65
68
  }
66
69
  else if (stats.isFile()) {
67
70
  schemaPath = inputPath;
@@ -79,22 +82,29 @@ This reads the schema and runs only the types generator. The generated type file
79
82
  catch {
80
83
  program.error(`Cannot find schema file at ${schemaPath}`);
81
84
  }
82
- // Parse and validate schema
83
- let jsonSchema;
84
- try {
85
- const content = await fs.readFile(schemaPath, 'utf-8');
86
- jsonSchema = JSON.parse(content);
87
- }
88
- catch (error) {
89
- program.error(`Error reading or parsing JSON from ${schemaPath}: ${error instanceof Error ? error.message : String(error)}`);
85
+ let schema;
86
+ if (projectPath !== undefined) {
87
+ // Directory input: use the shared loader (picks up schema/*.model.json etc.)
88
+ schema = await (0, load_project_schema_1.loadProjectSchema)({ program, projectPath });
90
89
  }
91
- const result = schema_1.zProjectSchema.safeParse(jsonSchema);
92
- if (!result.success) {
93
- console.log('\nError parsing schema:\n');
94
- (0, log_schema_error_1.logSchemaValidationError)(result.error);
95
- process.exit(1);
90
+ else {
91
+ // Single schema file input: parse as-is, no split-file discovery.
92
+ let jsonSchema;
93
+ try {
94
+ const content = await fs.readFile(schemaPath, 'utf-8');
95
+ jsonSchema = JSON.parse(content);
96
+ }
97
+ catch (error) {
98
+ program.error(`Error reading or parsing JSON from ${schemaPath}: ${error instanceof Error ? error.message : String(error)}`);
99
+ }
100
+ const result = schema_1.zProjectSchema.safeParse(jsonSchema);
101
+ if (!result.success) {
102
+ console.log('\nError parsing schema:\n');
103
+ (0, log_schema_error_1.logSchemaValidationError)(result.error);
104
+ process.exit(1);
105
+ }
106
+ schema = result.data;
96
107
  }
97
- const schema = result.data;
98
108
  // Resolve output path
99
109
  let outputPath = options.output;
100
110
  if (!path.isAbsolute(outputPath)) {
@@ -43,9 +43,8 @@ const dotenv_1 = __importDefault(require("dotenv"));
43
43
  const fs = __importStar(require("node:fs/promises"));
44
44
  const path = __importStar(require("node:path"));
45
45
  const generator_1 = require("@postxl/generator");
46
- const schema_1 = require("@postxl/schema");
47
46
  const utils_1 = require("@postxl/utils");
48
- const log_schema_error_1 = require("./helpers/log-schema-error");
47
+ const load_project_schema_1 = require("./helpers/load-project-schema");
49
48
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
50
49
  function register(program) {
51
50
  program
@@ -60,6 +59,7 @@ This reads the schema from ${(0, utils_1.yellow)('schema.json')} and runs the ge
60
59
  .option('-e, --ejected', 'lists ejected files')
61
60
  .option('-f, --force', 'overwrites ejected files')
62
61
  .option('-i, --ignore-errors', 'ignores errors during generation')
62
+ .option('-m, --model <names...>', 'regenerate only for the given models (accepts multiple, e.g. -m Country City)')
63
63
  .option('-n, --no-fix', 'does not attempt to fix linting errors')
64
64
  .option('-p, --pattern <glob>', 'filters files by glob pattern (e.g., "**/*.ts" or "backend/libs/**")')
65
65
  .option('-q, --quiet', 'does not print errors')
@@ -78,7 +78,7 @@ This reads the schema from ${(0, utils_1.yellow)('schema.json')} and runs the ge
78
78
  }
79
79
  const envConfig = dotenv_1.default.config({ path: path.join(projectPath, '.env') });
80
80
  const localGenerators = await getLocalGenerators({ program, projectPath });
81
- const schema = await getSchema({ program, projectPath });
81
+ const schema = await (0, load_project_schema_1.loadProjectSchema)({ program, projectPath });
82
82
  targetPath = await resolveTargetPath({
83
83
  projectPath,
84
84
  targetPath,
@@ -94,10 +94,15 @@ This reads the schema from ${(0, utils_1.yellow)('schema.json')} and runs the ge
94
94
  if (options.pattern) {
95
95
  console.log(`Filtering files by pattern: ${options.pattern}`);
96
96
  }
97
+ const targetModelNames = resolveTargetModelNames({ program, schema, names: options.model });
98
+ if (targetModelNames) {
99
+ console.log(`Restricting generation to models: ${[...targetModelNames].join(', ')}`);
100
+ }
97
101
  const manager = new generator_1.GeneratorManager(localGenerators);
98
102
  const baseContext = (0, generator_1.prepareBaseContext)(schema, {
99
103
  ...options,
100
104
  ...(options.pattern ? { filePattern: options.pattern } : {}),
105
+ ...(targetModelNames ? { targetModelNames } : {}),
101
106
  });
102
107
  const generatedContext = await manager.generate(baseContext);
103
108
  return (new generator_1.Generator(generatedContext)
@@ -126,6 +131,7 @@ This reads the schema from ${(0, utils_1.yellow)('schema.json')} and runs the ge
126
131
  lockFilePath: path.join(targetPath, 'postxl-lock.json'),
127
132
  diskFilePath: targetPath,
128
133
  force: options.force ?? false,
134
+ ...(targetModelNames ? { selectiveGeneration: true } : {}),
129
135
  });
130
136
  (0, generator_1.logSyncResult)(result, {
131
137
  showEjectedStats: options.ejected ?? false,
@@ -223,24 +229,6 @@ async function getLocalGenerators(params) {
223
229
  }
224
230
  return localGenerate();
225
231
  }
226
- async function getSchema(params) {
227
- const { projectPath } = params;
228
- const schemaPath = path.join(projectPath, 'postxl-schema.json');
229
- try {
230
- await fs.access(schemaPath);
231
- }
232
- catch {
233
- params.program.error(`Cannot find file "postxl-schema.json" found at ${schemaPath}`);
234
- }
235
- const jsonSchema = JSON.parse((await fs.readFile(schemaPath)).toString());
236
- const schema = schema_1.zProjectSchema.safeParse(jsonSchema);
237
- if (!schema.success) {
238
- console.log('\nError parsing postxl-schema.json:\n');
239
- (0, log_schema_error_1.logSchemaValidationError)(schema.error);
240
- process.exit(1);
241
- }
242
- return schema.data;
243
- }
244
232
  /**
245
233
  * Checks if the current projectPath is within the PostXL monorepo workspace
246
234
  * by verifying if '../../packages/cli' exists relative to the project path.
@@ -255,6 +243,19 @@ async function isInPostxlWorkspace(projectPath) {
255
243
  return false;
256
244
  }
257
245
  }
246
+ function resolveTargetModelNames(params) {
247
+ const { program, schema, names } = params;
248
+ if (!names || names.length === 0) {
249
+ return undefined;
250
+ }
251
+ const available = [...schema.models.keys()];
252
+ const availableSet = new Set(available);
253
+ const unknown = names.filter((n) => !availableSet.has(n));
254
+ if (unknown.length > 0) {
255
+ program.error(`Model${unknown.length > 1 ? 's' : ''} ${unknown.map((n) => `"${n}"`).join(', ')} ${unknown.length > 1 ? 'are' : 'is'} not defined in this schema. Available models: ${available.join(', ')}`);
256
+ }
257
+ return new Set(names);
258
+ }
258
259
  async function resolveTargetPath({ projectPath, targetPath, envTargetPath, schemaSlug, projectType, }) {
259
260
  let resolved = targetPath ?? envTargetPath;
260
261
  if (!resolved) {
@@ -0,0 +1,14 @@
1
+ import { Command } from 'commander';
2
+ import { ProjectSchema } from '@postxl/schema';
3
+ /**
4
+ * Loads `postxl-schema.json` from the given project path, merges in any
5
+ * per-model / per-enum files under `schema/`, and validates the result
6
+ * against `zProjectSchema`.
7
+ *
8
+ * On validation error, logs the formatted Zod error and exits the process —
9
+ * preserves the behavior of the previous in-command schema loader.
10
+ */
11
+ export declare function loadProjectSchema(params: {
12
+ projectPath: string;
13
+ program: Command;
14
+ }): Promise<ProjectSchema>;
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadProjectSchema = loadProjectSchema;
37
+ const fs = __importStar(require("node:fs/promises"));
38
+ const path = __importStar(require("node:path"));
39
+ const schema_1 = require("@postxl/schema");
40
+ const load_split_schema_files_1 = require("./load-split-schema-files");
41
+ const log_schema_error_1 = require("./log-schema-error");
42
+ /**
43
+ * Loads `postxl-schema.json` from the given project path, merges in any
44
+ * per-model / per-enum files under `schema/`, and validates the result
45
+ * against `zProjectSchema`.
46
+ *
47
+ * On validation error, logs the formatted Zod error and exits the process —
48
+ * preserves the behavior of the previous in-command schema loader.
49
+ */
50
+ async function loadProjectSchema(params) {
51
+ const { projectPath, program } = params;
52
+ const schemaPath = path.join(projectPath, 'postxl-schema.json');
53
+ try {
54
+ await fs.access(schemaPath);
55
+ }
56
+ catch {
57
+ program.error(`Cannot find file "postxl-schema.json" at ${schemaPath}`);
58
+ }
59
+ let rawJson;
60
+ try {
61
+ const content = await fs.readFile(schemaPath, 'utf-8');
62
+ rawJson = JSON.parse(content);
63
+ }
64
+ catch (error) {
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ return program.error(`Error reading or parsing JSON from ${schemaPath}: ${message}`);
67
+ }
68
+ const modelFilesGlob = asStringArray(rawJson.modelFiles);
69
+ const enumFilesGlob = asStringArray(rawJson.enumFiles);
70
+ const split = await (0, load_split_schema_files_1.loadSplitSchemaFiles)({
71
+ projectPath,
72
+ ...(modelFilesGlob ? { modelFilesGlob } : {}),
73
+ ...(enumFilesGlob ? { enumFilesGlob } : {}),
74
+ });
75
+ const mergedInput = {
76
+ ...rawJson,
77
+ models: (0, schema_1.mergeSplitSchemaEntries)({
78
+ inline: rawJson.models,
79
+ fromFiles: toDefinitionMap(split.models),
80
+ kind: 'model',
81
+ fromFilesPaths: toPathMap(split.models),
82
+ }),
83
+ enums: (0, schema_1.mergeSplitSchemaEntries)({
84
+ inline: rawJson.enums,
85
+ fromFiles: toDefinitionMap(split.enums),
86
+ kind: 'enum',
87
+ fromFilesPaths: toPathMap(split.enums),
88
+ }),
89
+ };
90
+ const result = schema_1.zProjectSchema.safeParse(mergedInput);
91
+ if (!result.success) {
92
+ console.log('\nError parsing postxl-schema.json:\n');
93
+ (0, log_schema_error_1.logSchemaValidationError)(result.error);
94
+ process.exit(1);
95
+ }
96
+ return result.data;
97
+ }
98
+ function toDefinitionMap(entries) {
99
+ const out = {};
100
+ for (const [name, { definition }] of Object.entries(entries)) {
101
+ out[name] = definition;
102
+ }
103
+ return out;
104
+ }
105
+ function toPathMap(entries) {
106
+ const out = {};
107
+ for (const [name, { filePath }] of Object.entries(entries)) {
108
+ out[name] = filePath;
109
+ }
110
+ return out;
111
+ }
112
+ function asStringArray(value) {
113
+ if (!Array.isArray(value)) {
114
+ return undefined;
115
+ }
116
+ return value.every((item) => typeof item === 'string') ? value : undefined;
117
+ }
@@ -0,0 +1,19 @@
1
+ export type LoadedEntry = {
2
+ definition: unknown;
3
+ filePath: string;
4
+ };
5
+ /**
6
+ * Globs the project directory for per-model / per-enum JSON files and returns
7
+ * their parsed contents keyed by name (derived from the filename basename).
8
+ *
9
+ * Returns empty maps when no `schema/` directory exists or nothing matches —
10
+ * existing single-file schemas are unaffected.
11
+ */
12
+ export declare function loadSplitSchemaFiles(params: {
13
+ projectPath: string;
14
+ modelFilesGlob?: string[];
15
+ enumFilesGlob?: string[];
16
+ }): Promise<{
17
+ models: Record<string, LoadedEntry>;
18
+ enums: Record<string, LoadedEntry>;
19
+ }>;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadSplitSchemaFiles = loadSplitSchemaFiles;
37
+ const fs = __importStar(require("node:fs/promises"));
38
+ const path = __importStar(require("node:path"));
39
+ const DEFAULT_MODEL_GLOBS = ['schema/*.model.json'];
40
+ const DEFAULT_ENUM_GLOBS = ['schema/*.enum.json'];
41
+ const MODEL_SUFFIX = '.model.json';
42
+ const ENUM_SUFFIX = '.enum.json';
43
+ const FILE_BASENAME_REGEX = /^[a-z][A-Za-z0-9_]*$/;
44
+ /**
45
+ * Globs the project directory for per-model / per-enum JSON files and returns
46
+ * their parsed contents keyed by name (derived from the filename basename).
47
+ *
48
+ * Returns empty maps when no `schema/` directory exists or nothing matches —
49
+ * existing single-file schemas are unaffected.
50
+ */
51
+ async function loadSplitSchemaFiles(params) {
52
+ const { projectPath } = params;
53
+ const modelGlobs = params.modelFilesGlob ?? DEFAULT_MODEL_GLOBS;
54
+ const enumGlobs = params.enumFilesGlob ?? DEFAULT_ENUM_GLOBS;
55
+ const [models, enums] = await Promise.all([
56
+ loadEntries(projectPath, modelGlobs, MODEL_SUFFIX, 'model'),
57
+ loadEntries(projectPath, enumGlobs, ENUM_SUFFIX, 'enum'),
58
+ ]);
59
+ return { models, enums };
60
+ }
61
+ async function loadEntries(projectPath, globs, suffix, kind) {
62
+ const matched = new Set();
63
+ for (const pattern of globs) {
64
+ const iterator = fs.glob(pattern, { cwd: projectPath });
65
+ for await (const relPath of iterator) {
66
+ matched.add(relPath);
67
+ }
68
+ }
69
+ const sorted = [...matched].sort((a, b) => a.localeCompare(b));
70
+ const result = {};
71
+ for (const relPath of sorted) {
72
+ const basename = path.basename(relPath);
73
+ if (!basename.endsWith(suffix)) {
74
+ continue;
75
+ }
76
+ const fileBasename = basename.slice(0, -suffix.length);
77
+ if (!FILE_BASENAME_REGEX.test(fileBasename)) {
78
+ throw new Error(`Invalid ${kind} filename "${basename}" in "${relPath}". ${kind === 'model' ? 'Model' : 'Enum'} files must use camelCase basenames matching [a-z][A-Za-z0-9_]* (e.g. ${kind === 'model' ? 'country.model.json' : 'role.enum.json'}).`);
79
+ }
80
+ const name = fileBasename.charAt(0).toUpperCase() + fileBasename.slice(1);
81
+ const absPath = path.join(projectPath, relPath);
82
+ let raw;
83
+ try {
84
+ raw = await fs.readFile(absPath, 'utf-8');
85
+ }
86
+ catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ throw new Error(`Failed to read ${kind} file "${relPath}": ${message}`);
89
+ }
90
+ let definition;
91
+ try {
92
+ definition = JSON.parse(raw);
93
+ }
94
+ catch (error) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ throw new Error(`Failed to parse JSON in ${kind} file "${relPath}": ${message}`);
97
+ }
98
+ if (definition && typeof definition === 'object' && !Array.isArray(definition)) {
99
+ const obj = definition;
100
+ if ('name' in obj) {
101
+ if (typeof obj.name === 'string' && obj.name !== name) {
102
+ throw new Error(`${kind === 'model' ? 'Model' : 'Enum'} file "${relPath}" has "name": "${obj.name}" but the filename implies "${name}". Remove the "name" field or rename the file.`);
103
+ }
104
+ const { name: _stripped, ...rest } = obj;
105
+ definition = rest;
106
+ }
107
+ }
108
+ if (Object.prototype.hasOwnProperty.call(result, name)) {
109
+ throw new Error(`Duplicate ${kind} name "${name}" — matched by more than one file (last seen: "${relPath}"). Rename one of the files.`);
110
+ }
111
+ result[name] = { definition, filePath: relPath };
112
+ }
113
+ return result;
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/cli",
3
- "version": "1.7.1",
3
+ "version": "1.8.1",
4
4
  "description": "Command-line interface for PostXL code generation framework",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -44,9 +44,9 @@
44
44
  "commander": "14.0.3",
45
45
  "dotenv": "17.3.1",
46
46
  "zod-validation-error": "5.0.0",
47
- "@postxl/generator": "^1.4.1",
48
- "@postxl/generators": "^1.30.1",
49
- "@postxl/schema": "^1.10.1",
47
+ "@postxl/generator": "^1.6.0",
48
+ "@postxl/generators": "^2.0.0",
49
+ "@postxl/schema": "^2.0.0",
50
50
  "@postxl/utils": "^1.4.0"
51
51
  },
52
52
  "devDependencies": {},