@postxl/cli 1.0.13 → 1.1.0

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.
@@ -38,44 +38,19 @@ const fs = __importStar(require("node:fs/promises"));
38
38
  const path = __importStar(require("node:path"));
39
39
  const Generator = __importStar(require("@postxl/generator"));
40
40
  const generator_1 = require("@postxl/generator");
41
- const baseDevDependencies = [
41
+ const devDependencies = [
42
+ { packageName: Generator.toPostXlPackageName('@postxl/cli') },
42
43
  { packageName: Generator.toPostXlPackageName('@postxl/generator') },
43
44
  { packageName: Generator.toPostXlPackageName('@postxl/generators') },
44
45
  { packageName: Generator.toPostXlPackageName('@postxl/schema') },
45
- { packageName: '@typescript-eslint/eslint-plugin', version: '8.46.3' },
46
- { packageName: '@typescript-eslint/parser', version: '8.46.3' },
47
- { packageName: 'commander', version: '12.1.0' },
46
+ { packageName: 'prettier', version: '3.7.4' },
47
+ { packageName: 'eslint', version: '9.39.2' },
48
48
  ];
49
- const workspaceDevDependencies = [
50
- { packageName: '@prisma/client', version: '6.19.0' },
51
- { packageName: 'chokidar-cli', version: '3.0.0' },
52
- { packageName: 'eslint', version: '9.39.1' },
53
- { packageName: 'eslint-plugin-simple-import-sort', version: '12.1.1' },
54
- { packageName: 'jest', version: '29.7.0' },
55
- { packageName: 'prettier', version: '3.4.2' },
56
- { packageName: 'prisma', version: '6.19.0' },
57
- { packageName: 'rimraf', version: '6.1.0' },
58
- ];
59
- const baseScripts = [
60
- { name: 'prettier:check', command: 'prettier --check "**/*.{ts,tsx}" --config ../../prettier.config.js' },
61
- { name: 'pxl', command: 'node ../../packages/cli/dist/index.js' },
62
- ];
63
- const getWorkspaceScripts = (slug) => [
64
- { name: 'docker', command: './scripts/docker.sh' },
49
+ const scripts = [
65
50
  { name: 'generate', command: 'pnpm run generate:project && pnpm run generate:prisma' },
51
+ { name: 'generate:project', command: 'pnpm run generate:project:pxl' },
52
+ { name: 'generate:project:pxl', command: 'pnpm pxl generate' },
66
53
  { name: 'generate:prisma', command: 'prisma generate' },
67
- { name: 'generate:project', command: 'pnpm run generate:project:pxl && pnpm run generate:project:tsr' },
68
- { name: 'generate:project:pxl', command: 'node ../../packages/cli/dist/index.js generate' },
69
- { name: 'generate:project:tsr', command: `pnpm --filter @postxl/${slug}-frontend exec tsr generate` },
70
- {
71
- name: 'generate:watch',
72
- command: "chokidar '../../packages/**/dist/**' '../../generators/**/dist/**' '../../generators/**/template/**' './generate.ts' './postxl-schema.json' -c 'pnpm run generate:project:pxl -f -i && pnpm run generate:project:tsr'",
73
- },
74
- { name: 'setup', command: './scripts/setup.sh' },
75
- ];
76
- // TODO: Adjust this for standalone projects once full standalone generation is implemented!
77
- const standaloneScripts = [
78
- { name: 'generate', command: 'node ../../packages/cli/dist/index.js generate' },
79
54
  ];
80
55
  const tsConfig = {
81
56
  compilerOptions: {
@@ -114,9 +89,9 @@ const generators = [
114
89
  'e2eGenerator',
115
90
  'frontendAdminGenerator',
116
91
  { imports: 'Frontend', code: 'Frontend.configureFrontendGenerator(Frontend.all)' },
117
- 'frontendFormsGenerator',
118
92
  'frontendTablesGenerator',
119
93
  'frontendTrpcClientGenerator',
94
+ 'frontendFormsGenerator',
120
95
  'mockDataGenerator',
121
96
  'seedDataGenerator',
122
97
  'typesGenerator',
@@ -127,7 +102,7 @@ const generators = [
127
102
  */
128
103
  async function createProject(options) {
129
104
  const { projectPath, name, slug, schema } = options;
130
- const targetPath = projectPath.kind === 'standalone' ? projectPath.workspaceProjectPath : projectPath.projectGenerationPath;
105
+ const targetPath = projectPath;
131
106
  // Create target directory if it doesn't exist
132
107
  await fs.mkdir(targetPath, { recursive: true });
133
108
  // Update schema with new name and slug
@@ -136,7 +111,7 @@ async function createProject(options) {
136
111
  ...schema,
137
112
  name,
138
113
  slug,
139
- projectType: projectPath.kind,
114
+ projectType: 'standalone',
140
115
  };
141
116
  // Write source files directly to disk (not tracked as generated files)
142
117
  // These are the files that define the project, not generated output
@@ -148,14 +123,13 @@ async function createProject(options) {
148
123
  name: Generator.toPostXlPackageName(`@postxl/${slug}`),
149
124
  description: `A PostXL project for ${name}.`,
150
125
  dependencies: [],
151
- devDependencies: [...baseDevDependencies, ...(projectPath.kind === 'workspace' ? workspaceDevDependencies : [])],
152
- scripts: [...baseScripts, ...(projectPath.kind === 'workspace' ? getWorkspaceScripts(slug) : standaloneScripts)],
126
+ devDependencies,
127
+ scripts,
128
+ isInPostXlWorkspace: false,
129
+ packageManager: 'pnpm@10.26.1',
153
130
  };
154
131
  await fs.writeFile(path.join(targetPath, 'package.json'), await (0, generator_1.format)({ path: 'package.json', content: Generator.generatePackageJson(packageJsonConfig) }));
155
- // Generate .env and .env.example with database connection and target generation path
156
- const relativeGenerationPath = path.relative(targetPath, projectPath.projectGenerationPath) || '.';
157
- const envContent = `DATABASE_CONNECTION=postgresql://postgres:postgres@localhost:5432/${slug}\n` +
158
- `PXL_PROJECT_PATH=${relativeGenerationPath}\n`;
132
+ const envContent = `DATABASE_CONNECTION=postgresql://postgres:postgres@localhost:5432/${slug}`;
159
133
  await fs.writeFile(path.join(targetPath, '.env.example'), envContent);
160
134
  await fs.writeFile(path.join(targetPath, '.env'), envContent);
161
135
  }
@@ -1,13 +1,3 @@
1
1
  import { Command } from 'commander';
2
2
  export declare function register(program: Command): void;
3
- export type ProjectPath = ProjectPath_Standalone | ProjectPath_Workspace;
4
- type ProjectPath_Standalone = {
5
- kind: 'standalone';
6
- workspaceProjectPath: string;
7
- projectGenerationPath: string;
8
- };
9
- type ProjectPath_Workspace = {
10
- kind: 'workspace';
11
- projectGenerationPath: string;
12
- };
13
- export {};
3
+ export type ProjectPath = string;
@@ -43,6 +43,15 @@ const schema_1 = require("@postxl/schema");
43
43
  const utils_1 = require("@postxl/utils");
44
44
  const create_project_generator_1 = require("./create-project/create-project.generator");
45
45
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
46
+ async function run(command, options) {
47
+ const { stdout, stderr } = await execAsync(command, options);
48
+ if (stdout) {
49
+ console.log(stdout);
50
+ }
51
+ if (stderr) {
52
+ console.error(stderr);
53
+ }
54
+ }
46
55
  function register(program) {
47
56
  program
48
57
  .command('create-project')
@@ -51,12 +60,14 @@ function register(program) {
51
60
  .description('Creates a new PostXL project with the simple schema template.')
52
61
  .option('-n, --name <name>', 'Project name')
53
62
  .option('-s, --slug <slug>', 'Project slug')
54
- .option('-p, --project-path <path>', 'Path where the generated project should be written. If not specified, a workspace project will be created in the projects folder')
63
+ .option('-p, --project-path <path>', 'Path where the generated project should be written. If not specified, user will be prompted')
55
64
  .option('--skip-git', 'Skip initialing a git repository in the generated project directory')
56
65
  .option('--skip-generate', 'Skip running the initial project generation')
66
+ .option('-l, --link-postxl', 'Link project to local PostXL monorepo packages (for development purposes)')
57
67
  .action(async (options) => {
58
68
  try {
59
69
  await checkNodeVersion();
70
+ const link = options.linkPostxl === true;
60
71
  const name = await getProjectName(options.name);
61
72
  const slug = await getProjectSlug(options.slug, name);
62
73
  const schema = getProjectSchema();
@@ -68,18 +79,24 @@ function register(program) {
68
79
  projectPath,
69
80
  });
70
81
  // In case it is a workspace project, we install dependencies in the workspace part - else in the project
71
- await installDependencies(projectPath.kind === 'workspace' ? projectPath.projectGenerationPath : projectPath.workspaceProjectPath);
82
+ await installDependencies({ targetPath: projectPath, link });
72
83
  if (!options.skipGenerate) {
73
- await runGenerate(projectPath);
74
- await installDependencies(projectPath.projectGenerationPath);
75
- await generatePrismaClient(projectPath.projectGenerationPath);
76
- if (!options.skipInitGit && projectPath.kind === 'standalone') {
77
- await initializeGitRepository(projectPath.projectGenerationPath);
84
+ // We initially only install the PostXL generator - without prettier, eslint and eslint rules. Hence, we run the first generation
85
+ // without transformations. This generates the full package.json with all scripts and dependencies.
86
+ await runGenerate({ projectPath, transform: false });
87
+ // After the generation, we now have the full package.json including devDependencies for prettier, eslint, etc.
88
+ // So we install dependencies again to get those.
89
+ await installDependencies({ targetPath: projectPath, link });
90
+ // The second generation runs with transformations - setting up prettier, eslint, etc.
91
+ await runGenerate({ projectPath, transform: true });
92
+ await generatePrismaClient(projectPath);
93
+ if (!options.skipInitGit) {
94
+ await initializeGitRepository(projectPath);
78
95
  }
79
96
  }
80
- console.log(`\n✓ Project "${name}" created successfully at ${projectPath.projectGenerationPath}`);
97
+ console.log(`\n✓ Project "${name}" created successfully at ${projectPath}`);
81
98
  console.log('\nNext steps:');
82
- console.log(` cd ${path.relative(process.cwd(), projectPath.projectGenerationPath)}`);
99
+ console.log(` cd ${path.relative(process.cwd(), projectPath)}`);
83
100
  console.log(' pnpm run generate # generates the project to your configured path');
84
101
  console.log(' pnpm run dev # in backend/');
85
102
  console.log(' pnpm run dev # in frontend/');
@@ -117,53 +134,30 @@ function getProjectSchema() {
117
134
  return schema_1.sampleSchemas.simple;
118
135
  }
119
136
  /**
120
- * If projectPath is provided:
121
- * - if it is local within projects, check if it exists, if yes, throw error. if not, create it and return workspace project
122
- * - if it is outside of the cwd (regardless of local or absolute), check if exists. if so, throw error, if not create and return standalone project
123
137
  * If projectPath is not provided:
124
- * - Ask user if it should be standalone (in ../{slug}) or workspace (in ./projects/{slug}). Default is standalone.
125
- * - Create the folder and return the appropriate project path
138
+ * - Prompt user for path if it should be standalone (in ../{slug}) or workspace (in ./projects/{slug}). Default is standalone.
139
+ * With projectPath:
140
+ * - if it is local within projects, throw error as we do not support workspace project anymore.
141
+ * - if it is outside of the cwd (regardless of local or absolute), check if exists. if so, throw error, if not create and return-
126
142
  */
127
143
  async function resolveProjectPath({ slug, projectPath, }) {
128
144
  const repositoryRoot = process.cwd();
129
- const projectsFolder = path.join(repositoryRoot, 'projects');
130
145
  const defaultStandalonePath = path.resolve(repositoryRoot, '..', slug);
131
- const workspaceProjectPath = path.join(projectsFolder, slug);
132
- if (projectPath) {
133
- const projectGenerationPath = path.isAbsolute(projectPath) ? projectPath : path.resolve(process.cwd(), projectPath);
134
- await createFolderOrThrow(projectGenerationPath);
135
- const isInProjectsFolder = projectGenerationPath.startsWith(projectsFolder + path.sep);
136
- if (isInProjectsFolder) {
137
- return { kind: 'workspace', projectGenerationPath };
138
- }
139
- else {
140
- await createFolderOrThrow(workspaceProjectPath);
141
- return {
142
- kind: 'standalone',
143
- workspaceProjectPath,
144
- projectGenerationPath,
145
- };
146
- }
146
+ if (!projectPath) {
147
+ projectPath = await prompt('Project path', defaultStandalonePath);
147
148
  }
148
- // No projectPath provided, ask user
149
- const selectedOption = await select('Where should the project be created?', [`Standalone project at ${defaultStandalonePath}`, `Workspace project at ${workspaceProjectPath}`], 0);
150
- const useWorkspace = selectedOption === 1;
151
- if (useWorkspace) {
152
- await createFolderOrThrow(workspaceProjectPath);
153
- return {
154
- kind: 'workspace',
155
- projectGenerationPath: workspaceProjectPath,
156
- };
149
+ if (!projectPath) {
150
+ console.error('Project path is required!');
151
+ process.exit(1);
157
152
  }
158
- else {
159
- await createFolderOrThrow(workspaceProjectPath);
160
- await createFolderOrThrow(defaultStandalonePath);
161
- return {
162
- kind: 'standalone',
163
- workspaceProjectPath,
164
- projectGenerationPath: defaultStandalonePath,
165
- };
153
+ const projectGenerationPath = path.isAbsolute(projectPath) ? projectPath : path.resolve(repositoryRoot, projectPath);
154
+ const projectsFolder = path.join(repositoryRoot, 'projects');
155
+ const isInProjectsFolder = projectGenerationPath.startsWith(projectsFolder + path.sep);
156
+ if (isInProjectsFolder) {
157
+ throw new Error('Creating workspace projects is no longer supported. Please provide a different path outside of the "postxl" repo folder!');
166
158
  }
159
+ await createFolderOrThrow(projectGenerationPath);
160
+ return projectGenerationPath;
167
161
  }
168
162
  async function createFolderOrThrow(targetPath) {
169
163
  try {
@@ -177,7 +171,7 @@ async function createFolderOrThrow(targetPath) {
177
171
  }
178
172
  }
179
173
  async function createProjectStructure({ name, slug, schema, projectPath, }) {
180
- console.log(`\nCreating project "${name}" at ${projectPath.projectGenerationPath}...`);
174
+ console.log(`\nCreating project "${name}" at ${projectPath}...`);
181
175
  await (0, create_project_generator_1.createProject)({
182
176
  name: (0, schema_1.toProjectSchemaName)(name),
183
177
  slug: (0, schema_1.toProjectSlug)(slug),
@@ -186,16 +180,13 @@ async function createProjectStructure({ name, slug, schema, projectPath, }) {
186
180
  });
187
181
  console.log('Project files created successfully!');
188
182
  }
189
- async function installDependencies(path) {
183
+ async function installDependencies({ targetPath, link }) {
190
184
  console.log('\nInstalling dependencies...');
191
185
  try {
192
- const { stdout, stderr } = await execAsync('pnpm install', { cwd: path });
193
- if (stdout) {
194
- console.log(stdout);
195
- }
196
- if (stderr) {
197
- console.error(stderr);
186
+ if (link) {
187
+ await linkPostXlPackages(targetPath);
198
188
  }
189
+ await run('pnpm install', { cwd: targetPath });
199
190
  console.log('Dependencies installed successfully!');
200
191
  }
201
192
  catch (error) {
@@ -203,20 +194,94 @@ async function installDependencies(path) {
203
194
  console.log('You can manually run "pnpm install" in the project directory.');
204
195
  }
205
196
  }
206
- async function runGenerate(projectPath) {
207
- console.log('\nGenerating project code...');
197
+ async function fileExists(filePath) {
198
+ return fs
199
+ .access(filePath)
200
+ .then(() => true)
201
+ .catch(() => false);
202
+ }
203
+ async function readPackageJson(packageJsonPath) {
204
+ if (!(await fileExists(packageJsonPath))) {
205
+ return null;
206
+ }
207
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
208
+ return JSON.parse(content);
209
+ }
210
+ function extractPostXlPackages(packageJson) {
211
+ const filterPostXl = (deps) => Object.keys(deps ?? {}).filter((pkg) => pkg.startsWith('@postxl/'));
212
+ return {
213
+ deps: filterPostXl(packageJson.dependencies),
214
+ devDeps: filterPostXl(packageJson.devDependencies),
215
+ };
216
+ }
217
+ function toLocalPackagePaths(packages, relativePathToMonorepo) {
218
+ return packages.map((pkg) => {
219
+ const packageName = pkg.replace('@postxl/', '');
220
+ return path.join(relativePathToMonorepo, 'packages', packageName);
221
+ });
222
+ }
223
+ async function linkPackagesInDir(dir, packages, relativePathToMonorepo, workspaceFlag) {
224
+ const { deps, devDeps } = packages;
225
+ if (deps.length === 0 && devDeps.length === 0) {
226
+ return;
227
+ }
228
+ console.log(` Linking in ${dir}...`);
229
+ // Link regular dependencies
230
+ if (deps.length > 0) {
231
+ const localPaths = toLocalPackagePaths(deps, relativePathToMonorepo);
232
+ await run(`pnpm add ${localPaths.join(' ')}${workspaceFlag}`, { cwd: dir });
233
+ }
234
+ // Link devDependencies
235
+ if (devDeps.length > 0) {
236
+ const localPaths = toLocalPackagePaths(devDeps, relativePathToMonorepo);
237
+ await run(`pnpm add -D ${localPaths.join(' ')}${workspaceFlag}`, { cwd: dir });
238
+ }
239
+ }
240
+ async function linkPostXlPackages(targetPath) {
241
+ console.log('\nLinking PostXL packages...');
208
242
  try {
209
- const path = projectPath.kind === 'standalone' ? projectPath.workspaceProjectPath : projectPath.projectGenerationPath;
210
- const { stdout, stderr } = await execAsync('pnpm run generate:project -f', {
211
- cwd: path,
212
- env: { ...process.env, PXL_PROJECT_PATH: projectPath.projectGenerationPath },
213
- });
214
- if (stdout) {
215
- console.log(stdout);
243
+ const isWorkspace = await fileExists(path.join(targetPath, 'pnpm-workspace.yaml'));
244
+ const workspaceFlag = isWorkspace ? ' -w' : '';
245
+ const packageJsonPaths = [
246
+ { dir: targetPath, packageJson: path.join(targetPath, 'package.json') },
247
+ { dir: path.join(targetPath, 'frontend'), packageJson: path.join(targetPath, 'frontend', 'package.json') },
248
+ { dir: path.join(targetPath, 'backend'), packageJson: path.join(targetPath, 'backend', 'package.json') },
249
+ ];
250
+ let totalLinked = 0;
251
+ for (const { dir, packageJson: packageJsonPath } of packageJsonPaths) {
252
+ const packageJson = await readPackageJson(packageJsonPath);
253
+ if (!packageJson) {
254
+ continue;
255
+ }
256
+ const packages = extractPostXlPackages(packageJson);
257
+ const count = packages.deps.length + packages.devDeps.length;
258
+ if (count === 0) {
259
+ continue;
260
+ }
261
+ totalLinked += count;
262
+ const relPath = path.relative(dir, process.cwd()).replaceAll('\\', '/');
263
+ // For subdirs (frontend/backend), don't use workspace flag
264
+ const flag = dir === targetPath ? workspaceFlag : '';
265
+ await linkPackagesInDir(dir, packages, relPath, flag);
216
266
  }
217
- if (stderr) {
218
- console.error(stderr);
267
+ if (totalLinked === 0) {
268
+ console.log('No @postxl/* packages found.');
269
+ return;
219
270
  }
271
+ console.log('PostXL packages linked successfully!');
272
+ }
273
+ catch (error) {
274
+ console.error('Failed to link PostXL packages:', error);
275
+ console.log('You can manually run the linking commands in the project directory.');
276
+ }
277
+ }
278
+ async function runGenerate({ projectPath, transform, }) {
279
+ console.log('\nGenerating project code...');
280
+ try {
281
+ await run(`pnpm run generate:project:pxl -f${transform ? '' : ' -t'}`, {
282
+ cwd: projectPath,
283
+ env: { ...process.env, PXL_PROJECT_PATH: projectPath },
284
+ });
220
285
  console.log('Project code generated successfully!');
221
286
  }
222
287
  catch (error) {
@@ -224,17 +289,10 @@ async function runGenerate(projectPath) {
224
289
  console.log('You can manually run "pnpm run generate" in the project directory.');
225
290
  }
226
291
  }
227
- async function generatePrismaClient(path) {
228
- // 9. Generate Prisma
292
+ async function generatePrismaClient(targetPath) {
229
293
  console.log('\nGenerating Prisma client...');
230
294
  try {
231
- const { stdout, stderr } = await execAsync('pnpm run generate:prisma', { cwd: path });
232
- if (stdout) {
233
- console.log(stdout);
234
- }
235
- if (stderr) {
236
- console.error(stderr);
237
- }
295
+ await run('pnpm run generate:prisma', { cwd: targetPath });
238
296
  console.log('Prisma client generated successfully!');
239
297
  }
240
298
  catch (error) {
@@ -242,10 +300,10 @@ async function generatePrismaClient(path) {
242
300
  console.log('You can manually run "pnpm run generate:prisma" in the project directory.');
243
301
  }
244
302
  }
245
- async function initializeGitRepository(path) {
303
+ async function initializeGitRepository(targetPath) {
246
304
  console.log('\nInitializing git repository...');
247
305
  try {
248
- await execAsync('git init -b main', { cwd: path });
306
+ await run('git init -b main', { cwd: targetPath });
249
307
  console.log('Git repository initialized successfully!');
250
308
  }
251
309
  catch (error) {
@@ -293,41 +351,3 @@ async function prompt(question, defaultValue) {
293
351
  });
294
352
  });
295
353
  }
296
- // Helper to select from a list
297
- async function select(question, choices, defaultIndex = 0) {
298
- if (choices.length === 0) {
299
- throw new Error('No choices available');
300
- }
301
- if (defaultIndex < 0 || defaultIndex >= choices.length) {
302
- defaultIndex = 0;
303
- }
304
- console.log(`\n${question}`);
305
- for (const [index, choice] of choices.entries()) {
306
- const num = index + 1;
307
- const marker = index === defaultIndex ? ' (default)' : '';
308
- console.log(` ${num}. ${choice}${marker}`);
309
- }
310
- const rl = readline.createInterface({
311
- input: process.stdin,
312
- output: process.stdout,
313
- });
314
- return new Promise((resolve) => {
315
- const promptText = `\nEnter your choice (number) [default ${defaultIndex + 1}]: `;
316
- rl.question(promptText, (answer) => {
317
- rl.close();
318
- const trimmed = answer.trim();
319
- if (!trimmed) {
320
- resolve(defaultIndex);
321
- return;
322
- }
323
- const index = Number.parseInt(trimmed, 10) - 1;
324
- if (index >= 0 && index < choices.length) {
325
- resolve(index);
326
- }
327
- else {
328
- console.log('Invalid choice, using default option.');
329
- resolve(defaultIndex);
330
- }
331
- });
332
- });
333
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/cli",
3
- "version": "1.0.13",
3
+ "version": "1.1.0",
4
4
  "description": "Command-line interface for PostXL code generation framework",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -44,8 +44,8 @@
44
44
  "commander": "14.0.2",
45
45
  "dotenv": "17.2.3",
46
46
  "zod-validation-error": "3.4.0",
47
- "@postxl/generator": "^1.0.8",
48
- "@postxl/generators": "^1.2.0",
47
+ "@postxl/generator": "^1.0.9",
48
+ "@postxl/generators": "^1.2.1",
49
49
  "@postxl/schema": "^1.0.3",
50
50
  "@postxl/utils": "^1.0.1"
51
51
  },