create-nextjs-cms 0.5.66 → 0.5.68

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.
Files changed (70) hide show
  1. package/dist/cli/index.d.ts +9 -0
  2. package/dist/cli/index.d.ts.map +1 -0
  3. package/dist/cli/index.js +77 -0
  4. package/dist/helpers/check-directory.d.ts +6 -0
  5. package/dist/helpers/check-directory.d.ts.map +1 -0
  6. package/dist/helpers/check-directory.js +47 -0
  7. package/dist/{lib → helpers}/exec-utils.d.ts +1 -0
  8. package/dist/helpers/exec-utils.d.ts.map +1 -0
  9. package/dist/{lib → helpers}/exec-utils.js +2 -1
  10. package/dist/helpers/get-version.d.ts +2 -0
  11. package/dist/helpers/get-version.d.ts.map +1 -0
  12. package/dist/helpers/get-version.js +11 -0
  13. package/dist/helpers/logger.d.ts.map +1 -0
  14. package/dist/helpers/render-title.d.ts +3 -0
  15. package/dist/helpers/render-title.d.ts.map +1 -0
  16. package/dist/{lib → helpers}/render-title.js +10 -0
  17. package/dist/{lib → helpers}/utils.d.ts +9 -0
  18. package/dist/helpers/utils.d.ts.map +1 -0
  19. package/dist/{lib → helpers}/utils.js +42 -0
  20. package/dist/index.js +49 -235
  21. package/dist/lib/cms-setup.d.ts +1 -2
  22. package/dist/lib/cms-setup.d.ts.map +1 -1
  23. package/dist/lib/cms-setup.js +9 -13
  24. package/dist/lib/create-project.d.ts +9 -0
  25. package/dist/lib/create-project.d.ts.map +1 -0
  26. package/dist/lib/create-project.js +69 -0
  27. package/dist/lib/git.d.ts +6 -0
  28. package/dist/lib/git.d.ts.map +1 -0
  29. package/dist/lib/git.js +116 -0
  30. package/dist/lib/install-deps.d.ts.map +1 -1
  31. package/dist/lib/install-deps.js +13 -14
  32. package/dist/lib/validate-app-name.d.ts +2 -0
  33. package/dist/lib/validate-app-name.d.ts.map +1 -0
  34. package/dist/lib/validate-app-name.js +10 -0
  35. package/package.json +1 -1
  36. package/templates/default/app/(rootLayout)/(plugins)/[...slug]/PluginClient.tsx +55 -0
  37. package/templates/default/app/(rootLayout)/(plugins)/[...slug]/page.tsx +41 -0
  38. package/templates/default/app/(rootLayout)/(plugins)/[...slug]/plugin-registry.ts +16 -0
  39. package/templates/default/app/(rootLayout)/page.tsx +6 -5
  40. package/templates/default/app/_trpc/client.ts +1 -1
  41. package/templates/default/app/api/trpc/[trpc]/route.ts +3 -3
  42. package/templates/default/cms.config.ts +1 -2
  43. package/templates/default/components/AdminCard.tsx +1 -1
  44. package/templates/default/components/AdminPrivilegeCard.tsx +1 -1
  45. package/templates/default/components/CategorySectionSelectInput.tsx +1 -1
  46. package/templates/default/components/DashboardPage.tsx +1 -1
  47. package/templates/default/components/EmailCard.tsx +1 -1
  48. package/templates/default/components/EmailPasswordForm.tsx +1 -1
  49. package/templates/default/components/EmailQuotaForm.tsx +1 -1
  50. package/templates/default/components/EmailsPage.tsx +1 -1
  51. package/templates/default/components/NewAdminForm.tsx +1 -1
  52. package/templates/default/components/NewEmailForm.tsx +1 -1
  53. package/templates/default/components/SectionItemCard.tsx +1 -1
  54. package/templates/default/components/Sidebar.tsx +1 -1
  55. package/templates/default/components/form/Form.tsx +1 -1
  56. package/templates/default/components/form/helpers/_section-hot-reload.js +1 -1
  57. package/templates/default/package.json +2 -1
  58. package/dist/lib/exec-utils.d.ts.map +0 -1
  59. package/dist/lib/global-package.d.ts +0 -10
  60. package/dist/lib/global-package.d.ts.map +0 -1
  61. package/dist/lib/global-package.js +0 -158
  62. package/dist/lib/logger.d.ts.map +0 -1
  63. package/dist/lib/render-title.d.ts +0 -2
  64. package/dist/lib/render-title.d.ts.map +0 -1
  65. package/dist/lib/utils.d.ts.map +0 -1
  66. package/templates/default/app/(rootLayout)/analytics/page.tsx +0 -7
  67. package/templates/default/app/(rootLayout)/dashboard/page.tsx +0 -7
  68. package/templates/default/app/(rootLayout)/emails/page.tsx +0 -6
  69. /package/dist/{lib → helpers}/logger.d.ts +0 -0
  70. /package/dist/{lib → helpers}/logger.js +0 -0
