diff --git a/.changeset/append-separators.md b/.changeset/append-separators.md new file mode 100644 index 00000000..5712d04e --- /dev/null +++ b/.changeset/append-separators.md @@ -0,0 +1,5 @@ +--- +"@tanstack/create": patch +--- + +Keep appended add-on files on separate lines when existing content has no final newline, in both Node and edge generation. diff --git a/packages/create/src/edge-template-file.ts b/packages/create/src/edge-template-file.ts index a6a98d99..40719146 100644 --- a/packages/create/src/edge-template-file.ts +++ b/packages/create/src/edge-template-file.ts @@ -199,7 +199,14 @@ export function createTemplateFile(environment: Environment, options: Options) { } if (append) { - await environment.appendFile(joinPaths(options.targetDir, target), content) + const targetPath = joinPaths(options.targetDir, target) + if (content && !/^[\r\n]/.test(content) && environment.exists(targetPath)) { + const existing = await environment.readFile(targetPath) + if (existing && !/[\r\n]$/.test(existing)) { + content = `\n${content}` + } + } + await environment.appendFile(targetPath, content) } else { await environment.writeFile(joinPaths(options.targetDir, target), content) } diff --git a/packages/create/src/frameworks/react/add-ons/mcp/assets/_dot_gitignore.append b/packages/create/src/frameworks/react/add-ons/mcp/assets/_dot_gitignore.append index ca0fbdf6..9553c628 100644 --- a/packages/create/src/frameworks/react/add-ons/mcp/assets/_dot_gitignore.append +++ b/packages/create/src/frameworks/react/add-ons/mcp/assets/_dot_gitignore.append @@ -1 +1 @@ -mcp-todos.json \ No newline at end of file +mcp-todos.json diff --git a/packages/create/src/frameworks/react/add-ons/sentry/assets/_dot_env.local.append b/packages/create/src/frameworks/react/add-ons/sentry/assets/_dot_env.local.append index fe0ffad2..40845b82 100644 --- a/packages/create/src/frameworks/react/add-ons/sentry/assets/_dot_env.local.append +++ b/packages/create/src/frameworks/react/add-ons/sentry/assets/_dot_env.local.append @@ -8,4 +8,4 @@ VITE_SENTRY_ORG= VITE_SENTRY_PROJECT= # Your Sentry authentication token (from your Sentry account) -SENTRY_AUTH_TOKEN= \ No newline at end of file +SENTRY_AUTH_TOKEN= diff --git a/packages/create/src/frameworks/react/add-ons/strapi/assets/_dot_env.local.append b/packages/create/src/frameworks/react/add-ons/strapi/assets/_dot_env.local.append index b7185238..5389eee7 100644 --- a/packages/create/src/frameworks/react/add-ons/strapi/assets/_dot_env.local.append +++ b/packages/create/src/frameworks/react/add-ons/strapi/assets/_dot_env.local.append @@ -1,2 +1,2 @@ # Strapi configuration -VITE_STRAPI_URL="http://localhost:1337" \ No newline at end of file +VITE_STRAPI_URL="http://localhost:1337" diff --git a/packages/create/src/frameworks/solid/add-ons/strapi/assets/_dot_env.local.append b/packages/create/src/frameworks/solid/add-ons/strapi/assets/_dot_env.local.append index d9791ecc..65df1092 100644 --- a/packages/create/src/frameworks/solid/add-ons/strapi/assets/_dot_env.local.append +++ b/packages/create/src/frameworks/solid/add-ons/strapi/assets/_dot_env.local.append @@ -1,2 +1,2 @@ # Strapi configuration -VITE_STRAPI_URL="http://localhost:1337/api" \ No newline at end of file +VITE_STRAPI_URL="http://localhost:1337/api" diff --git a/packages/create/src/template-file.ts b/packages/create/src/template-file.ts index 6e6ccdee..19ebee8e 100644 --- a/packages/create/src/template-file.ts +++ b/packages/create/src/template-file.ts @@ -212,7 +212,14 @@ export function createTemplateFile(environment: Environment, options: Options) { } if (append) { - await environment.appendFile(resolve(options.targetDir, target), content) + const targetPath = resolve(options.targetDir, target) + if (content && !/^[\r\n]/.test(content) && environment.exists(targetPath)) { + const existing = await environment.readFile(targetPath) + if (existing && !/[\r\n]$/.test(existing)) { + content = `\n${content}` + } + } + await environment.appendFile(targetPath, content) } else { await environment.writeFile(resolve(options.targetDir, target), content) } diff --git a/packages/create/tests/append-separators.test.ts b/packages/create/tests/append-separators.test.ts new file mode 100644 index 00000000..b40f7818 --- /dev/null +++ b/packages/create/tests/append-separators.test.ts @@ -0,0 +1,59 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +import { createDefaultEnvironment, createMemoryEnvironment } from '../src/environment.js' +import { createMemoryEnvironment as createEdgeMemoryEnvironment } from '../src/edge-environment.js' +import { createTemplateFile } from '../src/template-file.js' +import { createTemplateFile as createEdgeTemplateFile } from '../src/edge-template-file.js' +import type { Options } from '../src/types.js' + +const cases = [ + { name: 'missing target', before: undefined, incoming: 'next\n', expected: 'next\n' }, + { name: 'empty target', before: '', incoming: 'next\n', expected: 'next\n' }, + { name: 'missing separator', before: '*.local', incoming: 'mcp-todos.json\n', expected: '*.local\nmcp-todos.json\n' }, + { name: 'existing trailing newline', before: 'first\n', incoming: 'next', expected: 'first\nnext' }, + { name: 'incoming leading newline', before: 'first', incoming: '\nnext', expected: 'first\nnext' }, + { name: 'intentional blank line', before: 'first\n', incoming: '\nnext', expected: 'first\n\nnext' }, + { name: 'empty append', before: 'first', incoming: '', expected: 'first' }, + { name: 'existing CRLF', before: 'first\r\n', incoming: 'next', expected: 'first\r\nnext' }, + { name: 'incoming CRLF', before: 'first', incoming: '\r\nnext', expected: 'first\r\nnext' }, +] + +for (const kind of ['disk', 'node-memory', 'edge-memory'] as const) { + describe(`${kind} append templates`, () => { + it.each(cases)('$name', async ({ before, incoming, expected }) => { + const targetDir = await mkdtemp(join(tmpdir(), 'tanstack-append-')) + const environment = kind === 'disk' + ? createDefaultEnvironment() + : kind === 'node-memory' + ? createMemoryEnvironment().environment + : createEdgeMemoryEnvironment().environment + const options: Options = { + projectName: 'test', targetDir, + framework: { + id: 'test', name: 'Test', description: '', version: '1', + basePackageJSON: {}, optionalPackages: {}, supportedModes: {}, + getFiles: async () => [], getFileContents: async () => '', + getDeletedFiles: async () => [], getAddOns: () => [], + }, + chosenAddOns: [], addOnOptions: {}, packageManager: 'npm', + typescript: true, tailwind: false, mode: 'file-router', + git: false, intent: false, + } + const templateFile = kind === 'edge-memory' + ? createEdgeTemplateFile(environment, options) + : createTemplateFile(environment, options) + try { + environment.startRun() + if (before !== undefined) await templateFile('_dot_gitignore', before) + await templateFile('_dot_gitignore.append', incoming) + expect(await environment.readFile(join(targetDir, '.gitignore'))).toBe(expected) + } finally { + environment.finishRun() + await rm(targetDir, { recursive: true, force: true }) + } + }) + }) +}