diff --git a/cspell.words.txt b/cspell.words.txt index 0339900..412caff 100644 --- a/cspell.words.txt +++ b/cspell.words.txt @@ -33,4 +33,8 @@ TRAEFIK traefik mariadb MARIADB -dts \ No newline at end of file +dts +nestjs +cloudflare +cldr +cldrjs \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e59cd7c..a2cf6f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "monux-cli", - "version": "2.2.0", + "version": "2.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "monux-cli", - "version": "2.2.0", + "version": "2.2.1", "license": "MIT", "dependencies": { "chalk": "^4.1.2", @@ -24,6 +24,7 @@ "@angular/common": "^18.2.13", "@faker-js/faker": "^9.0.3", "@jest/globals": "^29.7.0", + "@nestjs/common": "^11.0.20", "@types/death": "^1.1.5", "@types/figlet": "^1.5.8", "@types/js-yaml": "^4.0.9", @@ -2632,6 +2633,48 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/common": { + "version": "11.0.20", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.20.tgz", + "integrity": "sha512-/GH8NDCczjn6+6RNEtSNAts/nq/wQE8L1qZ9TRjqjNqEsZNE1vpFuRIhmcO2isQZ0xY5rySnpaRdrOAul3gQ3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "load-esm": "1.0.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -2784,6 +2827,32 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4409,9 +4478,9 @@ "integrity": "sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w==" }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "license": "MIT", "dependencies": { @@ -5962,6 +6031,13 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/figlet": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.8.0.tgz", @@ -5987,6 +6063,25 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -6553,8 +6648,7 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.2", @@ -7302,6 +7396,16 @@ "node": ">=8" } }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=6" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -8201,6 +8305,26 @@ "dev": true, "license": "MIT" }, + "node_modules/load-esm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", + "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -8818,6 +8942,20 @@ "dev": true, "license": "MIT" }, + "node_modules/peek-readable": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", + "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9194,6 +9332,14 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -10030,6 +10176,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", + "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10154,6 +10318,24 @@ "node": ">=8.0" } }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -10408,6 +10590,32 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/package.json b/package.json index 3fda527..045f9c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monux-cli", - "version": "2.2.1", + "version": "2.2.2", "license": "MIT", "main": "index.js", "engines": { @@ -51,6 +51,7 @@ "@angular/common": "^18.2.13", "@faker-js/faker": "^9.0.3", "@jest/globals": "^29.7.0", + "@nestjs/common": "^11.0.20", "@types/death": "^1.1.5", "@types/figlet": "^1.5.8", "@types/js-yaml": "^4.0.9", diff --git a/src/__testing__/mock/fake-compose-service.function.ts b/src/__testing__/mock/fake-compose-service.function.ts index bd2fcd3..4a222d3 100644 --- a/src/__testing__/mock/fake-compose-service.function.ts +++ b/src/__testing__/mock/fake-compose-service.function.ts @@ -2,15 +2,7 @@ import { faker } from '@faker-js/faker'; import { fakeStringKeyValue, fakeUniqueString, fakeArray } from './helpers'; -import { ComposePort, ComposeService, ComposeServiceVolume } from '../../docker'; - -function fakeComposeVolume(): ComposeServiceVolume { - const res: ComposeServiceVolume = { - path: faker.system.directoryPath(), - mount: faker.helpers.maybe(() => faker.system.directoryPath()) ?? '' - }; - return res; -} +import { ComposePort, ComposeService } from '../../docker'; function fakeComposePort(): ComposePort { return { @@ -26,7 +18,7 @@ export function fakeComposeService(): ComposeService { build: faker.helpers.maybe(() => faker.system.directoryPath()), image: faker.helpers.maybe(() => faker.word.noun()), networks: faker.helpers.maybe(() => fakeArray(() => fakeUniqueString(), faker.number.int({ min: 1, max: 5 }))), - volumes: faker.helpers.maybe(() => fakeArray(() => fakeComposeVolume(), faker.number.int({ min: 1, max: 5 }))), + volumes: faker.helpers.maybe(() => fakeArray(() => faker.system.directoryPath(), faker.number.int({ min: 1, max: 5 }))), command: faker.helpers.maybe(() => fakeArray(() => faker.word.noun(), faker.number.int({ min: 1, max: 5 }))), ports: faker.helpers.maybe(() => fakeArray(() => fakeComposePort(), faker.number.int({ min: 1, max: 5 }))), labels: faker.helpers.maybe(() => fakeArray(() => faker.word.noun(), faker.number.int({ min: 1, max: 5 }))) diff --git a/src/angular/angular.utilities.ts b/src/angular/angular.utilities.ts index 9e1a195..d6d93a7 100644 --- a/src/angular/angular.utilities.ts +++ b/src/angular/angular.utilities.ts @@ -166,7 +166,7 @@ export abstract class AngularUtilities { * @param titleSuffix - The suffix after the title. */ static async setupAuth( - projectRoot: string, + projectRoot: Path, name: string, apiName: string, domain: string, @@ -421,6 +421,14 @@ export abstract class AngularUtilities { getPath(root, 'src', 'app', 'models', 'admin.model.ts'), getAdminModelContent(apiName) ); + await FsUtilities.createFile( + getPath(root, 'src', 'app', 'models', 'roles.enum.ts'), + [ + 'export enum Roles {', + '\tADMIN = \'ADMIN\'', + '}' + ] + ); await FsUtilities.createFile( getPath(root, 'src', 'app', 'models', 'base-entity.model.ts'), baseEntityModelContent @@ -505,7 +513,7 @@ export abstract class AngularUtilities { * @param command - The command to run. * @param options - Options for running the command. */ - static runCommand(directory: string, command: AngularCliCommands, options: AngularCliOptions): void { + static runCommand(directory: Path, command: AngularCliCommands, options: AngularCliOptions): void { CPUtilities.execSync(`cd ${directory} && npx @angular/cli@${this.CLI_VERSION} ${command} ${optionsToCliString(options)}`); } @@ -517,7 +525,7 @@ export abstract class AngularUtilities { * @param domain - The domain of the project. */ static async generatePage( - root: string, + root: Path, pageName: string, navElement: AddNavElementConfig | undefined, domain: string | undefined @@ -780,9 +788,24 @@ export abstract class AngularUtilities { * @param root - The directory of the angular project to setup the pwa support for. * @param name - The name of the angular project to setup the pwa support for. */ - static async setupPwa(root: string, name: string): Promise { + static async setupPwa(root: Path, name: string): Promise { // eslint-disable-next-line no-console console.log('Adds pwa support'); + await this.addProvider( + root, + { provide: 'NGX_PWA_OFFLINE_SERVICE', useExisting: 'OfflineService' as unknown }, + [ + { defaultImport: false, element: 'NGX_PWA_OFFLINE_SERVICE', path: NpmPackage.NGX_PWA }, + { defaultImport: false, element: 'OfflineService', path: './services/offline.service' } + ] + ); + // TODO: enable OfflineRequestInterceptor. Need to fix parsing of provideServiceWorker first. + await this.addProvider( + root, + // eslint-disable-next-line typescript/no-unsafe-assignment, typescript/no-explicit-any + { provide: 'HTTP_INTERCEPTORS', useClass: 'OfflineRequestInterceptor' as any, multi: true }, + [{ defaultImport: false, element: 'OfflineRequestInterceptor', path: NpmPackage.NGX_PWA }] + ); this.runCommand(root, `add @angular/pwa@${this.CLI_VERSION}`, { '--skip-confirmation': true }); await NpmUtilities.install(name, [NpmPackage.NGX_PWA]); await FsUtilities.updateFile( @@ -804,20 +827,6 @@ export abstract class AngularUtilities { getPath(root, 'src', 'app', 'services', 'offline.service.ts'), offlineServiceContent ); - await this.addProvider( - root, - { provide: 'NGX_PWA_OFFLINE_SERVICE', useExisting: 'OfflineService' as unknown }, - [ - { defaultImport: false, element: 'NGX_PWA_OFFLINE_SERVICE', path: NpmPackage.NGX_PWA }, - { defaultImport: false, element: 'OfflineService', path: './services/offline.service' } - ] - ); - // TODO: enable OfflineRequestInterceptor. Need to fix parsing of provideServiceWorker first. - // await this.addProvider( - // root, - // { provide: 'HTTP_INTERCEPTORS', useClass: 'OfflineRequestInterceptor' as any, multi: true }, - // [{ defaultImport: false, element: 'OfflineRequestInterceptor', path: NpmPackage.NGX_PWA }] - // ); } /** diff --git a/src/angular/content/admin-model.content.ts b/src/angular/content/admin-model.content.ts index 5b912ce..e32aaf7 100644 --- a/src/angular/content/admin-model.content.ts +++ b/src/angular/content/admin-model.content.ts @@ -4,10 +4,11 @@ import { DefaultEnvKeys } from '../../env'; export function getAdminModelContent(apiName: string): string { return `import { ChangeSet } from 'ngx-material-change-sets'; import { array, custom, DecoratorTypes, string } from 'ngx-material-entity'; -import { environment } from '../../environment/environment'; import { BaseEntity } from './base-entity.model'; +import { Roles } from './roles.enum'; import { ChangeSetsInputComponent, ChangeSetsInputMetadata } from '../components/change-sets-input/change-sets-input.component'; +import { environment } from '../../environment/environment'; export class Admin extends BaseEntity { diff --git a/src/angular/content/auth-service.content.ts b/src/angular/content/auth-service.content.ts index 0bd7f72..d7f8e6e 100644 --- a/src/angular/content/auth-service.content.ts +++ b/src/angular/content/auth-service.content.ts @@ -9,6 +9,7 @@ import { Router } from '@angular/router'; import { BaseAuthData, BaseRole, BaseToken, JwtAuthService } from 'ngx-material-auth'; import { environment } from '../../environment/environment'; +import { Roles } from '../models/roles.enum'; /** * Provides information about a user role. diff --git a/src/commands/add/add-angular-library/add-angular-library.command.ts b/src/commands/add/add-angular-library/add-angular-library.command.ts index 01a0e69..00ae89f 100644 --- a/src/commands/add/add-angular-library/add-angular-library.command.ts +++ b/src/commands/add/add-angular-library/add-angular-library.command.ts @@ -25,7 +25,7 @@ type AddAngularLibraryConfiguration = AddConfiguration & { // eslint-disable-next-line jsdoc/require-jsdoc type CreateResult = { // eslint-disable-next-line jsdoc/require-jsdoc - root: string, + root: Path, // eslint-disable-next-line jsdoc/require-jsdoc oldPackageJson: PackageJson }; @@ -59,7 +59,7 @@ export class AddAngularLibraryCommand extends BaseAddCommand { + private async setupTsConfig(root: Path, config: AddAngularLibraryConfiguration): Promise { // eslint-disable-next-line no-console console.log('sets up tsconfig'); await Promise.all([ - TsConfigUtilities.updateTsConfig(projectName, { extends: '../../tsconfig.base.json' }), + TsConfigUtilities.updateTsConfig(config.name, { extends: '../../tsconfig.base.json' }), this.createTsConfigEslint(root), this.updateTsConfigLib(root), - this.updateTsConfigSpec(root) + this.updateTsConfigSpec(root), + this.updateBaseTsConfig(config, root) ]); } - private async updateTsConfigLib(root: string): Promise { + private async updateBaseTsConfig(config: AddAngularLibraryConfiguration, root: Path): Promise { + await TsConfigUtilities.updateBaseTsConfig({ + compilerOptions: { + paths: { + [`${config.scope}/${config.name}`]: [`${root}/src/public-api.ts`] + } + } + }); + } + + private async updateTsConfigLib(root: Path): Promise { const tsconfigPath: Path = getPath(root, 'tsconfig.lib.json'); const oldConfig: TsConfig = await FsUtilities.parseFileAs(tsconfigPath); const config: TsConfig = mergeDeep(oldConfig, { extends: './tsconfig.json', compilerOptions: { - outDir: 'out-tsc/spec' + outDir: 'out-tsc/lib' } }); await FsUtilities.updateFile(tsconfigPath, JsonUtilities.stringify(config), 'replace'); @@ -171,9 +183,9 @@ export class AddAngularLibraryCommand extends BaseAddCommand { const config: AddAngularWebsiteConfiguration = await this.getConfig(); - const root: string = await this.createProject(config); + const root: Path = await this.createProject(config); const prodRootDomain: string = await EnvUtilities.getEnvVariable( DefaultEnvKeys.PROD_ROOT_DOMAIN, @@ -95,9 +95,10 @@ export class AddAngularWebsiteCommand extends BaseAddCommand { + private async createDefaultPages(root: Path, titleSuffix: string, domain: string): Promise { await AngularUtilities.generatePage(root, 'Home', { addTo: 'navbar', rowIndex: 0, @@ -229,11 +230,11 @@ export class AddAngularWebsiteCommand extends BaseAddCommand { + private async createProject(config: AddAngularWebsiteConfiguration): Promise { // eslint-disable-next-line no-console console.log('Creates the base website'); AngularUtilities.runCommand( - APPS_DIRECTORY_NAME, + getPath(APPS_DIRECTORY_NAME), `new ${config.name}`, { '--skip-git': true, '--style': 'css', '--inline-style': true, '--ssr': true } ); diff --git a/src/commands/add/add-angular/add-angular.command.ts b/src/commands/add/add-angular/add-angular.command.ts index a04c651..2ac6d79 100644 --- a/src/commands/add/add-angular/add-angular.command.ts +++ b/src/commands/add/add-angular/add-angular.command.ts @@ -9,7 +9,7 @@ import { NpmPackage, NpmUtilities } from '../../../npm'; import { TailwindUtilities } from '../../../tailwind'; import { TsConfig, TsConfigUtilities } from '../../../tsconfig'; import { OmitStrict } from '../../../types'; -import { getPath, toPascalCase } from '../../../utilities'; +import { getPath, Path, toPascalCase } from '../../../utilities'; import { WorkspaceProject, WorkspaceUtilities } from '../../../workspace'; import { BaseAddCommand, AddConfiguration } from '../models'; @@ -70,7 +70,7 @@ export class AddAngularCommand extends BaseAddCommand { override async run(): Promise { const config: AddAngularConfiguration = await this.getConfig(); - const root: string = await this.createProject(config); + const root: Path = await this.createProject(config); await Promise.all([ this.cleanUp(root), this.setupTsConfig(root, config.name), @@ -85,16 +85,42 @@ export class AddAngularCommand extends BaseAddCommand { dockerfile: `./${root}/${DOCKER_FILE_NAME}`, context: '.' }, - volumes: [{ path: `/${config.name}` }] - // labels: DockerUtilities.getTraefikLabels(config.name, 4000, domain) + volumes: [`/${config.name}`] }, 4000, + config.port, true, config.subDomain ), AngularUtilities.updateAngularJson( getPath(root, ANGULAR_JSON_FILE_NAME), - { $schema: '../../node_modules/@angular/cli/lib/config/schema.json' } + { + $schema: '../../node_modules/@angular/cli/lib/config/schema.json', + projects: { + [config.name]: { + architect: { + build: { + configurations: { + production: { + budgets: [ + { + maximumError: '3MB', + maximumWarning: '500kB', + type: 'initial' + }, + { + maximumError: '4kB', + maximumWarning: '2kB', + type: 'anyComponentStyle' + } + ] + } + } + } + } + } + } + } ), AngularUtilities.setupMaterial(root) ]); @@ -135,7 +161,7 @@ export class AddAngularCommand extends BaseAddCommand { ], 'append'); } - private async createDefaultPages(root: string, config: AddAngularConfiguration): Promise { + private async createDefaultPages(root: Path, config: AddAngularConfiguration): Promise { await AngularUtilities.generatePage( root, 'Home', @@ -185,10 +211,10 @@ export class AddAngularCommand extends BaseAddCommand { ); } - private async createProject(config: AddAngularConfiguration): Promise { + private async createProject(config: AddAngularConfiguration): Promise { console.log('Creates the base app'); AngularUtilities.runCommand( - APPS_DIRECTORY_NAME, + getPath(APPS_DIRECTORY_NAME), `new ${config.name}`, { '--skip-git': true, '--style': 'css', '--inline-style': true, '--ssr': true } ); diff --git a/src/commands/add/add-loopback/add-loopback-command.test.ts b/src/commands/add/add-loopback/add-loopback-command.test.ts index e2d377a..2d3c41d 100644 --- a/src/commands/add/add-loopback/add-loopback-command.test.ts +++ b/src/commands/add/add-loopback/add-loopback-command.test.ts @@ -25,7 +25,7 @@ describe('AddLoopbackCommand', () => { test('should run and create new database', () => { // const baseConfig: AddConfiguration = { name: 'api', type: AddType.LOOPBACK }; // const command: AddLoopbackCommand = new AddLoopbackCommand(baseConfig); - // await command.run(); // TODO: enable + // await command.run(); // TODO: enable test expect(true).toBe(true); }, 50000); diff --git a/src/commands/add/add-loopback/add-loopback.command.ts b/src/commands/add/add-loopback/add-loopback.command.ts index 8ab9196..5657919 100644 --- a/src/commands/add/add-loopback/add-loopback.command.ts +++ b/src/commands/add/add-loopback/add-loopback.command.ts @@ -1,5 +1,6 @@ -import { APPS_DIRECTORY_NAME, PROD_DOCKER_COMPOSE_FILE_NAME, DOCKER_FILE_NAME, ENVIRONMENT_MODEL_TS_FILE_NAME, GIT_IGNORE_FILE_NAME, TS_CONFIG_FILE_NAME } from '../../../constants'; -import { DbUtilities } from '../../../db'; +import { loopbackWebpackContent } from './loopback-webpack.content'; +import { APPS_DIRECTORY_NAME, PROD_DOCKER_COMPOSE_FILE_NAME, DOCKER_FILE_NAME, ENVIRONMENT_MODEL_TS_FILE_NAME, GIT_IGNORE_FILE_NAME, TS_CONFIG_FILE_NAME, WEBPACK_CONFIG } from '../../../constants'; +import { DbType, DbUtilities } from '../../../db'; import { DockerUtilities } from '../../../docker'; import { FsUtilities, QuestionsFor } from '../../../encapsulation'; import { DefaultEnvKeys, EnvUtilities } from '../../../env'; @@ -82,8 +83,11 @@ export class AddLoopbackCommand extends BaseAddCommand override async run(): Promise { const config: AddLoopbackConfiguration = await this.getConfig(); - const { dbServiceName, databaseName } = await DbUtilities.configureDb(config.name, undefined, getPath('.')); - const root: string = await this.createProject(config); + const { dbServiceName, databaseName, dbType } = await DbUtilities.configureDb(config.name, DbType.POSTGRES, getPath('.')); + if (dbType !== DbType.POSTGRES) { + throw new Error('Error adding the app: Currently loopback only supports postgres as its database.'); + } + const root: Path = await this.createProject(config); await EnvUtilities.setupProjectEnvironment(root, false); await this.createLoopbackDatasource(dbServiceName, databaseName, root, config.name); @@ -100,34 +104,75 @@ export class AddLoopbackCommand extends BaseAddCommand dockerfile: `./${root}/${DOCKER_FILE_NAME}`, context: '.' }, - volumes: [{ path: `/${config.name}` }] + volumes: [`/${config.name}`] }, 3000, + config.port, true, config.subDomain ), - this.updateDockerFile(root) + this.updateDockerFile(root, config) + // this.setupWebpack(root, config.name) TODO: enable ]); await NpmUtilities.updatePackageJson(config.name, { scripts: { start: 'npm run start:watch', 'start:watch': 'tsc-watch --target es2017 --outDir ./dist --onSuccess \"node .\"' + // 'build:webpack': 'webpack' TODO: enable } }); await NpmUtilities.install(config.name, [NpmPackage.TSC_WATCH], true); - await LoopbackUtilities.setupAuth(root, config, dbServiceName); - await LoopbackUtilities.setupLogging(root, config.name); - await LoopbackUtilities.setupChangeSets(root, config.name); + await LoopbackUtilities.setupAuth(root, config, databaseName); + await LoopbackUtilities.setupLogging(root, config.name, databaseName); + await LoopbackUtilities.setupChangeSets(root, config.name, databaseName); await LoopbackUtilities.setupMigrations(root, config.name); const app: WorkspaceProject = await WorkspaceUtilities.findProjectOrFail(config.name, getPath('.')); await EnvUtilities.buildEnvironmentFileForApp(app, false, 'dev.docker-compose.yaml', getPath('.')); } - private async updateDockerFile(root: string): Promise { - // TODO: Update loopback 4 Dockerfile - await FsUtilities.updateFile(getPath(root, DOCKER_FILE_NAME), '', 'append'); + private async setupWebpack(root: Path, projectName: string): Promise { + await FsUtilities.createFile(getPath(root, WEBPACK_CONFIG), loopbackWebpackContent); + await NpmUtilities.install( + projectName, + [ + NpmPackage.WEBPACK, + NpmPackage.WEBPACK_CLI, + NpmPackage.TS_LOADER, + NpmPackage.WEBPACK_NODE_EXTERNALS, + NpmPackage.TSCONFIG_PATH_WEBPACK_PLUGIN, + NpmPackage.FORK_TS_CHECKER_WEBPACK_PLUGIN + ], + true + ); + await NpmUtilities.install(projectName, [ + NpmPackage.CLDRJS, + NpmPackage.CLDR_DATA + ]); + } + + private async updateDockerFile(root: string, config: AddLoopbackConfiguration): Promise { + await FsUtilities.updateFile( + getPath(root, DOCKER_FILE_NAME), + [ + 'FROM node:20 AS build', + '# Set to a non-root built-in user `node`', + 'USER node', + 'RUN mkdir -p /home/node/root', + 'COPY --chown=node . /home/node/root', + 'WORKDIR /home/node/root', + 'RUN npm install', + `RUN npm run build --workspace=${APPS_DIRECTORY_NAME}/${config.name} --omit=dev`, + '', + 'FROM node:20', + 'WORKDIR /usr/app', + `COPY --from=build /home/node/root/${APPS_DIRECTORY_NAME}/${config.name}/dist/${APPS_DIRECTORY_NAME}/${config.name} ./`, + 'COPY --from=build /home/node/root/node_modules ./node_modules', // TODO: get rid off + 'CMD node src' + ], + 'replace' + ); } private async updateIndexTs(root: string, port: number): Promise { @@ -141,6 +186,7 @@ export class AddLoopbackCommand extends BaseAddCommand await FsUtilities.replaceInFile(indexPath, 'env.PORT', 'env[\'PORT\']'); await FsUtilities.replaceInFile(indexPath, 'env.HOST', 'env[\'HOST\']'); await FsUtilities.replaceInFile(indexPath, '?? 3000', `?? ${port}`); + await TsUtilities.addImportStatements(indexPath, [{ defaultImport: false, element: 'Roles', path: './models' }]); } private async updateOpenApiSpec(root: string, port: number): Promise { @@ -173,7 +219,12 @@ export class AddLoopbackCommand extends BaseAddCommand * @param root - The root of the loopback project. * @param projectName - The name of the loopback app. */ - private async createLoopbackDatasource(dbServiceName: string, databaseName: string, root: string, projectName: string): Promise { + private async createLoopbackDatasource( + dbServiceName: string, + databaseName: string, + root: Path, + projectName: string + ): Promise { const lbDatabaseConfig: LbDatabaseConfig = { name: databaseName, connector: 'postgres' as 'postgresql' @@ -241,7 +292,7 @@ export class AddLoopbackCommand extends BaseAddCommand await EnvUtilities.addProjectVariableKey(projectName, environmentModel, DefaultEnvKeys.dbHost(dbServiceName), true, getPath('.')); } - private async createProject(config: AddLoopbackConfiguration): Promise { + private async createProject(config: AddLoopbackConfiguration): Promise { await LoopbackUtilities.runCommand(getPath(APPS_DIRECTORY_NAME), `new ${config.name}`, { '--yes': true, '--config': { diff --git a/src/commands/add/add-loopback/loopback-webpack.content.ts b/src/commands/add/add-loopback/loopback-webpack.content.ts new file mode 100644 index 0000000..48473b1 --- /dev/null +++ b/src/commands/add/add-loopback/loopback-webpack.content.ts @@ -0,0 +1,60 @@ + +// eslint-disable-next-line jsdoc/require-jsdoc +export const loopbackWebpackContent: string = `const path = require('path'); +const webpack = require('webpack'); +const nodeExternals = require('webpack-node-externals'); +const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); + +module.exports = { + // Adapted entry: points at LoopBack’s src/index.ts + entry: path.join(__dirname, 'src', 'index.ts'), + target: 'node', + mode: 'none', + devtool: false, + externalsPresets: { node: true }, + externals: [nodeExternals()], + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'bundle.js' + }, + ignoreWarnings: [/^(?!CriticalDependenciesWarning$)/], + node: { __filename: false, __dirname: false }, + module: { + rules: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + configFile: 'tsconfig.json' + } + } + ] + } + ] + }, + resolve: { + extensions: ['.ts', '.js'], + alias: { + cldr$: 'cldrjs', + cldr: 'cldrjs/dist/cldr', + 'cldr/event$': 'cldrjs/dist/cldr/event', + 'cldr/supplemental$': 'cldrjs/dist/cldr/supplemental' + }, + plugins: [new TsconfigPathsPlugin({ configFile: 'tsconfig.json' })] + }, + optimization: { nodeEnv: false }, + plugins: [ + new ForkTsCheckerWebpackPlugin({ + typescript: { configFile: 'tsconfig.json' } + }), + new webpack.BannerPlugin({ + banner: '#!/usr/bin/env node', + raw: true + }) + ] +};`; \ No newline at end of file diff --git a/src/commands/add/add-nest/add-nest.command.ts b/src/commands/add/add-nest/add-nest.command.ts new file mode 100644 index 0000000..fd78d89 --- /dev/null +++ b/src/commands/add/add-nest/add-nest.command.ts @@ -0,0 +1,443 @@ + +import { Type } from '@nestjs/common'; + +import { APPS_DIRECTORY_NAME, DOCKER_FILE_NAME, ENVIRONMENT_MODEL_TS_FILE_NAME, ESLINT_CONFIG_FILE_NAME, NEST_CLI_FILE_NAME, PROD_DOCKER_COMPOSE_FILE_NAME, WEBPACK_CONFIG } from '../../../constants'; +import { DbType, DbUtilities, defaultPortForDbType } from '../../../db'; +import { DockerUtilities } from '../../../docker'; +import { FsUtilities, QuestionsFor } from '../../../encapsulation'; +import { DefaultEnvKeys, EnvUtilities } from '../../../env'; +import { EslintUtilities } from '../../../eslint'; +import { NestUtilities } from '../../../nest'; +import { NpmPackage, NpmUtilities } from '../../../npm'; +import { TsUtilities } from '../../../ts'; +import { TsConfigUtilities } from '../../../tsconfig'; +import { OmitStrict } from '../../../types'; +import { getPath, Path } from '../../../utilities'; +import { WorkspaceProject, WorkspaceUtilities } from '../../../workspace'; +import { AddConfiguration, BaseAddCommand } from '../models'; + +/** + * Configuration for adding a new nest js api. + */ +type AddNestConfiguration = AddConfiguration & { + /** + * The name of the frontend where the reset password functionality is implemented. + */ + frontendName: string, + /** + * The port that should be used by the application. + * @default 3000 + */ + port: number, + /** + * The sub domain that this service should be reached under. + * If nothing is provided, Monux assumes that the service should be reached under the root domain + * and under the www sub domain. + */ + subDomain?: string, + /** + * The email for the default root user. + */ + defaultUserEmail: string, + /** + * The password for the default root user. + */ + defaultUserPassword: string +}; + +/** + * Command that handles adding a nest api to the monorepo. + */ +export class AddNestCommand extends BaseAddCommand { + + private readonly dbNpmPackages: Record = { + [DbType.POSTGRES]: [NpmPackage.PG], + [DbType.MARIADB]: [NpmPackage.MYSQL_2] + }; + + protected override configQuestions: QuestionsFor> = { + port: { + type: 'number', + message: 'port', + required: true, + default: 3000 + }, + subDomain: { + type: 'input', + message: 'sub domain', + required: false + }, + defaultUserEmail: { + type: 'input', + message: 'Email of the default user', + required: true, + default: async () => (await FsUtilities.readFile(getPath(PROD_DOCKER_COMPOSE_FILE_NAME))) + .split('.acme.email=')[1] + .split('\n')[0] + }, + defaultUserPassword: { + type: 'input', + message: 'Password of the default user', + required: true, + validate: (v) => v.length >= 12 ? true : 'Password must be at least 12 characters strong' + }, + frontendName: { + type: 'input', + message: 'Name of the frontend where the reset password ui is implemented', + required: true + } + }; + + override async run(): Promise { + const config: AddNestConfiguration = await this.getConfig(); + const { dbServiceName, databaseName, dbType } = await DbUtilities.configureDb(config.name, undefined, getPath('.')); + const root: Path = await this.createProject(config); + await EnvUtilities.setupProjectEnvironment(root, false); + await this.createNestDatasource(dbServiceName, databaseName, dbType, root, config.name); + await this.updateMainTs(root, config.port); + + await Promise.all([ + FsUtilities.rm(getPath(root, '.prettierrc')), + FsUtilities.rm(getPath(root, 'src', 'app.controller.ts')), + FsUtilities.rm(getPath(root, 'src', 'app.controller.spec.ts')), + FsUtilities.rm(getPath(root, 'src', 'app.service.ts')), + EslintUtilities.setupProjectEslint(root, true, 'tsconfig.json'), + this.setupTsConfig(config.name), + this.updatePackageJson(config.name), + this.updateNestCliJson(root), + this.setupSwagger(root, config.name), + DockerUtilities.addServiceToCompose( + { + name: config.name, + build: { + dockerfile: `./${root}/${DOCKER_FILE_NAME}`, + context: '.' + }, + volumes: [`/${config.name}`] + }, + 3000, + config.port, + true, + config.subDomain + ), + this.createDockerfile(root, config), + this.createWebpackConfig(root) + ]); + + await NpmUtilities.install( + config.name, + [ + NpmPackage.CLASS_VALIDATOR, + NpmPackage.CLASS_TRANSFORMER, + NpmPackage.NEST_JS_SWAGGER, + NpmPackage.NEST_JS_TYPEORM, + NpmPackage.TYPEORM, + ...this.dbNpmPackages[dbType] + ] + ); + } + + private async createWebpackConfig(root: Path): Promise { + await FsUtilities.createFile( + getPath(root, WEBPACK_CONFIG), + [ + '/* eslint-disable jsdoc/require-param-description */', + '/* eslint-disable jsdoc/require-returns-description */', + '/* eslint-disable jsdoc/no-types */', + '/* eslint-disable jsdoc/require-description */', + '', + 'const webpack = require(\'webpack\');', + '', + '/**', + ' * @param {import(\'webpack\').Configuration} config', + ' * @returns {import(\'webpack\').Configuration}', + ' */', + 'module.exports = (config) => {', + ' const existingPlugins = config.plugins ?? [];', + ' return {', + ' ...config,', + ' plugins: [', + ' ...existingPlugins,', + ' new webpack.IgnorePlugin({', + ' resourceRegExp: /^pg-native$|^cloudflare:sockets$/', + ' })', + ' ]', + ' };', + '};' + ] + ); + } + + private async setupSwagger(root: Path, name: string): Promise { + const mainPath: Path = getPath(root, 'src', 'main.ts'); + await TsUtilities.addBelowImports( + mainPath, + [ + 'function setupSwagger(app: NestExpressApplication): void {', + '\tconst config: Omit = new DocumentBuilder()', + `\t\t.setTitle('${name}')`, + '\t\t.setVersion(\'1.0\')', + '\t\t.addBearerAuth()', + '\t\t.build();', + '', + '\tconst document: OpenAPIObject = SwaggerModule.createDocument(app, config);', + '\tSwaggerModule.setup(\'api\', app, document);', + '\tSwaggerModule.setup(\'api-json\', app, document);', + '}' + ] + ); + await FsUtilities.replaceInFile( + mainPath, + // eslint-disable-next-line sonar/no-duplicate-string + ' app.enableCors();', + [ + ' app.enableCors();', + '', + ' setupSwagger(app);' + ].join('\n') + ); + await TsUtilities.addImportStatements( + mainPath, + [ + { defaultImport: false, element: 'OpenAPIObject', path: NpmPackage.NEST_JS_SWAGGER }, + { defaultImport: false, element: 'DocumentBuilder', path: NpmPackage.NEST_JS_SWAGGER }, + { defaultImport: false, element: 'SwaggerModule', path: NpmPackage.NEST_JS_SWAGGER } + ] + ); + } + + private async updateNestCliJson(root: Path): Promise { + await NestUtilities.updateNestCliJson( + getPath(root, NEST_CLI_FILE_NAME), + { + compilerOptions: { + builder: 'webpack' + } + } + ); + } + + private async createProject(config: AddNestConfiguration): Promise { + // eslint-disable-next-line no-console + console.log('Creates the base app'); + NestUtilities.runCommand( + getPath(APPS_DIRECTORY_NAME), + `new ${config.name}`, + { + '--skip-git': true, + '--language': 'TS', + '--package-manager': 'npm', + '--skip-install': true + } + ); + const newProject: WorkspaceProject = await WorkspaceUtilities.findProjectOrFail(config.name, getPath('.')); + + await FsUtilities.mkdir(getPath(newProject.path, 'src', 'modules')); + await FsUtilities.mkdir(getPath(newProject.path, 'src', 'utilities')); + await FsUtilities.mkdir(getPath(newProject.path, 'src', 'decorators')); + await FsUtilities.mkdir(getPath(newProject.path, 'src', 'guards')); + await FsUtilities.mkdir(getPath(newProject.path, 'src', 'migrations')); + await FsUtilities.mkdir(getPath(newProject.path, 'src', 'assets')); + + const appModuleTs: Path = getPath(newProject.path, 'src', 'app.module.ts'); + await FsUtilities.replaceInFile(appModuleTs, '\nimport { AppController } from \'./app.controller\';', ''); + await FsUtilities.replaceInFile(appModuleTs, '\nimport { AppService } from \'./app.service\';', ''); + await FsUtilities.replaceInFile(appModuleTs, 'AppService', ''); + await FsUtilities.replaceInFile(appModuleTs, 'AppController', ''); + + await FsUtilities.rm(getPath(newProject.path, ESLINT_CONFIG_FILE_NAME)); + + return newProject.path; + } + + private async setupTsConfig(projectName: string): Promise { + // eslint-disable-next-line no-console + console.log('sets up tsconfig'); + await TsConfigUtilities.updateTsConfig( + projectName, + { + extends: '../../tsconfig.base.json', + compilerOptions: { + removeComments: undefined, + emitDecoratorMetadata: undefined, + experimentalDecorators: undefined, + forceConsistentCasingInFileNames: undefined, + allowSyntheticDefaultImports: undefined, + sourceMap: undefined, + skipLibCheck: undefined, + noImplicitAny: undefined, + noFallthroughCasesInSwitch: undefined + } + } + ); + await NpmUtilities.updatePackageJson( + projectName, + { + main: `dist/${APPS_DIRECTORY_NAME}/${projectName}/src/main.js`, + types: `dist/${APPS_DIRECTORY_NAME}/${projectName}/src/main.d.ts` + } + ); + } + + private async updatePackageJson(name: string): Promise { + await NpmUtilities.updatePackageJson(name, { + license: 'MIT', + scripts: { + format: undefined + }, + devDependencies: { + '@eslint/eslintrc': undefined, + '@eslint/js': undefined, + eslint: undefined, + 'eslint-config-prettier': undefined, + 'eslint-plugin-prettier': undefined, + prettier: undefined, + 'typescript-eslint': undefined + } + }); + await NpmUtilities.install(name, []); + } + + private async updateMainTs(root: Path, port: number): Promise { + const mainPath: Path = getPath(root, 'src', 'main.ts'); + await FsUtilities.replaceInFile(mainPath, 'env.PORT', 'env[\'PORT\']'); + await FsUtilities.replaceInFile(mainPath, '?? 3000', `?? ${port}`); + await FsUtilities.replaceInFile(mainPath, 'async function bootstrap() {', 'async function bootstrap(): Promise {'); + await FsUtilities.replaceInFile(mainPath, 'bootstrap();', '\nvoid bootstrap();'); + await FsUtilities.replaceInFile( + mainPath, + ' const app = await NestFactory.create(AppModule);', + [ + ' const app: NestExpressApplication = await NestFactory.create(AppModule, { abortOnError: false });', + '', + ' app.set(\'trust proxy\', 1);', + ' app.useGlobalPipes(', + ' new ValidationPipe({', + ' transform: true,', + ' forbidUnknownValues: true,', + ' whitelist: true,', + ' forbidNonWhitelisted: true', + ' })', + ' );', + ' const reflector: Reflector = app.get(Reflector);', + ' app.useGlobalInterceptors(new ClassSerializerInterceptor(reflector));', + ' app.enableCors();', + '' + ].join('\n') + ); + await TsUtilities.addImportStatements( + mainPath, + [ + { defaultImport: false, element: 'NestExpressApplication', path: '@nestjs/platform-express' }, + { defaultImport: false, element: 'ValidationPipe', path: '@nestjs/common' }, + { defaultImport: false, element: 'Reflector', path: '@nestjs/core' }, + { defaultImport: false, element: 'ClassSerializerInterceptor', path: '@nestjs/common' } + ] + ); + } + + private async createDockerfile(root: string, config: AddNestConfiguration): Promise { + await FsUtilities.createFile( + getPath(root, DOCKER_FILE_NAME), + [ + 'FROM node:20 AS build', + '# Set to a non-root built-in user `node`', + 'USER node', + 'RUN mkdir -p /home/node/root', + 'COPY --chown=node . /home/node/root', + 'WORKDIR /home/node/root', + 'RUN npm install', + `RUN npm run build --workspace=${APPS_DIRECTORY_NAME}/${config.name} --omit=dev`, + '', + 'FROM node:20', + 'WORKDIR /usr/app', + `COPY --from=build /home/node/root/${APPS_DIRECTORY_NAME}/${config.name}/dist ./`, + 'CMD node main' + ] + ); + } + + private async createNestDatasource( + dbServiceName: string, + databaseName: string, + dbType: DbType, + root: Path, + projectName: string + ): Promise { + const DB_TYPE_PLACEHOLDER: string = 'DB_TYPE_PLACEHOLDER'; + const DB_HOST_PLACEHOLDER: string = 'DB_HOST_PLACEHOLDER'; + const DB_USERNAME_PLACEHOLDER: string = 'DB_USERNAME_PLACEHOLDER'; + const DB_PASSWORD_PLACEHOLDER: string = 'DB_PASSWORD_PLACEHOLDER'; + const DB_DATABASE_PLACEHOLDER: string = 'DB_DATABASE_PLACEHOLDER'; + + const appModuleTs: Path = getPath(root, 'src', 'app.module.ts'); + await NestUtilities.addModuleImports( + appModuleTs, + [ + `TypeOrmModule.forRoot({ + type: ${DB_TYPE_PLACEHOLDER}, + host: ${DB_HOST_PLACEHOLDER}, + port: ${defaultPortForDbType[dbType]}, + username: ${DB_USERNAME_PLACEHOLDER}, + password: ${DB_PASSWORD_PLACEHOLDER}, + database: ${DB_DATABASE_PLACEHOLDER}, + autoLoadEntities: true, + synchronize: true, + logging: false + })` as unknown as Type + ] + ); + await TsUtilities.addImportStatements( + appModuleTs, + [ + { defaultImport: false, element: 'TypeOrmModule', path: NpmPackage.NEST_JS_TYPEORM }, + { defaultImport: false, element: 'environment', path: './environment/environment' } + ] + ); + await FsUtilities.replaceInFile(appModuleTs, DB_TYPE_PLACEHOLDER, `'${dbType}'`); + await FsUtilities.replaceInFile(appModuleTs, '\'TypeOrmModule', 'TypeOrmModule'); + await FsUtilities.replaceInFile(appModuleTs, ')\'', ')'); + await FsUtilities.replaceAllInFile( + appModuleTs, + DB_HOST_PLACEHOLDER, + `environment.${DefaultEnvKeys.dbHost(dbServiceName)}` + ); + await FsUtilities.replaceAllInFile( + appModuleTs, + DB_USERNAME_PLACEHOLDER, + `environment.${DefaultEnvKeys.dbUser(dbServiceName, databaseName)}` + ); + await FsUtilities.replaceAllInFile( + appModuleTs, + DB_PASSWORD_PLACEHOLDER, + `environment.${DefaultEnvKeys.dbPassword(dbServiceName, databaseName)}` + ); + await FsUtilities.replaceAllInFile( + appModuleTs, + DB_DATABASE_PLACEHOLDER, + `environment.${DefaultEnvKeys.dbName(dbServiceName, databaseName)}` + ); + + const environmentModel: Path = getPath(root, 'src', 'environment', ENVIRONMENT_MODEL_TS_FILE_NAME); + await EnvUtilities.addProjectVariableKey( + projectName, + environmentModel, + DefaultEnvKeys.dbPassword(dbServiceName, databaseName), + true, + getPath('.') + ); + await EnvUtilities.addProjectVariableKey( + projectName, + environmentModel, + DefaultEnvKeys.dbUser(dbServiceName, databaseName), + true, + getPath('.') + ); + await EnvUtilities.addProjectVariableKey(projectName, + environmentModel, + DefaultEnvKeys.dbName(dbServiceName, databaseName), + true, + getPath('.')); + await EnvUtilities.addProjectVariableKey(projectName, environmentModel, DefaultEnvKeys.dbHost(dbServiceName), true, getPath('.')); + } +} \ No newline at end of file diff --git a/src/commands/add/add-nest/index.ts b/src/commands/add/add-nest/index.ts new file mode 100644 index 0000000..8de1490 --- /dev/null +++ b/src/commands/add/add-nest/index.ts @@ -0,0 +1 @@ +export * from './add-nest.command'; \ No newline at end of file diff --git a/src/commands/add/add-wordpress/add-wordpress.command.ts b/src/commands/add/add-wordpress/add-wordpress.command.ts index 553c4f0..cdadad1 100644 --- a/src/commands/add/add-wordpress/add-wordpress.command.ts +++ b/src/commands/add/add-wordpress/add-wordpress.command.ts @@ -44,12 +44,7 @@ export class AddWordpressCommand extends BaseAddCommand { await new AddLoopbackCommand(config).run(); return; } + case AddType.NEST: { + await new AddNestCommand(config).run(); + return; + } case AddType.TS_LIBRARY: { await new AddTsLibraryCommand(config).run(); return; diff --git a/src/commands/add/models/add-configuration.model.ts b/src/commands/add/models/add-configuration.model.ts index 5ddba08..1ce476d 100644 --- a/src/commands/add/models/add-configuration.model.ts +++ b/src/commands/add/models/add-configuration.model.ts @@ -1,19 +1,8 @@ +import { AddType } from './add-type.enum'; import { QuestionsFor } from '../../../encapsulation'; import { getPath } from '../../../utilities'; import { WorkspaceUtilities } from '../../../workspace'; -/** - * The type of project to add. - */ -export enum AddType { - ANGULAR = 'angular', - ANGULAR_WEBSITE = 'angular-website', - ANGULAR_LIBRARY = 'angular-library', - LOOPBACK = 'loopback', - TS_LIBRARY = 'ts-library', - WORDPRESS = 'wordpress' -} - /** * BaseConfiguration for the add cli command. */ diff --git a/src/commands/add/models/add-type.enum.ts b/src/commands/add/models/add-type.enum.ts new file mode 100644 index 0000000..804d6f5 --- /dev/null +++ b/src/commands/add/models/add-type.enum.ts @@ -0,0 +1,13 @@ + +/** + * The type of project to add. + */ +export enum AddType { + ANGULAR = 'angular', + ANGULAR_WEBSITE = 'angular-website', + ANGULAR_LIBRARY = 'angular-library', + LOOPBACK = 'loopback', + NEST = 'nest', + TS_LIBRARY = 'ts-library', + WORDPRESS = 'wordpress' +} \ No newline at end of file diff --git a/src/commands/add/models/base-add-command.model.ts b/src/commands/add/models/base-add-command.model.ts index 2fdc009..04d4b9a 100644 --- a/src/commands/add/models/base-add-command.model.ts +++ b/src/commands/add/models/base-add-command.model.ts @@ -13,11 +13,8 @@ export abstract class BaseAddCommand; + abstract run(): Promise; /** * Gets the complete configuration. diff --git a/src/commands/add/models/index.ts b/src/commands/add/models/index.ts index 9d8bf81..35b0478 100644 --- a/src/commands/add/models/index.ts +++ b/src/commands/add/models/index.ts @@ -1,2 +1,3 @@ export * from './base-add-command.model'; -export * from './add-configuration.model'; \ No newline at end of file +export * from './add-configuration.model'; +export * from './add-type.enum'; \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 61c20e9..cfbc12a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -126,6 +126,16 @@ export const APP_CONFIG_FILE_NAME: string = 'app.config.ts'; */ export const NG_PACKAGE_FILE_NAME: string = 'ng-package.json'; +/** + * The name of the nest cli file. + */ +export const NEST_CLI_FILE_NAME: string = 'nest-cli.json'; + +/** + * The name of the webpack config file. + */ +export const WEBPACK_CONFIG: string = 'webpack.config.js'; + /** * The message to notify the user of the help command. */ @@ -137,4 +147,15 @@ export const MORE_INFORMATION_MESSAGE: string = `run ${ChalkUtilities.secondary( * The possible file names of the different docker-compose files Monux provides. */ // eslint-disable-next-line stylistic/max-len -export type DockerComposeFileName = typeof PROD_DOCKER_COMPOSE_FILE_NAME | typeof DEV_DOCKER_COMPOSE_FILE_NAME | typeof LOCAL_DOCKER_COMPOSE_FILE_NAME; \ No newline at end of file +export type DockerComposeFileName = typeof PROD_DOCKER_COMPOSE_FILE_NAME | typeof DEV_DOCKER_COMPOSE_FILE_NAME | typeof LOCAL_DOCKER_COMPOSE_FILE_NAME; + +const dockerComposeFileNameRecord: Record = { + 'dev.docker-compose.yaml': 'dev.docker-compose.yaml', + 'docker-compose.yaml': 'docker-compose.yaml', + 'local.docker-compose.yaml': 'local.docker-compose.yaml' +}; + +/** + * All possible docker compose file names as an array. + */ +export const dockerComposeFileNames: DockerComposeFileName[] = Object.values(dockerComposeFileNameRecord); \ No newline at end of file diff --git a/src/db/db-type.enum.ts b/src/db/db-type.enum.ts index 0acd443..4cffc45 100644 --- a/src/db/db-type.enum.ts +++ b/src/db/db-type.enum.ts @@ -4,4 +4,12 @@ export enum DbType { POSTGRES = 'postgres', MARIADB = 'mariadb' -} \ No newline at end of file +} + +/** + * The default port for database servers of the given type. + */ +export const defaultPortForDbType: Record = { + [DbType.POSTGRES]: 5432, + [DbType.MARIADB]: 3306 +}; \ No newline at end of file diff --git a/src/db/db.utilities.ts b/src/db/db.utilities.ts index 0e473fc..9adc53e 100644 --- a/src/db/db.utilities.ts +++ b/src/db/db.utilities.ts @@ -9,6 +9,7 @@ import { DbType } from './db-type.enum'; import { dbTypeQuestion } from './db-type.question'; import { MariaDbConfig, mariaDbConfigQuestions } from './maria-db.questions'; import { PostgresDbConfig, postgresDbConfigQuestions } from './postgres-db.questions'; +import { OmitStrict } from '../types'; /** * Configuration for selecting a database. @@ -21,7 +22,11 @@ type DbConfig = { /** * The name of the database. */ - databaseName: string + databaseName: string, + /** + * The type of the database. + */ + dbType: DbType }; /** @@ -132,7 +137,7 @@ export abstract class DbUtilities { * @returns The name of the database service. */ static async configureDb(projectName: string, dbType: DbType | undefined, rootDir: string): Promise { - const baseDbQuestions: QuestionsFor = { + const baseDbQuestions: QuestionsFor> = { dbServiceName: { type: 'select', message: 'Database compose service', @@ -145,16 +150,37 @@ export abstract class DbUtilities { default: projectName } }; - const baseDbConfig: DbConfig = await InquirerUtilities.prompt(baseDbQuestions); + const baseDbConfig: OmitStrict = await InquirerUtilities.prompt(baseDbQuestions); if (baseDbConfig.dbServiceName !== 'NEW') { + const type: DbType = await this.getDbTypeForService(baseDbConfig.dbServiceName, rootDir); + const user: string = `${toSnakeCase(baseDbConfig.databaseName)}_user`; + const password: string = generatePlaceholderPassword(); + await EnvUtilities.addStaticVariable({ + key: DefaultEnvKeys.dbPassword(baseDbConfig.dbServiceName, baseDbConfig.databaseName), + value: password, + required: true, + type: 'string' + }); + await EnvUtilities.addStaticVariable({ + key: DefaultEnvKeys.dbUser(baseDbConfig.dbServiceName, baseDbConfig.databaseName), + value: user, + required: true, + type: 'string' + }); + await EnvUtilities.addStaticVariable({ + key: DefaultEnvKeys.dbName(baseDbConfig.dbServiceName, baseDbConfig.databaseName), + value: baseDbConfig.databaseName, + required: true, + type: 'string' + }); await this.addDbInitConfig(baseDbConfig.dbServiceName, { - type: await this.getDbTypeForService(baseDbConfig.dbServiceName, rootDir), + type, nameEnvVariable: DefaultEnvKeys.dbName(baseDbConfig.dbServiceName, baseDbConfig.databaseName), passwordEnvVariable: DefaultEnvKeys.dbPassword(baseDbConfig.dbServiceName, baseDbConfig.databaseName), userEnvVariable: DefaultEnvKeys.dbUser(baseDbConfig.dbServiceName, baseDbConfig.databaseName) }); - return baseDbConfig; + return { ...baseDbConfig, dbType: type }; } dbType = dbType ?? (await InquirerUtilities.prompt(dbTypeQuestion)).type; @@ -163,11 +189,11 @@ export abstract class DbUtilities { const dbConfig: PostgresDbConfig = { ...await InquirerUtilities.prompt(postgresDbConfigQuestions), databaseName: baseDbConfig.databaseName, - type: dbType + dbType: dbType }; await this.createPostgresDatabase(dbConfig.dbServiceName, dbConfig.databaseName); await this.addDbInitConfig(dbConfig.dbServiceName, { - type: dbConfig.type, + type: dbConfig.dbType, nameEnvVariable: DefaultEnvKeys.dbName(dbConfig.dbServiceName, dbConfig.databaseName), passwordEnvVariable: DefaultEnvKeys.dbPassword(dbConfig.dbServiceName, dbConfig.databaseName), userEnvVariable: DefaultEnvKeys.dbUser(dbConfig.dbServiceName, dbConfig.databaseName) @@ -178,11 +204,11 @@ export abstract class DbUtilities { const dbConfig: MariaDbConfig = { ...await InquirerUtilities.prompt(mariaDbConfigQuestions), databaseName: baseDbConfig.databaseName, - type: dbType + dbType: dbType }; await this.createMariaDbDatabase(dbConfig.dbServiceName, dbConfig.databaseName); await this.addDbInitConfig(dbConfig.dbServiceName, { - type: dbConfig.type, + type: dbConfig.dbType, nameEnvVariable: DefaultEnvKeys.dbName(dbConfig.dbServiceName, dbConfig.databaseName), passwordEnvVariable: DefaultEnvKeys.dbPassword(dbConfig.dbServiceName, dbConfig.databaseName), userEnvVariable: DefaultEnvKeys.dbUser(dbConfig.dbServiceName, dbConfig.databaseName) @@ -241,14 +267,8 @@ export abstract class DbUtilities { name: dbServiceName, image: `mariadb:${this.MARIADB_VERSION}`, volumes: [ - { - path: `${toKebabCase(dbServiceName)}-data`, - mount: '/var/lib/mysql' - }, - { - path: `./${DATABASES_DIRECTORY_NAME}/${toKebabCase(dbServiceName)}/init`, - mount: '/docker-entrypoint-initdb.d' - } + `${toKebabCase(dbServiceName)}-data:/var/lib/mysql`, + `./${DATABASES_DIRECTORY_NAME}/${toKebabCase(dbServiceName)}/init:/docker-entrypoint-initdb.d` ], environment: [ { @@ -257,19 +277,19 @@ export abstract class DbUtilities { } ] }; - await DockerUtilities.addServiceToCompose(serviceDefinition, 3306, false, undefined); - await DockerUtilities.addVolumeToCompose(`${toKebabCase(dbServiceName)}-data`); + await DockerUtilities.addServiceToCompose(serviceDefinition, 3306, 3306, false, undefined); + await DockerUtilities.addVolumeToComposeFiles(`${toKebabCase(dbServiceName)}-data`); await DockerUtilities.addServiceToCompose( { ...serviceDefinition, ports: [{ external: 3306, internal: 3306 }] }, 3306, + 3306, false, undefined, DEV_DOCKER_COMPOSE_FILE_NAME ); - await DockerUtilities.addVolumeToCompose(`${toKebabCase(dbServiceName)}-data`, DEV_DOCKER_COMPOSE_FILE_NAME); await EnvUtilities.addStaticVariable({ key: DefaultEnvKeys.dbPassword(dbServiceName, databaseName), value: password, @@ -329,14 +349,8 @@ export abstract class DbUtilities { name: dbServiceName, image: `postgres:${this.POSTGRES_VERSION}`, volumes: [ - { - path: `${toKebabCase(dbServiceName)}-data`, - mount: '/var/lib/postgresql/data' - }, - { - path: `./${DATABASES_DIRECTORY_NAME}/${toKebabCase(dbServiceName)}/init`, - mount: '/docker-entrypoint-initdb.d' - } + `${toKebabCase(dbServiceName)}-data:/var/lib/postgresql/data`, + `./${DATABASES_DIRECTORY_NAME}/${toKebabCase(dbServiceName)}/init:/docker-entrypoint-initdb.d` ], environment: [ { @@ -345,19 +359,19 @@ export abstract class DbUtilities { } ] }; - await DockerUtilities.addServiceToCompose(serviceDefinition, 5432, false); - await DockerUtilities.addVolumeToCompose(`${toKebabCase(dbServiceName)}-data`); + await DockerUtilities.addServiceToCompose(serviceDefinition, 5432, 5432, false); + await DockerUtilities.addVolumeToComposeFiles(`${toKebabCase(dbServiceName)}-data`); await DockerUtilities.addServiceToCompose( { ...serviceDefinition, ports: [{ external: 5432, internal: 5432 }] }, 5432, + 5432, false, undefined, DEV_DOCKER_COMPOSE_FILE_NAME ); - await DockerUtilities.addVolumeToCompose(`${toKebabCase(dbServiceName)}-data`, DEV_DOCKER_COMPOSE_FILE_NAME); await EnvUtilities.addStaticVariable({ key: DefaultEnvKeys.dbPassword(dbServiceName, databaseName), value: password, diff --git a/src/db/maria-db.questions.ts b/src/db/maria-db.questions.ts index b304deb..e1c4ad4 100644 --- a/src/db/maria-db.questions.ts +++ b/src/db/maria-db.questions.ts @@ -9,7 +9,7 @@ export type MariaDbConfig = { /** * The type of the databases. */ - type: DbType.MARIADB, + dbType: DbType.MARIADB, /** * The name of the mariadb service. */ @@ -23,7 +23,7 @@ export type MariaDbConfig = { /** * Questions for getting a maria db config. */ -export const mariaDbConfigQuestions: QuestionsFor> = { +export const mariaDbConfigQuestions: QuestionsFor> = { dbServiceName: { type: 'input', message: 'Compose service name', diff --git a/src/db/postgres-db.questions.ts b/src/db/postgres-db.questions.ts index 67bd674..8d0d00d 100644 --- a/src/db/postgres-db.questions.ts +++ b/src/db/postgres-db.questions.ts @@ -9,7 +9,7 @@ export type PostgresDbConfig = { /** * The type of the database. */ - type: DbType.POSTGRES, + dbType: DbType.POSTGRES, /** * The name of the database. */ @@ -23,7 +23,7 @@ export type PostgresDbConfig = { /** * Questions for getting a postgres db config. */ -export const postgresDbConfigQuestions: QuestionsFor> = { +export const postgresDbConfigQuestions: QuestionsFor> = { dbServiceName: { type: 'input', message: 'Compose service name', diff --git a/src/docker/compose-file.model.ts b/src/docker/compose-file.model.ts index a4a4a15..47723a9 100644 --- a/src/docker/compose-file.model.ts +++ b/src/docker/compose-file.model.ts @@ -37,7 +37,7 @@ export type ComposeService = { /** * The volumes that are used by the service. */ - volumes?: ComposeServiceVolume[], + volumes?: string[], /** * The image that the service is build upon. * See "build" if you don't depend on an image. @@ -94,21 +94,4 @@ export type ComposeBuild = string | { * The context to provide when building from the dockerfile. */ context: string -}; - -/** - * Definition for a volume that is used by a service. - * Consists of the path and an optional mount. - */ -export type ComposeServiceVolume = { - /** - * The volumes path. - * Supports relative paths, docker default volumes as well as volumes defined in the volumes section of the docker compose file. - */ - path: string, - /** - * Where the data of the volume should be mounted. - * Can be empty. - */ - mount?: string }; \ No newline at end of file diff --git a/src/docker/docker-utilities.test.ts b/src/docker/docker-utilities.test.ts index 33dcf02..3994a4b 100644 --- a/src/docker/docker-utilities.test.ts +++ b/src/docker/docker-utilities.test.ts @@ -47,9 +47,9 @@ describe('DockerUtilities', () => { test('createDockerCompose with prod service', async () => { const def: ComposeService = fakeComposeService(); - await DockerUtilities.addServiceToCompose(def, 4200, true, def.name); - const fileContent: ComposeDefinition = await DockerUtilities['yamlToComposeDefinition'](mockConstants.DOCKER_COMPOSE_YAML); - const service: ComposeService = fileContent.services[1]; + await DockerUtilities.addServiceToCompose(def, 4000, 4200, true, def.name); + const prodFileContent: ComposeDefinition = await DockerUtilities['yamlToComposeDefinition'](mockConstants.DOCKER_COMPOSE_YAML); + const prodService: ComposeService = prodFileContent.services[1]; expect({ ...def, labels: [ @@ -58,11 +58,11 @@ describe('DockerUtilities', () => { `traefik.http.routers.${def.name}.rule=Host(\`\${${def.name}_sub_domain}.\${prod_root_domain}\`)`, `traefik.http.routers.${def.name}.entrypoints=web_secure`, `traefik.http.routers.${def.name}.tls.certresolver=ssl_resolver`, - `traefik.http.services.${def.name}.loadbalancer.server.port=4200`, + `traefik.http.services.${def.name}.loadbalancer.server.port=4000`, 'traefik.http.middlewares.compression.compress=true', `traefik.http.routers.${def.name}.middlewares=compression` ] - }).toEqual(service); + }).toEqual(prodService); const localFileContent: ComposeDefinition = await DockerUtilities['yamlToComposeDefinition'](mockConstants.LOCAL_DOCKER_COMPOSE_YAML); const localService: ComposeService = localFileContent.services[1]; @@ -73,14 +73,16 @@ describe('DockerUtilities', () => { 'traefik.enable=true', `traefik.http.routers.${def.name}.rule=Host(\`\${${def.name}_sub_domain}.localhost\`)`, `traefik.http.routers.${def.name}.entrypoints=web`, - `traefik.http.services.${def.name}.loadbalancer.server.port=4200`, + `traefik.http.services.${def.name}.loadbalancer.server.port=4000`, 'traefik.http.middlewares.compression.compress=true', `traefik.http.routers.${def.name}.middlewares=compression` ] }).toEqual(localService); + + // Dev service does not need to be checked, as the service is not added there automatically }); test('createDockerCompose with dev service', async () => { - //TODO + //TODO: Add test }); }); \ No newline at end of file diff --git a/src/docker/docker.utilities.ts b/src/docker/docker.utilities.ts index ea4c471..de00238 100644 --- a/src/docker/docker.utilities.ts +++ b/src/docker/docker.utilities.ts @@ -1,8 +1,8 @@ import yaml from 'js-yaml'; -import { DEV_DOCKER_COMPOSE_FILE_NAME, PROD_DOCKER_COMPOSE_FILE_NAME, LOCAL_DOCKER_COMPOSE_FILE_NAME, DockerComposeFileName, GLOBAL_ENVIRONMENT_MODEL_FILE_NAME } from '../constants'; +import { DEV_DOCKER_COMPOSE_FILE_NAME, PROD_DOCKER_COMPOSE_FILE_NAME, LOCAL_DOCKER_COMPOSE_FILE_NAME, DockerComposeFileName, GLOBAL_ENVIRONMENT_MODEL_FILE_NAME, dockerComposeFileNames } from '../constants'; import { FsUtilities } from '../encapsulation'; -import { ComposeBuild, ComposeDefinition, ComposePort, ComposeService, ComposeServiceEnvironment, ComposeServiceVolume } from './compose-file.model'; +import { ComposeBuild, ComposeDefinition, ComposePort, ComposeService, ComposeServiceEnvironment } from './compose-file.model'; import { DefaultEnvKeys, EnvUtilities } from '../env'; import { OmitStrict } from '../types'; import { getPath, Path } from '../utilities'; @@ -55,11 +55,23 @@ export abstract class DockerUtilities { * Defaults to "" (which creates the file in the current directory). */ static async createComposeFiles(email: string): Promise { - await Promise.all([ - this.createProdDockerCompose(email), - this.createDevDockerCompose(), - this.createLocalDockerCompose() - ]); + await Promise.all( + dockerComposeFileNames.map(async d => { + switch (d) { + case PROD_DOCKER_COMPOSE_FILE_NAME: { + await this.createProdDockerCompose(email); + return; + } + case DEV_DOCKER_COMPOSE_FILE_NAME: { + await this.createDevDockerCompose(); + return; + } + case LOCAL_DOCKER_COMPOSE_FILE_NAME: { + await this.createLocalDockerCompose(); + } + } + }) + ); } private static async createDevDockerCompose(): Promise { @@ -105,12 +117,7 @@ export abstract class DockerUtilities { external: 443 } ], - volumes: [ - { - path: '/var/run/docker.sock', - mount: '/var/run/docker.sock:ro' - } - ], + volumes: ['/var/run/docker.sock:/var/run/docker.sock:ro'], labels: [] } ], @@ -151,14 +158,8 @@ export abstract class DockerUtilities { } ], volumes: [ - { - path: './letsencrypt', - mount: '/letsencrypt' - }, - { - path: '/var/run/docker.sock', - mount: '/var/run/docker.sock:ro' - } + './letsencrypt:/letsencrypt', + '/var/run/docker.sock:/var/run/docker.sock:ro' ], labels: [] } @@ -173,7 +174,8 @@ export abstract class DockerUtilities { /** * Adds the given compose service to the docker-compose.yaml. * @param service - The definition of the service to add. - * @param port - The port used in development. + * @param port - The port used in local and prod. + * @param devPort - The port used in development. * @param addTraefik - Whether or not the service should be exposed via traefik. * @param subDomain - The domain of the service. Optional. * Defaults to "" (which creates the file in the current directory). @@ -183,6 +185,7 @@ export abstract class DockerUtilities { static async addServiceToCompose( service: ComposeService, port: number, + devPort: number, addTraefik: boolean, subDomain?: string, composeFileName: DockerComposeFileName = PROD_DOCKER_COMPOSE_FILE_NAME @@ -200,13 +203,13 @@ export abstract class DockerUtilities { await FsUtilities.updateFile(composePath, this.composeDefinitionToYaml(definition), 'replace'); if (composeFileName === PROD_DOCKER_COMPOSE_FILE_NAME) { - await this.addServiceToCompose(service, port, addTraefik, subDomain, LOCAL_DOCKER_COMPOSE_FILE_NAME); + await this.addServiceToCompose(service, port, devPort, addTraefik, subDomain, LOCAL_DOCKER_COMPOSE_FILE_NAME); if (!addTraefik) { return; } await EnvUtilities.addStaticVariable( - { key: DefaultEnvKeys.port(service.name), value: port, required: true, type: 'number' } + { key: DefaultEnvKeys.port(service.name), value: devPort, required: true, type: 'number' } ); if (subDomain) { @@ -328,13 +331,16 @@ export abstract class DockerUtilities { } /** - * Adds a volume to the docker compose file. + * Adds a volume to the docker compose files. * @param volume - The volume to add. - * @param composeFileName - The name of the compose file. */ - static async addVolumeToCompose( + static async addVolumeToComposeFiles(volume: string): Promise { + await Promise.all(dockerComposeFileNames.map(d => this.addVolumeToCompose(volume, d))); + } + + private static async addVolumeToCompose( volume: string, - composeFileName: string = PROD_DOCKER_COMPOSE_FILE_NAME + composeFileName: DockerComposeFileName ): Promise { const composePath: Path = getPath(composeFileName); const definition: ComposeDefinition = await this.yamlToComposeDefinition(composePath); @@ -459,17 +465,14 @@ export abstract class DockerUtilities { return ['\t\tnetworks:', ...networks.map(n => `\t\t\t${n}:`)]; } - private static getServiceVolumesSection(volumes: ComposeServiceVolume[] | undefined): string[] { + private static getServiceVolumesSection(volumes: string[] | undefined): string[] { if (!volumes?.length) { return []; } return [ '\t\tvolumes:', - ...volumes.map(v => { - const mount: string = v.mount ? `:${v.mount}` : ''; - return `\t\t\t- ${v.path}${mount}`; - }) + ...volumes.map(v => `\t\t\t- ${v}`) ]; } @@ -483,7 +486,7 @@ export abstract class DockerUtilities { .map(([serviceName, serviceData]: [string, ParsedDockerComposeService]) => { const res: ComposeService = { name: serviceName, - volumes: this.parseServiceVolumes(serviceData.volumes), + volumes: serviceData.volumes, command: serviceData.command, image: serviceData.image, build: this.parseBuild(serviceData.build), @@ -521,18 +524,6 @@ export abstract class DockerUtilities { return Array.isArray(networks) ? networks : Object.keys(networks); } - private static parseServiceVolumes(volumes: string[] | undefined): ComposeServiceVolume[] | undefined { - if (volumes == undefined) { - return; - } - return volumes?.map((v: string) => this.parseServiceVolume(v)); - } - - private static parseServiceVolume(volumeStr: string): ComposeServiceVolume { - const [path, ...mount] = volumeStr.split(':'); - return { path, mount: mount.join('') }; - } - private static parseBuild(build: ComposeBuild | undefined): ComposeBuild | undefined { if (build == undefined) { return; diff --git a/src/env/env.utilities.ts b/src/env/env.utilities.ts index 7bf8e09..6bb51df 100644 --- a/src/env/env.utilities.ts +++ b/src/env/env.utilities.ts @@ -102,7 +102,6 @@ export abstract class EnvUtilities { getPath(environmentFolder, ENVIRONMENT_MODEL_TS_FILE_NAME) ); - // TODO: The first time getPath fails here because await FsUtilities.rm(getPath(environmentFolder, ENVIRONMENT_TS_FILE_NAME)); await this.createProjectEnvironmentFile(app.path, variableKeys, failOnMissingVariable, fileName, rootDir); } diff --git a/src/loopback/admin-controller.content.ts b/src/loopback/admin-controller.content.ts index 195404a..5e624ad 100644 --- a/src/loopback/admin-controller.content.ts +++ b/src/loopback/admin-controller.content.ts @@ -1,6 +1,9 @@ +import { toPascalCase } from '../utilities'; + // eslint-disable-next-line jsdoc/require-jsdoc -export const adminControllerContent: string -= `import { authenticate } from '@loopback/authentication'; +export function adminControllerContent(dbName: string): string { + const datasource: string = `${toPascalCase(dbName)}DataSource`; + return `import { authenticate } from '@loopback/authentication'; import { authorize } from '@loopback/authorization'; import { inject } from '@loopback/core'; import { IsolationLevel, juggler, repository } from '@loopback/repository'; @@ -10,9 +13,9 @@ import { BaseUser, BaseUserProfile, BaseUserRepository, BaseUserWithRelations, B import { FullAdmin } from './full-admin.model'; import { NewAdmin } from './new-admin.model'; -import { DbDataSource } from '../../datasources'; -import { Admin, AdminWithRelations } from '../../models/admin.model'; -import { AdminRepository } from '../../repositories/admin.repository'; +import { ${datasource} } from '../../datasources'; +import { Admin, AdminWithRelations, Roles } from '../../models'; +import { AdminRepository } from '../../repositories'; @authenticate('jwt') @authorize({ voters: [roleAuthorization], allowedRoles: [Roles.ADMIN] }) @@ -22,8 +25,8 @@ export class AdminController { private readonly baseUserRepository: BaseUserRepository, @repository(AdminRepository) private readonly adminRepository: AdminRepository, - @inject(DbDataSource.INJECTION_KEY) - private readonly dataSource: DbDataSource + @inject(${datasource}.INJECTION_KEY) + private readonly dataSource: ${datasource} ) { } @post('/admins') @@ -229,4 +232,5 @@ export class AdminController { throw error; } } -}`; \ No newline at end of file +}`; +} \ No newline at end of file diff --git a/src/loopback/full-admin-model.content.ts b/src/loopback/full-admin-model.content.ts index 3070ae3..9c6ed88 100644 --- a/src/loopback/full-admin-model.content.ts +++ b/src/loopback/full-admin-model.content.ts @@ -2,7 +2,7 @@ export const fullAdminModelContent: string = `import { model, property } from '@loopback/repository'; -import { Admin } from '../../models'; +import { Admin, Roles } from '../../models'; @model() export class FullAdmin extends Admin { diff --git a/src/loopback/loopback.utilities.ts b/src/loopback/loopback.utilities.ts index 1bfcc3e..c1c15f2 100644 --- a/src/loopback/loopback.utilities.ts +++ b/src/loopback/loopback.utilities.ts @@ -167,7 +167,7 @@ export abstract class LoopbackUtilities { * @param command - The command to run. * @param options - Options for running the command. */ - static async runCommand(directory: string, command: LoopbackCliCommands, options: LoopbackCliOptions): Promise { + static async runCommand(directory: Path, command: LoopbackCliCommands, options: LoopbackCliOptions): Promise { if (command.startsWith('new ')) { // for the new command, extract the name command = command.split(' ')[1] as LoopbackCliCommands; @@ -187,14 +187,14 @@ export abstract class LoopbackUtilities { * @param config - The configuration options. * @param dbName - The name of the database used by the api. */ - static async setupAuth(root: string, config: AddLoopbackConfiguration, dbName: string): Promise { + static async setupAuth(root: Path, config: AddLoopbackConfiguration, dbName: string): Promise { await NpmUtilities.install(config.name, [ NpmPackage.LBX_JWT, NpmPackage.LOOPBACK_AUTHENTICATION, NpmPackage.LOOPBACK_AUTHORIZATION ]); await NpmUtilities.install(config.name, [NpmPackage.NODEMAILER_TYPES], true); - await this.applyAuthToApplicationTs(root); + await this.applyAuthToApplicationTs(root, dbName); await this.createMailService(root, config); await this.createBiometricCredentialsService(root, config); await this.createAdminFiles(root, dbName); @@ -202,7 +202,7 @@ export abstract class LoopbackUtilities { await this.setupAuthVariables(root, config); } - private static async createBiometricCredentialsService(root: string, config: AddLoopbackConfiguration): Promise { + private static async createBiometricCredentialsService(root: Path, config: AddLoopbackConfiguration): Promise { await this.runCommand(root, 'service BiometricCredentials', { '--skip-install': true, '--type': 'class', '--yes': true }); const servicePath: Path = getPath(root, 'src', 'services', 'biometric-credentials.service.ts'); await TsUtilities.addImportStatements( @@ -224,9 +224,24 @@ export abstract class LoopbackUtilities { await FsUtilities.replaceInFile(servicePath, 'constructor() {}', ''); } - private static async createAdminFiles(root: string, dbName: string): Promise { + private static async createAdminFiles(root: Path, dbName: string): Promise { const adminModelTs: Path = getPath(root, 'src', 'models', 'admin.model.ts'); await FsUtilities.createFile(adminModelTs, adminModelContent); + await FsUtilities.createFile( + getPath(root, 'src', 'models', 'roles.enum.ts'), + [ + 'export enum Roles {', + '\tADMIN = \'ADMIN\'', + '}' + ] + ); + await FsUtilities.createFile( + getPath(root, 'src', 'models', 'index.ts'), + [ + 'export * from \'./admin.model\';', + 'export * from \'./roles.enum\';' + ] + ); await this.runCommand( root, @@ -244,7 +259,8 @@ export abstract class LoopbackUtilities { element: 'ChangeRepository, ChangeSetRepository, CrudChangeSetRepository', path: NpmPackage.LBX_CHANGE_SETS }, - { defaultImport: false, element: 'BaseUser, BaseUserProfile, BaseUserRepository', path: NpmPackage.LBX_JWT } + { defaultImport: false, element: 'BaseUser, BaseUserProfile, BaseUserRepository', path: NpmPackage.LBX_JWT }, + { defaultImport: false, element: 'Roles', path: '../models' } ]); await FsUtilities.replaceInFile(adminRepositoryTs, 'extends DefaultCrudRepository', 'extends CrudChangeSetRepository'); await FsUtilities.replaceAllInFile(adminRepositoryTs, 'DefaultCrudRepository', ''); @@ -286,14 +302,14 @@ export abstract class LoopbackUtilities { ].join('\n')); const controllerPath: string = getPath(root, 'src', 'controllers'); - await FsUtilities.createFile(getPath(controllerPath, 'admin', 'admin.controller.ts'), adminControllerContent); + await FsUtilities.createFile(getPath(controllerPath, 'admin', 'admin.controller.ts'), adminControllerContent(dbName)); await FsUtilities.updateFile(getPath(controllerPath, 'index.ts'), 'export * from \'./admin/admin.controller\';', 'append'); await FsUtilities.createFile(getPath(controllerPath, 'admin', 'new-admin.model.ts'), newAdminModelContent); await FsUtilities.createFile(getPath(controllerPath, 'admin', 'full-admin.model.ts'), fullAdminModelContent); } - private static async createMailService(root: string, config: AddLoopbackConfiguration): Promise { + private static async createMailService(root: Path, config: AddLoopbackConfiguration): Promise { await this.runCommand(root, 'service mail', { '--skip-install': true, '--type': 'class', '--yes': true }); const servicePath: Path = getPath(root, 'src', 'services', 'mail.service.ts'); await TsUtilities.addImportStatements( @@ -302,7 +318,8 @@ export abstract class LoopbackUtilities { { defaultImport: false, element: 'BaseMailService', path: NpmPackage.LBX_JWT }, { defaultImport: false, element: 'environment', path: '../environment/environment' }, { defaultImport: false, element: 'Transporter, createTransport', path: 'nodemailer' }, - { defaultImport: true, element: 'path', path: 'path' } + { defaultImport: true, element: 'path', path: 'path' }, + { defaultImport: false, element: 'Roles', path: '../models' } ] ); await FsUtilities.replaceInFile(servicePath, ' class MailService', ' class MailService extends BaseMailService'); @@ -441,7 +458,7 @@ export abstract class LoopbackUtilities { ], 'append'); } - private static async applyAuthToApplicationTs(root: string): Promise { + private static async applyAuthToApplicationTs(root: string, dbName: string): Promise { // eslint-disable-next-line sonar/no-duplicate-string const applicationTs: Path = getPath(root, 'src', 'application.ts'); await TsUtilities.addImportStatements( @@ -461,13 +478,15 @@ export abstract class LoopbackUtilities { element: 'AuthorizationBindings, AuthorizationComponent, AuthorizationDecision, AuthorizationOptions', path: '@loopback/authorization' }, - { defaultImport: false, element: 'BiometricCredentialsService', path: './services/biometric-credentials.service' } + { defaultImport: false, element: 'BiometricCredentialsService', path: './services/biometric-credentials.service' }, + { defaultImport: false, element: `${toPascalCase(dbName)}DataSource`, path: './datasources' } ] ); await TsUtilities.addToEndOfClass(applicationTs, [ '', ' private setupAuthentication(): void {', + ` this.bind(LbxJwtBindings.DATASOURCE_KEY).toClass(${toPascalCase(dbName)}DataSource);`, ' this.component(AuthenticationComponent);', ' this.component(LbxJwtComponent);', '', @@ -505,8 +524,9 @@ export abstract class LoopbackUtilities { * Sets up logging. * @param root - The root folder of the loopback app. * @param name - The name of the loopback app. + * @param dbName - The name of the database used by the api. */ - static async setupLogging(root: string, name: string): Promise { + static async setupLogging(root: string, name: string, dbName: string): Promise { await NpmUtilities.install(name, [NpmPackage.LBX_PERSISTENCE_LOGGER, NpmPackage.LOOPBACK_CRON]); const applicationTs: Path = getPath(root, 'src', 'application.ts'); @@ -528,6 +548,7 @@ export abstract class LoopbackUtilities { await TsUtilities.addToEndOfClass(applicationTs, [ '', ' private setupLogging(): void {', + ` this.bind(LbxPersistenceLoggerComponentBindings.DATASOURCE_KEY).toClass(${toPascalCase(dbName)}DataSource);`, ' this.component(LbxPersistenceLoggerComponent);', ' this.repository(LogRepository);', ' this.bind(LbxPersistenceLoggerComponentBindings.LOGGER_NOTIFICATION_SERVICE).toClass(MailService);', @@ -573,10 +594,11 @@ export abstract class LoopbackUtilities { /** * Sets up change sets. - * @param root - THe root of the loopback app. + * @param root - The root of the loopback app. * @param name - The name of the loopback app. + * @param dbName - The name of the database used by the api. */ - static async setupChangeSets(root: string, name: string): Promise { + static async setupChangeSets(root: string, name: string, dbName: string): Promise { await NpmUtilities.install(name, [NpmPackage.LBX_CHANGE_SETS]); const applicationTs: Path = getPath(root, 'src', 'application.ts'); await TsUtilities.addImportStatements( @@ -584,7 +606,7 @@ export abstract class LoopbackUtilities { [ { defaultImport: false, - element: 'ChangeRepository, ChangeSetRepository, LbxChangeSetsComponent', + element: 'ChangeRepository, ChangeSetRepository, LbxChangeSetsComponent, LbxChangeSetsBindings', path: NpmPackage.LBX_CHANGE_SETS } ] @@ -592,6 +614,7 @@ export abstract class LoopbackUtilities { await TsUtilities.addToEndOfClass(applicationTs, [ '', ' private setupChangeSets(): void {', + ` this.bind(LbxChangeSetsBindings.DATASOURCE_KEY).toClass(${toPascalCase(dbName)}DataSource);`, ' this.component(LbxChangeSetsComponent);', ' this.repository(ChangeRepository);', ' this.repository(ChangeSetRepository);', diff --git a/src/nest/index.ts b/src/nest/index.ts new file mode 100644 index 0000000..7fdfe66 --- /dev/null +++ b/src/nest/index.ts @@ -0,0 +1 @@ +export * from './nest.utilities'; \ No newline at end of file diff --git a/src/nest/nest-cli-json.model.ts b/src/nest/nest-cli-json.model.ts new file mode 100644 index 0000000..fb123af --- /dev/null +++ b/src/nest/nest-cli-json.model.ts @@ -0,0 +1,14 @@ +/** + * Model of the nest-cli.json file content. + */ +export interface NestCliJson { + /** + * Options for compiling the project. + */ + compilerOptions: { + /** + * The bundler to use. + */ + builder: 'tsc' | 'webpack' | 'swc' + } +} \ No newline at end of file diff --git a/src/nest/nest.utilities.ts b/src/nest/nest.utilities.ts new file mode 100644 index 0000000..047eded --- /dev/null +++ b/src/nest/nest.utilities.ts @@ -0,0 +1,100 @@ +import { ModuleMetadata } from '@nestjs/common'; + +import { CPUtilities, FsUtilities, JsonUtilities } from '../encapsulation'; +import { TsUtilities } from '../ts'; +import { DeepPartial } from '../types'; +import { mergeDeep, optionsToCliString, Path } from '../utilities'; +import { NestCliJson } from './nest-cli-json.model'; + +/** + * The `nest new {}` command. + */ +type CliNew = `new ${string}`; + +/** + * All possible nest cli commands. + */ +type NestCliCommands = CliNew; + +/** + * Cli Options for running ng new. + */ +type NewOptions = { + /** + * Whether or not npm install should be skipped. + */ + '--skip-install': true, + /** + * Whether or not git initialization should be skipped. + */ + '--skip-git': true, + /** + * The package manager to use. + */ + '--package-manager': 'npm', + /** + * The language to use. + */ + '--language': 'TS' +}; + +/** + * Possible nest cli options, narrowed down based on the provided command. + */ +type NestCliOptions = + T extends CliNew ? NewOptions + : never; + +/** + * Utilities for nest specific code generation/manipulation. + */ +export abstract class NestUtilities { + + private static readonly CLI_VERSION: number = 11; + + /** + * Runs an nest cli command inside the provided directory. + * @param directory - The directory to run the command inside. + * @param command - The command to run. + * @param options - Options for running the command. + */ + static runCommand(directory: Path, command: NestCliCommands, options: NestCliOptions): void { + CPUtilities.execSync(`cd ${directory} && npx @nestjs/cli@${this.CLI_VERSION} ${command} ${optionsToCliString(options)}`); + } + + /** + * Updates an nest-cli.json. + * @param path - The path of the nest-cli.json. + * @param data - The data to update with. + */ + static async updateNestCliJson(path: Path, data: DeepPartial): Promise { + const oldData: NestCliJson = await FsUtilities.parseFileAs(path); + const updatedData: NestCliJson = mergeDeep(oldData, data); + await FsUtilities.updateFile(path, JsonUtilities.stringify(updatedData), 'replace', false); + } + + /** + * Adds a import to the provided module. + * @param modulePath - The path of the module where the import should be added. + * @param imports - The imports to add. + */ + static async addModuleImports( + modulePath: Path, + imports: ModuleMetadata['imports'] + ): Promise { + if (imports === undefined) { + return; + } + const { result, contentString } = await TsUtilities.getArrayStartingWith(modulePath, 'imports: ['); + + result.push(...imports); + + const stringifiedArray: string = ` ${JsonUtilities.stringifyAsTs(result, 4)}`; + + await FsUtilities.replaceInFile( + modulePath, + contentString, + stringifiedArray + ); + } +} \ No newline at end of file diff --git a/src/npm/npm-package.enum.ts b/src/npm/npm-package.enum.ts index 89bb652..777cdfd 100644 --- a/src/npm/npm-package.enum.ts +++ b/src/npm/npm-package.enum.ts @@ -29,5 +29,20 @@ export enum NpmPackage { TSC_WATCH = 'tsc-watch', NG_PACKAGR = 'ng-packagr', TSLIB = 'tslib', - LOOPBACK_4_MIGRATION = 'loopback4-migration' + LOOPBACK_4_MIGRATION = 'loopback4-migration', + CLASS_VALIDATOR = 'class-validator', + CLASS_TRANSFORMER = 'class-transformer', + NEST_JS_SWAGGER = '@nestjs/swagger', + NEST_JS_TYPEORM = '@nestjs/typeorm', + TYPEORM = 'typeorm', + PG = 'pg', + MYSQL_2 = 'mysql2', + WEBPACK = 'webpack', + WEBPACK_CLI = 'webpack-cli', + TS_LOADER = 'ts-loader', + WEBPACK_NODE_EXTERNALS = 'webpack-node-externals', + TSCONFIG_PATH_WEBPACK_PLUGIN = 'tsconfig-paths-webpack-plugin', + FORK_TS_CHECKER_WEBPACK_PLUGIN = 'fork-ts-checker-webpack-plugin', + CLDRJS = 'cldrjs', + CLDR_DATA = 'cldr-data' } \ No newline at end of file diff --git a/src/npm/package-json.model.ts b/src/npm/package-json.model.ts index 86b716d..951b0c7 100644 --- a/src/npm/package-json.model.ts +++ b/src/npm/package-json.model.ts @@ -29,6 +29,10 @@ export type PackageJson = { * The version of the npm package. */ version?: string, + /** + * The license of the npm package. + */ + license?: string, /** * Files that should be included in the package. */ @@ -44,7 +48,7 @@ export type PackageJson = { /** * The scripts inside the file. */ - scripts: Record, + scripts: Record, /** * The workspaces section inside the file. */ @@ -60,11 +64,11 @@ export type PackageJson = { /** * Dependencies of the package. */ - dependencies: Record, + dependencies: Record, /** * DevDependencies of the package. */ - devDependencies: Record, + devDependencies: Record, /** * PeerDependencies of the package. */ diff --git a/src/robots/robots-utilities.test.ts b/src/robots/robots-utilities.test.ts index a054967..f3c6fad 100644 --- a/src/robots/robots-utilities.test.ts +++ b/src/robots/robots-utilities.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, test } from '@jest/globals'; import { FileMockUtilities, getMockConstants, MockConstants } from '../__testing__'; import { RobotsUtilities } from './robots.utilities'; -import { ROBOTS_FILE_NAME } from '../constants'; +import { APPS_DIRECTORY_NAME, ROBOTS_FILE_NAME } from '../constants'; import { FsUtilities } from '../encapsulation'; import { DefaultEnvKeys, EnvUtilities } from '../env'; import { getPath } from '../utilities'; @@ -26,7 +26,7 @@ describe('RobotsUtilities', () => { { path: mockConstants.ANGULAR_APP_DIR, name: mockConstants.ANGULAR_APP_NAME, - npmWorkspaceString: `apps/${mockConstants.ANGULAR_APP_NAME}` + npmWorkspaceString: `${APPS_DIRECTORY_NAME}/${mockConstants.ANGULAR_APP_NAME}` }, 'dev.docker-compose.yaml', undefined, diff --git a/src/storybook/storybook.utilities.ts b/src/storybook/storybook.utilities.ts index 2f836ac..4c317b4 100644 --- a/src/storybook/storybook.utilities.ts +++ b/src/storybook/storybook.utilities.ts @@ -1,4 +1,5 @@ import { CPUtilities } from '../encapsulation'; +import { Path } from '../utilities'; /** * Handles functionality around storybook. @@ -11,7 +12,7 @@ export abstract class StorybookUtilities { * Sets up storybook inside the given root. * @param root - The root of the project where storybook should be setup. */ - static setup(root: string): void { + static setup(root: Path): void { CPUtilities.execSync( `cd ${root} && npm create storybook@${this.CLI_VERSION} -- --no-dev --yes --features docs test --disable-telemetry` ); diff --git a/src/ts/ts.utilities.ts b/src/ts/ts.utilities.ts index b1e4f0d..37c06dc 100644 --- a/src/ts/ts.utilities.ts +++ b/src/ts/ts.utilities.ts @@ -159,6 +159,7 @@ export abstract class TsUtilities { for (let i: number = lines.length - 1; i >= 0; i--) { if (lines[i].startsWith('import ')) { replaceContent = lines[i]; + break; } } await FsUtilities.replaceInFile(path, replaceContent, `${replaceContent}\n\n${content.join('\n')}`); diff --git a/src/tsconfig/tsconfig.utilities.ts b/src/tsconfig/tsconfig.utilities.ts index 72a1870..ef64390 100644 --- a/src/tsconfig/tsconfig.utilities.ts +++ b/src/tsconfig/tsconfig.utilities.ts @@ -14,7 +14,7 @@ export abstract class TsConfigUtilities { * Initializes typescript inside the given path. * @param path - Where to initialize typescript. */ - static init(path: string): void { + static init(path: Path): void { CPUtilities.execSync(`cd ${path} && npx tsc --init`); }