From 9205c795ec7345178b68e0113686c11d919d16d3 Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Fri, 27 Feb 2026 11:36:45 +0100 Subject: [PATCH] feat(docker): setup new docker option to generete docker file --- e2e/cli-build.test.ts | 2 + e2e/cli-generate.test.ts | 2 + e2e/cli-help.test.ts | 1 + e2e/cli-install.test.ts | 2 + e2e/cli-new-config.test.ts | 3 + e2e/cli-new.test.ts | 81 +++++++++++++++++++ pnpm-lock.yaml | 12 +-- pnpm-workspace.yaml | 2 +- src/action/actions/new.action.ts | 15 ++++ src/command/commands/new.command.ts | 4 + src/lib/input/inputs/new/docker.input.spec.ts | 31 +++++++ src/lib/input/inputs/new/docker.input.ts | 16 ++++ src/lib/schematics/nanoforge.collection.ts | 5 ++ src/lib/ui/messages.ts | 1 + 14 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 src/lib/input/inputs/new/docker.input.spec.ts create mode 100644 src/lib/input/inputs/new/docker.input.ts diff --git a/e2e/cli-build.test.ts b/e2e/cli-build.test.ts index aff125c..d96f0ec 100644 --- a/e2e/cli-build.test.ts +++ b/e2e/cli-build.test.ts @@ -33,6 +33,7 @@ describe("nf build (TypeScript, no server)", () => { "--no-server", "--no-init-functions", "--no-skip-install", + "--no-docker", "-d", projectDir, ]); @@ -81,6 +82,7 @@ describe("nf build (TypeScript, with server)", () => { "--server", "--no-init-functions", "--no-skip-install", + "--no-docker", "-d", projectDir, ]); diff --git a/e2e/cli-generate.test.ts b/e2e/cli-generate.test.ts index a474638..cdf1b57 100644 --- a/e2e/cli-generate.test.ts +++ b/e2e/cli-generate.test.ts @@ -33,6 +33,7 @@ describe("nf generate (TypeScript, no server)", () => { "--no-server", "--init-functions", "--no-skip-install", + "--no-docker", "-d", projectDir, ]); @@ -228,6 +229,7 @@ describe("nf generate (TypeScript, with server)", () => { "--server", "--init-functions", "--no-skip-install", + "--no-docker", "-d", projectDir, ]); diff --git a/e2e/cli-help.test.ts b/e2e/cli-help.test.ts index 461dbc7..96d045f 100644 --- a/e2e/cli-help.test.ts +++ b/e2e/cli-help.test.ts @@ -45,6 +45,7 @@ describe("nf new --help", () => { expect(stdout).toContain("--init-functions"); expect(stdout).toContain("--skip-install"); expect(stdout).toContain("--directory"); + expect(stdout).toContain("--docker"); }); }); diff --git a/e2e/cli-install.test.ts b/e2e/cli-install.test.ts index 8c1bcfc..d366525 100644 --- a/e2e/cli-install.test.ts +++ b/e2e/cli-install.test.ts @@ -33,6 +33,7 @@ describe("nf install (with existing project)", () => { "--no-server", "--no-init-functions", "--no-skip-install", + "--no-docker", "-d", projectDir, ]); @@ -117,6 +118,7 @@ describe("nf install (without library name)", () => { "--no-server", "--no-init-functions", "--no-skip-install", + "--no-docker", "-d", projectDir, ]); diff --git a/e2e/cli-new-config.test.ts b/e2e/cli-new-config.test.ts index d77dd19..c8943df 100644 --- a/e2e/cli-new-config.test.ts +++ b/e2e/cli-new-config.test.ts @@ -33,6 +33,7 @@ describe("nf new config output (no server)", () => { "--no-server", "--no-init-functions", "--skip-install", + "--no-docker", "-d", projectDir, ]); @@ -75,6 +76,7 @@ describe("nf new config output (with server)", () => { "--server", "--no-init-functions", "--skip-install", + "--no-docker", "-d", projectDir, ]); @@ -122,6 +124,7 @@ describe("nf new package.json output", () => { "--no-server", "--no-init-functions", "--skip-install", + "--no-docker", "-d", projectDir, ]); diff --git a/e2e/cli-new.test.ts b/e2e/cli-new.test.ts index f5338c6..c867a16 100644 --- a/e2e/cli-new.test.ts +++ b/e2e/cli-new.test.ts @@ -34,6 +34,7 @@ describe("nf new (TypeScript, no server)", () => { "--no-server", "--no-init-functions", "--no-skip-install", + "--no-docker", "-d", projectDir, ]); @@ -103,6 +104,7 @@ describe("nf new (JavaScript, with server)", () => { "--server", "--init-functions", "--no-skip-install", + "--no-docker", "-d", projectDir, ]); @@ -172,6 +174,7 @@ describe("nf new (with --path option)", () => { "--no-server", "--no-init-functions", "--skip-install", + "--no-docker", "-d", projectDir, ]); @@ -184,3 +187,81 @@ describe("nf new (with --path option)", () => { expect(existsSync(resolve(projectDir, "custom/subdir"))).toBe(true); }); }); + +describe("nf new (with typescript with docker option)", () => { + const projectDir = resolve(tmpDir, "ts-with-docker"); + + beforeAll(async () => { + mkdirSync(projectDir, { recursive: true }); + }); + + it("should create a project successfully", async () => { + const { stdout, exitCode } = await runCli([ + "new", + "--name", + "ts-app", + "--language", + "ts", + "--package-manager", + "npm", + "--strict", + "--no-server", + "--no-init-functions", + "--no-skip-install", + "--docker", + "-d", + projectDir, + ]); + + expect(exitCode).toBe(0); + expect(stdout).toContain("NanoForge Project Creation"); + expect(stdout).toContain("Project successfully created"); + }); + + it("should create the project directory", () => { + expect(existsSync(resolve(projectDir, "ts-app"))).toBe(true); + }); + + it("should generate nanoforge.config.json", () => { + const configPath = resolve(projectDir, "ts-app/nanoforge.config.json"); + expect(existsSync(configPath)).toBe(true); + + const config = JSON.parse(readFileSync(configPath, "utf-8")); + expect(config.client).toBeDefined(); + expect(config.client.build.entryFile).toBe("client/main.ts"); + }); + + it("should generate package.json", () => { + expect(existsSync(resolve(projectDir, "ts-app/package.json"))).toBe(true); + }); + + it("should generate tsconfig.json for TypeScript", () => { + expect(existsSync(resolve(projectDir, "ts-app/tsconfig.json"))).toBe(true); + }); + + it("should generate client directory", () => { + expect(existsSync(resolve(projectDir, "ts-app/client"))).toBe(true); + }); + + it("should generate client main file", () => { + expect(existsSync(resolve(projectDir, "ts-app/client/main.ts"))).toBe(true); + }); + + it("should not generate server directory", () => { + expect(existsSync(resolve(projectDir, "ts-app/server"))).toBe(false); + }); + + it("should not have server enabled in config", () => { + const configPath = resolve(projectDir, "ts-app/nanoforge.config.json"); + const config = JSON.parse(readFileSync(configPath, "utf-8")); + expect(config.server?.enable).not.toBe(true); + }); + + it("should generate Dockerfile", () => { + expect(existsSync(resolve(projectDir, "ts-app/Dockerfile"))).toBe(true); + }); + + it("should generate .dockerignore", () => { + expect(existsSync(resolve(projectDir, "ts-app/.dockerignore"))).toBe(true); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95eedf5..94a0253 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,8 +101,8 @@ catalogs: specifier: ^21.1.4 version: 21.1.4 '@nanoforge-dev/schematics': - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^1.2.0 + version: 1.2.0 tests: '@vitest/coverage-v8': specifier: ^4.0.18 @@ -132,7 +132,7 @@ importers: version: 1.1.0 '@nanoforge-dev/schematics': specifier: catalog:schematics - version: 1.1.0(chokidar@5.0.0) + version: 1.2.0(chokidar@5.0.0) ansis: specifier: catalog:cli version: 4.2.0 @@ -895,8 +895,8 @@ packages: resolution: {integrity: sha512-6taExH65vAfUpIVrWSzPzLsGkdXcdREjOjvgY4vOwXet3JXRtA6+yHjqp2kVg9Yrp+OA3iUPNXOsLRXOrfNVJw==} engines: {node: 24.11.0} - '@nanoforge-dev/schematics@1.1.0': - resolution: {integrity: sha512-A1ei/V6NK8DURzM/KdQI3ue0wrryRHoL4PnOJlo8Sr/CIQE17H5n7kJUQtSWblqNkohToE3w5c4A8dGDFoUtMg==} + '@nanoforge-dev/schematics@1.2.0': + resolution: {integrity: sha512-ObxdyAi8D0Waj3gfuyzGuwEv7M9DTsYe81kv+2rduR6go/Wxbbt3FWykEZ7pWxXX9VgxmMxlptYRMrNEk7bLaA==} engines: {node: '25'} '@nanoforge-dev/utils-eslint-config@1.0.2': @@ -3354,7 +3354,7 @@ snapshots: '@nanoforge-dev/loader-website@1.1.0': {} - '@nanoforge-dev/schematics@1.1.0(chokidar@5.0.0)': + '@nanoforge-dev/schematics@1.2.0(chokidar@5.0.0)': dependencies: '@angular-devkit/core': 21.1.4(chokidar@5.0.0) '@angular-devkit/schematics': 21.1.4(chokidar@5.0.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3724ed0..81a0a1d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -36,7 +36,7 @@ catalogs: schematics: '@angular-devkit/schematics': ^21.1.4 '@angular-devkit/schematics-cli': ^21.1.4 - '@nanoforge-dev/schematics': ^1.1.0 + '@nanoforge-dev/schematics': ^1.2.0 tests: '@vitest/coverage-v8': ^4.0.18 vitest: ^4.0.18 diff --git a/src/action/actions/new.action.ts b/src/action/actions/new.action.ts index ae06181..3b96963 100644 --- a/src/action/actions/new.action.ts +++ b/src/action/actions/new.action.ts @@ -1,6 +1,7 @@ import { join } from "node:path"; import { type Input, getDirectoryInput } from "@lib/input"; +import { getDockerOrAsk } from "@lib/input/inputs/new/docker.input"; import { getNewInitFunctionsWithDefault } from "@lib/input/inputs/new/init-functions.input"; import { getNewLanguageInputOrAsk } from "@lib/input/inputs/new/language.input"; import { getNewNameInputOrAsk } from "@lib/input/inputs/new/name.input"; @@ -25,6 +26,7 @@ interface NewValues { server: boolean; initFunctions: boolean; skipInstall: boolean; + docker: boolean; } export class NewAction extends AbstractAction { @@ -57,6 +59,7 @@ export class NewAction extends AbstractAction { server: await getNewServerOrAsk(inputs), initFunctions: getNewInitFunctionsWithDefault(inputs), skipInstall: await getNewSkipInstallOrAsk(inputs), + docker: await getDockerOrAsk(inputs), }; } @@ -69,6 +72,7 @@ export class NewAction extends AbstractAction { await this.generateApplication(collection, values); await this.generateConfiguration(collection, values); await this.generateClientParts(collection, values); + await this.generateDocker(collection, values); if (values.server) { await this.generateServerParts(collection, values); @@ -126,6 +130,17 @@ export class NewAction extends AbstractAction { await executeSchematic("Server main file", collection, "part-main", partOptions); } + private async generateDocker( + collection: ReturnType, + values: NewValues, + ) { + await executeSchematic("Docker", collection, "docker", { + name: values.name, + directory: values.directory, + packageManager: values.packageManager, + }); + } + private partOptions(values: NewValues, part: "client" | "server") { return { name: values.name, diff --git a/src/command/commands/new.command.ts b/src/command/commands/new.command.ts index a92c0a0..0a8c880 100644 --- a/src/command/commands/new.command.ts +++ b/src/command/commands/new.command.ts @@ -12,6 +12,7 @@ interface NewOptions { server?: boolean; initFunctions?: boolean; skipInstall?: boolean; + docker?: boolean; } export class NewCommand extends AbstractCommand { @@ -32,6 +33,8 @@ export class NewCommand extends AbstractCommand { .option("--no-init-functions", "do not initialize functions") .option("--skip-install", "skip installing dependencies") .option("--no-skip-install", "do not skip installing dependencies") + .option("--docker", "generate docker files") + .option("--no-docker", "do not generate docker files") .action(async (rawOptions: NewOptions) => { const options = AbstractCommand.mapToInput({ directory: rawOptions.directory, @@ -43,6 +46,7 @@ export class NewCommand extends AbstractCommand { server: rawOptions.server, initFunctions: rawOptions.initFunctions, skipInstall: rawOptions.skipInstall, + docker: rawOptions.docker, }); await this.action.run(new Map(), options); diff --git a/src/lib/input/inputs/new/docker.input.spec.ts b/src/lib/input/inputs/new/docker.input.spec.ts new file mode 100644 index 0000000..336dc14 --- /dev/null +++ b/src/lib/input/inputs/new/docker.input.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from "vitest"; + +import { askConfirm } from "@lib/question"; + +import { type Input } from "../../input.type"; +import { getDockerOrAsk } from "./docker.input"; + +vi.mock("@lib/question", () => ({ + askConfirm: vi.fn(), +})); + +const createInput = (entries: [string, any][]): Input => { + return new Map(entries.map(([key, value]) => [key, { value }])); +}; + +describe("getDockerOrAsk", () => { + it("should return the docker input when provided", async () => { + const input = createInput([["docker", true]]); + + expect(await getDockerOrAsk(input)).toBe(true); + expect(askConfirm).not.toHaveBeenCalled(); + }); + + it("should call askConfirm when docker is not provided", async () => { + vi.mocked(askConfirm).mockResolvedValue(false); + const input = createInput([]); + + expect(await getDockerOrAsk(input)).toBe(false); + expect(askConfirm).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/lib/input/inputs/new/docker.input.ts b/src/lib/input/inputs/new/docker.input.ts new file mode 100644 index 0000000..c5c953d --- /dev/null +++ b/src/lib/input/inputs/new/docker.input.ts @@ -0,0 +1,16 @@ +import { askConfirm } from "@lib/question"; +import { Messages } from "@lib/ui"; + +import { getInputOrAsk } from "../../ask-inputs"; +import { getBooleanInput } from "../../base-inputs"; +import { type Input } from "../../input.type"; + +const getDockerInput = (inputs: Input) => { + return getBooleanInput(inputs, "docker"); +}; + +export const getDockerOrAsk = (inputs: Input) => { + return getInputOrAsk(getDockerInput(inputs), () => + askConfirm(Messages.NEW_DOCKER_QUESTION, { default: true }), + ); +}; diff --git a/src/lib/schematics/nanoforge.collection.ts b/src/lib/schematics/nanoforge.collection.ts index 8ee995e..9049119 100644 --- a/src/lib/schematics/nanoforge.collection.ts +++ b/src/lib/schematics/nanoforge.collection.ts @@ -31,6 +31,11 @@ export class NanoforgeCollection extends AbstractCollection { alias: "main", description: "Generate a NanoForge Part Main file", }, + { + name: "docker", + alias: "docker", + description: "Generate a Dockerfile for the application", + }, ]; constructor(runner: Runner, cwd?: string) { diff --git a/src/lib/ui/messages.ts b/src/lib/ui/messages.ts index c4e1f43..2cb2d9d 100644 --- a/src/lib/ui/messages.ts +++ b/src/lib/ui/messages.ts @@ -32,6 +32,7 @@ export const Messages = { NEW_STRICT_QUESTION: "Do you want to use strict type checking?", NEW_SERVER_QUESTION: "Do you want to generate a server for multiplayer?", NEW_SKIP_INSTALL_QUESTION: "Do you want to skip dependency installation?", + NEW_DOCKER_QUESTION: "Do you want to add a Dockerfile for containerization?", // --- Generate --- GENERATE_START: "NanoForge Generate",