startx 1.0.92 → 1.1.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.
@@ -26,6 +26,8 @@ export class InitCommand {
26
26
  const prefs = await this.getPrefs({ projectName, options, projects: availableApps });
27
27
  const nonAppPackages = packageList.filter(pkg => pkg.type !== "apps");
28
28
 
29
+ await this.checkTargetDirectory(prefs.directory.workspace);
30
+
29
31
  const config = await this.getConfigPrefs({
30
32
  selectedApps: prefs.selectedApps,
31
33
  packages: nonAppPackages,
@@ -40,7 +42,7 @@ export class InitCommand {
40
42
  const workspaceTags = [...packagePrefs.gTags, "runnable"] as TAGS[];
41
43
  await this.installWorkspace({
42
44
  name: prefs.projectName,
43
- tags: [...workspaceTags, "runnable"],
45
+ tags: workspaceTags,
44
46
  dir: prefs.directory,
45
47
  });
46
48
 
@@ -307,24 +309,48 @@ export class InitCommand {
307
309
 
308
310
  private static async writeVscodeSettings(props: { workspace: string; tags: TAGS[] }) {
309
311
  const usesBiome = props.tags.includes("biome");
312
+ const vscodeDir = path.join(props.workspace, ".vscode");
310
313
 
311
314
  const settings: Record<string, unknown> = {
312
315
  "editor.formatOnSave": true,
313
316
  "editor.defaultFormatter": usesBiome ? "biomejs.biome" : "esbenp.prettier-vscode",
314
317
  "editor.codeActionsOnSave": {
315
- ...(usesBiome ? { "source.organizeImports.biome": "explicit" } : {}),
318
+ ...(usesBiome
319
+ ? {
320
+ "source.organizeImports.biome": "explicit",
321
+ "source.fixAll.biome": "explicit",
322
+ }
323
+ : {}),
316
324
  "source.fixAll.eslint": "explicit",
317
- "js/ts.suggest.autoImports": "explicit",
318
325
  "source.fixAll": "explicit",
319
326
  },
320
327
  "eslint.workingDirectories": [{ "mode": "auto" }],
321
328
  };
322
329
 
323
- await fsTool.writeJSONFile({
324
- dir: path.join(props.workspace, ".vscode"),
325
- file: "settings",
326
- content: settings,
330
+ const extensions = {
331
+ recommendations: ["dbaeumer.vscode-eslint", ...(usesBiome ? ["biomejs.biome"] : ["esbenp.prettier-vscode"])],
332
+ };
333
+
334
+ await Promise.all([
335
+ fsTool.writeJSONFile({ dir: vscodeDir, file: "settings", content: settings }),
336
+ fsTool.writeJSONFile({ dir: vscodeDir, file: "extensions", content: extensions }),
337
+ ]);
338
+ }
339
+
340
+ private static async checkTargetDirectory(workspace: string) {
341
+ const [files, dirs] = await Promise.all([
342
+ fsTool.listFiles({ dir: workspace }),
343
+ fsTool.listDirectories({ dir: workspace }),
344
+ ]);
345
+ if (files.length === 0 && dirs.length === 0) return;
346
+
347
+ const overwrite = await CommonInquirer.confirm({
348
+ message: `Directory "${workspace}" already exists and is not empty. Overwrite?`,
349
+ default: false,
327
350
  });
351
+ if (!overwrite) {
352
+ throw new Error("Aborted: target directory already exists.");
353
+ }
328
354
  }
329
355
 
330
356
  // Helpers
@@ -6,6 +6,7 @@ import fs from "fs/promises";
6
6
  import path from "path";
7
7
  import z from "zod";
8
8
 
9
+ import { DepCheck } from "../configs/deps";
9
10
  import { FileCheck } from "../configs/files";
10
11
  import type { StartXPackageJson, TAGS } from "../types";
11
12
  import { CliUtils, type PackageItem } from "../utils/cli-utils";
@@ -15,6 +16,7 @@ import { CommonInquirer } from "../utils/inquirer";
15
16
  type PackageOptions = {
16
17
  eslint?: boolean;
17
18
  install?: boolean;
19
+ name?: string;
18
20
  };
19
21
 
20
22
  type NewPackageOptions = PackageOptions & {
@@ -39,11 +41,12 @@ export class PackageCommand {
39
41
  )
40
42
  .addCommand(
41
43
  new Command("add")
42
- .description("Add an existing StartX package by name.")
44
+ .description("Add an existing StartX app or package, optionally with a new name.")
43
45
  .argument("[packageName]")
46
+ .option("-n, --name <name>", "override the name for the added package")
44
47
  .option("--eslint", "enable ESLint support for the added package")
45
48
  .option("--no-eslint", "skip ESLint support for the added package")
46
- .option("--no-install", "do not run the package manager after updating ESLint")
49
+ .option("--no-install", "do not run the package manager after updating dependencies")
47
50
  .action(PackageCommand.add.bind(PackageCommand))
48
51
  )
49
52
  .addCommand(
@@ -85,7 +88,7 @@ export class PackageCommand {
85
88
  const selectedName =
86
89
  packageName ??
87
90
  (await CommonInquirer.choose({
88
- message: "Select package to add",
91
+ message: "Select app or package to add",
89
92
  options: availablePackages.map(pkg => pkg.name),
90
93
  mode: "single",
91
94
  required: true,
@@ -96,6 +99,16 @@ export class PackageCommand {
96
99
  throw new Error(`Package "${selectedName}" was not found in the StartX template.`);
97
100
  }
98
101
 
102
+ const templateName = selectedPackage.packageJson?.name ?? selectedPackage.name;
103
+ const overrideName =
104
+ options.name ??
105
+ (await CommonInquirer.getText({
106
+ message: "Name for the new package (leave unchanged to keep the original)",
107
+ name: "overrideName",
108
+ default: templateName,
109
+ schema: packageNameSchema,
110
+ }));
111
+
99
112
  const directory = CliUtils.getDirectory();
100
113
  const eslintEnabled = await this.resolveEslintPreference(options);
101
114
  const packagesToInstall = this.resolvePackageClosure({
@@ -109,15 +122,24 @@ export class PackageCommand {
109
122
  eslintEnabled,
110
123
  });
111
124
 
125
+ await this.checkAndInstallMissingDeps({
126
+ workspace: directory.workspace,
127
+ tags,
128
+ install: options.install,
129
+ });
130
+
112
131
  for (const pkg of packagesToInstall) {
132
+ const isMain = pkg.name === selectedPackage.name;
113
133
  await this.installTemplatePackage({
114
134
  pkg,
115
135
  directory,
116
136
  tags,
137
+ overrideName: isMain ? overrideName : undefined,
138
+ overrideRelativePath: isMain ? this.getDestinationPath(pkg.relativePath, overrideName) : undefined,
117
139
  });
118
140
  }
119
141
 
120
- logger.info(`Package add complete: ${selectedPackage.name}`);
142
+ logger.info(`Done! Run \`pnpm install\` to link the new package.`);
121
143
  }
122
144
 
123
145
  private static async create(packageName: string | undefined, options: NewPackageOptions) {
@@ -136,8 +158,11 @@ export class PackageCommand {
136
158
  throw new Error(`Package directory already exists: ${packageDir}`);
137
159
  }
138
160
 
161
+ const rootPackage = await this.readRootPackage(directory.workspace);
139
162
  const eslintEnabled = await this.resolveEslintPreference(options);
163
+ const vitestEnabled = this.hasDependency(rootPackage, "vitest");
140
164
  const packages = await CliUtils.getPackageList();
165
+
141
166
  await this.ensureTemplatePackage({
142
167
  packages,
143
168
  name: "typescript-config",
@@ -154,8 +179,20 @@ export class PackageCommand {
154
179
  });
155
180
  }
156
181
 
182
+ if (vitestEnabled) {
183
+ await this.ensureTemplatePackage({
184
+ packages,
185
+ name: "vitest-config",
186
+ directory,
187
+ tags: ["common", "node", "vitest"],
188
+ });
189
+ }
190
+
157
191
  await fs.mkdir(path.join(packageDir, "src"), { recursive: true });
158
- await this.writeJson(path.join(packageDir, "package.json"), this.createPackageJson({ name, eslintEnabled }));
192
+ await this.writeJson(
193
+ path.join(packageDir, "package.json"),
194
+ this.createPackageJson({ name, eslintEnabled, vitestEnabled })
195
+ );
159
196
  await fs.writeFile(
160
197
  path.join(packageDir, "tsconfig.json"),
161
198
  `${JSON.stringify(
@@ -181,7 +218,15 @@ export class PackageCommand {
181
218
  );
182
219
  }
183
220
 
221
+ if (vitestEnabled) {
222
+ await fs.writeFile(
223
+ path.join(packageDir, "vitest.config.ts"),
224
+ `import vitestConfig from "vitest-config/node";\n\nexport default vitestConfig;\n`
225
+ );
226
+ }
227
+
184
228
  logger.info(`Created package ${name} at ${path.relative(directory.workspace, packageDir)}`);
229
+ logger.info(`Run \`pnpm install\` to link the new package.`);
185
230
  }
186
231
 
187
232
  private static async resolveEslintPreference(options: PackageOptions) {
@@ -228,6 +273,7 @@ export class PackageCommand {
228
273
  if (this.hasDependency(rootPackage, "@biomejs/biome")) tags.add("biome");
229
274
  if (this.hasDependency(rootPackage, "prettier")) tags.add("prettier");
230
275
  if (this.hasDependency(rootPackage, "vitest")) tags.add("vitest");
276
+ if (this.hasDependency(rootPackage, "tsdown")) tags.add("tsdown");
231
277
 
232
278
  for (const pkg of props.packages) {
233
279
  pkg.packageJson?.startx?.tags?.forEach(tag => tags.add(tag));
@@ -295,15 +341,25 @@ export class PackageCommand {
295
341
  pkg: PackageItem;
296
342
  directory: ReturnType<typeof CliUtils.getDirectory>;
297
343
  tags: TAGS[];
344
+ overrideName?: string;
345
+ overrideRelativePath?: string;
298
346
  }) {
299
347
  if (!props.pkg.packageJson) {
300
348
  throw new Error(`Missing package.json for ${props.pkg.name}`);
301
349
  }
302
350
 
303
- const destination = path.join(props.directory.workspace, props.pkg.relativePath);
351
+ const relativePath = props.overrideRelativePath ?? props.pkg.relativePath;
352
+ const destination = path.join(props.directory.workspace, relativePath);
353
+
304
354
  if (await this.pathExists(path.join(destination, "package.json"))) {
305
- logger.info(`Skipping ${props.pkg.name}; it already exists.`);
306
- return;
355
+ const overwrite = await CommonInquirer.confirm({
356
+ message: `"${relativePath}" already exists. Overwrite?`,
357
+ default: false,
358
+ });
359
+ if (!overwrite) {
360
+ logger.info(`Skipping ${props.pkg.name}.`);
361
+ return;
362
+ }
307
363
  }
308
364
 
309
365
  const tags = new Set<TAGS>([...props.tags, ...(props.pkg.packageJson.startx?.tags ?? [])]);
@@ -314,7 +370,7 @@ export class PackageCommand {
314
370
  const { packageJson, isWorkspace } = FileHandler.handlePackageJson({
315
371
  app: props.pkg.packageJson,
316
372
  tags: Array.from(tags),
317
- name: props.pkg.packageJson.name || props.pkg.name,
373
+ name: props.overrideName ?? props.pkg.packageJson.name ?? props.pkg.name,
318
374
  });
319
375
 
320
376
  if (isWorkspace) {
@@ -329,7 +385,7 @@ export class PackageCommand {
329
385
  exclude: !tags.has("vitest") ? /\.test\.tsx?$/ : undefined,
330
386
  });
331
387
 
332
- logger.info(`Installed ${props.pkg.name}`);
388
+ logger.info(`Installed ${props.overrideName ?? props.pkg.name} at ${relativePath}`);
333
389
  }
334
390
 
335
391
  private static async copyValidatedFilesFromFolder(source: string, destination: string, tags: Set<TAGS>) {
@@ -346,7 +402,7 @@ export class PackageCommand {
346
402
  }
347
403
  }
348
404
 
349
- private static createPackageJson(props: { name: string; eslintEnabled: boolean }) {
405
+ private static createPackageJson(props: { name: string; eslintEnabled: boolean; vitestEnabled: boolean }) {
350
406
  const scripts: Record<string, string> = {
351
407
  typecheck: "tsc --noEmit",
352
408
  clean: "rimraf dist .turbo",
@@ -354,11 +410,21 @@ export class PackageCommand {
354
410
  const devDependencies: Record<string, string> = {
355
411
  "typescript-config": "workspace:*",
356
412
  };
413
+ const ignore: string[] = [];
357
414
 
358
415
  if (props.eslintEnabled) {
359
416
  scripts.lint = "eslint .";
360
417
  scripts["lint:fix"] = "eslint . --fix";
361
418
  devDependencies["eslint-config"] = "workspace:*";
419
+ } else {
420
+ ignore.push("eslint-config");
421
+ }
422
+
423
+ if (props.vitestEnabled) {
424
+ scripts.test = "vitest run";
425
+ devDependencies["vitest-config"] = "workspace:*";
426
+ } else {
427
+ ignore.push("vitest-config");
362
428
  }
363
429
 
364
430
  return {
@@ -371,11 +437,66 @@ export class PackageCommand {
371
437
  startx: {
372
438
  iTags: ["node"],
373
439
  requiredDevDeps: ["typescript-config"],
374
- ...(props.eslintEnabled ? {} : { ignore: ["eslint-config"] }),
440
+ ...(ignore.length > 0 ? { ignore } : {}),
375
441
  },
376
442
  };
377
443
  }
378
444
 
445
+ private static async checkAndInstallMissingDeps(props: {
446
+ workspace: string;
447
+ tags: TAGS[];
448
+ install?: boolean;
449
+ }) {
450
+ const rootPackage = await this.readRootPackage(props.workspace);
451
+ const pnpmWorkspace = await CliUtils.parsePnpmWorkspace({ dir: props.workspace });
452
+
453
+ const missing: Array<{ name: string; version: string; isDev: boolean }> = [];
454
+ for (const [dep, config] of Object.entries(DepCheck)) {
455
+ if (!config.tags.every(tag => props.tags.includes(tag as TAGS))) continue;
456
+ if (config.tags.includes("root")) continue;
457
+ if (this.hasDependency(rootPackage, dep)) continue;
458
+ const version = pnpmWorkspace?.catalog?.[dep] ? "catalog:" : config.version;
459
+ missing.push({ name: dep, version, isDev: config.isDevDependency ?? true });
460
+ }
461
+
462
+ if (missing.length === 0) return;
463
+
464
+ logger.warn(`The following workspace dependencies are required but not installed:`);
465
+ for (const dep of missing) {
466
+ logger.warn(` - ${dep.name}`);
467
+ }
468
+
469
+ const shouldAdd = await CommonInquirer.confirm({
470
+ message: "Add them to the workspace root package.json?",
471
+ default: true,
472
+ });
473
+ if (!shouldAdd) {
474
+ logger.warn("Skipping. Some features may not work correctly without these dependencies.");
475
+ return;
476
+ }
477
+
478
+ for (const dep of missing) {
479
+ if (dep.isDev) {
480
+ (rootPackage.devDependencies as Record<string, string>)[dep.name] = dep.version;
481
+ } else {
482
+ (rootPackage.dependencies as Record<string, string>)[dep.name] = dep.version;
483
+ }
484
+ }
485
+
486
+ await this.writeJson(path.join(props.workspace, "package.json"), rootPackage);
487
+ logger.info("Added missing dependencies to root package.json.");
488
+
489
+ if (props.install !== false) {
490
+ await this.installRootDependencies(props.workspace);
491
+ }
492
+ }
493
+
494
+ private static getDestinationPath(templateRelativePath: string, newName: string): string {
495
+ const parentDir = path.dirname(templateRelativePath);
496
+ const leafName = newName.includes("/") ? newName.split("/").pop()! : newName;
497
+ return path.join(parentDir, leafName);
498
+ }
499
+
379
500
  private static getDefaultPackagePath(name: string) {
380
501
  if (name.startsWith("@")) {
381
502
  const [scope, packageName] = name.split("/");
@@ -16,6 +16,9 @@ export const FileCheck: WHITELIST_FILES = {
16
16
  ".prettierignore": {
17
17
  tags: ["biome"],
18
18
  },
19
+ "README.md": {
20
+ tags: ["never"],
21
+ },
19
22
  "biome.json": {
20
23
  tags: ["biome"],
21
24
  },
@@ -93,7 +93,7 @@ export const scripts: SCRIPT = {
93
93
  tags: ["node", "eslint", "root"],
94
94
  },
95
95
  {
96
- script: "eslint . src/**/*.ts --fix",
96
+ script: "eslint . --fix",
97
97
  tags: ["node", "eslint"],
98
98
  },
99
99
  ],
@@ -118,64 +118,74 @@ export const scripts: SCRIPT = {
118
118
  },
119
119
  ],
120
120
  "db:push": [
121
+ {
122
+ script: "turbo run db:push",
123
+ tags: ["db", "root"],
124
+ },
121
125
  {
122
126
  script: "drizzle-kit push",
123
127
  tags: ["drizzle", "db"],
124
128
  },
129
+ ],
130
+ "db:studio": [
125
131
  {
126
- script: "turbo run db:push",
132
+ script: "turbo run db:studio",
127
133
  tags: ["db", "root"],
128
134
  },
129
- ],
130
- "db:studio": [
131
135
  {
132
136
  script: "drizzle-kit studio",
133
137
  tags: ["drizzle", "db"],
134
138
  },
139
+ ],
140
+ "db:pull": [
135
141
  {
136
- script: "turbo run db:studio",
142
+ script: "turbo run db:pull",
137
143
  tags: ["db", "root"],
138
144
  },
139
- ],
140
- "db:pull": [
141
145
  {
142
146
  script: "drizzle-kit pull",
143
147
  tags: ["drizzle", "db"],
144
148
  },
149
+ ],
150
+ "db:generate": [
145
151
  {
146
- script: "turbo run db:pull",
152
+ script: "turbo run db:generate",
147
153
  tags: ["db", "root"],
148
154
  },
149
- ],
150
- "db:generate": [
151
155
  {
152
156
  script: "drizzle-kit generate",
153
157
  tags: ["drizzle", "db"],
154
158
  },
159
+ ],
160
+ "db:migrate": [
155
161
  {
156
- script: "turbo run db:generate",
162
+ script: "turbo run db:migrate",
157
163
  tags: ["db", "root"],
158
164
  },
159
- ],
160
- "db:migrate": [
161
165
  {
162
166
  script: "drizzle-kit migrate",
163
167
  tags: ["drizzle", "db"],
164
168
  },
169
+ ],
170
+ "db:check": [
165
171
  {
166
- script: "turbo run db:migrate",
172
+ script: "turbo run db:check",
167
173
  tags: ["db", "root"],
168
174
  },
169
- ],
170
- "db:check": [
171
175
  {
172
176
  script: "drizzle-kit check",
173
177
  tags: ["drizzle", "db"],
174
178
  },
179
+ ],
180
+ "db:up": [
175
181
  {
176
- script: "turbo run db:check",
182
+ script: "turbo run db:up",
177
183
  tags: ["db", "root"],
178
184
  },
185
+ {
186
+ script: "drizzle-kit up",
187
+ tags: ["drizzle", "db"],
188
+ },
179
189
  ],
180
190
  "typecheck": [
181
191
  {
@@ -3,6 +3,7 @@ import { logger } from "@repo/logger";
3
3
  import { Command } from "commander";
4
4
 
5
5
  import { InitCommand } from "./commands/init";
6
+ import { PackageCommand } from "./commands/package";
6
7
  import { version } from "../../../package.json";
7
8
 
8
9
  const program = new Command();
@@ -14,6 +15,6 @@ program.command("ping").action(() => {
14
15
  });
15
16
 
16
17
  program.addCommand(InitCommand.command);
17
- // program.addCommand(PackageCommand.command);
18
+ program.addCommand(PackageCommand.command);
18
19
 
19
20
  program.parse(process.argv);
@@ -27,9 +27,9 @@ export class FileHandler {
27
27
  tags: TAGS[];
28
28
  dependencies?: Record<string, string>;
29
29
  }) {
30
- const isWorkspace = !!props.app.devDependencies?.turbo;
30
+ const isWorkspace = props.tags.includes("root");
31
31
 
32
- const tags = isWorkspace ? [...props.tags, "root"] : [...props.tags];
32
+ const tags = [...props.tags];
33
33
  const workspaceAttr: Record<string, unknown> = isWorkspace
34
34
  ? {
35
35
  version: "1.0.0",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "startx",
3
3
  "description": "",
4
- "version": "1.0.92",
4
+ "version": "1.1.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/avinashid/startx.git"