@@ -0,0 +1,9 @@
1
+ interface CliResults {
2
+ appName: string;
3
+ sectionsToAdd: ('blog' | 'category' | 'simple')[];
4
+ git: boolean;
5
+ databaseProvider: 'mysql';
6
+ }
7
+ export declare const runCli: () => Promise<CliResults>;
8
+ export {};
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAOA,UAAU,UAAU;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,CAAC,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAC,EAAE,CAAA;IACjD,GAAG,EAAE,OAAO,CAAA;IACZ,gBAAgB,EAAE,OAAO,CAAA;CAC5B;AASD,eAAO,MAAM,MAAM,QAAa,OAAO,CAAC,UAAU,CAwEjD,CAAA"}
@@ -0,0 +1,77 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { validateAppName } from '../lib/validate-app-name.js';
4
+ import { Command } from 'commander';
5
+ import { removeTrailingSlash } from '../helpers/utils.js';
6
+ import { getCreateNextjsCmsVersion } from '../helpers/get-version.js';
7
+ const defaultOptions = {
8
+ appName: 'my-cms-app',
9
+ sectionsToAdd: [],
10
+ git: false,
11
+ databaseProvider: 'mysql',
12
+ };
13
+ export const runCli = async () => {
14
+ const version = getCreateNextjsCmsVersion();
15
+ const program = new Command('create-nextjs-cms')
16
+ .version(version, '-v, --version', 'Output the current version of create-nextjs-cms.')
17
+ .argument('[directory]', 'The directory to create the project in')
18
+ .usage('[directory] [options]')
19
+ .helpOption('-h, --help', 'Display this help message.')
20
+ .allowUnknownOption()
21
+ .parse(process.argv);
22
+ let providedName = program.args[0];
23
+ if (providedName) {
24
+ providedName = removeTrailingSlash(providedName).trim();
25
+ // Validate name
26
+ const _notValid = validateAppName(providedName);
27
+ if (_notValid !== undefined) {
28
+ p.log.message(`${chalk.gray(`${_notValid}`)}`);
29
+ providedName = undefined;
30
+ }
31
+ }
32
+ const project = await p.group({
33
+ name: providedName
34
+ ? () => Promise.resolve(providedName)
35
+ : () => p.text({
36
+ message: 'What is your project named?',
37
+ placeholder: defaultOptions.appName,
38
+ defaultValue: defaultOptions.appName,
39
+ validate: validateAppName,
40
+ }),
41
+ sections: () => {
42
+ return p.multiselect({
43
+ message: `Add sample sections? ${chalk.gray('(hit enter with no selections to skip)')}`,
44
+ options: [
45
+ { value: 'blog', label: 'Blog' },
46
+ { value: 'category', label: 'Category' },
47
+ { value: 'simple', label: 'Settings' },
48
+ ],
49
+ required: false,
50
+ });
51
+ },
52
+ database: () => {
53
+ return p.select({
54
+ message: 'What database provider would you like to use?',
55
+ options: [{ value: 'mysql', label: 'MySQL', hint: 'Other database providers coming soon' }],
56
+ initialValue: 'mysql',
57
+ });
58
+ },
59
+ git: () => {
60
+ return p.confirm({
61
+ message: 'Should we initialize a Git repository and stage the changes?',
62
+ initialValue: false,
63
+ });
64
+ },
65
+ }, {
66
+ onCancel() {
67
+ p.outro('❌ Aborting installation');
68
+ process.exit(1);
69
+ },
70
+ });
71
+ return {
72
+ appName: project.name,
73
+ sectionsToAdd: project.sections,
74
+ git: project.git,
75
+ databaseProvider: project.database,
76
+ };
77
+ };
@@ -0,0 +1,6 @@
1
+ export declare const resolveDirectory: (appName: string) => Promise<{
2
+ targetDir: string;
3
+ projectName: string;
4
+ targetIsCwd: boolean;
5
+ }>;
6
+ //# sourceMappingURL=check-directory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-directory.d.ts","sourceRoot":"","sources":["../../src/helpers/check-directory.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,gBAAgB,GACzB,SAAS,MAAM,KAChB,OAAO,CAAC;IACP,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,OAAO,CAAA;CACvB,CAwCA,CAAA"}
@@ -0,0 +1,47 @@
1
+ import { expandHome } from './utils.js';
2
+ import path from 'node:path';
3
+ import { resolve } from 'node:path';
4
+ import { basename } from 'node:path';
5
+ import fs from 'fs-extra';
6
+ import { isEmptyDir } from './utils.js';
7
+ import * as p from '@clack/prompts';
8
+ import chalk from 'chalk';
9
+ export const resolveDirectory = async (appName) => {
10
+ // Resolve target path from the caller's CWD
11
+ const rawTarget = expandHome(appName);
12
+ const targetDir = path.isAbsolute(rawTarget) ? rawTarget : resolve(process.cwd(), rawTarget);
13
+ // Derive package name from final path
14
+ const projectName = basename(targetDir);
15
+ const targetIsCwd = path.normalize(targetDir) === path.normalize(process.cwd());
16
+ if (targetIsCwd) {
17
+ // Using current directory (".")
18
+ if (!(await fs.pathExists(targetDir))) {
19
+ await fs.ensureDir(targetDir);
20
+ }
21
+ else if (!(await isEmptyDir(targetDir))) {
22
+ p.log.error('Current directory is not empty. Choose an empty folder or a new directory name.');
23
+ p.log.message(chalk.gray('Tip: You can also specify a directory name like this: \n') +
24
+ chalk.green('pnpm create nextjs-cms ') +
25
+ chalk.italic.magenta('my-app'));
26
+ p.log.message(' ');
27
+ process.exit(1);
28
+ }
29
+ }
30
+ else {
31
+ if (await fs.pathExists(targetDir)) {
32
+ if (!(await isEmptyDir(targetDir))) {
33
+ p.log.error(`Directory "${targetDir}" is not empty.`);
34
+ p.log.message('Please choose an empty directory or a different directory name.');
35
+ process.exit(1);
36
+ }
37
+ }
38
+ else {
39
+ await fs.ensureDir(targetDir);
40
+ }
41
+ }
42
+ return {
43
+ targetDir,
44
+ projectName,
45
+ targetIsCwd,
46
+ };
47
+ };
@@ -20,6 +20,7 @@ export declare const execWithSpinner: (options: ExecWithSpinnerOptions) => Promi
20
20
  */
