Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"./dist"
],
"type": "module",
"exports": {
"./env/register": "./dist/env/register.mjs",
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public"
},
Expand Down
32 changes: 29 additions & 3 deletions src/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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 { addDependencies, getNpmPackageVersion, readPackageJson } from "../lib/packageJson";
import { appendTrailingSlash } from "../lib/url";
import { addRoute, removeRoute, updateRoute } from "../project";
import { findProjectRoot, getLibraries } from "../project";
Expand Down Expand Up @@ -206,3 +206,29 @@ export abstract class Adapter {
return output;
}
}

export async function addEnvRegisterImport(configFilenames: string[]): Promise<void> {
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}`);

// The config now imports `prismic/env/register`, so the project needs
// `prismic` installed to resolve it.
await addDependencies({ prismic: `^${await getNpmPackageVersion("prismic")}` });
}
6 changes: 4 additions & 2 deletions src/adapters/nextjs.templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
`;
Expand All @@ -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}
`;
Expand Down
3 changes: 2 additions & 1 deletion src/adapters/nextjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<void> {
Expand Down
3 changes: 2 additions & 1 deletion src/adapters/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<void> {
Expand Down
8 changes: 6 additions & 2 deletions src/adapters/sveltekit.templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/adapters/sveltekit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<void> {
Expand Down
2 changes: 1 addition & 1 deletion src/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof EnvironmentsSchema>;
export type Environments = z.infer<typeof EnvironmentsSchema>;

export async function getEnvironment(): Promise<string | undefined> {
const projectRoot = await findProjectRoot();
Expand Down
44 changes: 44 additions & 0 deletions src/exports/env/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { existsSync, readFileSync, realpathSync } from "node:fs";
import { fileURLToPath, pathToFileURL } from "node:url";

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;
}
}
6 changes: 6 additions & 0 deletions src/lib/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export async function exists(path: URL): Promise<boolean> {
}
}

export async function findFirstFile(candidates: URL[]): Promise<URL | undefined> {
for (const candidate of candidates) {
if (await exists(candidate)) return candidate;
}
}

export async function writeFileRecursive(
path: URL,
data: Parameters<typeof writeFile>[1],
Expand Down
26 changes: 26 additions & 0 deletions src/lib/packageJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof PackageJsonSchema>;

Expand Down Expand Up @@ -75,6 +76,31 @@ const INSTALL_COMMANDS = {
bun: ["bun", "install"],
};

const ADD_COMMANDS = {
npm: "npm install",
yarn: "yarn add",
pnpm: "pnpm add",
bun: "bun add",
};

// Returns a package-manager-specific command to update a dependency to its
// latest version, or undefined if the package is not a project dependency.
export async function getUpdateCommand(name: string): Promise<string | undefined> {
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<void> {
const packageJsonPath = await findPackageJson();
const cwd = new URL(".", packageJsonPath);
Expand Down
11 changes: 9 additions & 2 deletions src/lib/update-notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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}`);
Expand Down
34 changes: 34 additions & 0 deletions test/env-register.serial.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
37 changes: 36 additions & 1 deletion test/gen-setup.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir, readFile, writeFile } from "node:fs/promises";

import { it } from "./it";

Expand Down Expand Up @@ -61,6 +61,41 @@ 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');

// 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 ({
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);
Expand Down
1 change: 1 addition & 0 deletions tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading