create-nolly-template 1.0.8 โ†’ 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/builder.js CHANGED
@@ -66,6 +66,13 @@ async function loadDirectoryIntoMap(root, baseDir, fileMap) {
66
66
  }
67
67
  }
68
68
  export async function buildProject(template, selectedFeatures, answers, outputDir) {
69
+ for (const feature of selectedFeatures) {
70
+ if (feature.group && feature.exclusive) {
71
+ const conflict = selectedFeatures.find(f => f.group === feature.group && f.key !== feature.key);
72
+ if (conflict)
73
+ throw new Error(`Cannot select "${feature.name}" because it is exclusive with "${conflict.name}" in group "${feature.group}"`);
74
+ }
75
+ }
69
76
  const fileMap = new Map();
70
77
  const baseRoot = path.join(TEMPLATES_ROOT, template.templateRoot);
71
78
  await loadDirectoryIntoMap(baseRoot, baseRoot, fileMap);
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import prompts from 'prompts';
2
+ import Enquirer from 'enquirer';
3
3
  import { registry } from './registry/index.js';
4
4
  import { buildProject } from './builder.js';
5
5
  import packageJSON from '../package.json' with { type: 'json' };
6
+ const { Select, MultiSelect, Input } = Enquirer;
6
7
  async function printHelp() {
7
8
  console.log(`
8
9
  ๐Ÿš€ create-nolly-template v${packageJSON.version}
@@ -69,57 +70,76 @@ async function main() {
69
70
  if (args.includes('--list') || args.includes('-l'))
70
71
  return printList();
71
72
  console.log('\n๐Ÿš€ Welcome to create-nolly-template!\n');
72
- const { category } = await prompts({
73
- type: 'select',
73
+ const category = await new Select({
74
74
  name: 'category',
75
75
  message: 'Main category',
76
- choices: registry.map(category => ({ title: category.name, value: category }))
77
- });
78
- const { subCategory } = await prompts({
79
- type: 'select',
76
+ choices: registry.map(c => c.name),
77
+ result(name) { return registry.find(c => c.name === name); }
78
+ }).run();
79
+ const subCategory = await new Select({
80
80
  name: 'subCategory',
81
81
  message: 'Sub category',
82
- choices: category.subCategories.map(subCategory => ({ title: subCategory.name, value: subCategory }))
83
- });
84
- if (!subCategory)
85
- process.exit(0);
86
- const { template } = await prompts({
87
- type: 'select',
82
+ choices: category.subCategories.map((s) => s.name),
83
+ result(name) { return category.subCategories.find((s) => s.name === name); }
84
+ }).run();
85
+ const template = await new Select({
88
86
  name: 'template',
89
87
  message: 'Base template',
90
- choices: subCategory.templates.map(template => ({ title: template.name, value: template }))
91
- });
92
- if (!template)
93
- process.exit(0);
88
+ choices: subCategory.templates.map((t) => t.name),
89
+ result(name) { return subCategory.templates.find((t) => t.name === name); }
90
+ }).run();
94
91
  let selectedFeatures = [];
95
92
  if (template.features && template.features.length > 0) {
96
- const { features } = await prompts({
97
- type: 'multiselect',
93
+ const prompt = new MultiSelect({
98
94
  name: 'features',
99
95
  message: 'Optional features (space to toggle)',
100
- choices: template.features.map((feature) => ({
101
- title: feature.name,
102
- value: feature,
103
- description: feature.description
96
+ hint: 'Space to select ยท Enter to submit',
97
+ choices: template.features.map((f) => ({
98
+ name: f.key,
99
+ message: f.name,
100
+ hint: f.description
104
101
  })),
105
- hint: '- Space to select. Return to submit'
102
+ result(names) { return names.map(name => template.features.find((f) => f.key === name)); }
106
103
  });
107
- selectedFeatures = features ?? [];
104
+ const selected = await prompt.run();
105
+ const groups = new Map();
106
+ for (const feat of selected) {
107
+ if (feat.group && feat.exclusive) {
108
+ if (groups.has(feat.group)) {
109
+ throw new Error(`Feature "${feat.name}" is exclusive with another selected feature in group "${feat.group}"`);
110
+ }
111
+ groups.set(feat.group, feat.key);
112
+ }
113
+ }
114
+ selectedFeatures = selected;
115
+ }
116
+ const answers = {};
117
+ for (const prompt of template.prompts) {
118
+ const value = await new Input({
119
+ name: prompt.name,
120
+ message: prompt.message,
121
+ initial: prompt.initial
122
+ }).run();
123
+ answers[prompt.name] = value;
108
124
  }
109
- const answers = await prompts(template.prompts);
110
- const { outputDir } = await prompts({
111
- type: 'text',
125
+ const outputDir = await new Input({
112
126
  name: 'outputDir',
113
127
  message: 'Output directory',
114
128
  initial: `./${answers.name || template.key}`
115
- });
129
+ }).run();
116
130
  if (!outputDir)
117
131
  process.exit(0);
118
132
  await buildProject(template, selectedFeatures, answers, outputDir);
119
133
  console.log('\nโœ… Project created at', outputDir);
120
134
  console.log(' Base:', template.name);
121
135
  if (selectedFeatures.length > 0)
122
- console.log(' Features:', selectedFeatures.map((feature) => feature.name).join(', '));
136
+ console.log(' Features:', selectedFeatures.map(f => f.name).join(', '));
137
+ for (const feature of selectedFeatures) {
138
+ if (feature.additionalMessages && feature.additionalMessages.length > 0) {
139
+ console.log(`\n๐Ÿ“Œ ${feature.name} - ${feature.description}`);
140
+ feature.additionalMessages.forEach((msg) => console.log(` โ€ข ${msg}`));
141
+ }
142
+ }
123
143
  console.log('\n cd', outputDir, '&& pnpm install\n');
124
144
  }
125
145
  main().catch(error => {
@@ -1,6 +1,7 @@
1
1
  import { swagger } from './swagger.js';
2
2
  import { websocket } from './websocket.js';
3
3
  import { mongodb } from './mongodb.js';
4
+ import { postgresql } from './postgresql.js';
4
5
  export const fastify = {
5
6
  key: 'fastify',
6
7
  name: 'Fastify TypeScript',
@@ -12,6 +13,7 @@ export const fastify = {
12
13
  features: [
13
14
  swagger,
14
15
  websocket,
15
- mongodb
16
+ mongodb,
17
+ postgresql
16
18
  ]
17
19
  };
@@ -1,20 +1,35 @@
1
1
  export const mongodb = {
2
2
  key: 'mongodb',
3
3
  name: 'MongoDB support',
4
- description: 'Adds MongoDB support',
4
+ description: 'Adds MongoDB support via prisma directly to your Fastify application.',
5
5
  templateRoot: 'web/backend/fastify/mongodb',
6
6
  dependencies: {
7
- 'fastify-plugin': '^5.1.0',
8
- 'mongodb': '^7.1.0'
7
+ '@prisma/client': '6.19'
9
8
  },
9
+ devDependencies: {
10
+ '@types/node': '^18.0.0',
11
+ 'prisma': '6.19'
12
+ },
13
+ group: 'database',
14
+ exclusive: true,
15
+ additionalMessages: [
16
+ 'This feature will set up Prisma with MongoDB as the database provider.',
17
+ 'It comes with a premade test model in the prisma schema as well as a plugin for Fastify',
18
+ 'Run `pnpx prisma generate` and `pnpx prisma db push` to set up your database after adding this feature.'
19
+ ],
10
20
  patches: [
11
21
  {
12
- targetPath: 'src/utils/env.ts',
22
+ targetPath: 'tsconfig.json',
13
23
  operations: [
14
24
  {
15
25
  type: 'insertAfter',
16
- pattern: "NODE_ENV: z.enum\\(\\['development', 'production', 'test'\\]\\)\\.default\\('development'\\)",
17
- insert: ",\tMONGO_URI: z.string().default('mongodb://localhost:27017/myapp'),\n\tMONGO_DB: z.string().default('myapp')"
26
+ pattern: "\"skipLibCheck\": true",
27
+ insert: ",\n\t\"types\": [\"node\"]"
28
+ },
29
+ {
30
+ type: 'insertAfter',
31
+ pattern: "\"src\"",
32
+ insert: ", \"prisma.config.ts\""
18
33
  }
19
34
  ]
20
35
  }
@@ -0,0 +1,40 @@
1
+ export const postgresql = {
2
+ key: 'postgresql',
3
+ name: 'PostgreSQL support',
4
+ description: 'Adds PostgreSQL support via prisma directly to your Fastify application.',
5
+ templateRoot: 'web/backend/fastify/postgresql',
6
+ dependencies: {
7
+ '@prisma/adapter-pg': '^7.4.2',
8
+ '@prisma/client': '^7.4.2',
9
+ 'pg': '^8.20.0'
10
+ },
11
+ devDependencies: {
12
+ '@types/node': '^18.0.0',
13
+ '@types/pg': '^8.18.0',
14
+ 'prisma': '^7.4.2'
15
+ },
16
+ group: 'database',
17
+ exclusive: true,
18
+ additionalMessages: [
19
+ 'This feature will set up Prisma with PostgreSQL as the database provider.',
20
+ 'It comes with a premade test model in the prisma schema as well as a plugin for Fastify',
21
+ 'Run `pnpx prisma generate` and `pnpx prisma db push` to set up your database after adding this feature.'
22
+ ],
23
+ patches: [
24
+ {
25
+ targetPath: 'tsconfig.json',
26
+ operations: [
27
+ {
28
+ type: 'insertAfter',
29
+ pattern: "\"skipLibCheck\": true",
30
+ insert: ",\n\t\"types\": [\"node\"]"
31
+ },
32
+ {
33
+ type: 'insertAfter',
34
+ pattern: "\"src\"",
35
+ insert: ", \"prisma.config.ts\""
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ };
@@ -22,8 +22,8 @@ export const websocket = {
22
22
  },
23
23
  {
24
24
  type: 'insertAfter',
25
- pattern: `await autoRegister\\(app, 'routes/\\*\\*/\\.routes\\.{ts,js}\\)`,
26
- insert: ` await autoRegister(app, 'routes/**/*.ws.{ts,js}', '/ws')`
25
+ pattern: `routes\.\\{ts,js\\}\'\\)`,
26
+ insert: `await autoRegister(app, 'routes/**/*.ws.{ts,js}', '/ws')`
27
27
  }
28
28
  ]
29
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nolly-template",
3
- "version": "1.0.8",
3
+ "version": "1.0.11",
4
4
  "description": "All of my opiniated templates in one place. This is a CLI tool to create a new project based on one of my templates.",
5
5
  "license": "MIT",
6
6
  "author": "Nolly",
@@ -46,13 +46,12 @@
46
46
  "package.json"
47
47
  ],
48
48
  "dependencies": {
49
- "fs-extra": "^11.3.4",
50
- "prompts": "^2.4.2"
49
+ "enquirer": "^2.4.1",
50
+ "fs-extra": "^11.3.4"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/fs-extra": "^11.0.4",
54
54
  "@types/node": "^25.5.0",
55
- "@types/prompts": "^2.4.9",
56
55
  "tsx": "^4.21.0",
57
56
  "typescript": "^5.9.3"
58
57
  },
@@ -78,7 +77,7 @@
78
77
  }
