Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions e2e/docker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
import { resolve } from "node:path";
import { beforeAll, describe, expect, it } from "vitest";

const collectionPath = resolve(__dirname, "../dist/collection.json");

describe("docker schematic", () => {
const runner = new SchematicTestRunner("schematics", collectionPath);

describe("with npm package manager", () => {
let tree: UnitTestTree;

describe.each(["npm", "yarn", "pnpm", "bun"] as const)(
"with ${packageManager} package manager",
(packageManager) => {
beforeAll(async () => {
tree = await runner.runSchematic("docker", {
name: `${packageManager}-test-app`,
packageManager,
});
});

it("should create Dockerfile", () => {
expect(tree.files).toContain(`/${packageManager}-test-app/Dockerfile`);
expect(tree.files).toContain(`/${packageManager}-test-app/.dockerignore`);
});

it("should create docker file with install command", () => {
const content = tree.readContent(`/${packageManager}-test-app/Dockerfile`);
expect(content).toContain(`RUN ${packageManager} install`);
});

it("should create docker file with build command", () => {
const content = tree.readContent(`/${packageManager}-test-app/Dockerfile`);
expect(content).toContain(`RUN ${packageManager} run build`);
});

it("should create docker file with start command", () => {
const content = tree.readContent(`/${packageManager}-test-app/Dockerfile`);
expect(content).toContain(`CMD ["${packageManager}", "run", "start"]`);
});
},
);
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"build": "tsc --noEmit && tsup",
"postbuild": "pnpm run copy:collection && pnpm run copy:lib",
"copy:collection": "cpx src/collection.json dist && cpx 'src/libs/**/schema.json' dist/libs",
"copy:lib": "cpx 'src/libs/**/files/**/*' dist/libs",
"copy:lib": "cpx 'src/libs/**/files/**/{.,}*' dist/libs",
"lint": "prettier --check . && eslint --format=pretty src",
"format": "prettier --write . && eslint --fix --format=pretty src",
"prepack": "pnpm run build && pnpm run lint",
Expand Down
5 changes: 5 additions & 0 deletions src/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"factory": "./libs/part-main/part-main.factory#main",
"description": "Create a Main file for client or server.",
"schema": "./libs/part-main/schema.json"
},
"docker": {
"factory": "./libs/docker/docker.factory#main",
"description": "Create a Dockerfile for the application.",
"schema": "./libs/docker/schema.json"
}
}
}
43 changes: 43 additions & 0 deletions src/libs/docker/docker.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { type Path, strings } from "@angular-devkit/core";
import {
type Rule,
type Source,
apply,
mergeWith,
move,
template,
url,
} from "@angular-devkit/schematics";

import { toKebabCase } from "@utils/formatting";
import { resolvePackageName } from "@utils/name";

import { DEFAULT_APP_NAME, DEFAULT_PACKAGE_MANAGER } from "~/defaults";

import { type DockerOptions } from "./docker.options";
import { type DockerSchema } from "./docker.schema";

const transform = (schema: DockerSchema): DockerOptions => {
const name = resolvePackageName(toKebabCase(schema.name?.toString() ?? DEFAULT_APP_NAME));

return {
name,
packageManager: schema.packageManager ?? DEFAULT_PACKAGE_MANAGER,
directory: schema.directory,
};
};

const generate = (options: DockerOptions, path: string): Source => {
return apply(url("./files" as Path), [
template({
...strings,
...options,
}),
move(path),
]);
};

export const main = (schema: DockerSchema): Rule => {
const options = transform(schema);
return mergeWith(generate(options, schema.directory ?? options.name));
};
16 changes: 16 additions & 0 deletions src/libs/docker/docker.options.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface DockerOptions {
/**
* NanoForge application name
*/
name: string;

/**
* NanoForge application package manager
*/
packageManager: "npm" | "yarn" | "pnpm" | "bun";

/**
* Target directory for generated Docker files
*/
directory?: string;
}
16 changes: 16 additions & 0 deletions src/libs/docker/docker.schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface DockerSchema {
/**
* NanoForge application name
*/
name: string;

/**
* NanoForge application package manager
*/
packageManager: "npm" | "yarn" | "pnpm" | "bun";

/**
* Target directory for generated Docker files
*/
directory?: string;
}
1 change: 1 addition & 0 deletions src/libs/docker/files/.dockerignore
Comment thread
MartinFillon marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
39 changes: 39 additions & 0 deletions src/libs/docker/files/Dockerfile
Comment thread
MartinFillon marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<% if (packageManager === 'bun') { %>
FROM oven/bun:1.3
<% } else { %>
FROM node:25-alpine
<% } %>

<% if (packageManager === 'pnpm') { %>
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN npm install -g pnpm
<% } %>
Comment thread
MartinFillon marked this conversation as resolved.

<% if (packageManager === 'yarn') { %>
RUN npm install -g yarn
<% } %>
Comment thread
MartinFillon marked this conversation as resolved.

WORKDIR /app
COPY package.json ./
<% if (packageManager === 'npm') { %>
COPY package-lock.json ./
<% } else if (packageManager === 'yarn') { %>
COPY yarn.lock ./
<% } else if (packageManager === 'pnpm') { %>
COPY pnpm-lock.yaml ./
<% } else if (packageManager === 'bun') { %>
COPY bun.lock ./
<% } %>
Comment thread
MartinFillon marked this conversation as resolved.
<% if (packageManager === 'npm') { %>
RUN npm install --no-lockfile
<% } if (packageManager === 'pnpm') { %>
RUN pnpm install --frozen-lockfile --allow-build=bun
<% } else { %>
RUN <%= packageManager %> install --frozen-lockfile
Comment thread
MartinFillon marked this conversation as resolved.
<% } %>
COPY . .

RUN <%= packageManager %> run build

CMD ["<%= packageManager %>", "run", "start"]
28 changes: 28 additions & 0 deletions src/libs/docker/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "SchematicsNanoForgeDocker",
"title": "NanoForge Base Docker Options Schema",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the application",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the new project ?"
},
"packageManager": {
"type": "string",
"enum": ["npm", "yarn", "pnpm", "bun"],
"description": "NanoForge application package manager",
"default": "npm"
},
"directory": {
"type": "string",
"description": "NanoForge application destination directory"
}
},
"required": ["name"]
}
1 change: 1 addition & 0 deletions tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@ export default [
createLibTsupConfig("configuration"),
createLibTsupConfig("part-base"),
createLibTsupConfig("part-main"),
createLibTsupConfig("docker"),
];