create-forgeon 0.3.6 → 0.3.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-forgeon",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Forgeon project generator CLI",
5
5
  "license": "MIT",
6
6
  "author": "Forgeon",
@@ -324,6 +324,11 @@ function assertFilesAccessWiring(projectRoot) {
324
324
  const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
325
325
  assert.match(apiPackage, /@forgeon\/files-access/);
326
326
  assert.match(apiPackage, /pnpm --filter @forgeon\/files-access build/);
327
+ assert.equal(
328
+ apiPackage.indexOf('pnpm --filter @forgeon/files-access build') <
329
+ apiPackage.indexOf('pnpm --filter @forgeon/files build'),
330
+ true,
331
+ );
327
332
 
328
333
  const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
329
334
  assert.match(filesPackage, /@forgeon\/files-access/);
@@ -335,6 +340,11 @@ function assertFilesAccessWiring(projectRoot) {
335
340
  );
336
341
  assert.match(apiDockerfile, /COPY packages\/files-access packages\/files-access/);
337
342
  assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-access build/);
343
+ assert.equal(
344
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-access build') <
345
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
346
+ true,
347
+ );
338
348
 
339
349
  const filesController = fs.readFileSync(
340
350
  path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
@@ -373,6 +383,14 @@ function assertFilesQuotasWiring(projectRoot) {
373
383
  const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
374
384
  assert.match(apiPackage, /@forgeon\/files-quotas/);
375
385
  assert.match(apiPackage, /pnpm --filter @forgeon\/files-quotas build/);
386
+ assert.equal(
387
+ apiPackage.indexOf('pnpm --filter @forgeon/files-quotas build') <
388
+ apiPackage.indexOf('pnpm --filter @forgeon/files build'),
389
+ true,
390
+ );
391
+
392
+ const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
393
+ assert.match(filesPackage, /@forgeon\/files-quotas/);
376
394
 
377
395
  const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
378
396
  assert.match(
@@ -381,6 +399,11 @@ function assertFilesQuotasWiring(projectRoot) {
381
399
  );
382
400
  assert.match(apiDockerfile, /COPY packages\/files-quotas packages\/files-quotas/);
383
401
  assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-quotas build/);
402
+ assert.equal(
403
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-quotas build') <
404
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
405
+ true,
406
+ );
384
407
 
385
408
  const filesController = fs.readFileSync(
386
409
  path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
@@ -421,6 +444,11 @@ function assertFilesImageWiring(projectRoot) {
421
444
  const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
422
445
  assert.match(apiPackage, /@forgeon\/files-image/);
423
446
  assert.match(apiPackage, /pnpm --filter @forgeon\/files-image build/);
447
+ assert.equal(
448
+ apiPackage.indexOf('pnpm --filter @forgeon/files-image build') <
449
+ apiPackage.indexOf('pnpm --filter @forgeon/files build'),
450
+ true,
451
+ );
424
452
 
425
453
  const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
426
454
  assert.match(filesPackage, /@forgeon\/files-image/);
@@ -457,6 +485,11 @@ function assertFilesImageWiring(projectRoot) {
457
485
  );
458
486
  assert.match(apiDockerfile, /COPY packages\/files-image packages\/files-image/);
459
487
  assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-image build/);
488
+ assert.equal(
489
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-image build') <
490
+ apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
491
+ true,
492
+ );
460
493
 
461
494
  const healthController = fs.readFileSync(
462
495
  path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
4
  import {
5
+ ensureBuildStepBefore,
5
6
  ensureBuildSteps,
6
7
  ensureClassMember,
7
8
  ensureDependency,
@@ -29,6 +30,12 @@ function patchApiPackage(targetRoot) {
29
30
  const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
30
31
  ensureDependency(packageJson, '@forgeon/files-access', 'workspace:*');
31
32
  ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/files-access build']);
33
+ ensureBuildStepBefore(
34
+ packageJson,
35
+ 'predev',
36
+ 'pnpm --filter @forgeon/files-access build',
37
+ 'pnpm --filter @forgeon/files build',
38
+ );
32
39
  writeJson(packagePath, packageJson);
33
40
  }
34
41
 
@@ -373,9 +380,11 @@ function patchApiDockerfile(targetRoot) {
373
380
  content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-access packages/files-access');
374
381
 
375
382
  content = content.replace(/^RUN pnpm --filter @forgeon\/files-access build\r?\n?/gm, '');
376
- const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
377
- ? 'RUN pnpm --filter @forgeon/api prisma:generate'
378
- : 'RUN pnpm --filter @forgeon/api build';
383
+ const buildAnchor = content.includes('RUN pnpm --filter @forgeon/files build')
384
+ ? 'RUN pnpm --filter @forgeon/files build'
385
+ : content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
386
+ ? 'RUN pnpm --filter @forgeon/api prisma:generate'
387
+ : 'RUN pnpm --filter @forgeon/api build';
379
388
  content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-access build');
380
389
 
381
390
  fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
4
  import {
5
+ ensureBuildStepBefore,
5
6
  ensureBuildSteps,
6
7
  ensureClassMember,
7
8
  ensureDependency,
@@ -32,6 +33,12 @@ function patchApiPackage(targetRoot) {
32
33
  const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
33
34
  ensureDependency(packageJson, '@forgeon/files-image', 'workspace:*');
34
35
  ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/files-image build']);
36
+ ensureBuildStepBefore(
37
+ packageJson,
38
+ 'predev',
39
+ 'pnpm --filter @forgeon/files-image build',
40
+ 'pnpm --filter @forgeon/files build',
41
+ );
35
42
  writeJson(packagePath, packageJson);
36
43
  }
37
44
 
@@ -402,9 +409,11 @@ function patchApiDockerfile(targetRoot) {
402
409
  content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-image packages/files-image');
403
410
 
404
411
  content = content.replace(/^RUN pnpm --filter @forgeon\/files-image build\r?\n?/gm, '');
405
- const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
406
- ? 'RUN pnpm --filter @forgeon/api prisma:generate'
407
- : 'RUN pnpm --filter @forgeon/api build';
412
+ const buildAnchor = content.includes('RUN pnpm --filter @forgeon/files build')
413
+ ? 'RUN pnpm --filter @forgeon/files build'
414
+ : content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
415
+ ? 'RUN pnpm --filter @forgeon/api prisma:generate'
416
+ : 'RUN pnpm --filter @forgeon/api build';
408
417
  content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-image build');
409
418
 
410
419
  fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
4
  import {
5
+ ensureBuildStepBefore,
5
6
  ensureBuildSteps,
6
7
  ensureClassMember,
7
8
  ensureDependency,
@@ -32,6 +33,23 @@ function patchApiPackage(targetRoot) {
32
33
  const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
33
34
  ensureDependency(packageJson, '@forgeon/files-quotas', 'workspace:*');
34
35
  ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/files-quotas build']);
36
+ ensureBuildStepBefore(
37
+ packageJson,
38
+ 'predev',
39
+ 'pnpm --filter @forgeon/files-quotas build',
40
+ 'pnpm --filter @forgeon/files build',
41
+ );
42
+ writeJson(packagePath, packageJson);
43
+ }
44
+
45
+ function patchFilesPackage(targetRoot) {
46
+ const packagePath = path.join(targetRoot, 'packages', 'files', 'package.json');
47
+ if (!fs.existsSync(packagePath)) {
48
+ return;
49
+ }
50
+
51
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
52
+ ensureDependency(packageJson, '@forgeon/files-quotas', 'workspace:*');
35
53
  writeJson(packagePath, packageJson);
36
54
  }
37
55
 
@@ -280,9 +298,11 @@ function patchApiDockerfile(targetRoot) {
280
298
  content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-quotas packages/files-quotas');
281
299
 
282
300
  content = content.replace(/^RUN pnpm --filter @forgeon\/files-quotas build\r?\n?/gm, '');
283
- const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
284
- ? 'RUN pnpm --filter @forgeon/api prisma:generate'
285
- : 'RUN pnpm --filter @forgeon/api build';
301
+ const buildAnchor = content.includes('RUN pnpm --filter @forgeon/files build')
302
+ ? 'RUN pnpm --filter @forgeon/files build'
303
+ : content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
304
+ ? 'RUN pnpm --filter @forgeon/api prisma:generate'
305
+ : 'RUN pnpm --filter @forgeon/api build';
286
306
  content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-quotas build');
287
307
 
288
308
  fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
@@ -360,6 +380,7 @@ Key env:
360
380
  export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
361
381
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-quotas'));
362
382
  patchApiPackage(targetRoot);
383
+ patchFilesPackage(targetRoot);
363
384
  patchAppModule(targetRoot);
364
385
  patchFilesController(targetRoot);
365
386
  patchHealthController(targetRoot);
@@ -46,6 +46,31 @@ export function ensureBuildSteps(packageJson, scriptName, requiredCommands) {
46
46
  }
47
47
  }
48
48
 
49
+ export function ensureBuildStepBefore(packageJson, scriptName, command, beforeCommand) {
50
+ if (!packageJson.scripts) {
51
+ packageJson.scripts = {};
52
+ }
53
+
54
+ const current = packageJson.scripts[scriptName];
55
+ const steps =
56
+ typeof current === 'string' && current.trim().length > 0
57
+ ? current
58
+ .split('&&')
59
+ .map((item) => item.trim())
60
+ .filter(Boolean)
61
+ : [];
62
+
63
+ const withoutCommand = steps.filter((item) => item !== command);
64
+ const beforeIndex = withoutCommand.indexOf(beforeCommand);
65
+ if (beforeIndex >= 0) {
66
+ withoutCommand.splice(beforeIndex, 0, command);
67
+ } else {
68
+ withoutCommand.push(command);
69
+ }
70
+
71
+ packageJson.scripts[scriptName] = withoutCommand.join(' && ');
72
+ }
73
+
49
74
  export function ensureLineAfter(content, anchorLine, lineToInsert) {
50
75
  if (content.includes(lineToInsert)) {
51
76
  return content;
@@ -12,6 +12,7 @@ import {
12
12
  UseInterceptors,
13
13
  } from '@nestjs/common';
14
14
  import { FileInterceptor } from '@nestjs/platform-express';
15
+ import { Readable } from 'node:stream';
15
16
  import { CreateFileDto } from './dto/create-file.dto';
16
17
  import { FilesService } from './files.service';
17
18
  import type { FileVariantKey } from './files.types';
@@ -58,7 +59,7 @@ export class FilesController {
58
59
  async download(@Param('publicId') publicId: string, @Query('variant') variantQuery?: string) {
59
60
  const variant = this.parseVariant(variantQuery);
60
61
  const payload = await this.filesService.openDownload(publicId, variant);
61
- return new StreamableFile(payload.stream, {
62
+ return new StreamableFile(payload.stream as Readable, {
62
63
  disposition: `inline; filename="${payload.fileName}"`,
63
64
  type: payload.mimeType,
64
65
  });
@@ -15,6 +15,17 @@ import { PrismaService } from '@forgeon/db-prisma';
15
15
  import { FilesConfigService } from './files-config.service';
16
16
  import type { FileRecordDto, FileVariantKey, StoredFileInput } from './files.types';
17
17
 
18
+ type S3ModuleLike = {
19
+ S3Client: new (config: Record<string, unknown>) => {
20
+ send: (command: unknown) => Promise<{
21
+ Body?: unknown;
22
+ }>;
23
+ };
24
+ PutObjectCommand: new (input: Record<string, unknown>) => unknown;
25
+ GetObjectCommand: new (input: Record<string, unknown>) => unknown;
26
+ DeleteObjectCommand: new (input: Record<string, unknown>) => unknown;
27
+ };
28
+
18
29
  type BlobRef = {
19
30
  id: string;
20
31
  hash: string;
@@ -225,7 +236,7 @@ export class FilesService {
225
236
  }
226
237
 
227
238
  async openDownload(publicId: string, variant: FileVariantKey = 'original'): Promise<{
228
- stream: NodeJS.ReadableStream;
239
+ stream: Readable;
229
240
  mimeType: string;
230
241
  fileName: string;
231
242
  }> {
@@ -439,7 +450,7 @@ export class FilesService {
439
450
  }
440
451
 
441
452
  private async storeS3(buffer: Buffer, storageKey: string): Promise<{ storageKey: string }> {
442
- const { PutObjectCommand } = await import('@aws-sdk/client-s3');
453
+ const { PutObjectCommand } = await this.loadS3Module();
443
454
  const client = await this.getS3Client();
444
455
  const config = this.resolveS3Config();
445
456
 
@@ -454,8 +465,8 @@ export class FilesService {
454
465
  return { storageKey };
455
466
  }
456
467
 
457
- private async openS3(storageKey: string): Promise<NodeJS.ReadableStream> {
458
- const { GetObjectCommand } = await import('@aws-sdk/client-s3');
468
+ private async openS3(storageKey: string): Promise<Readable> {
469
+ const { GetObjectCommand } = await this.loadS3Module();
459
470
  const client = await this.getS3Client();
460
471
  const config = this.resolveS3Config();
461
472
 
@@ -473,7 +484,7 @@ export class FilesService {
473
484
  }
474
485
 
475
486
  private async deleteS3(storageKey: string): Promise<void> {
476
- const { DeleteObjectCommand } = await import('@aws-sdk/client-s3');
487
+ const { DeleteObjectCommand } = await this.loadS3Module();
477
488
  const client = await this.getS3Client();
478
489
  const config = this.resolveS3Config();
479
490
  await client.send(
@@ -566,7 +577,7 @@ export class FilesService {
566
577
  if (this.s3Client) {
567
578
  return this.s3Client;
568
579
  }
569
- const { S3Client } = await import('@aws-sdk/client-s3');
580
+ const { S3Client } = await this.loadS3Module();
570
581
  const config = this.resolveS3Config();
571
582
  this.s3Client = new S3Client({
572
583
  region: config.region,
@@ -581,7 +592,7 @@ export class FilesService {
581
592
  return this.s3Client;
582
593
  }
583
594
 
584
- private toNodeReadable(body: unknown): NodeJS.ReadableStream {
595
+ private toNodeReadable(body: unknown): Readable {
585
596
  if (body instanceof Readable) {
586
597
  return body;
587
598
  }
@@ -594,12 +605,19 @@ export class FilesService {
594
605
  typeof (body as { transformToWebStream?: unknown }).transformToWebStream === 'function'
595
606
  ) {
596
607
  return Readable.fromWeb(
597
- (body as { transformToWebStream: () => ReadableStream<Uint8Array> }).transformToWebStream(),
608
+ (body as { transformToWebStream: () => unknown }).transformToWebStream() as never,
598
609
  );
599
610
  }
600
611
  throw new InternalServerErrorException('Unsupported S3 response body type');
601
612
  }
602
613
 
614
+ private async loadS3Module(): Promise<S3ModuleLike> {
615
+ const importModule = new Function('specifier', 'return import(specifier)') as (
616
+ specifier: string,
617
+ ) => Promise<S3ModuleLike>;
618
+ return importModule('@aws-sdk/client-s3');
619
+ }
620
+
603
621
  private generatePublicId(): string {
604
622
  return crypto.randomUUID().replace(/-/g, '');
605
623
  }