Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/init-trailing-newline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": patch
---

Keep the trailing newline on files that `clerk init` edits, so they no longer show "No newline at end of file" in `git diff`.
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default defineConfig({
if (config.type === "modify") {
expect(config.content).toContain("clerk");
expect(config.content).toContain("@clerk/astro");
expect(config.content.endsWith("\n")).toBe(true);
}

const mw = findAction(plan.actions, "src/middleware.ts");
Expand Down
5 changes: 3 additions & 2 deletions packages/cli-core/src/commands/init/frameworks/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
hasTailwindStyles,
headerHtmlBlock,
htmlAuthComponentMarkup,
printModule,
scaffoldAuthFiles,
scaffoldConfigFile,
scaffoldEnvVars,
Expand Down Expand Up @@ -38,7 +39,7 @@ function addClerkImport(content: string): string {
try {
const mod = parseModule(content);
mod.imports.$add({ from: "@clerk/astro", imported: "default", local: "clerk" });
return mod.generate().code;
return printModule(mod, content);
} catch {
return `import clerk from "@clerk/astro";\n${content}`;
}
Expand All @@ -53,7 +54,7 @@ function addClerkToIntegrationsViaAst(content: string): string | null {
if (!Array.isArray(defaultExport.integrations)) return null;

defaultExport.integrations.push(builders.raw("clerk()"));
return mod.generate().code;
return printModule(mod, content);
} catch {
return null;
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli-core/src/commands/init/frameworks/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
hasClerkImport,
indentBlock,
safeAddImport,
printModule,
insertAfterLastImport,
addSuspenseImport,
wrapBodyWithProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</body>
</html>
);
}`,
}
`,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ test("scaffolds all actions for a fresh Nuxt project (Nuxt 3 layout)", async ()
expect(config.type).toBe("modify");
if (config.type === "modify") {
expect(config.content).toContain("@clerk/nuxt");
expect(config.content.endsWith("\n")).toBe(true);
}

// Nuxt 3 layout: pages at pages/ (no app/ directory)
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-core/src/commands/init/frameworks/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
headerHtmlBlock,
htmlAuthComponentMarkup,
indentBlock,
printModule,
scaffoldAuthFiles,
scaffoldConfigFile,
scaffoldEnvVars,
Expand Down Expand Up @@ -46,7 +47,7 @@ function addNuxtModule(content: string): string {

if (!defaultExport.modules) defaultExport.modules = [];
if (Array.isArray(defaultExport.modules)) defaultExport.modules.push("@clerk/nuxt");
return mod.generate().code;
return printModule(mod, content);
} catch {
if (content.includes("modules:")) {
return content.replace(/(modules:\s*\[)/, "$1\n '@clerk/nuxt',");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ export default {
if (configAction?.type !== "modify") throw new Error("Expected config modify action");

expect(configAction.content).toContain("v8_middleware: true");
expect(configAction.content.endsWith("\n")).toBe(true);
});

test("does not enable v8_middleware for React Router 8", async () => {
Expand Down Expand Up @@ -496,6 +497,7 @@ export default defineConfig({
expect(viteAction.content).toContain("noExternal");
expect(viteAction.content).toContain("@clerk/react-router");
expect(viteAction.content.match(/ssr\s*:/g)?.length).toBe(1);
expect(viteAction.content.endsWith("\n")).toBe(true);
});

test("appends to an existing ssr.noExternal array for React Router 8", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
jsxAuthPageContent,
jsxExt,
parseMajorVersion,
printModule,
safeAddImport,
scaffoldAuthFiles,
scaffoldConfigFile,
Expand Down Expand Up @@ -215,7 +216,7 @@ function enableV8Middleware(content: string): string {

if (!defaultExport.future) defaultExport.future = {};
defaultExport.future.v8_middleware = true;
return mod.generate().code;
return printModule(mod, content);
} catch {
if (content.includes("future:")) {
return content.replace(/(future:\s*\{)/, "$1\n v8_middleware: true,");
Expand Down Expand Up @@ -415,7 +416,7 @@ function addClerkNoExternal(content: string): string {
if (!config.ssr.noExternal.includes("@clerk/react-router")) {
config.ssr.noExternal.push("@clerk/react-router");
}
return mod.generate().code;
return printModule(mod, content);
} catch {
return content;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
import { test, expect, describe } from "bun:test";
import { addSuspenseImport, insertAfterLastImport } from "./transformations.ts";
import { parseModule } from "magicast";
import {
addSuspenseImport,
insertAfterLastImport,
printModule,
safeAddImport,
} from "./transformations.ts";

describe("printModule", () => {
test.each([
["LF", "\n"],
["CRLF", "\r\n"],
])("keeps the source's %s line endings, including the trailing one", (_name, eol) => {
const source = `export default {${eol} a: 1,${eol}};${eol}`;
const mod = parseModule(source);
mod.exports.default.b = 2;

const result = printModule(mod, source);

expect(result).toBe(`export default {${eol} a: 1,${eol} b: 2,${eol}};${eol}`);
});

test("keeps CRLF line endings when the source ended without one", () => {
const source = `export default {\r\n a: 1,\r\n};`;
const mod = parseModule(source);
mod.exports.default.b = 2;

expect(printModule(mod, source)).toBe(`export default {\r\n a: 1,\r\n b: 2,\r\n};`);
});

test("adds no line break when the source ended without one", () => {
const source = `export default {\n a: 1,\n};`;
const mod = parseModule(source);
mod.exports.default.b = 2;

expect(printModule(mod, source).endsWith(";")).toBe(true);
});
});

describe("safeAddImport", () => {
test("keeps the trailing newline", () => {
const source = `import "./globals.css";\n\nexport default function RootLayout() {\n return null;\n}\n`;

const result = safeAddImport(source, "@clerk/nextjs", "ClerkProvider");

expect(result).toContain('import { ClerkProvider } from "@clerk/nextjs";');
expect(result.endsWith("}\n")).toBe(true);
});
});

describe("insertAfterLastImport", () => {
test("inserts after a single-line import", () => {
Expand Down
19 changes: 17 additions & 2 deletions packages/cli-core/src/commands/init/frameworks/transformations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,21 @@
* These functions take source code as input and return modified source code.
* Used by framework scaffolders for import injection, provider wrapping, and indentation.
*/
import { parseModule } from "magicast";
import { parseModule, type ProxifiedModule } from "magicast";
import { maskCommentsAndStrings } from "./source-scan.js";

/**
* Print a magicast module in the line endings of the source it was parsed from, keeping
* its trailing line break. magicast's printer joins lines with the host's EOL and drops
* the final line break, which leaves "No newline at end of file" in the user's diff.
*/
export function printModule(mod: ProxifiedModule, source: string): string {
const lineTerminator = /\r?\n/.exec(source)?.[0] ?? "\n";
const code = mod.generate({ lineTerminator }).code;
const eol = /\r?\n$/.exec(source)?.[0];
return eol && !code.endsWith("\n") ? code + eol : code;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

/** Check if file content already imports from a @clerk/ package. */
export function hasClerkImport(content: string): boolean {
return content.includes("@clerk/");
Expand All @@ -30,7 +42,10 @@ export function safeAddImport(content: string, source: string, imported: string)
// spacing, and no printer option overrides that. Projects without a
// formatter would keep `import {x}`, so re-space the one statement we
// added — anchored on `from` so only an import statement can match.
return mod.generate().code.replace(`import {${imported}} from`, `import { ${imported} } from`);
return printModule(mod, content).replace(
`import {${imported}} from`,
`import { ${imported} } from`,
);
} catch {
return `import { ${imported} } from "${source}";\n${content}`;
}
Expand Down
Loading