diff --git a/e2e/node/src/__snapshots__/node-server.test.ts.snap b/e2e/node/src/__snapshots__/node-server.test.ts.snap index 908cdfd96b..52a3e58a3c 100644 --- a/e2e/node/src/__snapshots__/node-server.test.ts.snap +++ b/e2e/node/src/__snapshots__/node-server.test.ts.snap @@ -18,6 +18,7 @@ RUN addgroup --system docker-express-app && \\ adduser --system -G docker-express-app docker-express-app COPY dist/apps/docker-express-app docker-express-app/ +COPY apps/docker-express-app/package.json docker-express-app/ RUN chown -R docker-express-app:docker-express-app . # You can remove this install step if you build with \`--bundle\` option. diff --git a/e2e/node/src/__snapshots__/node.test.ts.snap b/e2e/node/src/__snapshots__/node.test.ts.snap index 89ec9e052b..3133a8e2a2 100644 --- a/e2e/node/src/__snapshots__/node.test.ts.snap +++ b/e2e/node/src/__snapshots__/node.test.ts.snap @@ -18,6 +18,7 @@ RUN addgroup --system node-nest-docker-test && \\ adduser --system -G node-nest-docker-test node-nest-docker-test COPY dist/node-nest-docker-test node-nest-docker-test/ +COPY node-nest-docker-test/package.json node-nest-docker-test/ RUN chown -R node-nest-docker-test:node-nest-docker-test . # You can remove this install step if you build with \`--bundle\` option. diff --git a/packages/node/src/generators/setup-docker/files/Dockerfile__tmpl__ b/packages/node/src/generators/setup-docker/files/Dockerfile__tmpl__ index 6203ee33f9..f632c21f2e 100644 --- a/packages/node/src/generators/setup-docker/files/Dockerfile__tmpl__ +++ b/packages/node/src/generators/setup-docker/files/Dockerfile__tmpl__ @@ -3,7 +3,7 @@ # Build the docker image with `npx nx docker-build <%= project %>`. # Tip: Modify "docker-build" options in project.json to change docker build args. # -# Run the container with `docker run -p 3000:3000 -t <%= project %>`. +# Run the container with `docker run -p 3000:3000 -t <%= sanitizedProjectName %>`. FROM docker.io/node:lts-alpine ENV HOST=0.0.0.0 @@ -11,14 +11,15 @@ ENV PORT=3000 WORKDIR /app -RUN addgroup --system <%= project %> && \ - adduser --system -G <%= project %> <%= project %> +RUN addgroup --system <%= sanitizedProjectName %> && \ + adduser --system -G <%= sanitizedProjectName %> <%= sanitizedProjectName %> -COPY <%= buildLocation %> <%= project %>/ -RUN chown -R <%= project %>:<%= project %> . +COPY <%= buildLocation %> <%= sanitizedProjectName %>/ +COPY <%= projectPath %>/package.json <%= sanitizedProjectName %>/ +RUN chown -R <%= sanitizedProjectName %>:<%= sanitizedProjectName %> . # You can remove this install step if you build with `--bundle` option. # The bundled output will include external dependencies. -RUN npm --prefix <%= project %> --omit=dev -f install +RUN npm --prefix <%= sanitizedProjectName %> --omit=dev -f install -CMD [ "node", "<%= project %>" ] +CMD [ "node", "<%= sanitizedProjectName %>" ] diff --git a/packages/node/src/generators/setup-docker/setup-docker.spec.ts b/packages/node/src/generators/setup-docker/setup-docker.spec.ts index 6d39ca5093..8232d80277 100644 --- a/packages/node/src/generators/setup-docker/setup-docker.spec.ts +++ b/packages/node/src/generators/setup-docker/setup-docker.spec.ts @@ -1,6 +1,7 @@ import { readProjectConfiguration, Tree } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { applicationGenerator } from '../application/application'; +import { setupDockerGenerator } from './setup-docker'; describe('setupDockerGenerator', () => { let tree: Tree; @@ -65,4 +66,198 @@ describe('setupDockerGenerator', () => { ); }); }); + + describe('project name sanitization', () => { + it('should sanitize project names with special characters for Docker commands', async () => { + const projectName = '@myorg/my-app'; + + await applicationGenerator(tree, { + name: projectName, + directory: '.', + framework: 'express', + e2eTestRunner: 'none', + addPlugin: true, + }); + + await setupDockerGenerator(tree, { + project: projectName, + outputPath: 'dist/myorg/my-app', + }); + + const project = readProjectConfiguration(tree, projectName); + + expect(project.targets['docker-build']).toEqual({ + dependsOn: ['build'], + command: `docker build -f Dockerfile . -t myorg-my-app`, + }); + + expect(tree.read('Dockerfile', 'utf8')).toMatchInlineSnapshot(` + "# This file is generated by Nx. + # + # Build the docker image with \`npx nx docker-build @myorg/my-app\`. + # Tip: Modify "docker-build" options in project.json to change docker build args. + # + # Run the container with \`docker run -p 3000:3000 -t myorg-my-app\`. + FROM docker.io/node:lts-alpine + + ENV HOST=0.0.0.0 + ENV PORT=3000 + + WORKDIR /app + + RUN addgroup --system myorg-my-app && \\ + adduser --system -G myorg-my-app myorg-my-app + + COPY dist/myorg/my-app myorg-my-app/ + COPY ./package.json myorg-my-app/ + RUN chown -R myorg-my-app:myorg-my-app . + + # You can remove this install step if you build with \`--bundle\` option. + # The bundled output will include external dependencies. + RUN npm --prefix myorg-my-app --omit=dev -f install + + CMD [ "node", "myorg-my-app" ] + " + `); + }); + + it('should sanitize project names with slashes and other special characters', async () => { + const projectName = 'my/special@app'; + + await applicationGenerator(tree, { + name: projectName, + directory: '.', + framework: 'express', + e2eTestRunner: 'none', + addPlugin: true, + }); + + await setupDockerGenerator(tree, { + project: projectName, + outputPath: 'dist/basic-app', + }); + + const project = readProjectConfiguration(tree, projectName); + + expect(project.targets['docker-build']).toEqual({ + dependsOn: ['build'], + command: `docker build -f Dockerfile . -t my-special-app`, + }); + + expect(tree.read('Dockerfile', 'utf8')).toMatchInlineSnapshot(` + "# This file is generated by Nx. + # + # Build the docker image with \`npx nx docker-build my/special@app\`. + # Tip: Modify "docker-build" options in project.json to change docker build args. + # + # Run the container with \`docker run -p 3000:3000 -t my-special-app\`. + FROM docker.io/node:lts-alpine + + ENV HOST=0.0.0.0 + ENV PORT=3000 + + WORKDIR /app + + RUN addgroup --system my-special-app && \\ + adduser --system -G my-special-app my-special-app + + COPY dist/basic-app my-special-app/ + COPY ./package.json my-special-app/ + RUN chown -R my-special-app:my-special-app . + + # You can remove this install step if you build with \`--bundle\` option. + # The bundled output will include external dependencies. + RUN npm --prefix my-special-app --omit=dev -f install + + CMD [ "node", "my-special-app" ] + " + `); + }); + + it('should handle uppercase and multiple special characters', async () => { + const projectName = 'My_App@123/Test'; + + await applicationGenerator(tree, { + name: projectName, + directory: '.', + framework: 'express', + e2eTestRunner: 'none', + docker: true, + addPlugin: true, + }); + + const project = readProjectConfiguration(tree, projectName); + + expect(project.targets['docker-build']).toEqual({ + dependsOn: ['build'], + command: `docker build -f Dockerfile . -t my_app-123-test`, + }); + + expect(tree.read('Dockerfile', 'utf8')).toMatchInlineSnapshot(` + "# This file is generated by Nx. + # + # Build the docker image with \`npx nx docker-build My_App@123/Test\`. + # Tip: Modify "docker-build" options in project.json to change docker build args. + # + # Run the container with \`docker run -p 3000:3000 -t my_app-123-test\`. + FROM docker.io/node:lts-alpine + + ENV HOST=0.0.0.0 + ENV PORT=3000 + + WORKDIR /app + + RUN addgroup --system my_app-123-test && \\ + adduser --system -G my_app-123-test my_app-123-test + + COPY dist/My_App@123/Test my_app-123-test/ + COPY ./package.json my_app-123-test/ + RUN chown -R my_app-123-test:my_app-123-test . + + # You can remove this install step if you build with \`--bundle\` option. + # The bundled output will include external dependencies. + RUN npm --prefix my_app-123-test --omit=dev -f install + + CMD [ "node", "my_app-123-test" ] + " + `); + }); + + it('should ensure docker-build target works with sanitized names in Dockerfile', async () => { + const projectName = '@scope/my-app'; + + await applicationGenerator(tree, { + name: projectName, + directory: '.', + framework: 'express', + e2eTestRunner: 'none', + addPlugin: true, + }); + + await setupDockerGenerator(tree, { + project: projectName, + outputPath: 'dist/scope/my-app', + }); + + const dockerfileContent = tree.read('Dockerfile', 'utf8'); + + expect(dockerfileContent).toMatch(/^FROM\s+\S+/m); + expect(dockerfileContent).toMatch(/^WORKDIR\s+\S+/m); + expect(dockerfileContent).toMatch(/^CMD\s+\[/m); + + // Verify user/group names are valid (no special chars that would break Linux) + const userGroupMatches = dockerfileContent.match( + /addgroup --system (\S+)/ + ); + const userGroupName = userGroupMatches?.[1]; + expect(userGroupName).toMatch(/^[a-z0-9._-]+$/); + expect(userGroupName).not.toContain('@'); + expect(userGroupName).not.toContain('/'); + + const project = readProjectConfiguration(tree, projectName); + expect(project.targets['docker-build'].command).toContain( + `-t ${userGroupName}` + ); + }); + }); }); diff --git a/packages/node/src/generators/setup-docker/setup-docker.ts b/packages/node/src/generators/setup-docker/setup-docker.ts index 04ff39d8df..96c2e9b7d2 100644 --- a/packages/node/src/generators/setup-docker/setup-docker.ts +++ b/packages/node/src/generators/setup-docker/setup-docker.ts @@ -25,6 +25,15 @@ function normalizeOptions( }; } +function sanitizeProjectName(projectName: string): string { + return projectName + .toLowerCase() + .replace(/[@\/]/g, '-') + .replace(/[^a-z0-9._-]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); +} + function addDocker(tree: Tree, options: SetUpDockerOptions) { const projectConfig = readProjectConfiguration(tree, options.project); @@ -40,22 +49,30 @@ function addDocker(tree: Tree, options: SetUpDockerOptions) { `The output path for the project ${options.project} is not defined. Please provide it as an option to the generator.` ); } + + const sanitizedProjectName = sanitizeProjectName(options.project); + generateFiles(tree, join(__dirname, './files'), projectConfig.root, { tmpl: '', buildLocation: options.outputPath ?? outputPath, project: options.project, + projectPath: projectConfig.root, + sanitizedProjectName, }); } export function updateProjectConfig(tree: Tree, options: SetUpDockerOptions) { let projectConfig = readProjectConfiguration(tree, options.project); + // Use sanitized project name for Docker image tag + const sanitizedProjectName = sanitizeProjectName(options.project); + projectConfig.targets[`${options.targetName}`] = { dependsOn: [`${options.buildTarget}`], command: `docker build -f ${joinPathFragments( projectConfig.root, 'Dockerfile' - )} . -t ${options.project}`, + )} . -t ${sanitizedProjectName}`, }; updateProjectConfiguration(tree, options.project, projectConfig);