create-forgeon 0.2.2 → 0.2.3

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.
@@ -1,176 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { Project, QuoteKind } from 'ts-morph';
6
-
7
- function hasAnyImport(sourceFile, moduleSpecifier) {
8
- return sourceFile.getImportDeclarations().some((item) => item.getModuleSpecifierValue() === moduleSpecifier);
9
- }
10
-
11
- function ensureNamedImports(sourceFile, moduleSpecifier, names) {
12
- const declaration = sourceFile
13
- .getImportDeclarations()
14
- .find((item) => item.getModuleSpecifierValue() === moduleSpecifier);
15
-
16
- if (!declaration) {
17
- sourceFile.addImportDeclaration({
18
- moduleSpecifier,
19
- namedImports: [...names].sort(),
20
- });
21
- return;
22
- }
23
-
24
- const existing = new Set(declaration.getNamedImports().map((item) => item.getName()));
25
- for (const name of names) {
26
- if (!existing.has(name)) {
27
- declaration.addNamedImport(name);
28
- }
29
- }
30
- }
31
-
32
- function hasDecorator(node, decoratorName) {
33
- return node.getDecorators().some((item) => item.getName() === decoratorName);
34
- }
35
-
36
- function addDecoratorIfMissing(node, decoratorName, args = []) {
37
- if (hasDecorator(node, decoratorName)) {
38
- return false;
39
- }
40
- node.addDecorator({
41
- name: decoratorName,
42
- arguments: args,
43
- });
44
- return true;
45
- }
46
-
47
- function syncJwtSwagger({ rootDir, changedFiles }) {
48
- const controllerPath = path.join(
49
- rootDir,
50
- 'packages',
51
- 'auth-api',
52
- 'src',
53
- 'auth.controller.ts',
54
- );
55
- const loginDtoPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'dto', 'login.dto.ts');
56
- const refreshDtoPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'dto', 'refresh.dto.ts');
57
- const authApiPackagePath = path.join(rootDir, 'packages', 'auth-api', 'package.json');
58
-
59
- if (
60
- !fs.existsSync(controllerPath) ||
61
- !fs.existsSync(loginDtoPath) ||
62
- !fs.existsSync(refreshDtoPath) ||
63
- !fs.existsSync(authApiPackagePath)
64
- ) {
65
- return { applied: false, reason: 'jwt-auth source files are missing' };
66
- }
67
-
68
- const project = new Project({
69
- manipulationSettings: { quoteKind: QuoteKind.Single },
70
- skipAddingFilesFromTsConfig: true,
71
- });
72
-
73
- const controller = project.addSourceFileAtPath(controllerPath);
74
- const loginDto = project.addSourceFileAtPath(loginDtoPath);
75
- const refreshDto = project.addSourceFileAtPath(refreshDtoPath);
76
-
77
- const controllerDecorators = [
78
- 'ApiBearerAuth',
79
- 'ApiBody',
80
- 'ApiOkResponse',
81
- 'ApiOperation',
82
- 'ApiTags',
83
- 'ApiUnauthorizedResponse',
84
- ];
85
- const dtoDecorators = ['ApiProperty'];
86
-
87
- ensureNamedImports(controller, '@nestjs/swagger', controllerDecorators);
88
- ensureNamedImports(loginDto, '@nestjs/swagger', dtoDecorators);
89
- ensureNamedImports(refreshDto, '@nestjs/swagger', dtoDecorators);
90
-
91
- const authController = controller.getClass('AuthController');
92
- if (!authController) {
93
- return { applied: false, reason: 'AuthController not found' };
94
- }
95
-
96
- addDecoratorIfMissing(authController, 'ApiTags', ["'auth'"]);
97
-
98
- const loginMethod = authController.getMethod('login');
99
- const refreshMethod = authController.getMethod('refresh');
100
- const logoutMethod = authController.getMethod('logout');
101
- const meMethod = authController.getMethod('me');
102
-
103
- if (loginMethod) {
104
- addDecoratorIfMissing(loginMethod, 'ApiOperation', ["{ summary: 'Authenticate demo user' }"]);
105
- addDecoratorIfMissing(loginMethod, 'ApiBody', ['{ type: LoginDto }']);
106
- addDecoratorIfMissing(loginMethod, 'ApiOkResponse', ["{ description: 'JWT token pair' }"]);
107
- addDecoratorIfMissing(loginMethod, 'ApiUnauthorizedResponse', ["{ description: 'Invalid credentials' }"]);
108
- }
109
-
110
- if (refreshMethod) {
111
- addDecoratorIfMissing(refreshMethod, 'ApiOperation', ["{ summary: 'Refresh access token' }"]);
112
- addDecoratorIfMissing(refreshMethod, 'ApiBody', ['{ type: RefreshDto }']);
113
- addDecoratorIfMissing(refreshMethod, 'ApiOkResponse', ["{ description: 'New JWT token pair' }"]);
114
- addDecoratorIfMissing(refreshMethod, 'ApiUnauthorizedResponse', [
115
- "{ description: 'Refresh token is invalid or expired' }",
116
- ]);
117
- }
118
-
119
- if (logoutMethod) {
120
- addDecoratorIfMissing(logoutMethod, 'ApiBearerAuth');
121
- addDecoratorIfMissing(logoutMethod, 'ApiOperation', ["{ summary: 'Logout and clear refresh token state' }"]);
122
- addDecoratorIfMissing(logoutMethod, 'ApiOkResponse', ["{ description: 'Logout accepted' }"]);
123
- }
124
-
125
- if (meMethod) {
126
- addDecoratorIfMissing(meMethod, 'ApiBearerAuth');
127
- addDecoratorIfMissing(meMethod, 'ApiOperation', ["{ summary: 'Get current authenticated user' }"]);
128
- addDecoratorIfMissing(meMethod, 'ApiOkResponse', ["{ description: 'Current user payload' }"]);
129
- }
130
-
131
- const loginDtoClass = loginDto.getClass('LoginDto');
132
- if (loginDtoClass) {
133
- const emailProp = loginDtoClass.getProperty('email');
134
- const passwordProp = loginDtoClass.getProperty('password');
135
- if (emailProp) {
136
- addDecoratorIfMissing(emailProp, 'ApiProperty', [
137
- "{ example: 'demo@forgeon.local', description: 'Demo account email' }",
138
- ]);
139
- }
140
- if (passwordProp) {
141
- addDecoratorIfMissing(passwordProp, 'ApiProperty', [
142
- "{ example: 'forgeon-demo-password', description: 'Demo account password' }",
143
- ]);
144
- }
145
- }
146
-
147
- const refreshDtoClass = refreshDto.getClass('RefreshDto');
148
- if (refreshDtoClass) {
149
- const tokenProp = refreshDtoClass.getProperty('refreshToken');
150
- if (tokenProp) {
151
- addDecoratorIfMissing(tokenProp, 'ApiProperty', [
152
- "{ example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', description: 'Refresh token' }",
153
- ]);
154
- }
155
- }
156
-
157
- project.saveSync();
158
- changedFiles.add(controllerPath);
159
- changedFiles.add(loginDtoPath);
160
- changedFiles.add(refreshDtoPath);
161
-
162
- const authApiPackage = JSON.parse(fs.readFileSync(authApiPackagePath, 'utf8'));
163
- if (!authApiPackage.dependencies) {
164
- authApiPackage.dependencies = {};
165
- }
166
- if (!authApiPackage.dependencies['@nestjs/swagger']) {
167
- authApiPackage.dependencies['@nestjs/swagger'] = '^11.2.0';
168
- fs.writeFileSync(authApiPackagePath, `${JSON.stringify(authApiPackage, null, 2)}\n`, 'utf8');
169
- changedFiles.add(authApiPackagePath);
170
- }
171
-
172
- return { applied: true };
173
- }
174
4
 