79
78
  ],
80
79
  "scripts": {
81
- "build": "rm -rf dist && tsc --build tsconfig.cli.json",
80
+ "build": "rm -rf dist && tsc",
82
81
  "dev": "tsx src/index.ts",
83
82
  "start": "node dist/index.js"
84
83
  }
@@ -1,2 +1 @@
1
- MONGO_URI=YOUR_MONGODB_URI
2
- MONGO_DB=YOUR_DATABASE_NAME
1
+ DATABASE_URL="mongodb://localhost:27017/mydatabase"
@@ -0,0 +1,25 @@
1
+ generator client {
2
+ provider = "prisma-client"
3
+ output = "../src/prisma"
4
+ }
5
+
6
+ datasource db {
7
+ provider = "mongodb"
8
+ url = env("DATABASE_URL")
9
+ }
10
+
11
+ model User {
12
+ id String @id @default(auto()) @map("_id") @db.ObjectId
13
+ email String @unique
14
+ name String?
15
+ posts Post[]
16
+ }
17
+
18
+ model Post {
19
+ id String @id @default(auto()) @map("_id") @db.ObjectId
20
+ title String
21
+ content String?
22
+ published Boolean @default(false)
23
+ author User @relation(fields: [authorId], references: [id])
24
+ authorId String @db.ObjectId
25
+ }
@@ -0,0 +1,12 @@
1
+ import 'dotenv/config'
2
+ import { defineConfig, env } from 'prisma/config'
3
+
4
+ export default defineConfig({
5
+ schema: 'prisma/schema.prisma',
6
+ migrations: {
7
+ path: 'prisma/migrations',
8
+ },
9
+ datasource: {
10
+ url: env['DATABASE_URL'],
11
+ },
12
+ })
@@ -0,0 +1,23 @@
1
+ import FastifyPlugin from 'fastify-plugin'
2
+ import { FastifyInstance } from 'fastify'
3
+ import { PrismaClient } from '../prisma/client'
4
+ import { env } from 'prisma/config'
5
+
6
+ declare module 'fastify' {
7
+ interface FastifyInstance {
8
+ database: PrismaClient
9
+ }
10
+ }
11
+
12
+ function hidePassword(): string {
13
+ const url = env('DATABASE_URL')
14
+ return url.replace(/\/\/(.*):(.*)@/, '//$1:****@')
15
+ }
16
+
17
+ export default FastifyPlugin(async (fastify: FastifyInstance) => {
18
+ const database = new PrismaClient()
19
+ await database.$connect()
20
+ fastify.log.info(`Database connected to ${hidePassword()}`)
21
+ fastify.decorate('database', database)
22
+ fastify.addHook('onClose', async (fastify) => await fastify.database.$disconnect())
23
+ })
@@ -0,0 +1 @@
1
+ DATABASE_URL="mongodb://localhost:27017/mydatabase"
@@ -0,0 +1,24 @@
1
+ generator client {
2
+ provider = "prisma-client"
3
+ output = "../src/prisma"
4
+ }
5
+
6
+ datasource db {
7
+ provider = "postgresql"
8
+ }
9
+
10
+ model User {
11
+ id String @id @default(uuid())
12
+ email String @unique
13
+ name String?
14
+ posts Post[]
15
+ }
16
+
17
+ model Post {
18
+ id String @id @default(uuid())
19
+ title String
20
+ content String?
21
+ published Boolean @default(false)
22
+ author User @relation(fields: [authorId], references: [id])
23
+ authorId String
24
+ }
@@ -0,0 +1,12 @@
1
+ import 'dotenv/config'
2
+ import { defineConfig, env } from 'prisma/config'
3
+
4
+ export default defineConfig({
5
+ schema: 'prisma/schema.prisma',
6
+ migrations: {
7
+ path: 'prisma/migrations',
8
+ },
9
+ datasource: {
10
+ url: env['DATABASE_URL'],
11
+ },
12
+ })
@@ -0,0 +1,25 @@
1
+ import FastifyPlugin from 'fastify-plugin'
2
+ import { FastifyInstance } from 'fastify'
3
+ import { PrismaClient } from '../prisma/client'
4
+ import { PrismaPg } from '@prisma/adapter-pg'
5
+ import { env } from 'prisma/config'
6
+
7
+ declare module 'fastify' {
8
+ interface FastifyInstance {
9
+ database: PrismaClient
10
+ }
11
+ }
12
+
13
+ function hidePassword(): string {
14
+ const url = env('DATABASE_URL')
15
+ return url.replace(/\/\/(.*):(.*)@/, '//$1:****@')
16
+ }
17
+
18
+ export default FastifyPlugin(async (fastify: FastifyInstance) => {
19
+ const adapter = new PrismaPg({ connectionString: env('DATABASE_URL') })
20
+ const database = new PrismaClient({ adapter })
21
+ await database.connect()
22
+ fastify.log.info(`Database connected to ${hidePassword()}`)
23
+ fastify.decorate('database', database)
24
+ fastify.addHook('onClose', async (fastify) => await fastify.database.disconnect())
25
+ })
@@ -1,53 +0,0 @@
1
- import fastifyPlugin from 'fastify-plugin'
2
- import { MongoClient, Db } from 'mongodb'
3
- import { FastifyInstance } from 'fastify'
4
- import path from 'path'
5
- import { fileURLToPath, pathToFileURL } from 'url'
6
- import fastGlob from 'fast-glob'
7
- import { env } from '../utils/env'
8
-
9
- const __filename = fileURLToPath(import.meta.url)
10
- const __dirname = path.dirname(__filename)
11
-
12
- export type ModelFactory = (db: Db, app: FastifyInstance) => any
13
-
14
- declare module 'fastify' {
15
- interface FastifyInstance {
16
- mongo: {
17
- client: MongoClient
18
- db: Db
19
- }
20
- models: Record<string, any>
21
- }
22
- }
23
-
24
- async function autoLoadModels(app: FastifyInstance, db: Db) {
25
- const pattern = path.join(__dirname, '../models/**/*.model.{ts,js}').replace(/\\/g, '/')
26
- const files = fastGlob.sync(pattern)
27
- const models: Record<string, any> = {}
28
- for (const file of files) {
29
- const module = await import(pathToFileURL(file).href)
30
- const factory: ModelFactory = module.default || module[Object.keys(module)[0]]
31
- if (typeof factory !== 'function') continue
32
- const model = factory(db, app)
33
- if (!model?.name) throw new Error(`Model in ${file} must return { name: string }`)
34
- models[model.name] = model
35
- }
36
- return models
37
- }
38
-
39
- export default fastifyPlugin(async function mongoPlugin(app) {
40
- const client = new MongoClient(env.MONGO_URI, { serverSelectionTimeoutMS: 5000 })
41
- try {
42
- await client.connect()
43
- } catch (error: any) {
44
- app.log.error('Mongo connection failed:', error)
45
- throw error
46
- }
47
- const db = client.db(env.MONGO_DB)
48
- app.decorate('mongo', { client, db })
49
- const models = await autoLoadModels(app, db)
50
- app.decorate('models', models)
51
- app.addHook('onClose', async () => { await client.close() })
52
- app.log.info(`๐Ÿ“ฆ Mongo connected: ${env.MONGO_DB}`)
53
- }, { name: 'mongoPlugin' })