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 +7 -0
- package/dist/index.js +50 -30
- package/dist/registry/web/backend/fastify/base.js +3 -1
- package/dist/registry/web/backend/fastify/mongodb.js +21 -6
- package/dist/registry/web/backend/fastify/postgresql.js +40 -0
- package/dist/registry/web/backend/fastify/websocket.js +2 -2
- package/package.json +4 -5
- package/templates/web/backend/fastify/mongodb/.env +1 -2
- package/templates/web/backend/fastify/mongodb/prisma/schema.prisma +25 -0
- package/templates/web/backend/fastify/mongodb/prisma.config.ts +12 -0
- package/templates/web/backend/fastify/mongodb/src/plugins/prisma.plugin.ts +23 -0
- package/templates/web/backend/fastify/postgresql/.env +1 -0
- package/templates/web/backend/fastify/postgresql/prisma/schema.prisma +24 -0
- package/templates/web/backend/fastify/postgresql/prisma.config.ts +12 -0
- package/templates/web/backend/fastify/postgresql/src/plugins/prisma.plugin.ts +25 -0
- package/templates/web/backend/fastify/mongodb/src/plugins/mongo.plugin.ts +0 -53
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
|
|
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
|
|
73
|
-
type: 'select',
|
|
73
|
+
const category = await new Select({
|
|
74
74
|
name: 'category',
|
|
75
75
|
message: 'Main category',
|
|
76
|
-
choices: registry.map(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
97
|
-
type: 'multiselect',
|
|
93
|
+
const prompt = new MultiSelect({
|
|
98
94
|
name: 'features',
|
|
99
95
|
message: 'Optional features (space to toggle)',
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
102
|
+
result(names) { return names.map(name => template.features.find((f) => f.key === name)); }
|
|
106
103
|
});
|
|
107
|
-
|
|
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
|
|
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(
|
|
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
|
-
'
|
|
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: '
|
|
22
|
+
targetPath: 'tsconfig.json',
|
|
13
23
|
operations: [
|
|
14
24
|
{
|
|
15
25
|
type: 'insertAfter',
|
|
16
|
-
pattern: "
|
|
17
|
-
insert: ",\
|
|
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: `
|
|
26
|
-
insert: `
|
|
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.
|
|
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
|
-
"
|
|
50
|
-
"
|
|
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
|
|
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
|
-
|
|
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,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,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' })
|