21
21
  export declare const execWithoutSpinner: (command: string, args: string[], options?: {
22
22
  cwd?: string;
23
+ stdin?: "pipe" | "ignore" | "inherit";
23
24
  stdout?: "pipe" | "ignore" | "inherit";
24
25
  stderr?: "pipe" | "ignore" | "inherit";
25
26
  reject?: boolean;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec-utils.d.ts","sourceRoot":"","sources":["../../src/helpers/exec-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAE9B,MAAM,WAAW,sBAAsB;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAA;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAA;IACtC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACvD,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,aAAa,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAU,SAAS,sBAAsB,KAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAsDzF,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC3B,SAAS,MAAM,EACf,MAAM,MAAM,EAAE,EACd,UAAS;IACL,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAA;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAA;IACtC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC1B,KACP,OAAO,CAAC,IAAI,CAWd,CAAA"}
@@ -50,9 +50,10 @@ export const execWithSpinner = async (options) => {
50
50
  * Execute a command without a spinner (for cases where we want direct output)
51
51
  */
52
52
  export const execWithoutSpinner = async (command, args, options = {}) => {
53
- const { cwd, stdout, stderr, reject = true, env } = options;
53
+ const { cwd, stdin, stdout, stderr, reject = true, env } = options;
54
54
  await execa(command, args, {
55
55
  cwd,
56
+ stdin: stdin ?? (stdout === 'inherit' || stderr === 'inherit' ? 'inherit' : undefined),
56
57
  stdout,
57
58
  stderr,
58
59
  reject,
@@ -0,0 +1,2 @@
1
+ export declare const getCreateNextjsCmsVersion: () => string;
2
+ //# sourceMappingURL=get-version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-version.d.ts","sourceRoot":"","sources":["../../src/helpers/get-version.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,yBAAyB,QAAO,MAW5C,CAAA"}
@@ -0,0 +1,11 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname } from 'node:path';
4
+ import path from 'node:path';
5
+ export const getCreateNextjsCmsVersion = () => {
6
+ /** Resolve __dirname for ESM */
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const packageJson = JSON.parse(readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
10
+ return packageJson.version;
11
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/helpers/logger.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;qBACE,OAAO,EAAE;mBAGX,OAAO,EAAE;kBAGV,OAAO,EAAE;kBAGT,OAAO,EAAE;qBAGN,OAAO,EAAE;CAG7B,CAAA"}
@@ -0,0 +1,3 @@
1
+ export declare const renderTitle: () => void;
2
+ export declare const renderFooter: () => void;
3
+ //# sourceMappingURL=render-title.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-title.d.ts","sourceRoot":"","sources":["../../src/helpers/render-title.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,WAAW,YASvB,CAAA;AAED,eAAO,MAAM,YAAY,YAUxB,CAAA"}
@@ -19,3 +19,13 @@ export const renderTitle = () => {
19
19
  }
20
20
  console.log(titleGradient.multiline(TITLE_TEXT));
21
21
  };
22
+ export const renderFooter = () => {
23
+ const titleGradient = gradient(Object.values(poimandresTheme));
24
+ // resolves weird behavior where the ascii is offset
25
+ const pkgManager = detectPackageManager();
26
+ if (pkgManager === 'yarn' || pkgManager === 'pnpm') {
27
+ console.log('');
28
+ }
29
+ console.log();
30
+ console.log(titleGradient.multiline('Your NextJS CMS project has been created successfully!'));
31
+ };
@@ -1,6 +1,7 @@
1
1
  export declare const TITLE_TEXT = "\n _\n ___ _ __ ___ __ _| |_ ___\n / __| '__/ _ \\/ _\\` | __/ _ \\\n | (__| | | __/ (_| | || __/\n \\___|_| \\___|\\__,_|\\__\\___|\n _ _\n _ __ _____ _| |_ (_)___\n | '_ \\ / _ \\ \\/ / __|| / __|\n | | | | __/> <| |_ | \\__ \\\n |_| |_|\\___/_/\\_\\__|/ |___/\n |__/\n ___ _ __ ___ ___\n / __| '_ \\` _ \\/ __|\n | (__| | | | | \\__ \\\n \\___|_| |_| |_|___/\n";
2
2
  /** Expand ~ to home */
3
3
  export declare function expandHome(p: string): string;
4
+ export declare const removeTrailingSlash: (input: string) => string;
4
5
  /** Validate npm package name (simple rule) */
5
6
  export declare function isValidPkgName(name: string): boolean;
6
7
  /** Check if a directory is empty */
@@ -10,4 +11,12 @@ export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
10
11
  export declare function detectPackageManager(): 'pnpm' | 'npm' | 'yarn' | 'bun';
11
12
  /** Validate template structure */
12
13
  export declare function validateTemplate(templateDir: string): Promise<void>;
14
+ /**
15
+ * Generates a random secret string using crypto.randomBytes
16
+ */
17
+ export declare function generateSecret(): string;
18
+ /**
19
+ * Updates .env file with generated random secrets
20
+ */
21
+ export declare function updateEnvSecrets(targetDir: string): Promise<void>;
13
22
  //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/helpers/utils.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,UAAU,4jBAgBtB,CAAA;AAED,uBAAuB;AACvB,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG5C;AAED,eAAO,MAAM,mBAAmB,GAAI,OAAO,MAAM,WAMhD,CAAA;AAED,8CAA8C;AAC9C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGpD;AAED,oCAAoC;AACpC,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO9D;AAED,uCAAuC;AACvC,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;AAC5D,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAkBtE;AAED,kCAAkC;AAClC,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAczE;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBvE"}
@@ -1,6 +1,9 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { log } from '@clack/prompts';
6
+ import chalk from 'chalk';
4
7
  export const TITLE_TEXT = `
5
8
  _
6
9
  ___ _ __ ___ __ _| |_ ___
@@ -24,8 +27,16 @@ export function expandHome(p) {
24
27
  return path.join(os.homedir(), p.slice(1));
25
28
  return p;
26
29
  }
30
+ export const removeTrailingSlash = (input) => {
31
+ if (input.length > 1 && input.endsWith('/')) {
32
+ input = input.slice(0, -1);
33
+ }
34
+ return input;
35
+ };
27
36
  /** Validate npm package name (simple rule) */
28
37
  export function isValidPkgName(name) {
38
+ if (name === '.')
39
+ return true;
29
40
  return /^[a-zA-Z0-9-_]+$/.test(name);
30
41
  }
31
42
  /** Check if a directory is empty */
@@ -74,3 +85,34 @@ export async function validateTemplate(templateDir) {
74
85
  missingFiles.forEach((file) => console.warn(` - ${file}`));
75
86
  }
76
87
  }
88
+ /**
89
+ * Generates a random secret string using crypto.randomBytes
90
+ */
91
+ export function generateSecret() {
92
+ return randomBytes(32).toString('hex');
93
+ }
94
+ /**
95
+ * Updates .env file with generated random secrets
96
+ */
97
+ export async function updateEnvSecrets(targetDir) {
98
+ const envPath = path.join(targetDir, '.env');
99
+ if (!(await fs.pathExists(envPath))) {
100
+ log.warn('No .env file found; skipping secret generation.');
101
+ return;
102
+ }
103
+ try {
104
+ let envContent = await fs.readFile(envPath, 'utf-8');
105
+ // Replace placeholder secrets with generated ones
106
+ envContent = envContent.replace(/ACCESS_TOKEN_SECRET=.*/, `ACCESS_TOKEN_SECRET=${generateSecret()}`);
107
+ envContent = envContent.replace(/REFRESH_TOKEN_SECRET=.*/, `REFRESH_TOKEN_SECRET=${generateSecret()}`);
108
+ envContent = envContent.replace(/CSRF_TOKEN_SECRET=.*/, `CSRF_TOKEN_SECRET=${generateSecret()}`);
109
+ envContent = envContent.replace(/ACCESS_TOKEN_EXPIRATION=.*/, 'ACCESS_TOKEN_EXPIRATION=2h');
110
+ envContent = envContent.replace(/REFRESH_TOKEN_EXPIRATION=.*/, 'REFRESH_TOKEN_EXPIRATION=1y');
111
+ await fs.writeFile(envPath, envContent, 'utf-8');
112
+ log.message(` - Generated random secrets in root ${chalk.green('.env')} file`);
113
+ }
114
+ catch (e) {
115
+ log.warn('Could not update .env secrets automatically.');
116
+ log.error(` Error: ${e instanceof Error ? e.message : 'Unknown error'}`);
117
+ }
118
+ }
package/dist/index.js CHANGED
@@ -1,262 +1,76 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'fs-extra';
3
- import { fileURLToPath } from 'node:url';
4
- import { randomBytes } from 'node:crypto';
5
- import path, { dirname, resolve, relative, basename } from 'node:path';
6
- import { Command } from 'commander';
7
- import { text, log } from '@clack/prompts';
8
- import { expandHome, isValidPkgName, isEmptyDir, detectPackageManager, validateTemplate } from './lib/utils.js';
9
- import { readFileSync } from 'node:fs';
3
+ import * as p from '@clack/prompts';
4
+ import { detectPackageManager } from './helpers/utils.js';
10
5
  import chalk from 'chalk';
11
- import { renderTitle } from './lib/render-title.js';
6
+ import { renderFooter, renderTitle } from './helpers/render-title.js';
12
7
  import { installDependencies } from './lib/install-deps.js';
13
8
  import { runCmsSetup } from './lib/cms-setup.js';
14
- import { removeGlobalPackage, installGlobalPackage } from './lib/global-package.js';
15
- import { createBlogSection, createCategorySection, createSimpleSection } from './lib/section-creators.js';
16
- /** Resolve __dirname for ESM */
17
- const __filename = fileURLToPath(import.meta.url);
18
- const __dirname = dirname(__filename);
19
- const packageJson = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
20
- /**
21
- * Your build outputs to: dist/index.js
22
- * Template is published at: templates/default/
23
- * So at runtime we need ../templates/default from dist/index.js.
24
- */
25
- const templateDir = fileURLToPath(new URL('../templates/default/', import.meta.url));
9
+ import { runCli } from './cli/index.js';
10
+ import { initializeGit } from './lib/git.js';
11
+ import { resolveDirectory } from './helpers/check-directory.js';
12
+ import { createProject } from './lib/create-project.js';
13
+ import { logger } from './helpers/logger.js';
26
14
  const handleSigTerm = () => process.exit(0);
27
15
  process.on('SIGINT', handleSigTerm);
28
16
  process.on('SIGTERM', handleSigTerm);
29
- /**
30
- * Generates a random secret string using crypto.randomBytes
31
- */
32
- function generateSecret() {
33
- return randomBytes(32).toString('hex');
34
- }
35
- /**
36
- * Updates .env file with generated random secrets
37
- */
38
- async function updateEnvSecrets(targetDir) {
39
- const envPath = path.join(targetDir, '.env');
40
- if (!(await fs.pathExists(envPath))) {
41
- log.warn('No .env file found; skipping secret generation.');
42
- return;
43
- }
44
- try {
45
- let envContent = await fs.readFile(envPath, 'utf-8');
46
- // Replace placeholder secrets with generated ones
47
- envContent = envContent.replace(/ACCESS_TOKEN_SECRET=.*/, `ACCESS_TOKEN_SECRET=${generateSecret()}`);
48
- envContent = envContent.replace(/REFRESH_TOKEN_SECRET=.*/, `REFRESH_TOKEN_SECRET=${generateSecret()}`);
49
- envContent = envContent.replace(/CSRF_TOKEN_SECRET=.*/, `CSRF_TOKEN_SECRET=${generateSecret()}`);
50
- envContent = envContent.replace(/ACCESS_TOKEN_EXPIRATION=.*/, 'ACCESS_TOKEN_EXPIRATION=2h');
51
- envContent = envContent.replace(/REFRESH_TOKEN_EXPIRATION=.*/, 'REFRESH_TOKEN_EXPIRATION=1y');
52
- await fs.writeFile(envPath, envContent, 'utf-8');
53
- log.message('Generated random secrets in .env file');
54
- }
55
- catch (e) {
56
- log.warn('Could not update .env secrets automatically.');
57
- log.error(` Error: ${e instanceof Error ? e.message : 'Unknown error'}`);
58
- }
59
- }
60
17
  async function createNextjsCms() {
61
18
  renderTitle();
62
- log.info('Create your new NextJS CMS project');
63
- let projectPath = '';
64
- const program = new Command(packageJson.name)
65
- .version(packageJson.version, '-v, --version', 'Output the current version of create-nextjs-cms.')
66
- .argument('[directory]')
67
- .usage('[directory] [options]')
68
- .helpOption('-h, --help', 'Display this help message.')
69
- .action((name) => {
70
- // Commander does not implicitly support negated options. When they are used
71
- // by the user they will be interpreted as the positional argument (name) in
72
- // the action handler.
73
- if (name && typeof name === 'string' && !name.startsWith('--no-')) {
74
- projectPath = name;
75
- }
76
- })
77
- .allowUnknownOption()
78
- .parse(process.argv);
79
- const options = {
80
- targetDir: '',
81
- projectName: '',
82
- templateDir: templateDir,
83
- sectionsToAdd: [],
84
- };
85
- let targetIsCwd = false;
19
+ console.log();
20
+ p.intro(chalk.cyan('Create new project'));
21
+ // Run CLI to get user input
22
+ const { appName, sectionsToAdd, git /*, databaseProvider*/ } = await runCli();
23
+ // Resolve directory
24
+ const { targetDir, projectName, targetIsCwd } = await resolveDirectory(appName);
86
25
  try {
87
- // Ensure template folder is present in the published package
88
- if (!(await fs.pathExists(templateDir))) {
89
- log.error('Template directory not found in the published package.');
90
- log.message('Please open an issue on the create-nextjs-cms repository: https://github.com/next-cms/create-nextjs-cms/issues');
91
- process.exit(1);
92
- }
93
- // Validate template structure
94
- await validateTemplate(templateDir);
95
- // If no directory was provided, prompt for project name
96
- if (!projectPath) {
97
- const res = await text({
98
- message: 'What is your project named?',
99
- placeholder: 'my-cms-app',
100
- defaultValue: 'my-cms-app',
101
- validate: (name) => {
102
- const validation = isValidPkgName(basename(resolve(name)));
103
- if (validation) {
104
- return;
105
- }
106
- return 'Invalid project name: Project name can only contain letters, numbers, hyphens, and underscores.';
107
- },
108
- });
109
- if (typeof res === 'string') {
110
- projectPath = res.trim();
111
- }
112
- }
113
- if (!projectPath) {
114
- log.error('\nPlease specify the project directory:\n' +
115
- ` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}\n` +
116
- 'For example:\n' +
117
- ` ${chalk.cyan(program.name())} ${chalk.green('my-cms-app')}\n\n` +
118
- `Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`);
119
- process.exit(1);
120
- }
121
- // Resolve target path from the caller's CWD
122
- const rawTarget = expandHome(projectPath);
123
- options.targetDir = path.isAbsolute(rawTarget) ? rawTarget : resolve(process.cwd(), rawTarget);
124
- // Derive package name from final path
125
- options.projectName = basename(options.targetDir);
126
- // Validate name
127
- if (!isValidPkgName(options.projectName)) {
128
- log.error(`Could not create a project called ${chalk.red(`"${options.projectName}"`)} because of npm naming restrictions:`);
129
- log.message(` ${chalk.red('*')} Project name can only contain letters, numbers, hyphens, and underscores.`);
130
- process.exit(1);
131
- }
132
- targetIsCwd = path.normalize(options.targetDir) === path.normalize(process.cwd());
133
- if (targetIsCwd) {
134
- // Using current directory (e.g., ".")
135
- if (!(await fs.pathExists(options.targetDir))) {
136
- await fs.ensureDir(options.targetDir);
137
- }
138
- else if (!(await isEmptyDir(options.targetDir))) {
139
- log.error('Current directory is not empty. Choose an empty folder or a new directory name.');
140
- log.message(chalk.gray('Tip: You can also specify a directory name like this: \n') +
141
- chalk.green('pnpm create nextjs-cms ') +
142
- chalk.italic.magenta('my-app'));
143
- log.message(' ');
144
- process.exit(1);
145
- }
26
+ // Detect package manager
27
+ const preferredPM = detectPackageManager();
28
+ // Create project
29
+ await createProject({ targetDir, sectionsToAdd, projectName, preferredPM });
30
+ // Change working directory to the project
31
+ process.chdir(targetDir);
32
+ // Install dependencies first
33
+ let depsInstalled = false;
34
+ try {
35
+ await installDependencies({ projectDir: targetDir });
36
+ depsInstalled = true;
146
37
  }
147
- else {
148
- if (await fs.pathExists(options.targetDir)) {
149
- if (!(await isEmptyDir(options.targetDir))) {
150
- log.error(`Directory "${options.targetDir}" is not empty.`);
151
- log.message('Please choose an empty directory or a different directory name.');
152
- process.exit(1);
153
- }
154
- }
155
- else {
156
- await fs.ensureDir(options.targetDir);
157
- }
38
+ catch {
39
+ depsInstalled = false;
158
40
  }
159
- // Ask which sections to add
160
- const { multiselect } = await import('@clack/prompts');
161
- const sectionsRes = await multiselect({
162
- message: 'Would you like to add default sections?',
163
- options: [
164
- { value: 'blog', label: 'Blog' },
165
- { value: 'category', label: 'Category' },
166
- { value: 'simple', label: 'Settings' },
167
- ],
168
- required: false,
169
- });
170
- options.sectionsToAdd = sectionsRes;
171
- log.step(`Creating project in: ${options.targetDir}`);
172
- // Copy template → project dir with a filter to skip build/cache artifacts
173
- await fs.copy(options.templateDir, options.targetDir, {
174
- filter: (src) => {
175
- const rel = relative(options.templateDir, src);
176
- if (!rel || rel === '.')
177
- return true;
178
- return (!rel.startsWith('node_modules') &&
179
- !rel.startsWith('.next') &&
180
- !rel.startsWith('dist') &&
181
- !rel.startsWith('.turbo') &&
182
- !rel.includes('tsconfig.tsbuildinfo'));
183
- },
184
- // Overwrite is safe since target is ensured empty (or new)
185
- overwrite: true,
186
- errorOnExist: false,
187
- });
188
- // Generate random secrets for .env file
189
- await updateEnvSecrets(options.targetDir);
190
- // npm excludes .gitignore from packages, so we rename it back from _gitignore
191
- const gitignoreTemplatePath = path.join(options.targetDir, '_gitignore');
192
- const gitignorePath = path.join(options.targetDir, '.gitignore');
193
- if (await fs.pathExists(gitignoreTemplatePath)) {
194
- await fs.move(gitignoreTemplatePath, gitignorePath);
41
+ // Initialize Git repository if requested
42
+ if (git) {
43
+ await initializeGit(targetDir);
195
44
  }
196
- // Create selected sections
197
- if (options.sectionsToAdd.length > 0) {
198
- for (const section of options.sectionsToAdd) {
199
- switch (section) {
200
- case 'blog':
201
- await createBlogSection(options.targetDir);
202
- break;
203
- case 'category':
204
- await createCategorySection(options.targetDir);
205
- break;
206
- case 'simple':
207
- await createSimpleSection(options.targetDir);
208
- break;
209
- }
210
- }
211
- log.message(`${options.sectionsToAdd.join(', ')} section${options.sectionsToAdd.length > 1 ? 's' : ''} added successfully`);
45
+ if (depsInstalled) {
46
+ // Finally run CMS setup
47
+ await runCmsSetup({ dir: targetDir, preferredPM });
212
48
  }
213
- // Update package.json name (if template contains package.json)
214
- const packageJsonPath = path.join(options.targetDir, 'package.json');
215
- if (await fs.pathExists(packageJsonPath)) {
216
- try {
217
- const pkg = (await fs.readJson(packageJsonPath));
218
- pkg.name = options.projectName;
219
- await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
220
- }
221
- catch {
222
- log.warn('Could not update package.json name automatically.');
223
- }
49
+ renderFooter();
50
+ logger.message('\nNext steps:');
51
+ logger.message(`cd ${targetDir}`);
52
+ if (depsInstalled) {
53
+ logger.message(`${preferredPM} dev`);
224
54
  }
225
55
  else {
226
- log.warn('No package.json found in template root; skipping name update.');
227
- }
228
- process.chdir(options.targetDir);
229
- const preferredPM = detectPackageManager();
230
- let installed = false;
231
- try {
232
- log.step(`Using ${chalk.green(preferredPM)} as the package manager...`);
233
- // Install dependencies first
234
- await installDependencies({ projectDir: options.targetDir });
235
- installed = true;
236
- // Then remove any existing global package
237
- await removeGlobalPackage('nextjs-cms-kit');
238
- // Install the global package
239
- await installGlobalPackage('nextjs-cms-kit', packageJson.version);
240
- // Finally run CMS setup
241
- await runCmsSetup({ dir: options.targetDir, installed, preferredPM });
242
- }
243
- catch (error) {
244
- log.error(`❌ Failed to complete project setup: ${error instanceof Error ? error.message : 'Unknown error'}`);
245
- process.exit(1);
56
+ logger.message(`${preferredPM} install`);
57
+ logger.message(`${preferredPM} nextjs-cms-kit --dev setup`);
58
+ logger.message(`${preferredPM} dev`);
246
59
  }
60
+ console.log('\n');
247
61
  }
248
62
  catch (error) {
249
- log.error(`❌ Failed to create project: ${error instanceof Error ? error.message : 'Unknown error'}`);
63
+ p.outro(chalk.red(`❌ Failed to create project: ${error instanceof Error ? error.message : 'Unknown error'}`));
250
64
  // Clean up partial installation on error
251
- if ((await fs.pathExists(options.targetDir)) && !targetIsCwd) {
252
- log.info('🧹 Cleaning up partial installation...');
65
+ if ((await fs.pathExists(targetDir)) && !targetIsCwd) {
66
+ p.log.info('🧹 Cleaning up partial installation...');
253
67
  try {
254
- await fs.remove(options.targetDir);
255
- log.success('✅ Cleanup completed.');
68
+ await fs.remove(targetDir);
69
+ p.log.success('✅ Cleanup completed.');
256
70
  }
257
71
  catch {
258
- log.error('⚠️ Could not clean up partial installation.');
259
- log.message(` Please manually remove: ${options.targetDir}`);
72
+ p.log.error('⚠️ Could not clean up partial installation.');
73
+ p.log.message(` Please manually remove: ${targetDir}`);
260
74
  }
261
75
  }
262
76
  process.exit(1);
@@ -1,6 +1,5 @@
1
- export declare const runCmsSetup: ({ dir, installed, preferredPM, }: {
1
+ export declare const runCmsSetup: ({ dir, preferredPM }: {
2
2
  dir: string;
3
- installed: boolean;
4
3
  preferredPM: string;
5
4
  }) => Promise<void>;
6
5
  //# sourceMappingURL=cms-setup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cms-setup.d.ts","sourceRoot":"","sources":["../../src/lib/cms-setup.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,WAAW,GAAU,kCAI/B;IACC,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,OAAO,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACtB,kBAcA,CAAA"}
1
+ {"version":3,"file":"cms-setup.d.ts","sourceRoot":"","sources":["../../src/lib/cms-setup.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,WAAW,GAAU,sBAAsB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,kBAU3F,CAAA"}