From dafee7a660c414564555cda429d2f6e8ca5b53eb Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Wed, 8 Jul 2026 23:47:12 +0000 Subject: [PATCH 1/4] feat: inject the active environment via `prismic/env/register` Add the `prismic/env/register` export, which sets the framework's public environment variable from the active environment at config-load time, and wire `gen setup` to import it in the framework config for Next.js, Nuxt, and SvelteKit. Co-Authored-By: Claude Opus 4.8 --- package.json | 4 ++++ src/adapters/index.ts | 26 ++++++++++++++++++++++-- src/adapters/nextjs.ts | 3 ++- src/adapters/nuxt.ts | 3 ++- src/adapters/sveltekit.ts | 3 ++- src/exports/env/register.ts | 8 ++++++++ src/lib/file.ts | 6 ++++++ src/lib/packageJson.ts | 1 + test/env-register.serial.test.ts | 34 ++++++++++++++++++++++++++++++++ test/gen-setup.test.ts | 33 ++++++++++++++++++++++++++++++- tsdown.config.ts | 1 + 11 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 src/exports/env/register.ts create mode 100644 test/env-register.serial.test.ts diff --git a/package.json b/package.json index d9cc4c10..18775dcd 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "./dist" ], "type": "module", + "exports": { + "./env/register": "./dist/env/register.mjs", + "./package.json": "./package.json" + }, "publishConfig": { "access": "public" }, diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 4e3fc7df..36a1875c 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -1,12 +1,12 @@ import type { CustomType, SharedSlice } from "@prismicio/types-internal/lib/customtypes"; import { pascalCase } from "change-case"; -import { rm } from "node:fs/promises"; +import { readFile, rm, writeFile } from "node:fs/promises"; import { pathToFileURL } from "node:url"; import { generateTypes } from "prismic-ts-codegen"; import { glob } from "tinyglobby"; -import { readJsonFile, writeFileRecursive } from "../lib/file"; +import { findFirstFile, readJsonFile, writeFileRecursive } from "../lib/file"; import { stringify } from "../lib/json"; import { readPackageJson } from "../lib/packageJson"; import { appendTrailingSlash } from "../lib/url"; @@ -206,3 +206,25 @@ export abstract class Adapter { return output; } } + +export async function addEnvRegisterImport(configFilenames: string[]): Promise { + const projectRoot = await findProjectRoot(); + const configUrl = await findFirstFile( + configFilenames.map((filename) => new URL(filename, projectRoot)), + ); + if (!configUrl) return; + + // The register module uses top-level await, so it can only be + // imported from an ESM config file. + let isEsm = configUrl.pathname.endsWith(".mjs") || configUrl.pathname.endsWith(".ts"); + if (!isEsm) { + const packageJson = await readPackageJson(); + isEsm = packageJson.type === "module"; + } + if (!isEsm) return; + + const statement = 'import "prismic/env/register";'; + const contents = await readFile(configUrl, "utf8"); + if (contents.includes(statement)) return; + await writeFile(configUrl, `${statement}\n\n${contents}`); +} diff --git a/src/adapters/nextjs.ts b/src/adapters/nextjs.ts index c21d83b5..937459de 100644 --- a/src/adapters/nextjs.ts +++ b/src/adapters/nextjs.ts @@ -5,7 +5,7 @@ import { createRequire } from "node:module"; import { relative } from "node:path"; import { fileURLToPath } from "node:url"; -import { Adapter } from "."; +import { Adapter, addEnvRegisterImport } from "."; import { getHost, getToken } from "../auth"; import { addPreview, getPreviews, getSimulatorUrl, setSimulatorUrl } from "../clients/core"; import { exists, writeFileRecursive } from "../lib/file"; @@ -38,6 +38,7 @@ export class NextJsAdapter extends Adapter { await createPreviewRoute(); await createExitPreviewRoute(); await createRevalidateRoute(); + await addEnvRegisterImport(["next.config.mjs", "next.config.ts", "next.config.js"]); } async onProjectInitialized(): Promise { diff --git a/src/adapters/nuxt.ts b/src/adapters/nuxt.ts index 7d358f4e..d3c7a3a3 100644 --- a/src/adapters/nuxt.ts +++ b/src/adapters/nuxt.ts @@ -6,7 +6,7 @@ import { readFile, rm } from "node:fs/promises"; import { relative } from "node:path"; import { fileURLToPath } from "node:url"; -import { Adapter } from "."; +import { Adapter, addEnvRegisterImport } from "."; import { getHost, getToken } from "../auth"; import { addPreview, getPreviews, getSimulatorUrl, setSimulatorUrl } from "../clients/core"; import { exists, writeFileRecursive } from "../lib/file"; @@ -31,6 +31,7 @@ export class NuxtAdapter extends Adapter { await createSliceSimulatorPage(); await moveOrDeleteAppVue(); await modifySliceLibraryPath(this); + await addEnvRegisterImport(["nuxt.config.ts", "nuxt.config.js"]); } async onProjectInitialized(): Promise { diff --git a/src/adapters/sveltekit.ts b/src/adapters/sveltekit.ts index d200626a..9436417e 100644 --- a/src/adapters/sveltekit.ts +++ b/src/adapters/sveltekit.ts @@ -7,7 +7,7 @@ import { createRequire } from "node:module"; import { relative } from "node:path"; import { fileURLToPath } from "node:url"; -import { Adapter } from "."; +import { Adapter, addEnvRegisterImport } from "."; import { getHost, getToken } from "../auth"; import { addPreview, getPreviews, getSimulatorUrl, setSimulatorUrl } from "../clients/core"; import { exists, writeFileRecursive } from "../lib/file"; @@ -42,6 +42,7 @@ export class SvelteKitAdapter extends Adapter { await createRootLayoutServerFile(); await createRootLayoutFile(); await modifyViteConfig(); + await addEnvRegisterImport(["vite.config.ts", "vite.config.js"]); } async onProjectInitialized(): Promise { diff --git a/src/exports/env/register.ts b/src/exports/env/register.ts new file mode 100644 index 00000000..a7e8dbb3 --- /dev/null +++ b/src/exports/env/register.ts @@ -0,0 +1,8 @@ +import { getEnvironment } from "../../environments"; + +const environment = await getEnvironment(); +if (environment) { + process.env.NEXT_PUBLIC_PRISMIC_ENVIRONMENT ??= environment; + process.env.PUBLIC_PRISMIC_ENVIRONMENT ??= environment; + process.env.NUXT_PUBLIC_PRISMIC_ENVIRONMENT ??= environment; +} diff --git a/src/lib/file.ts b/src/lib/file.ts index bf644731..d4551cee 100644 --- a/src/lib/file.ts +++ b/src/lib/file.ts @@ -49,6 +49,12 @@ export async function exists(path: URL): Promise { } } +export async function findFirstFile(candidates: URL[]): Promise { + for (const candidate of candidates) { + if (await exists(candidate)) return candidate; + } +} + export async function writeFileRecursive( path: URL, data: Parameters[1], diff --git a/src/lib/packageJson.ts b/src/lib/packageJson.ts index 9b71f119..8ab32477 100644 --- a/src/lib/packageJson.ts +++ b/src/lib/packageJson.ts @@ -12,6 +12,7 @@ const PackageJsonSchema = z.object({ devDependencies: z.optional(z.record(z.string(), z.string())), peerDependencies: z.optional(z.record(z.string(), z.string())), packageManager: z.optional(z.string()), + type: z.optional(z.string()), }); type PackageJson = z.infer; diff --git a/test/env-register.serial.test.ts b/test/env-register.serial.test.ts new file mode 100644 index 00000000..366f5d5e --- /dev/null +++ b/test/env-register.serial.test.ts @@ -0,0 +1,34 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { vi } from "vitest"; + +import { it } from "./it"; + +const REGISTER = fileURLToPath(new URL("../dist/env/register.mjs", import.meta.url)); + +const ENV_VARS = [ + "NEXT_PUBLIC_PRISMIC_ENVIRONMENT", + "PUBLIC_PRISMIC_ENVIRONMENT", + "NUXT_PUBLIC_PRISMIC_ENVIRONMENT", +] as const; + +it("does not set env vars when no environment is configured", async ({ expect, home, project }) => { + process.chdir(fileURLToPath(project)); + vi.stubEnv("PRISMIC_CONFIG_DIR", fileURLToPath(new URL(".config/prismic/", home))); + vi.resetModules(); + await import(REGISTER); + for (const key of ENV_VARS) expect(process.env[key]).toBeUndefined(); +}); + +it("sets env vars when an environment is configured", async ({ expect, home, project }) => { + process.chdir(fileURLToPath(project)); + vi.stubEnv("PRISMIC_CONFIG_DIR", fileURLToPath(new URL(".config/prismic/", home))); + await mkdir(new URL(".config/prismic/", home), { recursive: true }); + await writeFile( + new URL(".config/prismic/environments.json", home), + JSON.stringify({ [project.toString()]: "my-stage-env" }), + ); + vi.resetModules(); + await import(REGISTER); + for (const key of ENV_VARS) expect(process.env[key]).toBe("my-stage-env"); +}); diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index 113e0a5f..a111f754 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -1,4 +1,4 @@ -import { mkdir, writeFile } from "node:fs/promises"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; import { it } from "./it"; @@ -61,6 +61,37 @@ it("generates valid script tags for SvelteKit", { timeout: 30_000 }, async ({ }); }); +it("adds the env register import to the framework config", { timeout: 30_000 }, async ({ + expect, + project, + prismic, +}) => { + const configPath = new URL("next.config.mjs", project); + await writeFile(configPath, "export default {};\n"); + + const { exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode).toBe(0); + + const contents = await readFile(configPath, "utf8"); + expect(contents).toBe('import "prismic/env/register";\n\nexport default {};\n'); +}); + +it("skips the env register import for a CommonJS config", { timeout: 30_000 }, async ({ + expect, + project, + prismic, +}) => { + // The fixture's package.json has no "type": "module", so a .js config is CommonJS. + const configPath = new URL("next.config.js", project); + await writeFile(configPath, "module.exports = {};\n"); + + const { exitCode } = await prismic("gen", ["setup", "--no-install"]); + expect(exitCode).toBe(0); + + const contents = await readFile(configPath, "utf8"); + expect(contents).toBe("module.exports = {};\n"); +}); + it("skips installation with --no-install", async ({ expect, project, prismic }) => { const { exitCode, stdout } = await prismic("gen", ["setup", "--no-install"]); expect(exitCode).toBe(0); diff --git a/tsdown.config.ts b/tsdown.config.ts index 241d2f17..a3673d6e 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -6,6 +6,7 @@ const TEST = MODE === "test"; export default defineConfig({ entry: { index: "./src/index.ts", + "env/register": "./src/exports/env/register.ts", "subprocesses/refreshToken": "./src/subprocesses/refreshToken.ts", "subprocesses/sendSegmentEvents": "./src/subprocesses/sendSegmentEvents.ts", "subprocesses/updateVersionState": "./src/subprocesses/updateVersionState.ts", From db399e01c4e6e9f25ac9911c4777fe470035f776 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Thu, 9 Jul 2026 00:21:52 +0000 Subject: [PATCH 2/4] feat: install `prismic` for the env register import and tailor update notice `gen setup` adds `prismic` as a project dependency when it injects the `prismic/env/register` import, so the import resolves at runtime. When `prismic` is installed as a project dependency, the update notice now points to the package manager (e.g. `npm install prismic@latest`) instead of `npx`, which would fetch a fresh copy without changing the installed version. Co-Authored-By: Claude Opus 4.8 --- src/adapters/index.ts | 6 +++++- src/lib/packageJson.ts | 25 +++++++++++++++++++++++++ src/lib/update-notifier.ts | 11 +++++++++-- test/gen-setup.test.ts | 4 ++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 36a1875c..0727b8d9 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -8,7 +8,7 @@ import { glob } from "tinyglobby"; import { findFirstFile, readJsonFile, writeFileRecursive } from "../lib/file"; import { stringify } from "../lib/json"; -import { readPackageJson } from "../lib/packageJson"; +import { addDependencies, getNpmPackageVersion, readPackageJson } from "../lib/packageJson"; import { appendTrailingSlash } from "../lib/url"; import { addRoute, removeRoute, updateRoute } from "../project"; import { findProjectRoot, getLibraries } from "../project"; @@ -227,4 +227,8 @@ export async function addEnvRegisterImport(configFilenames: string[]): Promise { + let packageJson: PackageJson; + try { + packageJson = await readPackageJson(); + } catch { + return undefined; + } + const installed = + packageJson.dependencies?.[name] ?? + packageJson.devDependencies?.[name] ?? + packageJson.peerDependencies?.[name]; + if (!installed) return undefined; + const packageManager = await detectPackageManager(); + return `${ADD_COMMANDS[packageManager]} ${name}@latest`; +} + export async function installDependencies(): Promise { const packageJsonPath = await findPackageJson(); const cwd = new URL(".", packageJsonPath); diff --git a/src/lib/update-notifier.ts b/src/lib/update-notifier.ts index fbd4ee5c..5fc7cc8b 100644 --- a/src/lib/update-notifier.ts +++ b/src/lib/update-notifier.ts @@ -6,7 +6,7 @@ import * as z from "zod/mini"; import packageJson from "../../package.json" with { type: "json" }; import { stringify } from "./json"; -import { getNpmPackageVersion } from "./packageJson"; +import { getNpmPackageVersion, getUpdateCommand } from "./packageJson"; const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; @@ -29,7 +29,14 @@ export async function initUpdateNotifier(options: UpdateNotifierOptions): Promis const currentVersion = packageJson.version; if (state?.latestKnownVersion && isNewer(state.latestKnownVersion, currentVersion)) { - const message = `Update available: ${currentVersion} → ${state.latestKnownVersion}. Run \`npx ${options.npmPackageName}@latest --version\` to update.`; + // When `prismic` is a project dependency, `npx` would fetch a fresh + // copy instead of updating the installed one, so point the user at + // their package manager instead. + const updateCommand = await getUpdateCommand(options.npmPackageName); + const instruction = updateCommand + ? `Run \`${updateCommand}\` to update.` + : `Run \`npx ${options.npmPackageName}@latest --version\` to update.`; + const message = `Update available: ${currentVersion} → ${state.latestKnownVersion}. ${instruction}`; process.on("exit", () => { try { console.error(`\n${message}`); diff --git a/test/gen-setup.test.ts b/test/gen-setup.test.ts index a111f754..fae6f122 100644 --- a/test/gen-setup.test.ts +++ b/test/gen-setup.test.ts @@ -74,6 +74,10 @@ it("adds the env register import to the framework config", { timeout: 30_000 }, const contents = await readFile(configPath, "utf8"); expect(contents).toBe('import "prismic/env/register";\n\nexport default {};\n'); + + // The import needs `prismic` installed to resolve, so it is added as a dependency. + const packageJson = JSON.parse(await readFile(new URL("package.json", project), "utf8")); + expect(packageJson.dependencies).toHaveProperty("prismic"); }); it("skips the env register import for a CommonJS config", { timeout: 30_000 }, async ({ From c29e9c2c5473b3f20d160fe4117eb117823ad278 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Thu, 9 Jul 2026 02:49:11 +0000 Subject: [PATCH 3/4] fix: read the active environment synchronously in `prismic/env/register` The module used top-level `await`, which fails when a bundler loads a config file that doesn't support it (e.g. Next.js loading `next.config.ts`), throwing `ReferenceError: await is not defined`. Resolve the active environment synchronously instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/environments.ts | 2 +- src/exports/env/register.ts | 40 +++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/environments.ts b/src/environments.ts index 3ed8a75f..033846c6 100644 --- a/src/environments.ts +++ b/src/environments.ts @@ -9,7 +9,7 @@ import { stringify } from "./lib/json"; import { findProjectRoot } from "./project"; const EnvironmentsSchema = z.partialRecord(z.string(), z.string()); -type Environments = z.infer; +export type Environments = z.infer; export async function getEnvironment(): Promise { const projectRoot = await findProjectRoot(); diff --git a/src/exports/env/register.ts b/src/exports/env/register.ts index a7e8dbb3..f00a931e 100644 --- a/src/exports/env/register.ts +++ b/src/exports/env/register.ts @@ -1,8 +1,44 @@ -import { getEnvironment } from "../../environments"; +import { existsSync, readFileSync, realpathSync } from "node:fs"; +import { fileURLToPath, pathToFileURL } from "node:url"; -const environment = await getEnvironment(); +import type { Environments } from "../../environments"; + +import { ENVIRONMENTS_PATH } from "../../config"; +import { appendTrailingSlash } from "../../lib/url"; + +const environment = getEnvironment(); if (environment) { process.env.NEXT_PUBLIC_PRISMIC_ENVIRONMENT ??= environment; process.env.PUBLIC_PRISMIC_ENVIRONMENT ??= environment; process.env.NUXT_PUBLIC_PRISMIC_ENVIRONMENT ??= environment; } + +// Reads the active environment synchronously so this module can run without +// top-level `await`. Bundlers that load config files (e.g. Next.js loading +// `next.config.ts`) don't support top-level `await` and fail otherwise. +function getEnvironment(): string | undefined { + const projectRoot = findProjectRoot(); + if (!projectRoot) return; + try { + const contents = readFileSync(ENVIRONMENTS_PATH, "utf-8"); + const environments: Environments = JSON.parse(contents); + return environments[projectRoot.toString()]; + } catch { + return; + } +} + +function findProjectRoot(): URL | undefined { + let dir = appendTrailingSlash(pathToFileURL(process.cwd())); + while (true) { + if ( + existsSync(new URL("prismic.config.json", dir)) || + existsSync(new URL("package.json", dir)) + ) { + return appendTrailingSlash(pathToFileURL(realpathSync(fileURLToPath(dir)))); + } + const parent = new URL("..", dir); + if (parent.href === dir.href) return; + dir = parent; + } +} From b2fa3111d62b5a32d1b6ab4365cfefeb0bdf19f1 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Thu, 9 Jul 2026 03:45:30 +0000 Subject: [PATCH 4/4] feat: read the active environment in the generated `prismicio` file Fall back to the repository name from `prismic.config.json` when no active environment is set. Next.js reads `process.env.NEXT_PUBLIC_PRISMIC_ENVIRONMENT`; SvelteKit reads `PUBLIC_PRISMIC_ENVIRONMENT` via `$env/dynamic/public`. Nuxt needs no change (`@nuxtjs/prismic` reads `NUXT_PUBLIC_PRISMIC_ENVIRONMENT`). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/adapters/nextjs.templates.ts | 6 ++++-- src/adapters/sveltekit.templates.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/adapters/nextjs.templates.ts b/src/adapters/nextjs.templates.ts index fce35bca..3cac94b1 100644 --- a/src/adapters/nextjs.templates.ts +++ b/src/adapters/nextjs.templates.ts @@ -357,7 +357,8 @@ export function prismicIOFileTemplate(args: { /** * The project's Prismic repository name. */ - export const repositoryName = prismicConfig.repositoryName; + export const repositoryName = + process.env.NEXT_PUBLIC_PRISMIC_ENVIRONMENT ?? prismicConfig.repositoryName; ${createClientContents} `; @@ -369,7 +370,8 @@ export function prismicIOFileTemplate(args: { /** * The project's Prismic repository name. */ - export const repositoryName = prismicConfig.repositoryName; + export const repositoryName = + process.env.NEXT_PUBLIC_PRISMIC_ENVIRONMENT ?? prismicConfig.repositoryName; ${createClientContents} `; diff --git a/src/adapters/sveltekit.templates.ts b/src/adapters/sveltekit.templates.ts index 9a07509a..32699181 100644 --- a/src/adapters/sveltekit.templates.ts +++ b/src/adapters/sveltekit.templates.ts @@ -11,12 +11,14 @@ export function prismicIOFileTemplate(args: { typescript: boolean }): string { 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 = prismicConfig.repositoryName; + export const repositoryName = + env.PUBLIC_PRISMIC_ENVIRONMENT ?? prismicConfig.repositoryName; /** * Creates a Prismic client for the project's repository. The client is used to @@ -40,12 +42,14 @@ export function prismicIOFileTemplate(args: { typescript: boolean }): string { 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 = prismicConfig.repositoryName; + export const repositoryName = + env.PUBLIC_PRISMIC_ENVIRONMENT ?? prismicConfig.repositoryName; /** * Creates a Prismic client for the project's repository. The client is used to