From 969451b92be8a4176fd77d6e27e0fd0d732fe896 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 02:20:18 +0000 Subject: [PATCH 01/22] feat(sveltekit): install the @prismicio/svelte major that fits the project The CLI always installed the newest @prismicio/svelte. After v3 is published, that version does not support older Svelte and SvelteKit projects. The generated slice simulator page also used the slot syntax that v3 removes. The CLI now installs v3 when Svelte is 5.16 or later and SvelteKit is 2.27 or later. Otherwise, it installs v2. The slice simulator page uses the syntax of the installed major. Setup now stops with an error for Svelte 4. @prismicio/svelte v1 has no slice simulator, and the generated files need Svelte 5. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/adapters/index.ts | 15 +++-- src/adapters/sveltekit.templates.ts | 54 +++++++---------- src/adapters/sveltekit.ts | 30 ++++++++-- src/lib/packageJson.ts | 25 ++++++-- test/gen-setup.test.ts | 90 +++++++++++++++++++++-------- test/it.ts | 13 ++++- test/preview-add.test.ts | 18 ++++++ 7 files changed, 176 insertions(+), 69 deletions(-) diff --git a/src/adapters/index.ts b/src/adapters/index.ts index b4b83e82..3a616b8e 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -139,14 +139,21 @@ export async function getJsFileExtension(): Promise { } export async function getInstalledMajor(packageName: string): Promise { + const [major] = await getInstalledVersion(packageName); + return major; +} + +export async function getInstalledVersion( + packageName: string, +): Promise<[major: number, minor: number]> { const require = createRequire(await findPackageJson()); try { const { version } = require(`${packageName}/package.json`); - const major = Number.parseInt(version.split(".")[0]); - return Number.isNaN(major) ? Infinity : major; + const [major, minor] = version.split(".").map((part: string) => Number.parseInt(part)); + return Number.isNaN(major) ? [Infinity, Infinity] : [major, Number.isNaN(minor) ? 0 : minor]; } catch { - // Not installed yet, so assume the newest major. - return Infinity; + // Not installed yet, so assume the newest version. + return [Infinity, Infinity]; } } diff --git a/src/adapters/sveltekit.templates.ts b/src/adapters/sveltekit.templates.ts index 99b229a7..e839194f 100644 --- a/src/adapters/sveltekit.templates.ts +++ b/src/adapters/sveltekit.templates.ts @@ -67,20 +67,28 @@ export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): `; } -export function sliceSimulatorPageTemplate({ version }: { version: number }): string { - const v5 = dedent` - +export function sliceSimulatorPageTemplate({ + prismicSvelteMajor, +}: { + prismicSvelteMajor: number; +}): string { + if (prismicSvelteMajor >= 3) { + return dedent` + - - - - - `; + + {#snippet children({ slices })} + + {/snippet} + + `; + } - const v4 = dedent` + // @prismicio/svelte v2 and older pass slices through a slot. + return dedent` - - ${HEAD_MARKUP} - - - `; - - return version <= 4 ? v4 : v5; } export function pageTemplate({ typescript }: { typescript: boolean }): string { diff --git a/src/adapters/sveltekit.ts b/src/adapters/sveltekit.ts index d43d5d6b..bb345f48 100644 --- a/src/adapters/sveltekit.ts +++ b/src/adapters/sveltekit.ts @@ -9,10 +9,12 @@ import { Adapter, checkSourceContains, getInstalledMajor, + getInstalledVersion, getJsFileExtension, type PageFile, writeFileIfMissing, } from "."; +import { CommandError } from "../lib/command"; import { exists, writeFileRecursive } from "../lib/file"; import { addDependencies, getNpmPackageVersion } from "../lib/packageJson"; import { dedent, formatObjectKey } from "../lib/string"; @@ -46,15 +48,33 @@ export class SvelteKitAdapter extends Adapter { }; async setupProject(): Promise { + const [svelteMajor, svelteMinor] = await getInstalledVersion("svelte"); + // @prismicio/svelte v1 has no , and the generated files use Svelte 5 APIs. + if (svelteMajor < 5) { + throw new CommandError(` + Prismic needs Svelte 5 or later, but this project uses Svelte ${svelteMajor}. + + Upgrade Svelte, then run \`prismic gen setup\`. + See https://svelte.dev/docs/svelte/v5-migration-guide. + `); + } + const [kitMajor, kitMinor] = await getInstalledVersion("@sveltejs/kit"); + // @prismicio/svelte v3 needs Svelte 5.16 or later and SvelteKit 2.27 or later. + const supportsV3 = + (svelteMajor > 5 || svelteMinor >= 16) && + (kitMajor > 2 || (kitMajor === 2 && kitMinor >= 27)); + const prismicSvelteVersion = await getNpmPackageVersion( + "@prismicio/svelte", + supportsV3 ? 3 : 2, + ); await addDependencies({ "@prismicio/client": `^${await getNpmPackageVersion("@prismicio/client")}`, - "@prismicio/svelte": `^${await getNpmPackageVersion("@prismicio/svelte")}`, + "@prismicio/svelte": `^${prismicSvelteVersion}`, }); const projectRoot = await findProjectRoot(); const extension = await getJsFileExtension(); const typescript = await checkIsTypeScriptProject(); - const version = await getInstalledMajor("svelte"); await writeFileIfMissing( new URL(`src/lib/prismicio.${extension}`, projectRoot), @@ -62,7 +82,9 @@ export class SvelteKitAdapter extends Adapter { ); await writeFileIfMissing( new URL("src/routes/slice-simulator/+page.svelte", projectRoot), - sliceSimulatorPageTemplate({ version }), + sliceSimulatorPageTemplate({ + prismicSvelteMajor: Number.parseInt(prismicSvelteVersion), + }), ); await writeFileIfMissing( new URL(`src/params/preview.${extension}`, projectRoot), @@ -104,7 +126,7 @@ export class SvelteKitAdapter extends Adapter { if (returnsRepositoryName) { await writeFileIfMissing( new URL("src/routes/+layout.svelte", projectRoot), - rootLayoutTemplate({ version }), + rootLayoutTemplate(), ); } await deleteStarterHomePage(); diff --git a/src/lib/packageJson.ts b/src/lib/packageJson.ts index 5afddd5a..469e0ce4 100644 --- a/src/lib/packageJson.ts +++ b/src/lib/packageJson.ts @@ -92,12 +92,27 @@ export async function updatePackageJsonName(name: string): Promise { }); } -export async function getNpmPackageVersion(name: string): Promise { - const url = new URL(`${name}/latest`, "https://registry.npmjs.org/"); - const { version } = await request(url, { - schema: z.object({ version: z.string() }), +// Returns the newest version, or with maxMajor, the newest stable version up to that major. +export async function getNpmPackageVersion(name: string, maxMajor?: number): Promise { + if (maxMajor === undefined) { + const url = new URL(`${name}/latest`, "https://registry.npmjs.org/"); + const { version } = await request(url, { + schema: z.object({ version: z.string() }), + }); + return version; + } + + const { versions } = await request(new URL(name, "https://registry.npmjs.org/"), { + headers: { Accept: "application/vnd.npm.install-v1+json" }, + schema: z.object({ versions: z.record(z.string(), z.unknown()) }), }); - return version; + const [newest] = Object.keys(versions) + .filter((version) => /^\d+\.\d+\.\d+$/.test(version)) + .map((version) => version.split(".").map(Number)) + .filter(([major]) => major <= maxMajor) + .sort((a, b) => b[0] - a[0] || b[1] - a[1] || b[2] - a[2]); + if (!newest) throw new Error(`No version of ${name} up to major ${maxMajor} found on npm.`); + return newest.join("."); } const INSTALL_COMMANDS = { diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 17cfa028..92bd1549 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -180,20 +180,81 @@ it( ); it( - "passes the repository name to a Svelte 4 layout it generated itself", + "installs @prismicio/svelte v3 with a snippet simulator page for Svelte 5.16 and SvelteKit 2.27", { timeout: 30_000 }, async ({ expect, project, prismic }) => { - await useSvelteKit(project, "4.2.19"); + await useSvelteKit(project, "5.16.0", "2.27.0"); const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); expect(exitCode, stderr).toBe(0); - const layout = await readFile(new URL("src/routes/+layout.svelte", project), "utf8"); - expect(layout).toContain("export let data;"); - expect(layout).toContain(""); - expect(layout).not.toContain("$lib/prismicio"); + const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); + expect(packageJson.dependencies["@prismicio/svelte"]).toMatch(/^\^3\./); + const page = await readFile( + new URL("src/routes/slice-simulator/+page.svelte", project), + "utf8", + ); + expect(page).toContain("{#snippet children({ slices })}"); + expect(page).toContain(""); + expect(page).not.toContain("let:slices"); + }, +); + +it( + "installs @prismicio/svelte v3 for SvelteKit 3", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "5.57.1", "3.0.1"); + + const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); + expect(packageJson.dependencies["@prismicio/svelte"]).toMatch(/^\^3\./); + }, +); + +it( + "installs @prismicio/svelte v2 with a slot simulator page for Svelte before 5.16", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "5.15.0", "2.27.0"); + + const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); + expect(packageJson.dependencies["@prismicio/svelte"]).toMatch(/^\^2\./); + const page = await readFile( + new URL("src/routes/slice-simulator/+page.svelte", project), + "utf8", + ); + expect(page).toContain(""); + expect(page).not.toContain("{#snippet"); }, ); +it( + "installs @prismicio/svelte v2 for SvelteKit before 2.27", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "5.16.0", "2.26.0"); + + const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); + expect(packageJson.dependencies["@prismicio/svelte"]).toMatch(/^\^2\./); + }, +); + +it("fails for a Svelte 4 project", { timeout: 30_000 }, async ({ expect, project, prismic }) => { + await useSvelteKit(project, "4.2.19"); + const packageJson = await readFile(new URL("package.json", project), "utf8"); + + const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode).toBe(1); + expect(stderr).toContain("Prismic needs Svelte 5 or later, but this project uses Svelte 4."); + expect(await readFile(new URL("package.json", project), "utf8")).toBe(packageJson); + await expect(project).not.toHaveFile("src/routes/slice-simulator/+page.svelte"); +}); + it( "returns the repository name from a SvelteKit server layout", { timeout: 30_000 }, @@ -331,23 +392,6 @@ it( }, ); -it( - "prints Svelte 4 syntax in the instructions for a Svelte 4 project", - { timeout: 30_000 }, - async ({ expect, project, prismic }) => { - await useSvelteKit(project, "4.2.19"); - await mkdir(new URL("src/routes/", project), { recursive: true }); - await writeFile(new URL("src/routes/+layout.svelte", project), ""); - - const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); - expect(exitCode, stderr).toBe(0); - expect(stdout).toContain("+ export let data;"); - expect(stdout).toContain(""); - expect(stdout).not.toContain("{@render children()}"); - expect(stdout).not.toContain("$props()"); - }, -); - it( "prints SvelteKit instructions when the project already has a root layout", { timeout: 30_000 }, diff --git a/test/it.ts b/test/it.ts index aa6480c5..d68b01e4 100644 --- a/test/it.ts +++ b/test/it.ts @@ -196,7 +196,11 @@ export const it = test.extend({ }, }); -export async function useSvelteKit(project: URL, version = "5.0.0"): Promise { +export async function useSvelteKit( + project: URL, + version = "5.0.0", + kitVersion?: string, +): Promise { await rm(new URL("node_modules/next/", project), { recursive: true, force: true }); await rm(new URL("app/", project), { recursive: true, force: true }); await writeFile( @@ -208,6 +212,13 @@ export async function useSvelteKit(project: URL, version = "5.0.0"): Promise { + await useSvelteKit(project, "4.2.19"); + await mkdir(new URL("src/routes/", project), { recursive: true }); + await writeFile(new URL("src/routes/+layout.svelte", project), ""); + const previewUrl = `https://test-${crypto.randomUUID()}.example.com/api/preview`; + + const { stdout, stderr, exitCode } = await prismic("preview", ["add", previewUrl]); + expect(exitCode, stderr).toBe(0); + expect(stdout).toContain("+ export let data;"); + expect(stdout).toContain(""); + expect(stdout).not.toContain("{@render children()}"); + expect(stdout).not.toContain("$props()"); +}); From b23e382b4744980b2e6a5aa5dce0bfd8ad17bcb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 02:26:59 +0000 Subject: [PATCH 02/22] refactor(sveltekit): support only @prismicio/svelte v3 Choosing a @prismicio/svelte major from the project's Svelte and SvelteKit versions added complexity for old projects. SvelteKit setup now supports only @prismicio/svelte v3. It installs the latest version and always generates Svelte 5 code. The package manager reports projects that are too old through the v3 peer dependencies. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/adapters/index.ts | 15 ++--- src/adapters/sveltekit.templates.ts | 85 ++++++----------------------- src/adapters/sveltekit.ts | 63 ++++----------------- src/lib/packageJson.ts | 25 ++------- test/gen-setup.test.ts | 63 +-------------------- test/it.ts | 13 +---- test/preview-add.test.ts | 18 ------ 7 files changed, 42 insertions(+), 240 deletions(-) diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 3a616b8e..b4b83e82 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -139,21 +139,14 @@ export async function getJsFileExtension(): Promise { } export async function getInstalledMajor(packageName: string): Promise { - const [major] = await getInstalledVersion(packageName); - return major; -} - -export async function getInstalledVersion( - packageName: string, -): Promise<[major: number, minor: number]> { const require = createRequire(await findPackageJson()); try { const { version } = require(`${packageName}/package.json`); - const [major, minor] = version.split(".").map((part: string) => Number.parseInt(part)); - return Number.isNaN(major) ? [Infinity, Infinity] : [major, Number.isNaN(minor) ? 0 : minor]; + const major = Number.parseInt(version.split(".")[0]); + return Number.isNaN(major) ? Infinity : major; } catch { - // Not installed yet, so assume the newest version. - return [Infinity, Infinity]; + // Not installed yet, so assume the newest major. + return Infinity; } } diff --git a/src/adapters/sveltekit.templates.ts b/src/adapters/sveltekit.templates.ts index e839194f..de689a03 100644 --- a/src/adapters/sveltekit.templates.ts +++ b/src/adapters/sveltekit.templates.ts @@ -67,35 +67,17 @@ export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): `; } -export function sliceSimulatorPageTemplate({ - prismicSvelteMajor, -}: { - prismicSvelteMajor: number; -}): string { - if (prismicSvelteMajor >= 3) { - return dedent` - - - - {#snippet children({ slices })} - - {/snippet} - - `; - } - - // @prismicio/svelte v2 and older pass slices through a slot. +export function sliceSimulatorPageTemplate(): string { return dedent` - - + + {#snippet children({ slices })} + + {/snippet} `; } @@ -267,31 +249,25 @@ const SLICE_MARKUP = dedent` `; -export function sliceTemplate({ - name, - typescript, - version, -}: { - name: string; - typescript: boolean; - version: number; -}): string { +export function sliceTemplate({ name, typescript }: { name: string; typescript: boolean }): string { const pascalName = pascalCase(name); - const v5TS = dedent` - + const { slice }: Props = $props(); + - ${SLICE_MARKUP} - `; + ${SLICE_MARKUP} + `; + } - const v5JS = dedent` + return dedent` - - ${SLICE_MARKUP} - `; - - const v4JS = dedent` - - - ${SLICE_MARKUP} - `; - - if (typescript) { - return version <= 4 ? v4TS : v5TS; - } - - return version <= 4 ? v4JS : v5JS; } diff --git a/src/adapters/sveltekit.ts b/src/adapters/sveltekit.ts index bb345f48..13055a47 100644 --- a/src/adapters/sveltekit.ts +++ b/src/adapters/sveltekit.ts @@ -8,13 +8,10 @@ import { pascalCase } from "change-case"; import { Adapter, checkSourceContains, - getInstalledMajor, - getInstalledVersion, getJsFileExtension, type PageFile, writeFileIfMissing, } from "."; -import { CommandError } from "../lib/command"; import { exists, writeFileRecursive } from "../lib/file"; import { addDependencies, getNpmPackageVersion } from "../lib/packageJson"; import { dedent, formatObjectKey } from "../lib/string"; @@ -48,28 +45,9 @@ export class SvelteKitAdapter extends Adapter { }; async setupProject(): Promise { - const [svelteMajor, svelteMinor] = await getInstalledVersion("svelte"); - // @prismicio/svelte v1 has no , and the generated files use Svelte 5 APIs. - if (svelteMajor < 5) { - throw new CommandError(` - Prismic needs Svelte 5 or later, but this project uses Svelte ${svelteMajor}. - - Upgrade Svelte, then run \`prismic gen setup\`. - See https://svelte.dev/docs/svelte/v5-migration-guide. - `); - } - const [kitMajor, kitMinor] = await getInstalledVersion("@sveltejs/kit"); - // @prismicio/svelte v3 needs Svelte 5.16 or later and SvelteKit 2.27 or later. - const supportsV3 = - (svelteMajor > 5 || svelteMinor >= 16) && - (kitMajor > 2 || (kitMajor === 2 && kitMinor >= 27)); - const prismicSvelteVersion = await getNpmPackageVersion( - "@prismicio/svelte", - supportsV3 ? 3 : 2, - ); await addDependencies({ "@prismicio/client": `^${await getNpmPackageVersion("@prismicio/client")}`, - "@prismicio/svelte": `^${prismicSvelteVersion}`, + "@prismicio/svelte": `^${await getNpmPackageVersion("@prismicio/svelte")}`, }); const projectRoot = await findProjectRoot(); @@ -82,9 +60,7 @@ export class SvelteKitAdapter extends Adapter { ); await writeFileIfMissing( new URL("src/routes/slice-simulator/+page.svelte", projectRoot), - sliceSimulatorPageTemplate({ - prismicSvelteMajor: Number.parseInt(prismicSvelteVersion), - }), + sliceSimulatorPageTemplate(), ); await writeFileIfMissing( new URL(`src/params/preview.${extension}`, projectRoot), @@ -154,33 +130,19 @@ export class SvelteKitAdapter extends Adapter { private async getPreviewStep(): Promise { if (await checkSourceContains("PrismicPreview")) return; - const layoutStep = - (await getInstalledMajor("svelte")) <= 4 - ? dedent` - Add the lines marked + to src/routes/+layout.svelte: - - + const layoutStep = dedent` + Change the lines marked - and + in src/routes/+layout.svelte: - - + - ` - : dedent` - Change the lines marked - and + in src/routes/+layout.svelte: + - - let { children } = $props(); - + let { data, children } = $props(); - - - {@render children()} - + - `; + {@render children()} + + + `; // The layout reads repositoryName from the server layout's data. const serverLayoutPath = @@ -237,7 +199,6 @@ export class SvelteKitAdapter extends Adapter { const contents = sliceTemplate({ name: model.name, typescript: await checkIsTypeScriptProject(), - version: await getInstalledMajor("svelte"), }); await writeFileRecursive(new URL("index.svelte", directory), contents); } diff --git a/src/lib/packageJson.ts b/src/lib/packageJson.ts index 469e0ce4..5afddd5a 100644 --- a/src/lib/packageJson.ts +++ b/src/lib/packageJson.ts @@ -92,27 +92,12 @@ export async function updatePackageJsonName(name: string): Promise { }); } -// Returns the newest version, or with maxMajor, the newest stable version up to that major. -export async function getNpmPackageVersion(name: string, maxMajor?: number): Promise { - if (maxMajor === undefined) { - const url = new URL(`${name}/latest`, "https://registry.npmjs.org/"); - const { version } = await request(url, { - schema: z.object({ version: z.string() }), - }); - return version; - } - - const { versions } = await request(new URL(name, "https://registry.npmjs.org/"), { - headers: { Accept: "application/vnd.npm.install-v1+json" }, - schema: z.object({ versions: z.record(z.string(), z.unknown()) }), +export async function getNpmPackageVersion(name: string): Promise { + const url = new URL(`${name}/latest`, "https://registry.npmjs.org/"); + const { version } = await request(url, { + schema: z.object({ version: z.string() }), }); - const [newest] = Object.keys(versions) - .filter((version) => /^\d+\.\d+\.\d+$/.test(version)) - .map((version) => version.split(".").map(Number)) - .filter(([major]) => major <= maxMajor) - .sort((a, b) => b[0] - a[0] || b[1] - a[1] || b[2] - a[2]); - if (!newest) throw new Error(`No version of ${name} up to major ${maxMajor} found on npm.`); - return newest.join("."); + return version; } const INSTALL_COMMANDS = { diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 92bd1549..20815ef1 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -180,81 +180,22 @@ it( ); it( - "installs @prismicio/svelte v3 with a snippet simulator page for Svelte 5.16 and SvelteKit 2.27", + "generates a slice simulator page with a snippet", { timeout: 30_000 }, async ({ expect, project, prismic }) => { - await useSvelteKit(project, "5.16.0", "2.27.0"); + await useSvelteKit(project); const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); expect(exitCode, stderr).toBe(0); - const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); - expect(packageJson.dependencies["@prismicio/svelte"]).toMatch(/^\^3\./); const page = await readFile( new URL("src/routes/slice-simulator/+page.svelte", project), "utf8", ); expect(page).toContain("{#snippet children({ slices })}"); - expect(page).toContain(""); expect(page).not.toContain("let:slices"); }, ); -it( - "installs @prismicio/svelte v3 for SvelteKit 3", - { timeout: 30_000 }, - async ({ expect, project, prismic }) => { - await useSvelteKit(project, "5.57.1", "3.0.1"); - - const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); - expect(exitCode, stderr).toBe(0); - const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); - expect(packageJson.dependencies["@prismicio/svelte"]).toMatch(/^\^3\./); - }, -); - -it( - "installs @prismicio/svelte v2 with a slot simulator page for Svelte before 5.16", - { timeout: 30_000 }, - async ({ expect, project, prismic }) => { - await useSvelteKit(project, "5.15.0", "2.27.0"); - - const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); - expect(exitCode, stderr).toBe(0); - const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); - expect(packageJson.dependencies["@prismicio/svelte"]).toMatch(/^\^2\./); - const page = await readFile( - new URL("src/routes/slice-simulator/+page.svelte", project), - "utf8", - ); - expect(page).toContain(""); - expect(page).not.toContain("{#snippet"); - }, -); - -it( - "installs @prismicio/svelte v2 for SvelteKit before 2.27", - { timeout: 30_000 }, - async ({ expect, project, prismic }) => { - await useSvelteKit(project, "5.16.0", "2.26.0"); - - const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); - expect(exitCode, stderr).toBe(0); - const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); - expect(packageJson.dependencies["@prismicio/svelte"]).toMatch(/^\^2\./); - }, -); - -it("fails for a Svelte 4 project", { timeout: 30_000 }, async ({ expect, project, prismic }) => { - await useSvelteKit(project, "4.2.19"); - const packageJson = await readFile(new URL("package.json", project), "utf8"); - - const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); - expect(exitCode).toBe(1); - expect(stderr).toContain("Prismic needs Svelte 5 or later, but this project uses Svelte 4."); - expect(await readFile(new URL("package.json", project), "utf8")).toBe(packageJson); - await expect(project).not.toHaveFile("src/routes/slice-simulator/+page.svelte"); -}); - it( "returns the repository name from a SvelteKit server layout", { timeout: 30_000 }, diff --git a/test/it.ts b/test/it.ts index d68b01e4..aa6480c5 100644 --- a/test/it.ts +++ b/test/it.ts @@ -196,11 +196,7 @@ export const it = test.extend({ }, }); -export async function useSvelteKit( - project: URL, - version = "5.0.0", - kitVersion?: string, -): Promise { +export async function useSvelteKit(project: URL, version = "5.0.0"): Promise { await rm(new URL("node_modules/next/", project), { recursive: true, force: true }); await rm(new URL("app/", project), { recursive: true, force: true }); await writeFile( @@ -212,13 +208,6 @@ export async function useSvelteKit( new URL("node_modules/svelte/package.json", project), JSON.stringify({ version }), ); - if (kitVersion) { - await mkdir(new URL("node_modules/@sveltejs/kit/", project), { recursive: true }); - await writeFile( - new URL("node_modules/@sveltejs/kit/package.json", project), - JSON.stringify({ version: kitVersion }), - ); - } } // Makes the package manager install fail by shadowing npm in node_modules/.bin. diff --git a/test/preview-add.test.ts b/test/preview-add.test.ts index 0aada470..fd9c8852 100644 --- a/test/preview-add.test.ts +++ b/test/preview-add.test.ts @@ -77,21 +77,3 @@ it("asks for a SvelteKit server layout when the project has none", async ({ expect(stdout).toContain('export const prerender = "auto";'); expect(stdout).toContain("return { repositoryName };"); }); - -it("prints Svelte 4 syntax in the instructions for a Svelte 4 project", async ({ - expect, - project, - prismic, -}) => { - await useSvelteKit(project, "4.2.19"); - await mkdir(new URL("src/routes/", project), { recursive: true }); - await writeFile(new URL("src/routes/+layout.svelte", project), ""); - const previewUrl = `https://test-${crypto.randomUUID()}.example.com/api/preview`; - - const { stdout, stderr, exitCode } = await prismic("preview", ["add", previewUrl]); - expect(exitCode, stderr).toBe(0); - expect(stdout).toContain("+ export let data;"); - expect(stdout).toContain(""); - expect(stdout).not.toContain("{@render children()}"); - expect(stdout).not.toContain("$props()"); -}); From 06163b01f961cc40d95f042517bfd35f3d1c678d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 02:30:35 +0000 Subject: [PATCH 03/22] refactor(sveltekit): remove code that only served Svelte 4 The root layout head markup and the preview layout step each had one use left, so they are now inline. The tests no longer fake a Svelte version because the SvelteKit adapter does not read it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/adapters/sveltekit.templates.ts | 26 +++++++--------- src/adapters/sveltekit.ts | 27 ++++++++--------- test/gen-setup.test.ts | 46 ++--------------------------- test/it.ts | 7 +---- 4 files changed, 26 insertions(+), 80 deletions(-) diff --git a/src/adapters/sveltekit.templates.ts b/src/adapters/sveltekit.templates.ts index de689a03..3bd1d50c 100644 --- a/src/adapters/sveltekit.templates.ts +++ b/src/adapters/sveltekit.templates.ts @@ -110,20 +110,6 @@ export function previewAPIRouteTemplate({ typescript }: { typescript: boolean }) `; } -const HEAD_MARKUP = dedent` - - {page.data.page?.data.meta_title} - - {#if isFilled.keyText(page.data.page?.data.meta_description)} - - - {/if} - {#if isFilled.image(page.data.page?.data.meta_image)} - - {/if} - -`; - export function layoutServerTemplate(): string { return dedent` import { repositoryName } from "$lib/prismicio"; @@ -146,7 +132,17 @@ export function rootLayoutTemplate(): string { const { data, children } = $props(); - ${HEAD_MARKUP} + + {page.data.page?.data.meta_title} + + {#if isFilled.keyText(page.data.page?.data.meta_description)} + + + {/if} + {#if isFilled.image(page.data.page?.data.meta_image)} + + {/if} + {@render children()} `; diff --git a/src/adapters/sveltekit.ts b/src/adapters/sveltekit.ts index 13055a47..d05f3dc5 100644 --- a/src/adapters/sveltekit.ts +++ b/src/adapters/sveltekit.ts @@ -130,20 +130,6 @@ export class SvelteKitAdapter extends Adapter { private async getPreviewStep(): Promise { if (await checkSourceContains("PrismicPreview")) return; - const layoutStep = dedent` - Change the lines marked - and + in src/routes/+layout.svelte: - - - - {@render children()} - + - `; - // The layout reads repositoryName from the server layout's data. const serverLayoutPath = (await findServerLayout()) ?? `src/routes/+layout.server.${await getJsFileExtension()}`; @@ -161,8 +147,19 @@ export class SvelteKitAdapter extends Adapter { Previews do not work until you do this, and the CLI cannot edit your layout for you. Make the change now. + + Change the lines marked - and + in src/routes/+layout.svelte: + + + + {@render children()} + + `, - layoutStep, serverLayoutStep, "Run `prismic docs view sveltekit` for details.", ] diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 20815ef1..0a6b8c96 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -36,32 +36,6 @@ it("skips existing files", { timeout: 30_000 }, async ({ expect, project, prismi await expect(project).toHaveFile("prismicio.js", { contains: "// custom client file" }); }); -it( - "generates valid script tags for SvelteKit", - { timeout: 30_000 }, - async ({ expect, project, prismic }) => { - // Reconfigure the fixture as a SvelteKit project so a file with a ", not the bundler-escaped "<\/script>". - await expect(project).toHaveFile("src/routes/slice-simulator/+page.svelte", { - contains: "", - }); - }, -); - it("skips installation with --no-install", async ({ expect, project, prismic }) => { const { stderr, exitCode, stdout } = await prismic("gen", ["setup", "--no-install"]); expect(exitCode, stderr).toBe(0); @@ -191,6 +165,8 @@ it( new URL("src/routes/slice-simulator/+page.svelte", project), "utf8", ); + // The closing tag must be "", not the bundler-escaped "<\/script>". + expect(page).toContain(""); expect(page).toContain("{#snippet children({ slices })}"); expect(page).not.toContain("let:slices"); }, @@ -315,24 +291,6 @@ it("works when Next.js is declared but not installed", async ({ expect, project, await expect(project).toHaveFile("app/api/revalidate/route.js"); }); -it( - "works when Svelte is declared but not installed", - { timeout: 30_000 }, - async ({ expect, project, prismic }) => { - // node_modules is absent, so the Svelte version cannot be read. - await writeFile( - new URL("package.json", project), - JSON.stringify({ dependencies: { "@sveltejs/kit": "latest", svelte: "latest" } }), - ); - await mkdir(new URL("src/routes/", project), { recursive: true }); - await writeFile(new URL("src/routes/+layout.svelte", project), "{@render children()}"); - - const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); - expect(exitCode, stderr).toBe(0); - expect(stdout).toContain("add to your root layout"); - }, -); - it( "prints SvelteKit instructions when the project already has a root layout", { timeout: 30_000 }, diff --git a/test/it.ts b/test/it.ts index aa6480c5..31918bbc 100644 --- a/test/it.ts +++ b/test/it.ts @@ -196,18 +196,13 @@ export const it = test.extend({ }, }); -export async function useSvelteKit(project: URL, version = "5.0.0"): Promise { +export async function useSvelteKit(project: URL): Promise { await rm(new URL("node_modules/next/", project), { recursive: true, force: true }); await rm(new URL("app/", project), { recursive: true, force: true }); await writeFile( new URL("package.json", project), JSON.stringify({ dependencies: { "@sveltejs/kit": "latest", svelte: "latest" } }), ); - await mkdir(new URL("node_modules/svelte/", project), { recursive: true }); - await writeFile( - new URL("node_modules/svelte/package.json", project), - JSON.stringify({ version }), - ); } // Makes the package manager install fail by shadowing npm in node_modules/.bin. From e3cbe2877a54d03e2df2cec4340c221b52e675b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 02:44:10 +0000 Subject: [PATCH 04/22] fix(sveltekit): import from #lib instead of $lib SvelteKit 3 removes the $lib alias. Projects that used the generated files failed to build with a module_removed_lib error. The generated files now import from #lib with a file extension, as SvelteKit 3 projects require. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- evals/set-up-previews.eval.ts | 2 +- src/adapters/sveltekit.templates.ts | 24 ++++++++++++------------ src/adapters/sveltekit.ts | 6 +++--- test/gen-setup.test.ts | 7 ++++--- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/evals/set-up-previews.eval.ts b/evals/set-up-previews.eval.ts index c004f4c0..6fd43151 100644 --- a/evals/set-up-previews.eval.ts +++ b/evals/set-up-previews.eval.ts @@ -37,7 +37,7 @@ it.for(trials)( await mkdir(new URL("src/routes/", project), { recursive: true }); await writeFile( new URL("src/routes/+layout.svelte", project), - "\n\n" + '\n\t\n\n\n' + "

KEEP-ME

\n{@render children()}\n", diff --git a/src/adapters/sveltekit.templates.ts b/src/adapters/sveltekit.templates.ts index 3bd1d50c..e7493857 100644 --- a/src/adapters/sveltekit.templates.ts +++ b/src/adapters/sveltekit.templates.ts @@ -67,11 +67,11 @@ export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): `; } -export function sliceSimulatorPageTemplate(): string { +export function sliceSimulatorPageTemplate({ typescript }: { typescript: boolean }): string { return dedent` @@ -86,7 +86,7 @@ export function previewAPIRouteTemplate({ typescript }: { typescript: boolean }) if (typescript) { return dedent` import { redirectToPreviewURL } from '@prismicio/svelte/kit'; - import { createClient } from '$lib/prismicio'; + import { createClient } from '#lib/prismicio.ts'; import type { RequestHandler } from "./$types"; export const GET: RequestHandler = async ({ fetch, request, cookies }) => { @@ -99,7 +99,7 @@ export function previewAPIRouteTemplate({ typescript }: { typescript: boolean }) return dedent` import { redirectToPreviewURL } from '@prismicio/svelte/kit'; - import { createClient } from '$lib/prismicio'; + import { createClient } from '#lib/prismicio.js'; /* @type {import("./$types").RequestHandler} */ export async function GET({ fetch, request, cookies }) { @@ -110,9 +110,9 @@ export function previewAPIRouteTemplate({ typescript }: { typescript: boolean }) `; } -export function layoutServerTemplate(): string { +export function layoutServerTemplate({ typescript }: { typescript: boolean }): string { return dedent` - import { repositoryName } from "$lib/prismicio"; + import { repositoryName } from "#lib/prismicio.${typescript ? "ts" : "js"}"; export const prerender = "auto"; @@ -153,7 +153,7 @@ export function pageTemplate({ typescript }: { typescript: boolean }): string { return dedent` @@ -186,7 +186,7 @@ export function pageServerTemplate({ if (typescript) { return dedent` import type { PageServerLoad } from "./$types"; - import { createClient } from "$lib/prismicio"; + import { createClient } from "#lib/prismicio.ts"; export const load: PageServerLoad = async ({ params, fetch, cookies }) => { const client = createClient({ fetch, cookies }); @@ -198,7 +198,7 @@ export function pageServerTemplate({ } return dedent` - import { createClient } from "$lib/prismicio"; + import { createClient } from "#lib/prismicio.js"; /** @type {import("./$types").PageServerLoad} */ export async function load({ params, fetch, cookies }) { @@ -213,7 +213,7 @@ export function pageServerTemplate({ if (typescript) { return dedent` import type { PageServerLoad } from "./$types"; - import { createClient } from "$lib/prismicio"; + import { createClient } from "#lib/prismicio.ts"; export const load: PageServerLoad = async ({ fetch, cookies }) => { const client = createClient({ fetch, cookies }); @@ -225,7 +225,7 @@ export function pageServerTemplate({ } return dedent` - import { createClient } from "$lib/prismicio"; + import { createClient } from "#lib/prismicio.js"; /* @type {import("./$types").PageServerLoad} */ export async function load({ fetch, cookies }) { diff --git a/src/adapters/sveltekit.ts b/src/adapters/sveltekit.ts index d05f3dc5..e16b8bab 100644 --- a/src/adapters/sveltekit.ts +++ b/src/adapters/sveltekit.ts @@ -60,7 +60,7 @@ export class SvelteKitAdapter extends Adapter { ); await writeFileIfMissing( new URL("src/routes/slice-simulator/+page.svelte", projectRoot), - sliceSimulatorPageTemplate(), + sliceSimulatorPageTemplate({ typescript }), ); await writeFileIfMissing( new URL(`src/params/preview.${extension}`, projectRoot), @@ -91,7 +91,7 @@ export class SvelteKitAdapter extends Adapter { if (!serverLayout) { await writeFileRecursive( new URL(`src/routes/+layout.server.${extension}`, projectRoot), - layoutServerTemplate(), + layoutServerTemplate({ typescript }), ); } // The generated layout reads repositoryName from the server layout. When an existing @@ -139,7 +139,7 @@ export class SvelteKitAdapter extends Adapter { ).catch(() => ""); const serverLayoutStep = !serverLayout.includes("repositoryName") && - `Return repositoryName from load in ${serverLayoutPath}, creating the file if needed:\n\n${layoutServerTemplate().replace(/^/gm, " ")}`; + `Return repositoryName from load in ${serverLayoutPath}, creating the file if needed:\n\n${layoutServerTemplate({ typescript: serverLayoutPath.endsWith(".ts") }).replace(/^/gm, " ")}`; return [ dedent` diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 0a6b8c96..593998e5 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -149,7 +149,7 @@ it( const layout = await readFile(new URL("src/routes/+layout.svelte", project), "utf8"); expect(layout).toContain("const { data, children } = $props();"); expect(layout).toContain(""); - expect(layout).not.toContain("$lib/prismicio"); + expect(layout).not.toContain("#lib/prismicio"); }, ); @@ -167,6 +167,7 @@ it( ); // The closing tag must be "", not the bundler-escaped "<\/script>". expect(page).toContain(""); + expect(page).toContain("import { components } from '#lib/slices/index.js';"); expect(page).toContain("{#snippet children({ slices })}"); expect(page).not.toContain("let:slices"); }, @@ -181,7 +182,7 @@ it( const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); expect(exitCode, stderr).toBe(0); const serverLayout = await readFile(new URL("src/routes/+layout.server.js", project), "utf8"); - expect(serverLayout).toContain('import { repositoryName } from "$lib/prismicio";'); + expect(serverLayout).toContain('import { repositoryName } from "#lib/prismicio.js";'); expect(serverLayout).toContain('export const prerender = "auto";'); expect(serverLayout).toContain("return { repositoryName };"); }, @@ -271,7 +272,7 @@ it( await writeFile(new URL("src/routes/+layout.svelte", project), "{@render children()}"); await writeFile( new URL("src/routes/+layout.server.js", project), - 'import { repositoryName } from "$lib/prismicio";\nexport const load = () => ({ repositoryName });', + 'import { repositoryName } from "#lib/prismicio.js";\nexport const load = () => ({ repositoryName });', ); const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); From e1f5e1d904155941d7fbc5f9abce0ec8a0349015 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 17:15:23 +0000 Subject: [PATCH 05/22] feat(sveltekit): declare the Prismic environment in src/env SvelteKit 3 deprecates $env/dynamic/public. The generated Prismic client used it, so it warned in development and failed type checks. The client now reads PUBLIC_PRISMIC_ENVIRONMENT from $app/env/public. Setup creates src/env.ts or src/env.js to declare the variable. If the project already has an env file without the variable, setup leaves the file as it is and tells the user which lines to add. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/adapters/sveltekit.templates.ts | 28 ++++++++++++--- src/adapters/sveltekit.ts | 41 ++++++++++++++++++---- test/gen-setup.test.ts | 54 +++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/adapters/sveltekit.templates.ts b/src/adapters/sveltekit.templates.ts index e7493857..1bdcca97 100644 --- a/src/adapters/sveltekit.templates.ts +++ b/src/adapters/sveltekit.templates.ts @@ -8,14 +8,14 @@ export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): return dedent` import { createClient as baseCreateClient } from "@prismicio/client"; import { type CreateClientConfig, enableAutoPreviews } from '@prismicio/svelte/kit'; - import { env } from '$env/dynamic/public'; + import { PUBLIC_PRISMIC_ENVIRONMENT } from '$app/env/public'; import prismicConfig from "../../prismic.config.json"; /** * The project's Prismic repository name. */ export const repositoryName = - env.PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; + PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; /** * Creates a Prismic client for the project's repository. The client is used to @@ -39,14 +39,14 @@ export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): return dedent` import { createClient as baseCreateClient } from "@prismicio/client"; import { enableAutoPreviews } from '@prismicio/svelte/kit'; - import { env } from '$env/dynamic/public'; + import { PUBLIC_PRISMIC_ENVIRONMENT } from '$app/env/public'; import prismicConfig from "../../prismic.config.json"; /** * The project's Prismic repository name. */ export const repositoryName = - env.PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; + PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; /** * Creates a Prismic client for the project's repository. The client is used to @@ -67,6 +67,26 @@ export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): `; } +export function envVarTemplate(): string { + return dedent` + PUBLIC_PRISMIC_ENVIRONMENT: { + public: true, + schema: (value) => value, + description: "The Prismic environment to use instead of the production repository.", + }, + `; +} + +export function envFileTemplate(): string { + return dedent` + import { defineEnvVars } from "@sveltejs/kit/env"; + + export const variables = defineEnvVars({ + ${envVarTemplate()} + }); + `; +} + export function sliceSimulatorPageTemplate({ typescript }: { typescript: boolean }): string { return dedent` @@ -130,9 +130,9 @@ export function previewAPIRouteTemplate({ typescript }: { typescript: boolean }) `; } -export function layoutServerTemplate({ typescript }: { typescript: boolean }): string { +export function layoutServerTemplate({ extension }: { extension: string }): string { return dedent` - import { repositoryName } from "#lib/prismicio.${typescript ? "ts" : "js"}"; + import { repositoryName } from "#lib/prismicio.${extension}"; export const prerender = "auto"; diff --git a/src/adapters/sveltekit.ts b/src/adapters/sveltekit.ts index a9a7857b..c52d9747 100644 --- a/src/adapters/sveltekit.ts +++ b/src/adapters/sveltekit.ts @@ -65,7 +65,7 @@ export class SvelteKitAdapter extends Adapter { } await writeFileIfMissing( new URL("src/routes/slice-simulator/+page.svelte", projectRoot), - sliceSimulatorPageTemplate({ typescript }), + sliceSimulatorPageTemplate({ extension }), ); await writeFileIfMissing( new URL(`src/params/preview.${extension}`, projectRoot), @@ -96,7 +96,7 @@ export class SvelteKitAdapter extends Adapter { if (!serverLayout) { await writeFileRecursive( new URL(`src/routes/+layout.server.${extension}`, projectRoot), - layoutServerTemplate({ typescript }), + layoutServerTemplate({ extension }), ); } // The generated layout reads repositoryName from the server layout. When an existing @@ -167,7 +167,7 @@ export class SvelteKitAdapter extends Adapter { ).catch(() => ""); const serverLayoutStep = !serverLayout.includes("repositoryName") && - `Return repositoryName from load in ${serverLayoutPath}, creating the file if needed:\n\n${layoutServerTemplate({ typescript: serverLayoutPath.endsWith(".ts") }).replace(/^/gm, " ")}`; + `Return repositoryName from load in ${serverLayoutPath}, creating the file if needed:\n\n${layoutServerTemplate({ extension: serverLayoutPath.split(".").pop()! }).replace(/^/gm, " ")}`; return [ dedent` @@ -255,7 +255,6 @@ async function deleteStarterHomePage(): Promise { await rm(path); } -// Finds a JavaScript or TypeScript file by its path without an extension. async function findSourceFile(path: string): Promise { const projectRoot = await findProjectRoot(); for (const extension of ["js", "ts"]) { From 4ec76cb71f5e10aa8a58c2e50c666863dd1e3248 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 18:03:06 +0000 Subject: [PATCH 08/22] feat(sveltekit): keep SvelteKit 2 projects on @prismicio/svelte v2 The SvelteKit adapter now generates code for SvelteKit 3. SvelteKit 2 projects cannot use that code or @prismicio/svelte v3. A separate adapter now handles SvelteKit 2 projects. It installs @prismicio/svelte v2 and generates $lib and $env/dynamic/public code, as before. It extends the SvelteKit 3 adapter, so we can delete it when SvelteKit 2 support ends. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- AGENTS.md | 4 +- src/adapters/index.ts | 4 + src/adapters/sveltekit.ts | 4 +- src/adapters/sveltekit2.templates.ts | 212 +++++++++++++++++++++++++++ src/adapters/sveltekit2.ts | 160 ++++++++++++++++++++ test/gen-page.test.ts | 28 +++- test/gen-setup.test.ts | 42 ++++++ test/it.ts | 8 +- 8 files changed, 457 insertions(+), 5 deletions(-) create mode 100644 src/adapters/sveltekit2.templates.ts create mode 100644 src/adapters/sveltekit2.ts diff --git a/AGENTS.md b/AGENTS.md index 7e65a9c0..181203ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,9 @@ console output. ## `src/adapters/` Framework-specific integrations. One file per framework, with a sibling -`*.templates.ts` file for generated file content. +`*.templates.ts` file for generated file content. When an older framework major +needs different generated code, it gets its own adapter that extends the current +one, such as `sveltekit2.ts`. Delete it when support for that major ends. ## `src/subprocesses/` diff --git a/src/adapters/index.ts b/src/adapters/index.ts index b4b83e82..f8523ac0 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -64,6 +64,10 @@ export async function getAdapter(): Promise { return new NuxtAdapter(); } if ("@sveltejs/kit" in allDependencies) { + if ((await getInstalledMajor("@sveltejs/kit")) < 3) { + const { SvelteKit2Adapter } = await import("./sveltekit2"); + return new SvelteKit2Adapter(); + } const { SvelteKitAdapter } = await import("./sveltekit"); return new SvelteKitAdapter(); } diff --git a/src/adapters/sveltekit.ts b/src/adapters/sveltekit.ts index c52d9747..d18ee556 100644 --- a/src/adapters/sveltekit.ts +++ b/src/adapters/sveltekit.ts @@ -247,7 +247,7 @@ export class SvelteKitAdapter extends Adapter { } } -async function deleteStarterHomePage(): Promise { +export async function deleteStarterHomePage(): Promise { const path = new URL("src/routes/+page.svelte", await findProjectRoot()); const contents = await readFile(path, "utf8").catch(() => undefined); if (!STARTER_HOME_PAGES.includes(contents?.replace(/\s/g, "") ?? "")) return; @@ -255,7 +255,7 @@ async function deleteStarterHomePage(): Promise { await rm(path); } -async function findSourceFile(path: string): Promise { +export async function findSourceFile(path: string): Promise { const projectRoot = await findProjectRoot(); for (const extension of ["js", "ts"]) { if (await exists(new URL(`${path}.${extension}`, projectRoot))) return `${path}.${extension}`; diff --git a/src/adapters/sveltekit2.templates.ts b/src/adapters/sveltekit2.templates.ts new file mode 100644 index 00000000..a73ad6f6 --- /dev/null +++ b/src/adapters/sveltekit2.templates.ts @@ -0,0 +1,212 @@ +import type { DynamicCustomTypeModel } from "@prismicio/types-internal"; + +import { dedent } from "../lib/string"; + +// Templates for SvelteKit 2 projects, which use @prismicio/svelte v2, $lib, and +// $env/dynamic/public. The other templates are shared with SvelteKit 3. + +export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): string { + if (typescript) { + return dedent` + import { createClient as baseCreateClient } from "@prismicio/client"; + import { type CreateClientConfig, enableAutoPreviews } from '@prismicio/svelte/kit'; + import { env } from '$env/dynamic/public'; + import prismicConfig from "../../prismic.config.json"; + + /** + * The project's Prismic repository name. + */ + export const repositoryName = + env.PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; + + /** + * Creates a Prismic client for the project's repository. The client is used to + * query content from the Prismic API. + * + * @param config - Configuration for the Prismic client. + */ + export const createClient = ({ cookies, ...config }: CreateClientConfig = {}) => { + const client = baseCreateClient(repositoryName, { + routes: prismicConfig.routes, + ...config, + }); + + enableAutoPreviews({ client, cookies }); + + return client; + }; + `; + } + + return dedent` + import { createClient as baseCreateClient } from "@prismicio/client"; + import { enableAutoPreviews } from '@prismicio/svelte/kit'; + import { env } from '$env/dynamic/public'; + import prismicConfig from "../../prismic.config.json"; + + /** + * The project's Prismic repository name. + */ + export const repositoryName = + env.PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; + + /** + * Creates a Prismic client for the project's repository. The client is used to + * query content from the Prismic API. + * + * @param {import('@prismicio/svelte/kit').CreateClientConfig} config - Configuration for the Prismic client. + */ + export const createClient = ({ cookies, ...config } = {}) => { + const client = baseCreateClient(repositoryName, { + routes: prismicConfig.routes, + ...config, + }); + + enableAutoPreviews({ client, cookies }); + + return client; + }; + `; +} + +export function sliceSimulatorPageTemplate(): string { + return dedent` + + + + + + `; +} + +export function previewAPIRouteTemplate({ typescript }: { typescript: boolean }): string { + if (typescript) { + return dedent` + import { redirectToPreviewURL } from '@prismicio/svelte/kit'; + import { createClient } from '$lib/prismicio'; + import type { RequestHandler } from "./$types"; + + export const GET: RequestHandler = async ({ fetch, request, cookies }) => { + const client = createClient({ fetch }); + + return await redirectToPreviewURL({ client, request, cookies }); + } + `; + } + + return dedent` + import { redirectToPreviewURL } from '@prismicio/svelte/kit'; + import { createClient } from '$lib/prismicio'; + + /* @type {import("./$types").RequestHandler} */ + export async function GET({ fetch, request, cookies }) { + const client = createClient({ fetch }); + + return await redirectToPreviewURL({ client, request, cookies }); + } + `; +} + +export function layoutServerTemplate(): string { + return dedent` + import { repositoryName } from "$lib/prismicio"; + + export const prerender = "auto"; + + export function load() { + return { repositoryName }; + } + `; +} + +export function pageTemplate({ typescript }: { typescript: boolean }): string { + if (typescript) { + return dedent` + + + + `; + } + + return dedent` + + + + `; +} + +export function pageServerTemplate({ + model, + typescript, +}: { + model: DynamicCustomTypeModel; + typescript: boolean; +}): string { + if (model.repeatable) { + if (typescript) { + return dedent` + import type { PageServerLoad } from "./$types"; + import { createClient } from "$lib/prismicio"; + + export const load: PageServerLoad = async ({ params, fetch, cookies }) => { + const client = createClient({ fetch, cookies }); + const page = await client.getByUID("${model.id}", params.uid); + + return { page }; + }; + `; + } + + return dedent` + import { createClient } from "$lib/prismicio"; + + /** @type {import("./$types").PageServerLoad} */ + export async function load({ params, fetch, cookies }) { + const client = createClient({ fetch, cookies }); + const page = await client.getByUID("${model.id}", params.uid); + + return { page }; + } + `; + } + + if (typescript) { + return dedent` + import type { PageServerLoad } from "./$types"; + import { createClient } from "$lib/prismicio"; + + export const load: PageServerLoad = async ({ fetch, cookies }) => { + const client = createClient({ fetch, cookies }); + const page = await client.getSingle("${model.id}"); + + return { page }; + }; + `; + } + + return dedent` + import { createClient } from "$lib/prismicio"; + + /* @type {import("./$types").PageServerLoad} */ + export async function load({ fetch, cookies }) { + const client = createClient({ fetch, cookies }); + const page = await client.getSingle("${model.id}"); + + return { page }; + } + `; +} diff --git a/src/adapters/sveltekit2.ts b/src/adapters/sveltekit2.ts new file mode 100644 index 00000000..4f485b93 --- /dev/null +++ b/src/adapters/sveltekit2.ts @@ -0,0 +1,160 @@ +import { readFile } from "node:fs/promises"; + +import type { DynamicCustomTypeModel } from "@prismicio/types-internal"; + +import { checkSourceContains, getJsFileExtension, type PageFile, writeFileIfMissing } from "."; +import { exists, writeFileRecursive } from "../lib/file"; +import { addDependencies, getNpmPackageVersion } from "../lib/packageJson"; +import { dedent } from "../lib/string"; +import { checkIsTypeScriptProject, findProjectRoot } from "../project"; +import { deleteStarterHomePage, findSourceFile, SvelteKitAdapter } from "./sveltekit"; +import { rootLayoutTemplate } from "./sveltekit.templates"; +import { + layoutServerTemplate, + pageServerTemplate, + pageTemplate, + previewAPIRouteTemplate, + prismicIOFileTemplate, + sliceSimulatorPageTemplate, +} from "./sveltekit2.templates"; + +// SvelteKit 2 projects use @prismicio/svelte v2. Delete this file and sveltekit2.templates.ts +// when SvelteKit 2 support ends. +export class SvelteKit2Adapter extends SvelteKitAdapter { + async setupProject(): Promise { + await addDependencies({ + "@prismicio/client": `^${await getNpmPackageVersion("@prismicio/client")}`, + "@prismicio/svelte": "^2", + }); + + const projectRoot = await findProjectRoot(); + const extension = await getJsFileExtension(); + const typescript = await checkIsTypeScriptProject(); + + await writeFileIfMissing( + new URL(`src/lib/prismicio.${extension}`, projectRoot), + prismicIOFileTemplate({ typescript }), + ); + await writeFileIfMissing( + new URL("src/routes/slice-simulator/+page.svelte", projectRoot), + sliceSimulatorPageTemplate(), + ); + await writeFileIfMissing( + new URL(`src/params/preview.${extension}`, projectRoot), + dedent` + export function match(param) { + return param === 'preview'; + } + `, + ); + await writeFileIfMissing( + new URL(`src/routes/api/preview/+server.${extension}`, projectRoot), + previewAPIRouteTemplate({ typescript }), + ); + await writeFileIfMissing( + new URL("src/routes/[[preview=preview]]/README.md", projectRoot), + dedent` + This directory adds support for optional \`/preview\` routes. Do not remove this directory. + + All routes within this directory will be served using the following URLs: + + - \`/example-route\` (prerendered) + - \`/preview/example-route\` (server-rendered) + + See for more information. + `, + ); + const serverLayout = await findSourceFile("src/routes/+layout.server"); + if (!serverLayout) { + await writeFileRecursive( + new URL(`src/routes/+layout.server.${extension}`, projectRoot), + layoutServerTemplate(), + ); + } + // The generated layout reads repositoryName from the server layout. When an existing + // server layout does not return it, the setup instructions cover both files instead. + const returnsRepositoryName = + !serverLayout || + (await readFile(new URL(serverLayout, projectRoot), "utf8")).includes("repositoryName"); + if (returnsRepositoryName) { + await writeFileIfMissing( + new URL("src/routes/+layout.svelte", projectRoot), + rootLayoutTemplate(), + ); + } + await deleteStarterHomePage(); + } + + async getSetupInstructions(): Promise { + const projectRoot = await findProjectRoot(); + + // The layout reads repositoryName from the server layout's data. + const serverLayoutPath = + (await findSourceFile("src/routes/+layout.server")) ?? + `src/routes/+layout.server.${await getJsFileExtension()}`; + const serverLayout = await readFile(new URL(serverLayoutPath, projectRoot), "utf8").catch( + () => "", + ); + const previewStep = + !(await checkSourceContains("PrismicPreview")) && + [ + dedent` + Action required: add to your root layout. + + Previews do not work until you do this, and the CLI cannot edit your + layout for you. Make the change now. + + Change the lines marked - and + in src/routes/+layout.svelte: + + + + {@render children()} + + + `, + !serverLayout.includes("repositoryName") && + `Return repositoryName from load in ${serverLayoutPath}, creating the file if needed:\n\n${layoutServerTemplate().replace(/^/gm, " ")}`, + "Run `prismic docs view sveltekit` for details.", + ] + .filter(Boolean) + .join("\n\n"); + + // The Prismic home page lives in [[preview=preview]], so this page also serves / and wins. + const hidesHomePage = + (await exists(new URL("src/routes/+page.svelte", projectRoot))) && + (await exists(new URL("src/routes/[[preview=preview]]/+page.svelte", projectRoot))); + const homePageStep = + hidesHomePage && + dedent` + Action required: delete src/routes/+page.svelte. + + It also serves / and hides your Prismic home page in + src/routes/[[preview=preview]]/+page.svelte. Move anything you need + from it first. + `; + + return [previewStep, homePageStep].filter(Boolean).join("\n\n") || undefined; + } + + protected async getPageFiles( + model: DynamicCustomTypeModel, + routePath: string, + ): Promise { + const routeDirectory = new URL( + `src/routes/[[preview=preview]]/${routePath}/`, + await findProjectRoot(), + ); + const typescript = await checkIsTypeScriptProject(); + return [ + { path: new URL("+page.svelte", routeDirectory), contents: pageTemplate({ typescript }) }, + { + path: new URL(`+page.server.${await getJsFileExtension()}`, routeDirectory), + contents: pageServerTemplate({ model, typescript }), + }, + ]; + } +} diff --git a/test/gen-page.test.ts b/test/gen-page.test.ts index 14dbb262..c64f36f8 100644 --- a/test/gen-page.test.ts +++ b/test/gen-page.test.ts @@ -1,7 +1,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { sep } from "node:path"; -import { buildCustomType, it, writeLocalCustomType } from "./it"; +import { buildCustomType, it, useSvelteKit, writeLocalCustomType } from "./it"; it("supports --help", async ({ expect, prismic }) => { const { stdout, stderr, exitCode } = await prismic("gen", ["page", "--help"]); @@ -22,6 +22,32 @@ it("writes a missing page", async ({ expect, project, prismic }) => { }); }); +it("writes SvelteKit 3 pages that import from #lib", async ({ expect, project, prismic }) => { + await useSvelteKit(project); + const customType = buildCustomType({ format: "page", repeatable: false }); + await writeLocalCustomType(project, customType); + + const { stderr, exitCode } = await prismic("gen", ["page", customType.id]); + expect(exitCode, stderr).toBe(0); + await expect(project).toHaveFile( + `src/routes/[[preview=preview]]/${customType.id.toLowerCase()}/+page.server.js`, + { contains: 'import { createClient } from "#lib/prismicio.js";' }, + ); +}); + +it("writes SvelteKit 2 pages that import from $lib", async ({ expect, project, prismic }) => { + await useSvelteKit(project, "2.27.0"); + const customType = buildCustomType({ format: "page", repeatable: false }); + await writeLocalCustomType(project, customType); + + const { stderr, exitCode } = await prismic("gen", ["page", customType.id]); + expect(exitCode, stderr).toBe(0); + await expect(project).toHaveFile( + `src/routes/[[preview=preview]]/${customType.id.toLowerCase()}/+page.server.js`, + { contains: 'import { createClient } from "$lib/prismicio";' }, + ); +}); + it("prints the code for an existing page", async ({ expect, project, prismic }) => { const customType = buildCustomType({ format: "page", repeatable: false }); await writeLocalCustomType(project, customType); diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 1787361e..421f6683 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -227,6 +227,48 @@ it( }, ); +it( + "sets up a SvelteKit 2 project with @prismicio/svelte v2", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "2.27.0"); + + const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); + expect(packageJson.dependencies["@prismicio/svelte"]).toBe("^2"); + await expect(project).toHaveFile("src/lib/prismicio.js", { + contains: "import { env } from '$env/dynamic/public';", + }); + await expect(project).toHaveFile("src/routes/slice-simulator/+page.svelte", { + contains: "", + }); + await expect(project).toHaveFile("src/routes/+layout.server.js", { + contains: 'import { repositoryName } from "$lib/prismicio";', + }); + await expect(project).not.toHaveFile("src/env.js"); + }, +); + +it( + "prints SvelteKit 2 instructions for an existing server layout", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "2.27.0"); + await mkdir(new URL("src/routes/", project), { recursive: true }); + await writeFile(new URL("src/routes/+layout.svelte", project), "{@render children()}"); + await writeFile( + new URL("src/routes/+layout.server.js", project), + 'export const load = () => ({ user: "x" });', + ); + + const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + expect(stdout).toContain('import { repositoryName } from "$lib/prismicio";'); + expect(stdout).not.toContain("PUBLIC_PRISMIC_ENVIRONMENT"); + }, +); + it( "returns the repository name from a SvelteKit server layout", { timeout: 30_000 }, diff --git a/test/it.ts b/test/it.ts index 31918bbc..7d93a8a9 100644 --- a/test/it.ts +++ b/test/it.ts @@ -196,13 +196,19 @@ export const it = test.extend({ }, }); -export async function useSvelteKit(project: URL): Promise { +export async function useSvelteKit(project: URL, kitVersion?: string): Promise { await rm(new URL("node_modules/next/", project), { recursive: true, force: true }); await rm(new URL("app/", project), { recursive: true, force: true }); await writeFile( new URL("package.json", project), JSON.stringify({ dependencies: { "@sveltejs/kit": "latest", svelte: "latest" } }), ); + if (!kitVersion) return; + await mkdir(new URL("node_modules/@sveltejs/kit/", project), { recursive: true }); + await writeFile( + new URL("node_modules/@sveltejs/kit/package.json", project), + JSON.stringify({ version: kitVersion }), + ); } // Makes the package manager install fail by shadowing npm in node_modules/.bin. From 9b90320b6b7afc66df7b07a511c69c4592eadf0d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 18:07:05 +0000 Subject: [PATCH 09/22] refactor(sveltekit): derive SvelteKit 2 templates from SvelteKit 3 Most SvelteKit 2 templates differed from the SvelteKit 3 templates only in their $lib and $env imports. They now reuse the SvelteKit 3 output and change only those imports. The generated files are the same. The CLI now picks the SvelteKit 2 adapter from the @sveltejs/kit range in package.json. Before, a SvelteKit 2 project without node_modules got the SvelteKit 3 adapter. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/adapters/index.ts | 4 +- src/adapters/sveltekit2.templates.ts | 192 +++------------------------ test/gen-page.test.ts | 2 +- test/gen-setup.test.ts | 4 +- test/it.ts | 10 +- 5 files changed, 26 insertions(+), 186 deletions(-) diff --git a/src/adapters/index.ts b/src/adapters/index.ts index f8523ac0..4e2c34b5 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -64,7 +64,9 @@ export async function getAdapter(): Promise { return new NuxtAdapter(); } if ("@sveltejs/kit" in allDependencies) { - if ((await getInstalledMajor("@sveltejs/kit")) < 3) { + // The declared range works before dependencies are installed. + const kitMajor = Number(/\d+/.exec(allDependencies["@sveltejs/kit"])?.[0]); + if (kitMajor < 3) { const { SvelteKit2Adapter } = await import("./sveltekit2"); return new SvelteKit2Adapter(); } diff --git a/src/adapters/sveltekit2.templates.ts b/src/adapters/sveltekit2.templates.ts index a73ad6f6..526f1fe4 100644 --- a/src/adapters/sveltekit2.templates.ts +++ b/src/adapters/sveltekit2.templates.ts @@ -1,74 +1,25 @@ import type { DynamicCustomTypeModel } from "@prismicio/types-internal"; import { dedent } from "../lib/string"; +import * as sveltekit from "./sveltekit.templates"; -// Templates for SvelteKit 2 projects, which use @prismicio/svelte v2, $lib, and -// $env/dynamic/public. The other templates are shared with SvelteKit 3. +// SvelteKit 2 has no #lib, so it imports from $lib without file extensions. +function useLib(code: string): string { + return code.replace(/#lib\/(\w+)(?:\/index)?\.\w+/g, "$lib/$1"); +} +// SvelteKit 2 has no $app/env. export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): string { - if (typescript) { - return dedent` - import { createClient as baseCreateClient } from "@prismicio/client"; - import { type CreateClientConfig, enableAutoPreviews } from '@prismicio/svelte/kit'; - import { env } from '$env/dynamic/public'; - import prismicConfig from "../../prismic.config.json"; - - /** - * The project's Prismic repository name. - */ - export const repositoryName = - env.PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; - - /** - * Creates a Prismic client for the project's repository. The client is used to - * query content from the Prismic API. - * - * @param config - Configuration for the Prismic client. - */ - export const createClient = ({ cookies, ...config }: CreateClientConfig = {}) => { - const client = baseCreateClient(repositoryName, { - routes: prismicConfig.routes, - ...config, - }); - - enableAutoPreviews({ client, cookies }); - - return client; - }; - `; - } - - return dedent` - import { createClient as baseCreateClient } from "@prismicio/client"; - import { enableAutoPreviews } from '@prismicio/svelte/kit'; - import { env } from '$env/dynamic/public'; - import prismicConfig from "../../prismic.config.json"; - - /** - * The project's Prismic repository name. - */ - export const repositoryName = - env.PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; - - /** - * Creates a Prismic client for the project's repository. The client is used to - * query content from the Prismic API. - * - * @param {import('@prismicio/svelte/kit').CreateClientConfig} config - Configuration for the Prismic client. - */ - export const createClient = ({ cookies, ...config } = {}) => { - const client = baseCreateClient(repositoryName, { - routes: prismicConfig.routes, - ...config, - }); - - enableAutoPreviews({ client, cookies }); - - return client; - }; - `; + return sveltekit + .prismicIOFileTemplate({ typescript }) + .replace( + "{ PUBLIC_PRISMIC_ENVIRONMENT } from '$app/env/public'", + "{ env } from '$env/dynamic/public'", + ) + .replace("PUBLIC_PRISMIC_ENVIRONMENT ||", "env.PUBLIC_PRISMIC_ENVIRONMENT ||"); } +// @prismicio/svelte v2 passes slices through a slot. export function sliceSimulatorPageTemplate(): string { return dedent` - - - `; - } - - return dedent` - - - - `; + return useLib(sveltekit.pageTemplate({ typescript })); } export function pageServerTemplate({ @@ -156,57 +52,5 @@ export function pageServerTemplate({ model: DynamicCustomTypeModel; typescript: boolean; }): string { - if (model.repeatable) { - if (typescript) { - return dedent` - import type { PageServerLoad } from "./$types"; - import { createClient } from "$lib/prismicio"; - - export const load: PageServerLoad = async ({ params, fetch, cookies }) => { - const client = createClient({ fetch, cookies }); - const page = await client.getByUID("${model.id}", params.uid); - - return { page }; - }; - `; - } - - return dedent` - import { createClient } from "$lib/prismicio"; - - /** @type {import("./$types").PageServerLoad} */ - export async function load({ params, fetch, cookies }) { - const client = createClient({ fetch, cookies }); - const page = await client.getByUID("${model.id}", params.uid); - - return { page }; - } - `; - } - - if (typescript) { - return dedent` - import type { PageServerLoad } from "./$types"; - import { createClient } from "$lib/prismicio"; - - export const load: PageServerLoad = async ({ fetch, cookies }) => { - const client = createClient({ fetch, cookies }); - const page = await client.getSingle("${model.id}"); - - return { page }; - }; - `; - } - - return dedent` - import { createClient } from "$lib/prismicio"; - - /* @type {import("./$types").PageServerLoad} */ - export async function load({ fetch, cookies }) { - const client = createClient({ fetch, cookies }); - const page = await client.getSingle("${model.id}"); - - return { page }; - } - `; + return useLib(sveltekit.pageServerTemplate({ model, typescript })); } diff --git a/test/gen-page.test.ts b/test/gen-page.test.ts index c64f36f8..9d0adfc3 100644 --- a/test/gen-page.test.ts +++ b/test/gen-page.test.ts @@ -36,7 +36,7 @@ it("writes SvelteKit 3 pages that import from #lib", async ({ expect, project, p }); it("writes SvelteKit 2 pages that import from $lib", async ({ expect, project, prismic }) => { - await useSvelteKit(project, "2.27.0"); + await useSvelteKit(project, "^2.27.0"); const customType = buildCustomType({ format: "page", repeatable: false }); await writeLocalCustomType(project, customType); diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 421f6683..cd159ec6 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -231,7 +231,7 @@ it( "sets up a SvelteKit 2 project with @prismicio/svelte v2", { timeout: 30_000 }, async ({ expect, project, prismic }) => { - await useSvelteKit(project, "2.27.0"); + await useSvelteKit(project, "^2.27.0"); const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); expect(exitCode, stderr).toBe(0); @@ -254,7 +254,7 @@ it( "prints SvelteKit 2 instructions for an existing server layout", { timeout: 30_000 }, async ({ expect, project, prismic }) => { - await useSvelteKit(project, "2.27.0"); + await useSvelteKit(project, "^2.27.0"); await mkdir(new URL("src/routes/", project), { recursive: true }); await writeFile(new URL("src/routes/+layout.svelte", project), "{@render children()}"); await writeFile( diff --git a/test/it.ts b/test/it.ts index 7d93a8a9..27f58e7e 100644 --- a/test/it.ts +++ b/test/it.ts @@ -196,18 +196,12 @@ export const it = test.extend({ }, }); -export async function useSvelteKit(project: URL, kitVersion?: string): Promise { +export async function useSvelteKit(project: URL, kitVersion = "latest"): Promise { await rm(new URL("node_modules/next/", project), { recursive: true, force: true }); await rm(new URL("app/", project), { recursive: true, force: true }); await writeFile( new URL("package.json", project), - JSON.stringify({ dependencies: { "@sveltejs/kit": "latest", svelte: "latest" } }), - ); - if (!kitVersion) return; - await mkdir(new URL("node_modules/@sveltejs/kit/", project), { recursive: true }); - await writeFile( - new URL("node_modules/@sveltejs/kit/package.json", project), - JSON.stringify({ version: kitVersion }), + JSON.stringify({ dependencies: { "@sveltejs/kit": kitVersion, svelte: "latest" } }), ); } From e7f8998da5e016c54249d6461453c5c9304b0b71 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 18:47:40 +0000 Subject: [PATCH 10/22] refactor(adapters): read the declared range in getInstalledMajor getAdapter parsed the @sveltejs/kit range itself. getInstalledMajor now reads a simple range from package.json when the package is not installed, so getAdapter uses it directly. Other ranges, like "latest" or a Git URL, still mean the newest major. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/adapters/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 4e2c34b5..9dca49a6 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -64,9 +64,7 @@ export async function getAdapter(): Promise { return new NuxtAdapter(); } if ("@sveltejs/kit" in allDependencies) { - // The declared range works before dependencies are installed. - const kitMajor = Number(/\d+/.exec(allDependencies["@sveltejs/kit"])?.[0]); - if (kitMajor < 3) { + if ((await getInstalledMajor("@sveltejs/kit")) < 3) { const { SvelteKit2Adapter } = await import("./sveltekit2"); return new SvelteKit2Adapter(); } @@ -151,8 +149,12 @@ export async function getInstalledMajor(packageName: string): Promise { const major = Number.parseInt(version.split(".")[0]); return Number.isNaN(major) ? Infinity : major; } catch { - // Not installed yet, so assume the newest major. - return Infinity; + // Not installed yet, so read a simple range like "^2.27.0" from package.json. Other + // ranges, like "latest" or a Git URL, mean the newest major. + const { dependencies, devDependencies } = await readPackageJson(); + const range = { ...dependencies, ...devDependencies }[packageName] ?? ""; + const major = /^[\^~]?(\d+)/.exec(range)?.[1]; + return major ? Number(major) : Infinity; } } From b332d3e8578e43957406b86cd4f810bf19492858 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 18:51:13 +0000 Subject: [PATCH 11/22] refactor: move getInstalledMajor to lib/packageJson The helper only reads package.json and installed packages, so it belongs with the other package.json helpers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/adapters/index.ts | 19 +------------------ src/adapters/nextjs.ts | 3 +-- src/lib/packageJson.ts | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 9dca49a6..31b938ee 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -1,5 +1,4 @@ import { readFile, rm } from "node:fs/promises"; -import { createRequire } from "node:module"; import { sep } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -19,7 +18,7 @@ import { writeFileRecursive, } from "../lib/file"; import { stringify } from "../lib/json"; -import { findPackageJson, readPackageJson } from "../lib/packageJson"; +import { getInstalledMajor, readPackageJson } from "../lib/packageJson"; import { addPreview, getPreviews, @@ -142,22 +141,6 @@ export async function getJsFileExtension(): Promise { return (await checkIsTypeScriptProject()) ? "ts" : "js"; } -export async function getInstalledMajor(packageName: string): Promise { - const require = createRequire(await findPackageJson()); - try { - const { version } = require(`${packageName}/package.json`); - const major = Number.parseInt(version.split(".")[0]); - return Number.isNaN(major) ? Infinity : major; - } catch { - // Not installed yet, so read a simple range like "^2.27.0" from package.json. Other - // ranges, like "latest" or a Git URL, mean the newest major. - const { dependencies, devDependencies } = await readPackageJson(); - const range = { ...dependencies, ...devDependencies }[packageName] ?? ""; - const major = /^[\^~]?(\d+)/.exec(range)?.[1]; - return major ? Number(major) : Infinity; - } -} - export abstract class Adapter { abstract readonly id: string; abstract readonly environmentEnvVarName: string; diff --git a/src/adapters/nextjs.ts b/src/adapters/nextjs.ts index 42707749..5e788d82 100644 --- a/src/adapters/nextjs.ts +++ b/src/adapters/nextjs.ts @@ -7,13 +7,12 @@ import { pascalCase } from "change-case"; import { Adapter, checkSourceContains, - getInstalledMajor, getJsFileExtension, type PageFile, writeFileIfMissing, } from "."; import { exists, writeFileRecursive } from "../lib/file"; -import { addDependencies, getNpmPackageVersion } from "../lib/packageJson"; +import { addDependencies, getInstalledMajor, getNpmPackageVersion } from "../lib/packageJson"; import { dedent, formatObjectKey } from "../lib/string"; import { checkIsTypeScriptProject, findProjectRoot } from "../project"; import { diff --git a/src/lib/packageJson.ts b/src/lib/packageJson.ts index 5afddd5a..f46a4022 100644 --- a/src/lib/packageJson.ts +++ b/src/lib/packageJson.ts @@ -1,4 +1,5 @@ import { readFile, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import detectIndent from "detect-indent"; @@ -59,6 +60,22 @@ export async function addDependencies(dependencies: Record): Pro }); } +export async function getInstalledMajor(packageName: string): Promise { + const require = createRequire(await findPackageJson()); + try { + const { version } = require(`${packageName}/package.json`); + const major = Number.parseInt(version.split(".")[0]); + return Number.isNaN(major) ? Infinity : major; + } catch { + // Not installed yet, so read a simple range like "^2.27.0" from package.json. Other + // ranges, like "latest" or a Git URL, mean the newest major. + const { dependencies, devDependencies } = await readPackageJson(); + const range = { ...dependencies, ...devDependencies }[packageName] ?? ""; + const major = /^[\^~]?(\d+)/.exec(range)?.[1]; + return major ? Number(major) : Infinity; + } +} + // Returns the packages whose major version changed between two reads of package.json. export function getMajorDependencyUpdates( before: PackageJson, From 9b0653bf84f0e43b2ef15af2dfe53e4935d5aa7d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 18:56:49 +0000 Subject: [PATCH 12/22] test(sveltekit): cover the setup logic in the SvelteKit 2 adapter The SvelteKit 2 adapter has its own copy of the setup and instruction logic. These tests make sure that the copy keeps the same behavior as the SvelteKit 3 adapter. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- test/gen-setup.test.ts | 63 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index cd159ec6..c6c0b723 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -647,3 +647,66 @@ it( expect(stdout).not.toContain("delete src/routes/+page.svelte"); }, ); + +// SvelteKit 2 projects use their own adapter, which copies this setup logic. + +it( + "deletes the SvelteKit starter home page in a SvelteKit 2 project", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "^2.27.0"); + await mkdir(new URL("src/routes/", project), { recursive: true }); + await writeFile(new URL("src/routes/+page.svelte", project), SVELTEKIT_STARTER_HOME_PAGE); + + const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + await expect(project).not.toHaveFile("src/routes/+page.svelte"); + }, +); + +it( + "asks to delete a changed home page in a SvelteKit 2 project", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "^2.27.0"); + await mkdir(new URL("src/routes/[[preview=preview]]/", project), { recursive: true }); + await writeFile(new URL("src/routes/+page.svelte", project), "

My site

\n"); + await writeFile(new URL("src/routes/[[preview=preview]]/+page.svelte", project), ""); + + const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + expect(stdout).toContain("Action required: delete src/routes/+page.svelte."); + }, +); + +it( + "does not generate a SvelteKit 2 layout next to a server layout without the repository name", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "^2.27.0"); + await mkdir(new URL("src/routes/", project), { recursive: true }); + await writeFile( + new URL("src/routes/+layout.server.js", project), + 'export const load = () => ({ user: "x" });', + ); + + const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + await expect(project).not.toHaveFile("src/routes/+layout.svelte"); + expect(stdout).toContain("Return repositoryName from load in src/routes/+layout.server.js"); + }, +); + +it( + "does not ask for the Prismic environment variable in a SvelteKit 2 project with its own src/env", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "^2.27.0"); + await mkdir(new URL("src/", project), { recursive: true }); + await writeFile(new URL("src/env.ts", project), "export const apiKey = process.env.API_KEY;\n"); + + const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + expect(stdout).not.toContain("PUBLIC_PRISMIC_ENVIRONMENT"); + }, +); From 977df0fce64a4bb07feaa6300b8003ecf470dbf9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 19:00:45 +0000 Subject: [PATCH 13/22] test(sveltekit): merge two SvelteKit 2 server layout tests Both tests set up the same server layout. One test now checks all of the results. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- test/gen-setup.test.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index c6c0b723..fc4e264b 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -250,25 +250,6 @@ it( }, ); -it( - "prints SvelteKit 2 instructions for an existing server layout", - { timeout: 30_000 }, - async ({ expect, project, prismic }) => { - await useSvelteKit(project, "^2.27.0"); - await mkdir(new URL("src/routes/", project), { recursive: true }); - await writeFile(new URL("src/routes/+layout.svelte", project), "{@render children()}"); - await writeFile( - new URL("src/routes/+layout.server.js", project), - 'export const load = () => ({ user: "x" });', - ); - - const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); - expect(exitCode, stderr).toBe(0); - expect(stdout).toContain('import { repositoryName } from "$lib/prismicio";'); - expect(stdout).not.toContain("PUBLIC_PRISMIC_ENVIRONMENT"); - }, -); - it( "returns the repository name from a SvelteKit server layout", { timeout: 30_000 }, @@ -694,6 +675,7 @@ it( expect(exitCode, stderr).toBe(0); await expect(project).not.toHaveFile("src/routes/+layout.svelte"); expect(stdout).toContain("Return repositoryName from load in src/routes/+layout.server.js"); + expect(stdout).toContain('import { repositoryName } from "$lib/prismicio";'); }, ); From 3c5d8171c68d6e5b7d9db63fcb48b712a91ecfa2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 19:04:30 +0000 Subject: [PATCH 14/22] refactor(sveltekit): stop generating Svelte 4 code @prismicio/svelte v2 requires Svelte 5, so the Svelte 4 code that SvelteKit setup generated did not work. Setup, slice components, and setup instructions now always use Svelte 5 syntax. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/adapters/sveltekit.templates.ts | 121 +++++++--------------------- src/adapters/sveltekit.ts | 48 ++++------- test/gen-setup.test.ts | 63 +-------------- test/it.ts | 7 +- 4 files changed, 45 insertions(+), 194 deletions(-) diff --git a/src/adapters/sveltekit.templates.ts b/src/adapters/sveltekit.templates.ts index 99b229a7..77cc8493 100644 --- a/src/adapters/sveltekit.templates.ts +++ b/src/adapters/sveltekit.templates.ts @@ -67,20 +67,8 @@ export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): `; } -export function sliceSimulatorPageTemplate({ version }: { version: number }): string { - const v5 = dedent` - - - - - - - `; - - const v4 = dedent` +export function sliceSimulatorPageTemplate(): string { + return dedent` - ${HEAD_MARKUP} + + {page.data.page?.data.meta_title} + + {#if isFilled.keyText(page.data.page?.data.meta_description)} + + + {/if} + {#if isFilled.image(page.data.page?.data.meta_image)} + + {/if} + {@render children()} `; - - const v4 = dedent` - - - ${HEAD_MARKUP} - - - `; - - return version <= 4 ? v4 : v5; } export function pageTemplate({ typescript }: { typescript: boolean }): string { @@ -277,31 +243,25 @@ const SLICE_MARKUP = dedent` `; -export function sliceTemplate({ - name, - typescript, - version, -}: { - name: string; - typescript: boolean; - version: number; -}): string { +export function sliceTemplate({ name, typescript }: { name: string; typescript: boolean }): string { const pascalName = pascalCase(name); - const v5TS = dedent` - + const { slice }: Props = $props(); + - ${SLICE_MARKUP} - `; + ${SLICE_MARKUP} + `; + } - const v5JS = dedent` + return dedent` - - ${SLICE_MARKUP} - `; - - const v4JS = dedent` - - - ${SLICE_MARKUP} - `; - - if (typescript) { - return version <= 4 ? v4TS : v5TS; - } - - return version <= 4 ? v4JS : v5JS; } diff --git a/src/adapters/sveltekit.ts b/src/adapters/sveltekit.ts index d43d5d6b..d05f3dc5 100644 --- a/src/adapters/sveltekit.ts +++ b/src/adapters/sveltekit.ts @@ -8,7 +8,6 @@ import { pascalCase } from "change-case"; import { Adapter, checkSourceContains, - getInstalledMajor, getJsFileExtension, type PageFile, writeFileIfMissing, @@ -54,7 +53,6 @@ export class SvelteKitAdapter extends Adapter { const projectRoot = await findProjectRoot(); const extension = await getJsFileExtension(); const typescript = await checkIsTypeScriptProject(); - const version = await getInstalledMajor("svelte"); await writeFileIfMissing( new URL(`src/lib/prismicio.${extension}`, projectRoot), @@ -62,7 +60,7 @@ export class SvelteKitAdapter extends Adapter { ); await writeFileIfMissing( new URL("src/routes/slice-simulator/+page.svelte", projectRoot), - sliceSimulatorPageTemplate({ version }), + sliceSimulatorPageTemplate(), ); await writeFileIfMissing( new URL(`src/params/preview.${extension}`, projectRoot), @@ -104,7 +102,7 @@ export class SvelteKitAdapter extends Adapter { if (returnsRepositoryName) { await writeFileIfMissing( new URL("src/routes/+layout.svelte", projectRoot), - rootLayoutTemplate({ version }), + rootLayoutTemplate(), ); } await deleteStarterHomePage(); @@ -132,34 +130,6 @@ export class SvelteKitAdapter extends Adapter { private async getPreviewStep(): Promise { if (await checkSourceContains("PrismicPreview")) return; - const layoutStep = - (await getInstalledMajor("svelte")) <= 4 - ? dedent` - Add the lines marked + to src/routes/+layout.svelte: - - - - - + - ` - : dedent` - Change the lines marked - and + in src/routes/+layout.svelte: - - - - {@render children()} - + - `; - // The layout reads repositoryName from the server layout's data. const serverLayoutPath = (await findServerLayout()) ?? `src/routes/+layout.server.${await getJsFileExtension()}`; @@ -177,8 +147,19 @@ export class SvelteKitAdapter extends Adapter { Previews do not work until you do this, and the CLI cannot edit your layout for you. Make the change now. + + Change the lines marked - and + in src/routes/+layout.svelte: + + + + {@render children()} + + `, - layoutStep, serverLayoutStep, "Run `prismic docs view sveltekit` for details.", ] @@ -215,7 +196,6 @@ export class SvelteKitAdapter extends Adapter { const contents = sliceTemplate({ name: model.name, typescript: await checkIsTypeScriptProject(), - version: await getInstalledMajor("svelte"), }); await writeFileRecursive(new URL("index.svelte", directory), contents); } diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 17cfa028..2594b30c 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -40,17 +40,8 @@ it( "generates valid script tags for SvelteKit", { timeout: 30_000 }, async ({ expect, project, prismic }) => { - // Reconfigure the fixture as a SvelteKit project so a file with a \n\n" + '\n\t\n\n\n' + "

KEEP-ME

\n{@render children()}\n", diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 2a38dae2..31b938ee 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -18,7 +18,7 @@ import { writeFileRecursive, } from "../lib/file"; import { stringify } from "../lib/json"; -import { readPackageJson } from "../lib/packageJson"; +import { getInstalledMajor, readPackageJson } from "../lib/packageJson"; import { addPreview, getPreviews, @@ -63,6 +63,10 @@ export async function getAdapter(): Promise { return new NuxtAdapter(); } if ("@sveltejs/kit" in allDependencies) { + if ((await getInstalledMajor("@sveltejs/kit")) < 3) { + const { SvelteKit2Adapter } = await import("./sveltekit2"); + return new SvelteKit2Adapter(); + } const { SvelteKitAdapter } = await import("./sveltekit"); return new SvelteKitAdapter(); } diff --git a/src/adapters/sveltekit.templates.ts b/src/adapters/sveltekit.templates.ts index 77cc8493..c0849ad8 100644 --- a/src/adapters/sveltekit.templates.ts +++ b/src/adapters/sveltekit.templates.ts @@ -8,14 +8,14 @@ export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): return dedent` import { createClient as baseCreateClient } from "@prismicio/client"; import { type CreateClientConfig, enableAutoPreviews } from '@prismicio/svelte/kit'; - import { env } from '$env/dynamic/public'; + import { PUBLIC_PRISMIC_ENVIRONMENT } from '$app/env/public'; import prismicConfig from "../../prismic.config.json"; /** * The project's Prismic repository name. */ export const repositoryName = - env.PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; + PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; /** * Creates a Prismic client for the project's repository. The client is used to @@ -39,14 +39,14 @@ export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): return dedent` import { createClient as baseCreateClient } from "@prismicio/client"; import { enableAutoPreviews } from '@prismicio/svelte/kit'; - import { env } from '$env/dynamic/public'; + import { PUBLIC_PRISMIC_ENVIRONMENT } from '$app/env/public'; import prismicConfig from "../../prismic.config.json"; /** * The project's Prismic repository name. */ export const repositoryName = - env.PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; + PUBLIC_PRISMIC_ENVIRONMENT || prismicConfig.repositoryName; /** * Creates a Prismic client for the project's repository. The client is used to @@ -67,15 +67,37 @@ export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): `; } -export function sliceSimulatorPageTemplate(): string { +export function envVarTemplate(): string { + return dedent` + PUBLIC_PRISMIC_ENVIRONMENT: { + public: true, + schema: (value) => value, + description: "The Prismic environment to use instead of the production repository.", + }, + `; +} + +export function envFileTemplate(): string { + return dedent` + import { defineEnvVars } from "@sveltejs/kit/env"; + + export const variables = defineEnvVars({ + ${envVarTemplate()} + }); + `; +} + +export function sliceSimulatorPageTemplate({ extension }: { extension: string }): string { return dedent` - - + + {#snippet children({ slices })} + + {/snippet} `; } @@ -84,7 +106,7 @@ export function previewAPIRouteTemplate({ typescript }: { typescript: boolean }) if (typescript) { return dedent` import { redirectToPreviewURL } from '@prismicio/svelte/kit'; - import { createClient } from '$lib/prismicio'; + import { createClient } from '#lib/prismicio.ts'; import type { RequestHandler } from "./$types"; export const GET: RequestHandler = async ({ fetch, request, cookies }) => { @@ -97,7 +119,7 @@ export function previewAPIRouteTemplate({ typescript }: { typescript: boolean }) return dedent` import { redirectToPreviewURL } from '@prismicio/svelte/kit'; - import { createClient } from '$lib/prismicio'; + import { createClient } from '#lib/prismicio.js'; /* @type {import("./$types").RequestHandler} */ export async function GET({ fetch, request, cookies }) { @@ -108,9 +130,9 @@ export function previewAPIRouteTemplate({ typescript }: { typescript: boolean }) `; } -export function layoutServerTemplate(): string { +export function layoutServerTemplate({ extension }: { extension: string }): string { return dedent` - import { repositoryName } from "$lib/prismicio"; + import { repositoryName } from "#lib/prismicio.${extension}"; export const prerender = "auto"; @@ -151,7 +173,7 @@ export function pageTemplate({ typescript }: { typescript: boolean }): string { return dedent` @@ -184,7 +206,7 @@ export function pageServerTemplate({ if (typescript) { return dedent` import type { PageServerLoad } from "./$types"; - import { createClient } from "$lib/prismicio"; + import { createClient } from "#lib/prismicio.ts"; export const load: PageServerLoad = async ({ params, fetch, cookies }) => { const client = createClient({ fetch, cookies }); @@ -196,7 +218,7 @@ export function pageServerTemplate({ } return dedent` - import { createClient } from "$lib/prismicio"; + import { createClient } from "#lib/prismicio.js"; /** @type {import("./$types").PageServerLoad} */ export async function load({ params, fetch, cookies }) { @@ -211,7 +233,7 @@ export function pageServerTemplate({ if (typescript) { return dedent` import type { PageServerLoad } from "./$types"; - import { createClient } from "$lib/prismicio"; + import { createClient } from "#lib/prismicio.ts"; export const load: PageServerLoad = async ({ fetch, cookies }) => { const client = createClient({ fetch, cookies }); @@ -223,7 +245,7 @@ export function pageServerTemplate({ } return dedent` - import { createClient } from "$lib/prismicio"; + import { createClient } from "#lib/prismicio.js"; /* @type {import("./$types").PageServerLoad} */ export async function load({ fetch, cookies }) { diff --git a/src/adapters/sveltekit.ts b/src/adapters/sveltekit.ts index d05f3dc5..d18ee556 100644 --- a/src/adapters/sveltekit.ts +++ b/src/adapters/sveltekit.ts @@ -17,6 +17,8 @@ import { addDependencies, getNpmPackageVersion } from "../lib/packageJson"; import { dedent, formatObjectKey } from "../lib/string"; import { checkIsTypeScriptProject, findProjectRoot } from "../project"; import { + envFileTemplate, + envVarTemplate, layoutServerTemplate, pageServerTemplate, pageTemplate, @@ -58,9 +60,12 @@ export class SvelteKitAdapter extends Adapter { new URL(`src/lib/prismicio.${extension}`, projectRoot), prismicIOFileTemplate({ typescript }), ); + if (!(await findSourceFile("src/env"))) { + await writeFileRecursive(new URL(`src/env.${extension}`, projectRoot), envFileTemplate()); + } await writeFileIfMissing( new URL("src/routes/slice-simulator/+page.svelte", projectRoot), - sliceSimulatorPageTemplate(), + sliceSimulatorPageTemplate({ extension }), ); await writeFileIfMissing( new URL(`src/params/preview.${extension}`, projectRoot), @@ -87,11 +92,11 @@ export class SvelteKitAdapter extends Adapter { See for more information. `, ); - const serverLayout = await findServerLayout(); + const serverLayout = await findSourceFile("src/routes/+layout.server"); if (!serverLayout) { await writeFileRecursive( new URL(`src/routes/+layout.server.${extension}`, projectRoot), - layoutServerTemplate(), + layoutServerTemplate({ extension }), ); } // The generated layout reads repositoryName from the server layout. When an existing @@ -124,7 +129,29 @@ export class SvelteKitAdapter extends Adapter { from it first. `; - return [await this.getPreviewStep(), homePageStep].filter(Boolean).join("\n\n") || undefined; + return ( + [await this.getEnvStep(), await this.getPreviewStep(), homePageStep] + .filter(Boolean) + .join("\n\n") || undefined + ); + } + + // The generated Prismic client imports PUBLIC_PRISMIC_ENVIRONMENT, which SvelteKit only + // provides when the project's env file declares it. + private async getEnvStep(): Promise { + const envFile = await findSourceFile("src/env"); + if (!envFile) return; + const contents = await readFile(new URL(envFile, await findProjectRoot()), "utf8"); + if (contents.includes("PUBLIC_PRISMIC_ENVIRONMENT")) return; + + return dedent` + Action required: add PUBLIC_PRISMIC_ENVIRONMENT to ${envFile}. + + Your project does not build until you do this. Add these lines to + defineEnvVars({ ... }): + + ${envVarTemplate().replace(/^/gm, " ")} + `; } private async getPreviewStep(): Promise { @@ -132,14 +159,15 @@ export class SvelteKitAdapter extends Adapter { // The layout reads repositoryName from the server layout's data. const serverLayoutPath = - (await findServerLayout()) ?? `src/routes/+layout.server.${await getJsFileExtension()}`; + (await findSourceFile("src/routes/+layout.server")) ?? + `src/routes/+layout.server.${await getJsFileExtension()}`; const serverLayout = await readFile( new URL(serverLayoutPath, await findProjectRoot()), "utf8", ).catch(() => ""); const serverLayoutStep = !serverLayout.includes("repositoryName") && - `Return repositoryName from load in ${serverLayoutPath}, creating the file if needed:\n\n${layoutServerTemplate().replace(/^/gm, " ")}`; + `Return repositoryName from load in ${serverLayoutPath}, creating the file if needed:\n\n${layoutServerTemplate({ extension: serverLayoutPath.split(".").pop()! }).replace(/^/gm, " ")}`; return [ dedent` @@ -219,7 +247,7 @@ export class SvelteKitAdapter extends Adapter { } } -async function deleteStarterHomePage(): Promise { +export async function deleteStarterHomePage(): Promise { const path = new URL("src/routes/+page.svelte", await findProjectRoot()); const contents = await readFile(path, "utf8").catch(() => undefined); if (!STARTER_HOME_PAGES.includes(contents?.replace(/\s/g, "") ?? "")) return; @@ -227,9 +255,9 @@ async function deleteStarterHomePage(): Promise { await rm(path); } -async function findServerLayout(): Promise { +export async function findSourceFile(path: string): Promise { const projectRoot = await findProjectRoot(); - for (const path of ["src/routes/+layout.server.js", "src/routes/+layout.server.ts"]) { - if (await exists(new URL(path, projectRoot))) return path; + for (const extension of ["js", "ts"]) { + if (await exists(new URL(`${path}.${extension}`, projectRoot))) return `${path}.${extension}`; } } diff --git a/src/adapters/sveltekit2.templates.ts b/src/adapters/sveltekit2.templates.ts new file mode 100644 index 00000000..526f1fe4 --- /dev/null +++ b/src/adapters/sveltekit2.templates.ts @@ -0,0 +1,56 @@ +import type { DynamicCustomTypeModel } from "@prismicio/types-internal"; + +import { dedent } from "../lib/string"; +import * as sveltekit from "./sveltekit.templates"; + +// SvelteKit 2 has no #lib, so it imports from $lib without file extensions. +function useLib(code: string): string { + return code.replace(/#lib\/(\w+)(?:\/index)?\.\w+/g, "$lib/$1"); +} + +// SvelteKit 2 has no $app/env. +export function prismicIOFileTemplate({ typescript }: { typescript: boolean }): string { + return sveltekit + .prismicIOFileTemplate({ typescript }) + .replace( + "{ PUBLIC_PRISMIC_ENVIRONMENT } from '$app/env/public'", + "{ env } from '$env/dynamic/public'", + ) + .replace("PUBLIC_PRISMIC_ENVIRONMENT ||", "env.PUBLIC_PRISMIC_ENVIRONMENT ||"); +} + +// @prismicio/svelte v2 passes slices through a slot. +export function sliceSimulatorPageTemplate(): string { + return dedent` + + + + + + `; +} + +export function previewAPIRouteTemplate({ typescript }: { typescript: boolean }): string { + return useLib(sveltekit.previewAPIRouteTemplate({ typescript })); +} + +export function layoutServerTemplate(): string { + return useLib(sveltekit.layoutServerTemplate({ extension: "js" })); +} + +export function pageTemplate({ typescript }: { typescript: boolean }): string { + return useLib(sveltekit.pageTemplate({ typescript })); +} + +export function pageServerTemplate({ + model, + typescript, +}: { + model: DynamicCustomTypeModel; + typescript: boolean; +}): string { + return useLib(sveltekit.pageServerTemplate({ model, typescript })); +} diff --git a/src/adapters/sveltekit2.ts b/src/adapters/sveltekit2.ts new file mode 100644 index 00000000..4f485b93 --- /dev/null +++ b/src/adapters/sveltekit2.ts @@ -0,0 +1,160 @@ +import { readFile } from "node:fs/promises"; + +import type { DynamicCustomTypeModel } from "@prismicio/types-internal"; + +import { checkSourceContains, getJsFileExtension, type PageFile, writeFileIfMissing } from "."; +import { exists, writeFileRecursive } from "../lib/file"; +import { addDependencies, getNpmPackageVersion } from "../lib/packageJson"; +import { dedent } from "../lib/string"; +import { checkIsTypeScriptProject, findProjectRoot } from "../project"; +import { deleteStarterHomePage, findSourceFile, SvelteKitAdapter } from "./sveltekit"; +import { rootLayoutTemplate } from "./sveltekit.templates"; +import { + layoutServerTemplate, + pageServerTemplate, + pageTemplate, + previewAPIRouteTemplate, + prismicIOFileTemplate, + sliceSimulatorPageTemplate, +} from "./sveltekit2.templates"; + +// SvelteKit 2 projects use @prismicio/svelte v2. Delete this file and sveltekit2.templates.ts +// when SvelteKit 2 support ends. +export class SvelteKit2Adapter extends SvelteKitAdapter { + async setupProject(): Promise { + await addDependencies({ + "@prismicio/client": `^${await getNpmPackageVersion("@prismicio/client")}`, + "@prismicio/svelte": "^2", + }); + + const projectRoot = await findProjectRoot(); + const extension = await getJsFileExtension(); + const typescript = await checkIsTypeScriptProject(); + + await writeFileIfMissing( + new URL(`src/lib/prismicio.${extension}`, projectRoot), + prismicIOFileTemplate({ typescript }), + ); + await writeFileIfMissing( + new URL("src/routes/slice-simulator/+page.svelte", projectRoot), + sliceSimulatorPageTemplate(), + ); + await writeFileIfMissing( + new URL(`src/params/preview.${extension}`, projectRoot), + dedent` + export function match(param) { + return param === 'preview'; + } + `, + ); + await writeFileIfMissing( + new URL(`src/routes/api/preview/+server.${extension}`, projectRoot), + previewAPIRouteTemplate({ typescript }), + ); + await writeFileIfMissing( + new URL("src/routes/[[preview=preview]]/README.md", projectRoot), + dedent` + This directory adds support for optional \`/preview\` routes. Do not remove this directory. + + All routes within this directory will be served using the following URLs: + + - \`/example-route\` (prerendered) + - \`/preview/example-route\` (server-rendered) + + See for more information. + `, + ); + const serverLayout = await findSourceFile("src/routes/+layout.server"); + if (!serverLayout) { + await writeFileRecursive( + new URL(`src/routes/+layout.server.${extension}`, projectRoot), + layoutServerTemplate(), + ); + } + // The generated layout reads repositoryName from the server layout. When an existing + // server layout does not return it, the setup instructions cover both files instead. + const returnsRepositoryName = + !serverLayout || + (await readFile(new URL(serverLayout, projectRoot), "utf8")).includes("repositoryName"); + if (returnsRepositoryName) { + await writeFileIfMissing( + new URL("src/routes/+layout.svelte", projectRoot), + rootLayoutTemplate(), + ); + } + await deleteStarterHomePage(); + } + + async getSetupInstructions(): Promise { + const projectRoot = await findProjectRoot(); + + // The layout reads repositoryName from the server layout's data. + const serverLayoutPath = + (await findSourceFile("src/routes/+layout.server")) ?? + `src/routes/+layout.server.${await getJsFileExtension()}`; + const serverLayout = await readFile(new URL(serverLayoutPath, projectRoot), "utf8").catch( + () => "", + ); + const previewStep = + !(await checkSourceContains("PrismicPreview")) && + [ + dedent` + Action required: add to your root layout. + + Previews do not work until you do this, and the CLI cannot edit your + layout for you. Make the change now. + + Change the lines marked - and + in src/routes/+layout.svelte: + + + + {@render children()} + + + `, + !serverLayout.includes("repositoryName") && + `Return repositoryName from load in ${serverLayoutPath}, creating the file if needed:\n\n${layoutServerTemplate().replace(/^/gm, " ")}`, + "Run `prismic docs view sveltekit` for details.", + ] + .filter(Boolean) + .join("\n\n"); + + // The Prismic home page lives in [[preview=preview]], so this page also serves / and wins. + const hidesHomePage = + (await exists(new URL("src/routes/+page.svelte", projectRoot))) && + (await exists(new URL("src/routes/[[preview=preview]]/+page.svelte", projectRoot))); + const homePageStep = + hidesHomePage && + dedent` + Action required: delete src/routes/+page.svelte. + + It also serves / and hides your Prismic home page in + src/routes/[[preview=preview]]/+page.svelte. Move anything you need + from it first. + `; + + return [previewStep, homePageStep].filter(Boolean).join("\n\n") || undefined; + } + + protected async getPageFiles( + model: DynamicCustomTypeModel, + routePath: string, + ): Promise { + const routeDirectory = new URL( + `src/routes/[[preview=preview]]/${routePath}/`, + await findProjectRoot(), + ); + const typescript = await checkIsTypeScriptProject(); + return [ + { path: new URL("+page.svelte", routeDirectory), contents: pageTemplate({ typescript }) }, + { + path: new URL(`+page.server.${await getJsFileExtension()}`, routeDirectory), + contents: pageServerTemplate({ model, typescript }), + }, + ]; + } +} diff --git a/test/env-set.test.ts b/test/env-set.test.ts index 3ff921ce..fd9fb748 100644 --- a/test/env-set.test.ts +++ b/test/env-set.test.ts @@ -1,6 +1,6 @@ -import { readFile, writeFile } from "node:fs/promises"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { it } from "./it"; +import { it, useSvelteKit } from "./it"; it("supports --help", async ({ expect, prismic }) => { const { stdout, exitCode } = await prismic("env", ["set", "--help"]); @@ -30,6 +30,21 @@ it("clears the active environment when setting to production", async ({ expect(after).not.toContain("my-stage-env"); }); +it("asks for the Prismic environment variable in a SvelteKit env file", async ({ + expect, + prismic, + project, + repo, +}) => { + await useSvelteKit(project); + await mkdir(new URL("src/", project), { recursive: true }); + await writeFile(new URL("src/env.ts", project), "export const variables = {};\n"); + + const { stdout, exitCode } = await prismic("env", ["set", repo]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Action required: add PUBLIC_PRISMIC_ENVIRONMENT to src/env.ts."); +}); + it("prints the open setup steps", async ({ expect, prismic, repo }) => { const { stdout, exitCode } = await prismic("env", ["set", repo]); expect(exitCode).toBe(0); diff --git a/test/gen-page.test.ts b/test/gen-page.test.ts index 14dbb262..9d0adfc3 100644 --- a/test/gen-page.test.ts +++ b/test/gen-page.test.ts @@ -1,7 +1,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { sep } from "node:path"; -import { buildCustomType, it, writeLocalCustomType } from "./it"; +import { buildCustomType, it, useSvelteKit, writeLocalCustomType } from "./it"; it("supports --help", async ({ expect, prismic }) => { const { stdout, stderr, exitCode } = await prismic("gen", ["page", "--help"]); @@ -22,6 +22,32 @@ it("writes a missing page", async ({ expect, project, prismic }) => { }); }); +it("writes SvelteKit 3 pages that import from #lib", async ({ expect, project, prismic }) => { + await useSvelteKit(project); + const customType = buildCustomType({ format: "page", repeatable: false }); + await writeLocalCustomType(project, customType); + + const { stderr, exitCode } = await prismic("gen", ["page", customType.id]); + expect(exitCode, stderr).toBe(0); + await expect(project).toHaveFile( + `src/routes/[[preview=preview]]/${customType.id.toLowerCase()}/+page.server.js`, + { contains: 'import { createClient } from "#lib/prismicio.js";' }, + ); +}); + +it("writes SvelteKit 2 pages that import from $lib", async ({ expect, project, prismic }) => { + await useSvelteKit(project, "^2.27.0"); + const customType = buildCustomType({ format: "page", repeatable: false }); + await writeLocalCustomType(project, customType); + + const { stderr, exitCode } = await prismic("gen", ["page", customType.id]); + expect(exitCode, stderr).toBe(0); + await expect(project).toHaveFile( + `src/routes/[[preview=preview]]/${customType.id.toLowerCase()}/+page.server.js`, + { contains: 'import { createClient } from "$lib/prismicio";' }, + ); +}); + it("prints the code for an existing page", async ({ expect, project, prismic }) => { const customType = buildCustomType({ format: "page", repeatable: false }); await writeLocalCustomType(project, customType); diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 511187f1..4ef70fd2 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -36,23 +36,6 @@ it("skips existing files", { timeout: 30_000 }, async ({ expect, project, prismi await expect(project).toHaveFile("prismicio.js", { contains: "// custom client file" }); }); -it( - "generates valid script tags for SvelteKit", - { timeout: 30_000 }, - async ({ expect, project, prismic }) => { - // A SvelteKit project generates a file with a ", not the bundler-escaped "<\/script>". - await expect(project).toHaveFile("src/routes/slice-simulator/+page.svelte", { - contains: "", - }); - }, -); - it("skips installation with --no-install", async ({ expect, project, prismic }) => { const { stderr, exitCode, stdout } = await prismic("gen", ["setup", "--no-install"]); expect(exitCode, stderr).toBe(0); @@ -166,7 +149,104 @@ it( const layout = await readFile(new URL("src/routes/+layout.svelte", project), "utf8"); expect(layout).toContain("const { data, children } = $props();"); expect(layout).toContain(""); - expect(layout).not.toContain("$lib/prismicio"); + expect(layout).not.toContain("#lib/prismicio"); + }, +); + +it( + "generates a slice simulator page with a snippet", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project); + + const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + const page = await readFile( + new URL("src/routes/slice-simulator/+page.svelte", project), + "utf8", + ); + // The closing tag must be "", not the bundler-escaped "<\/script>". + expect(page).toContain(""); + expect(page).toContain("import { components } from '#lib/slices/index.js';"); + expect(page).toContain("{#snippet children({ slices })}"); + expect(page).not.toContain("let:slices"); + }, +); + +it( + "declares the Prismic environment variable in a new SvelteKit env file", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project); + + const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + const env = await readFile(new URL("src/env.js", project), "utf8"); + expect(env).toContain("PUBLIC_PRISMIC_ENVIRONMENT: {"); + expect(env).toContain("public: true,"); + const client = await readFile(new URL("src/lib/prismicio.js", project), "utf8"); + expect(client).toContain("import { PUBLIC_PRISMIC_ENVIRONMENT } from '$app/env/public';"); + expect(stdout).not.toContain("PUBLIC_PRISMIC_ENVIRONMENT"); + }, +); + +it( + "asks for the Prismic environment variable in an existing SvelteKit env file", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project); + const env = + 'import { defineEnvVars } from "@sveltejs/kit/env";\n' + + "export const variables = defineEnvVars({ API_KEY: {} });\n"; + await mkdir(new URL("src/", project), { recursive: true }); + await writeFile(new URL("src/env.ts", project), env); + + const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + expect(await readFile(new URL("src/env.ts", project), "utf8")).toBe(env); + await expect(project).not.toHaveFile("src/env.js"); + expect(stdout).toContain("Action required: add PUBLIC_PRISMIC_ENVIRONMENT to src/env.ts."); + expect(stdout).toContain(" PUBLIC_PRISMIC_ENVIRONMENT: {"); + }, +); + +it( + "does not ask for the Prismic environment variable when the env file declares it", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project); + await mkdir(new URL("src/", project), { recursive: true }); + await writeFile( + new URL("src/env.js", project), + "export const variables = { PUBLIC_PRISMIC_ENVIRONMENT: { public: true } };\n", + ); + + const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + expect(stdout).not.toContain("PUBLIC_PRISMIC_ENVIRONMENT"); + }, +); + +it( + "sets up a SvelteKit 2 project with @prismicio/svelte v2", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "^2.27.0"); + + const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); + expect(packageJson.dependencies["@prismicio/svelte"]).toBe("^2"); + await expect(project).toHaveFile("src/lib/prismicio.js", { + contains: "import { env } from '$env/dynamic/public';", + }); + await expect(project).toHaveFile("src/routes/slice-simulator/+page.svelte", { + contains: "", + }); + await expect(project).toHaveFile("src/routes/+layout.server.js", { + contains: 'import { repositoryName } from "$lib/prismicio";', + }); + await expect(project).not.toHaveFile("src/env.js"); }, ); @@ -179,7 +259,7 @@ it( const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); expect(exitCode, stderr).toBe(0); const serverLayout = await readFile(new URL("src/routes/+layout.server.js", project), "utf8"); - expect(serverLayout).toContain('import { repositoryName } from "$lib/prismicio";'); + expect(serverLayout).toContain('import { repositoryName } from "#lib/prismicio.js";'); expect(serverLayout).toContain('export const prerender = "auto";'); expect(serverLayout).toContain("return { repositoryName };"); }, @@ -269,7 +349,7 @@ it( await writeFile(new URL("src/routes/+layout.svelte", project), "{@render children()}"); await writeFile( new URL("src/routes/+layout.server.js", project), - 'import { repositoryName } from "$lib/prismicio";\nexport const load = () => ({ repositoryName });', + 'import { repositoryName } from "#lib/prismicio.js";\nexport const load = () => ({ repositoryName });', ); const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); @@ -558,3 +638,67 @@ it( expect(stdout).not.toContain("delete src/routes/+page.svelte"); }, ); + +// SvelteKit 2 projects use their own adapter, which copies this setup logic. + +it( + "deletes the SvelteKit starter home page in a SvelteKit 2 project", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "^2.27.0"); + await mkdir(new URL("src/routes/", project), { recursive: true }); + await writeFile(new URL("src/routes/+page.svelte", project), SVELTEKIT_STARTER_HOME_PAGE); + + const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + await expect(project).not.toHaveFile("src/routes/+page.svelte"); + }, +); + +it( + "asks to delete a changed home page in a SvelteKit 2 project", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "^2.27.0"); + await mkdir(new URL("src/routes/[[preview=preview]]/", project), { recursive: true }); + await writeFile(new URL("src/routes/+page.svelte", project), "

My site

\n"); + await writeFile(new URL("src/routes/[[preview=preview]]/+page.svelte", project), ""); + + const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + expect(stdout).toContain("Action required: delete src/routes/+page.svelte."); + }, +); + +it( + "does not generate a SvelteKit 2 layout next to a server layout without the repository name", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "^2.27.0"); + await mkdir(new URL("src/routes/", project), { recursive: true }); + await writeFile( + new URL("src/routes/+layout.server.js", project), + 'export const load = () => ({ user: "x" });', + ); + + const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + await expect(project).not.toHaveFile("src/routes/+layout.svelte"); + expect(stdout).toContain("Return repositoryName from load in src/routes/+layout.server.js"); + expect(stdout).toContain('import { repositoryName } from "$lib/prismicio";'); + }, +); + +it( + "does not ask for the Prismic environment variable in a SvelteKit 2 project with its own src/env", + { timeout: 30_000 }, + async ({ expect, project, prismic }) => { + await useSvelteKit(project, "^2.27.0"); + await mkdir(new URL("src/", project), { recursive: true }); + await writeFile(new URL("src/env.ts", project), "export const apiKey = process.env.API_KEY;\n"); + + const { stdout, stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + expect(stdout).not.toContain("PUBLIC_PRISMIC_ENVIRONMENT"); + }, +); diff --git a/test/it.ts b/test/it.ts index 31918bbc..27f58e7e 100644 --- a/test/it.ts +++ b/test/it.ts @@ -196,12 +196,12 @@ export const it = test.extend({ }, }); -export async function useSvelteKit(project: URL): Promise { +export async function useSvelteKit(project: URL, kitVersion = "latest"): Promise { await rm(new URL("node_modules/next/", project), { recursive: true, force: true }); await rm(new URL("app/", project), { recursive: true, force: true }); await writeFile( new URL("package.json", project), - JSON.stringify({ dependencies: { "@sveltejs/kit": "latest", svelte: "latest" } }), + JSON.stringify({ dependencies: { "@sveltejs/kit": kitVersion, svelte: "latest" } }), ); } From b2e7ccd214ded3ee376b1ec21131444d8324aa44 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 19:13:43 +0000 Subject: [PATCH 18/22] test(env): rename the setup instructions test and drop a comment Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/commands/env-set.ts | 1 - test/env-set.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/env-set.ts b/src/commands/env-set.ts index 090719fb..720142f1 100644 --- a/src/commands/env-set.ts +++ b/src/commands/env-set.ts @@ -32,7 +32,6 @@ export default createCommand(config, async ({ positionals }) => { console.info(`Set the active environment to "${environment}".`); } - // The website only reads the environment once setup is finished. const setupInstructions = await adapter.getSetupInstructions(); if (setupInstructions) console.info(`\n${setupInstructions}`); }); diff --git a/test/env-set.test.ts b/test/env-set.test.ts index 3ff921ce..e80f0cf5 100644 --- a/test/env-set.test.ts +++ b/test/env-set.test.ts @@ -30,7 +30,7 @@ it("clears the active environment when setting to production", async ({ expect(after).not.toContain("my-stage-env"); }); -it("prints the open setup steps", async ({ expect, prismic, repo }) => { +it("prints setup instructions", async ({ expect, prismic, repo }) => { const { stdout, exitCode } = await prismic("env", ["set", repo]); expect(exitCode).toBe(0); expect(stdout).toContain("Action required: add to your root layout."); From 02ff55f5dba30b534b6421e4deeb5555543d4a83 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 19:18:04 +0000 Subject: [PATCH 19/22] fix: read peer dependencies in getInstalledMajor getAdapter detects frameworks from peer dependencies too, so getInstalledMajor now reads a peer range when the package is not a dependency or dev dependency. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/lib/packageJson.ts | 7 +++---- test/gen-setup.test.ts | 6 +----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/lib/packageJson.ts b/src/lib/packageJson.ts index f46a4022..b074532c 100644 --- a/src/lib/packageJson.ts +++ b/src/lib/packageJson.ts @@ -67,10 +67,9 @@ export async function getInstalledMajor(packageName: string): Promise { const major = Number.parseInt(version.split(".")[0]); return Number.isNaN(major) ? Infinity : major; } catch { - // Not installed yet, so read a simple range like "^2.27.0" from package.json. Other - // ranges, like "latest" or a Git URL, mean the newest major. - const { dependencies, devDependencies } = await readPackageJson(); - const range = { ...dependencies, ...devDependencies }[packageName] ?? ""; + // Not installed yet. Read the major and fall back to latest. + const { dependencies, devDependencies, peerDependencies } = await readPackageJson(); + const range = { ...peerDependencies, ...dependencies, ...devDependencies }[packageName] ?? ""; const major = /^[\^~]?(\d+)/.exec(range)?.[1]; return major ? Number(major) : Infinity; } diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 511187f1..639f7f96 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -279,11 +279,7 @@ it( }, ); -it("reads the Next.js version from package.json when it is not installed", async ({ - expect, - project, - prismic, -}) => { +it("works when Next.js is declared but not installed", async ({ expect, project, prismic }) => { await rm(new URL("node_modules/next/", project), { recursive: true }); await writeFile( new URL("package.json", project), From a7d1ee00894d48acb943ac7aab9427bdbe359797 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 19:20:41 +0000 Subject: [PATCH 20/22] refactor: remove a comment in getInstalledMajor Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- src/lib/packageJson.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/packageJson.ts b/src/lib/packageJson.ts index b074532c..4aaf16e5 100644 --- a/src/lib/packageJson.ts +++ b/src/lib/packageJson.ts @@ -67,7 +67,6 @@ export async function getInstalledMajor(packageName: string): Promise { const major = Number.parseInt(version.split(".")[0]); return Number.isNaN(major) ? Infinity : major; } catch { - // Not installed yet. Read the major and fall back to latest. const { dependencies, devDependencies, peerDependencies } = await readPackageJson(); const range = { ...peerDependencies, ...dependencies, ...devDependencies }[packageName] ?? ""; const major = /^[\^~]?(\d+)/.exec(range)?.[1]; From ec6e180641f9f71a02424c3f5be531137e16c96d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 19:22:49 +0000 Subject: [PATCH 21/22] test: split the Next.js declared version test The test for a project without node_modules now checks only that setup works. A separate test checks that setup uses the declared Next.js version. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- test/gen-setup.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 639f7f96..24af24e4 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -280,6 +280,20 @@ it( ); it("works when Next.js is declared but not installed", async ({ expect, project, prismic }) => { + // node_modules is absent, so the Next.js version cannot be read. + await rm(new URL("node_modules/next/", project), { recursive: true }); + + const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode, stderr).toBe(0); + + await expect(project).toHaveFile("app/api/revalidate/route.js"); +}); + +it("generates a Next.js 15 revalidate route before Next.js is installed", async ({ + expect, + project, + prismic, +}) => { await rm(new URL("node_modules/next/", project), { recursive: true }); await writeFile( new URL("package.json", project), From 3fa2665f22c06b74717a89ffc2537f5dedca3142 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 19:24:24 +0000 Subject: [PATCH 22/22] test: remove a comment in the Next.js 15 test Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L53y5wowGHwtT4aZJ3UsHY --- test/gen-setup.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 24af24e4..6962757f 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -303,7 +303,6 @@ it("generates a Next.js 15 revalidate route before Next.js is installed", async const { stderr, exitCode } = await prismic("gen", ["setup", "--no-install"]); expect(exitCode, stderr).toBe(0); - // Next.js 15 does not support cache life profiles in revalidateTag(). await expect(project).toHaveFile("app/api/revalidate/route.js", { contains: 'revalidateTag("prismic");', });