From 37d9ccc71cfe00ee0fbcc757bb57dc5b5b810437 Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Wed, 23 Sep 2026 12:55:05 +0200 Subject: [PATCH] refactor: end generated files with a newline Adds a `fileContent` tag, which is `codeBlock` from common-tags plus the trailing newline every common-tags tag trims away, and uses it wherever the content is written to a file: the apt sources, the conan profile, the maven and gradle settings, the mono scripts, the node tool env and the shell wrappers. `PathService.exportToolEnvContent` no longer trims the content it appends, so the caller decides how the block ends, and the shell wrapper is built from a single template plus pieces which each end with a newline. The wrapper itself is unchanged. Co-Authored-By: Claude Opus 5 --- src/cli/services/link-tool.service.ts | 24 +++++++++--------------- src/cli/services/path.service.ts | 6 +++++- src/cli/tools/dotnet/mono.spec.ts | 2 +- src/cli/tools/dotnet/mono.ts | 13 ++++++------- src/cli/tools/git/index.spec.ts | 5 ++--- src/cli/tools/git/index.ts | 10 +++++++--- src/cli/tools/java/utils.ts | 12 ++++++++---- src/cli/tools/node/index.ts | 5 ++--- src/cli/tools/python/conan.ts | 5 ++--- src/cli/utils/index.ts | 1 + src/cli/utils/tags.spec.ts | 17 +++++++++++++++++ src/cli/utils/tags.ts | 26 ++++++++++++++++++++++++++ 12 files changed, 86 insertions(+), 40 deletions(-) create mode 100644 src/cli/utils/tags.spec.ts create mode 100644 src/cli/utils/tags.ts diff --git a/src/cli/services/link-tool.service.ts b/src/cli/services/link-tool.service.ts index 826f597cfc..b01cbe17ac 100644 --- a/src/cli/services/link-tool.service.ts +++ b/src/cli/services/link-tool.service.ts @@ -1,8 +1,7 @@ import fs from 'node:fs/promises'; import { join } from 'node:path'; -import { codeBlock } from 'common-tags'; import { bindingScopeValues, inject, injectable } from 'inversify'; -import { pathExists, tool2path } from '../utils/index.ts'; +import { fileContent, pathExists, tool2path } from '../utils/index.ts'; import { EnvService } from './env.service.ts'; import { PathService } from './path.service.ts'; @@ -59,7 +58,7 @@ export class LinkToolService { : `${srcDir}/${name ?? tool}`; const envs = [...(extraToolEnvs ?? []), tool].map(tool2path); - let content = codeBlock` + let content = fileContent` #!/bin/bash if [[ -z "\${CONTAINERBASE_ENV+x}" ]]; then @@ -70,34 +69,29 @@ export class LinkToolService { # set logging to only warn and above to not interfere with tool output CONTAINERBASE_LOG_LEVEL=warn containerbase-cli init tool "${tool}" fi - `; - - // `envs` always holds at least the tool itself - content += - '\n' + - codeBlock` # load tool envs include () { local file=${this.pathSvc.toolsPath}/$1/env.sh [[ -f "$file" ]] && source "$file" } - `; + `; + // `envs` always holds at least the tool itself for (const t of envs) { - content += `\ninclude ${t}`; + content += `include ${t}\n`; } - content += `\nunset include`; + content += `unset include\n`; if (exports) { - content += `\nexport ${exports}`; + content += `export ${exports}\n`; } if (body) { - content += `\n${body}`; + content += `${body}\n`; } - content += `\n${src}`; + content += src; if (args) { content += ` ${args}`; } diff --git a/src/cli/services/path.service.ts b/src/cli/services/path.service.ts index ef2fc7af30..1ca3e8c2d1 100644 --- a/src/cli/services/path.service.ts +++ b/src/cli/services/path.service.ts @@ -284,9 +284,13 @@ export class PathService { await fs.rm(file, { force: true }); } + /** + * Appends a block to the tool env, `content` should end with a newline, eg. + * by using the `fileContent` tag. + */ async exportToolEnvContent(tool: string, content: string): Promise { const file = join(await this.ensureToolPath(tool), 'env.sh'); - await fs.appendFile(file, `\n${content.trim()}\n`); + await fs.appendFile(file, `\n${content}`); await this.setOwner({ path: file, mode: 0o644 }); } diff --git a/src/cli/tools/dotnet/mono.spec.ts b/src/cli/tools/dotnet/mono.spec.ts index f679e53755..3e80d6a0d6 100644 --- a/src/cli/tools/dotnet/mono.spec.ts +++ b/src/cli/tools/dotnet/mono.spec.ts @@ -73,7 +73,7 @@ describe('cli/tools/dotnet/mono', () => { const certSync = join(path, 'bin/cert-sync'); expect(await fs.readFile(certSync, 'utf8')).toBe( - `#!/bin/sh\n${path}/bin/mono ${path}/lib/mono/4.5/cert-sync.exe "$@"`, + `#!/bin/sh\n${path}/bin/mono ${path}/lib/mono/4.5/cert-sync.exe "$@"\n`, ); expect(execaMock).toHaveBeenCalledWith( certSync, diff --git a/src/cli/tools/dotnet/mono.ts b/src/cli/tools/dotnet/mono.ts index ee18301943..27e386843c 100644 --- a/src/cli/tools/dotnet/mono.ts +++ b/src/cli/tools/dotnet/mono.ts @@ -1,9 +1,8 @@ import fs from 'fs/promises'; import { join } from 'node:path'; -import { codeBlock } from 'common-tags'; import { injectFromHierarchy, injectable } from 'inversify'; import { BasePrepareService } from '../../prepare-tool/base-prepare.service.ts'; -import { logger, pathExists } from '../../utils/index.ts'; +import { fileContent, logger, pathExists } from '../../utils/index.ts'; import { PrebuildInstallService, PrebuildVersionResolver, @@ -32,7 +31,7 @@ export class MonoPrepareService extends BasePrepareService { this.envSvc.rootDir, 'etc/ca-certificates/update.d/containerbase-mono-keystore', ), - codeBlock` + fileContent` #!/bin/sh set -e @@ -80,10 +79,10 @@ export class MonoInstallService extends PrebuildInstallService { if (!(await pathExists(join(path, 'bin/cert-sync')))) { await fs.writeFile( join(path, 'bin/cert-sync'), - codeBlock` - #!/bin/sh - ${path}/bin/mono ${path}/lib/mono/4.5/cert-sync.exe "$@" - `, + fileContent` + #!/bin/sh + ${path}/bin/mono ${path}/lib/mono/4.5/cert-sync.exe "$@" + `, { mode: this.envSvc.umask, }, diff --git a/src/cli/tools/git/index.spec.ts b/src/cli/tools/git/index.spec.ts index 2070b60d28..ec55464580 100644 --- a/src/cli/tools/git/index.spec.ts +++ b/src/cli/tools/git/index.spec.ts @@ -1,7 +1,6 @@ import { readFile } from 'node:fs/promises'; -import { codeBlock } from 'common-tags'; import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; -import { getDistro } from '../../utils/index.ts'; +import { fileContent, getDistro } from '../../utils/index.ts'; import { GitInstallService, GitPrepareService } from './index.ts'; import { scope } from '~test/http-mock.ts'; import { ensurePaths, rootPath } from '~test/path.ts'; @@ -53,7 +52,7 @@ describe('cli/tools/git/index', () => { ); expect( await readFile(rootPath('etc/apt/sources.list.d/git.sources'), 'utf8'), - ).toBe(codeBlock` + ).toBe(fileContent` Types: deb URIs: https://ppa.launchpadcontent.net/git-core/ppa/ubuntu Suites: noble diff --git a/src/cli/tools/git/index.ts b/src/cli/tools/git/index.ts index 1df15693dc..7a49d107cf 100644 --- a/src/cli/tools/git/index.ts +++ b/src/cli/tools/git/index.ts @@ -1,12 +1,16 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { codeBlock } from 'common-tags'; import { execa } from 'execa'; import { inject, injectFromHierarchy, injectable } from 'inversify'; import { BaseInstallService } from '../../install-tool/base-install.service.ts'; import { BasePrepareService } from '../../prepare-tool/base-prepare.service.ts'; import { AptService, HttpService } from '../../services/index.ts'; -import { getDistro, semverCoerce, semverGte } from '../../utils/index.ts'; +import { + fileContent, + getDistro, + semverCoerce, + semverGte, +} from '../../utils/index.ts'; /** * Keep in sync with the minimum git version renovate needs. @@ -41,7 +45,7 @@ export class GitPrepareService extends BasePrepareService { await writeFile(join(this.envSvc.rootDir, keyPath), key, { mode: 0o644 }); await writeFile( join(this.envSvc.rootDir, 'etc/apt/sources.list.d/git.sources'), - codeBlock` + fileContent` Types: deb URIs: https://ppa.launchpadcontent.net/git-core/ppa/ubuntu Suites: ${distro.versionCode} diff --git a/src/cli/tools/java/utils.ts b/src/cli/tools/java/utils.ts index 636171ebb6..b4e6edd865 100644 --- a/src/cli/tools/java/utils.ts +++ b/src/cli/tools/java/utils.ts @@ -1,7 +1,11 @@ import path from 'node:path'; -import { codeBlock } from 'common-tags'; import type { HttpService, PathService } from '../../services'; -import { type Arch, logger, pathExists } from '../../utils/index.ts'; +import { + type Arch, + fileContent, + logger, + pathExists, +} from '../../utils/index.ts'; import { type AdoptiumPackage, AdoptiumReleaseVersions, @@ -58,7 +62,7 @@ export async function createMavenSettings(pathSvc: PathService): Promise { logger.debug('Creating Maven settings'); await pathSvc.writeFile( file, - codeBlock` + fileContent` { const distro = await getDistro(); - const profile = codeBlock` + const profile = fileContent` [settings] arch=${getArchitecture(this.envSvc.arch)} build_type=Release diff --git a/src/cli/utils/index.ts b/src/cli/utils/index.ts index 8b145af2d3..b8c5a90d36 100644 --- a/src/cli/utils/index.ts +++ b/src/cli/utils/index.ts @@ -6,6 +6,7 @@ export * from './types.ts'; export * from './versions.ts'; export * from './logger.ts'; export * from './common.ts'; +export * from './tags.ts'; export type { Options as SpawnOptions, Result as SpawnResult, diff --git a/src/cli/utils/tags.spec.ts b/src/cli/utils/tags.spec.ts new file mode 100644 index 0000000000..974d75c904 --- /dev/null +++ b/src/cli/utils/tags.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from 'vitest'; +import { fileContent } from './tags.ts'; + +describe('cli/utils/tags', () => { + test('strips the indentation and keeps the trailing newline', () => { + const name = 'containerbase'; + + expect(fileContent` + Types: deb + Suites: ${name} + `).toBe('Types: deb\nSuites: containerbase\n'); + }); + + test('works without a leading newline', () => { + expect(fileContent`Types: deb`).toBe('Types: deb\n'); + }); +}); diff --git a/src/cli/utils/tags.ts b/src/cli/utils/tags.ts new file mode 100644 index 0000000000..890de3e885 --- /dev/null +++ b/src/cli/utils/tags.ts @@ -0,0 +1,26 @@ +import type { TemplateTransformer } from 'common-tags'; +import { + TemplateTag, + stripIndentTransformer, + trimResultTransformer, +} from 'common-tags'; + +const appendNewlineTransformer: TemplateTransformer = { + onEndResult: (endResult) => `${endResult}\n`, +}; + +/** + * Like `codeBlock` from common-tags, but ends with a newline. + * + * Every tag common-tags exports trims both ends of the result, which drops the + * newline a text file should end with. Use this one when the content is written + * to a file, and `codeBlock` when it is embedded into something else. + * + * `createTag` is only declared by the types, the installed common-tags does not + * export it, so the tag is composed with `TemplateTag` directly. + */ +export const fileContent = new TemplateTag( + stripIndentTransformer(), + trimResultTransformer(), + appendNewlineTransformer, +);