175
5
  const PRISMA_AUTH_STORE_CONTENT = `import {
176
6
  AuthRefreshTokenStore,
@@ -215,6 +45,32 @@ ALTER TABLE "User"
215
45
  ADD COLUMN "refreshTokenHash" TEXT;
216
46
  `;
217
47
 
48
+ function detectModules(rootDir) {
49
+ const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
50
+ const appModuleText = fs.existsSync(appModulePath) ? fs.readFileSync(appModulePath, 'utf8') : '';
51
+
52
+ return {
53
+ jwtAuth:
54
+ fs.existsSync(path.join(rootDir, 'packages', 'auth-api', 'package.json')) ||
55
+ appModuleText.includes("from '@forgeon/auth-api'"),
56
+ dbPrisma:
57
+ fs.existsSync(path.join(rootDir, 'packages', 'db-prisma', 'package.json')) ||
58
+ appModuleText.includes("from '@forgeon/db-prisma'"),
59
+ };
60
+ }
61
+
62
+ function ensureLineAfter(content, anchorLine, lineToInsert) {
63
+ if (content.includes(lineToInsert)) {
64
+ return content;
65
+ }
66
+ const index = content.indexOf(anchorLine);
67
+ if (index < 0) {
68
+ return `${content.trimEnd()}\n${lineToInsert}\n`;
69
+ }
70
+ const insertAt = index + anchorLine.length;
71
+ return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
72
+ }
73
+
218
74
  function syncJwtDbPrisma({ rootDir, changedFiles }) {
219
75
  const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
220
76
  const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
@@ -246,48 +102,33 @@ function syncJwtDbPrisma({ rootDir, changedFiles }) {
246
102
  let appModule = fs.readFileSync(appModulePath, 'utf8').replace(/\r\n/g, '\n');
247
103
  const originalAppModule = appModule;
248
104
 
249
- if (appModule.includes("import { AUTH_REFRESH_TOKEN_STORE,")) {
250
- // already includes token symbol
251
- } else {
105
+ if (!appModule.includes("AUTH_REFRESH_TOKEN_STORE, authConfig")) {
252
106
  appModule = appModule.replace(
253
- /import\s*\{([^}]*)\}\s*from '@forgeon\/auth-api';/m,
254
- (full, namesRaw) => {
255
- const names = namesRaw
256
- .split(',')
257
- .map((item) => item.trim())
258
- .filter(Boolean);
259
- if (!names.includes('AUTH_REFRESH_TOKEN_STORE')) {
260
- names.unshift('AUTH_REFRESH_TOKEN_STORE');
261
- }
262
- return `import { ${names.join(', ')} } from '@forgeon/auth-api';`;
263
- },
107
+ /import\s*\{\s*authConfig,\s*authEnvSchema,\s*ForgeonAuthModule\s*\}\s*from '@forgeon\/auth-api';/m,
108
+ "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
264
109
  );
265
110
  }
266
111
 
267
112
  const storeImportLine = "import { PrismaAuthRefreshTokenStore } from './auth/prisma-auth-refresh-token.store';";
268
113
  if (!appModule.includes(storeImportLine)) {
269
- const controllerImport = "import { HealthController } from './health/health.controller';";
270
- if (appModule.includes(controllerImport)) {
271
- appModule = appModule.replace(controllerImport, `${storeImportLine}\n${controllerImport}`);
272
- } else {
273
- appModule = `${appModule.trimEnd()}\n${storeImportLine}\n`;
274
- }
114
+ appModule = ensureLineAfter(
115
+ appModule,
116
+ "import { HealthController } from './health/health.controller';",
117
+ storeImportLine,
118
+ );
275
119
  }
276
120
 
277
- const authRegisterWithPrisma = `ForgeonAuthModule.register({
121
+ if (!appModule.includes('refreshTokenStoreProvider')) {
122
+ appModule = appModule.replace(
123
+ /ForgeonAuthModule\.register\(\),/m,
124
+ `ForgeonAuthModule.register({
278
125
  imports: [DbPrismaModule],
279
126
  refreshTokenStoreProvider: {
280
127
  provide: AUTH_REFRESH_TOKEN_STORE,
281
128
  useClass: PrismaAuthRefreshTokenStore,
282
129
  },
283
- }),`;
284
-
285
- if (!appModule.includes('refreshTokenStoreProvider')) {
286
- if (/ForgeonAuthModule\.register\(\s*\),/.test(appModule)) {
287
- appModule = appModule.replace(/ForgeonAuthModule\.register\(\s*\),/, authRegisterWithPrisma);
288
- } else if (/ForgeonAuthModule\.register\(\{[\s\S]*?\}\),/m.test(appModule)) {
289
- appModule = appModule.replace(/ForgeonAuthModule\.register\(\{[\s\S]*?\}\),/m, authRegisterWithPrisma);
290
- }
130
+ }),`,
131
+ );
291
132
  }
292
133
 
293
134
  if (appModule !== originalAppModule) {
@@ -299,10 +140,7 @@ function syncJwtDbPrisma({ rootDir, changedFiles }) {
299
140
  let schema = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
300
141
  const originalSchema = schema;
301
142
  if (!schema.includes('refreshTokenHash')) {
302
- schema = schema.replace(
303
- /email\s+String\s+@unique/g,
304
- 'email String @unique\n refreshTokenHash String?',
305
- );
143
+ schema = schema.replace(/email\s+String\s+@unique/g, 'email String @unique\n refreshTokenHash String?');
306
144
  }
307
145
  if (schema !== originalSchema) {
308
146
  fs.writeFileSync(schemaPath, `${schema.trimEnd()}\n`, 'utf8');
@@ -321,11 +159,11 @@ function syncJwtDbPrisma({ rootDir, changedFiles }) {
321
159
  let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
322
160
  const originalReadme = readme;
323
161
  readme = readme.replace(
324
- '- refresh token persistence: disabled (no supported DB adapter found)',
162
+ '- refresh token persistence: disabled by default (stateless mode)',
325
163
  '- refresh token persistence: enabled (`db-prisma` adapter)',
326
164
  );
327
165
  readme = readme.replace(
328
- /- to enable persistence later:[\s\S]*?2\. run `create-forgeon add jwt-auth --project \.` again to auto-wire the adapter\./m,
166
+ /- to enable persistence later:[\s\S]*?2\. run `pnpm forgeon:sync-integrations` to auto-wire pair integrations\./m,
329
167
  '- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
330
168
  );
331
169
  if (readme !== originalReadme) {
@@ -341,41 +179,12 @@ function syncJwtDbPrisma({ rootDir, changedFiles }) {
341
179
  return { applied: true };
342
180
  }
343
181
 
344
- function detectModules(rootDir) {
345
- const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
346
- const appModuleText = fs.existsSync(appModulePath) ? fs.readFileSync(appModulePath, 'utf8') : '';
347
-
348
- return {
349
- swagger:
350
- fs.existsSync(path.join(rootDir, 'packages', 'swagger', 'package.json')) ||
351
- appModuleText.includes("from '@forgeon/swagger'"),
352
- jwtAuth:
353
- fs.existsSync(path.join(rootDir, 'packages', 'auth-api', 'package.json')) ||
354
- appModuleText.includes("from '@forgeon/auth-api'"),
355
- dbPrisma:
356
- fs.existsSync(path.join(rootDir, 'packages', 'db-prisma', 'package.json')) ||
357
- appModuleText.includes("from '@forgeon/db-prisma'"),
358
- };
359
- }
360
-
361
182
  function run() {
362
183
  const rootDir = process.cwd();
363
184
  const changedFiles = new Set();
364
185
  const detected = detectModules(rootDir);
365
186
  const summary = [];
366
187
 
367
- if (detected.swagger && detected.jwtAuth) {
368
- summary.push({
369
- feature: 'jwt-auth + swagger',
370
- result: syncJwtSwagger({ rootDir, changedFiles }),
371
- });
372
- } else {
373
- summary.push({
374
- feature: 'jwt-auth + swagger',
375
- result: { applied: false, reason: 'required modules are not both installed' },
376
- });
377
- }
378
-
379
188
  if (detected.jwtAuth && detected.dbPrisma) {
380
189
  summary.push({
381
190
  feature: 'jwt-auth + db-prisma',
@@ -406,10 +215,4 @@ function run() {
406
215
  }
407
216
  }
408
217
 
409
- const isMain =
410
- process.argv[1] &&
411
- path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
412
-
413
- if (isMain) {
414
- run();
415
- }
218
+ run();