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
56 changes: 49 additions & 7 deletions src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import { openBrowser } from "../lib/browser";
import { createCommand, type CommandConfig, CommandError, exclusiveOptions } from "../lib/command";
import { readJsonFile, watchFiles, writeFileRecursive } from "../lib/file";
import { stringify } from "../lib/json";
import { createRelease, deleteRelease } from "../lib/prismic/clients/core";
import {
createRelease,
deleteRelease,
getPreviews,
getReleasePreviewURL,
refreshRelease,
} from "../lib/prismic/clients/core";
import type { CustomTypesConfig } from "../lib/prismic/clients/custom-types";
import {
canonicalizeCustomType,
Expand Down Expand Up @@ -41,8 +47,12 @@ const config = {
Type Builder changes are saved to local files, and local file changes
appear in the Type Builder.

The "Preview:" line opens your website with these models, and the
preview updates as they change.

The command runs until you stop it. Agents should run it in the
background and give the user the URL from its "Type Builder:" line.
background and give the user the URLs from its "Type Builder:" and
"Preview:" lines.

@experimental - This command may change or be removed in any release and does not follow semver.
`,
Expand Down Expand Up @@ -86,12 +96,23 @@ export default createCommand(config, async ({ values }) => {
session = await createSession({ repo, token, host });
}
const release = { repo, token, host, releaseId: session.releaseId };
// The release preview only shows model changes once rebuilt. A failed rebuild stays pending and is retried on the next sync.
let needsRefresh = true;
const refresh = async () => {
if (!needsRefresh) return;
await refreshRelease(release.releaseId, release);
needsRefresh = false;
};
const changed = (message: (ids: string[]) => string) => (ids: string[]) => {
needsRefresh = true;
log(message(ids));
};
const sync = (push = true) =>
syncModels(adapter, release, session, sessionPath, {
push,
onPull: (ids) => log(`↓ Pulled ${names(ids)}`),
onPush: (ids) => log(`↑ Pushed ${names(ids)}`),
onDelete: (ids) => log(`− Deleted ${names(ids)}`),
onPull: changed((ids) => `↓ Pulled ${names(ids)}`),
onPush: changed((ids) => `↑ Pushed ${names(ids)}`),
onDelete: changed((ids) => `− Deleted ${names(ids)}`),
});

const end = async (): Promise<boolean> => {
Expand All @@ -116,6 +137,7 @@ export default createCommand(config, async ({ values }) => {
const models = continueSession
? await sync()
: await seedRelease(adapter, release, session, sessionPath);
await refresh().catch(() => {});
const types = models.customTypes.length;
const slices = models.slices.length;
console.info(
Expand All @@ -125,8 +147,11 @@ export default createCommand(config, async ({ values }) => {
const url = new URL("builder/types", `https://${repo}.${host}/`);
url.searchParams.set("r", release.releaseId);
console.info(`Type Builder: ${url}`);
await printPreviewURL(release);
if (noBrowser) {
console.info("Open the URL above to start editing. Changes sync both ways while this runs.");
console.info(
"Open the Type Builder URL above to start editing. Changes sync both ways while this runs.",
);
} else {
openBrowser(url);
console.info("Opened in your browser. Changes sync both ways while this runs.");
Expand All @@ -138,7 +163,11 @@ export default createCommand(config, async ({ values }) => {
if (message !== lastError) log(`! ${message}`, console.error);
lastError = message;
};
await syncUntilStopped(sync, waitForChange, stopping.signal, {
const syncAndRefresh = async () => {
await sync();
await refresh();
};
await syncUntilStopped(syncAndRefresh, waitForChange, stopping.signal, {
onSync: () => {
lastError = undefined;
},
Expand Down Expand Up @@ -184,6 +213,19 @@ async function createSession({ repo, token, host }: CustomTypesConfig): Promise<
return { repo, releaseId, pid: process.pid, synced: {} };
}

async function printPreviewURL({ releaseId, ...config }: Release): Promise<void> {
try {
const [preview] = await getPreviews(config);
if (!preview) {
console.info("To preview your website with these models, run `prismic preview add <url>`.");
return;
}
console.info(`Preview: ${await getReleasePreviewURL(releaseId, preview.id, config)}`);
} catch (error) {
console.error(`Could not create a preview link: ${await getErrorMessage(error)}`);
}
}

async function endSession({ releaseId, ...config }: Release, sessionPath: URL): Promise<void> {
await deleteRelease(releaseId, config);
await rm(sessionPath, { force: true });
Expand Down
32 changes: 32 additions & 0 deletions src/lib/prismic/clients/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,38 @@ export async function deleteRelease(id: string, config: CoreConfig): Promise<voi
});
}

export async function refreshRelease(id: string, config: CoreConfig): Promise<void> {
const { repo, host } = config;
const url = new URL(
`core/releases/${encodeURIComponent(id)}/refresh`,
getCoreServiceUrl(repo, host),
);
await coreServiceRequest(url, config, {
method: "POST",
notFoundMessage: `Release not found: ${id}`,
unknownErrorMessage: "Failed to refresh the release preview",
});
}

export async function getReleasePreviewURL(
releaseId: string,
previewId: string,
config: CoreConfig,
): Promise<string> {
const { repo, host } = config;
const url = new URL(
`core/previews/session/releases/${encodeURIComponent(releaseId)}`,
getCoreServiceUrl(repo, host),
);
url.searchParams.set("previewId", previewId);
const response = await coreServiceRequest(url, config, {
schema: z.looseObject({ preview_url: z.string() }),
notFoundMessage: `Release not found: ${releaseId}`,
unknownErrorMessage: "Failed to create a preview link",
});
return response.preview_url;
}

function coreServiceRequest<T>(
url: URL,
config: CoreConfig,
Expand Down
35 changes: 34 additions & 1 deletion test/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ import {
writeLocalCustomType,
writeLocalSlice,
} from "./it";
import { getCustomTypes, getSlices, insertCustomType, updateCustomType } from "./prismic";
import {
addPreview,
getCustomTypes,
getReleasePreviewToken,
getSlices,
insertCustomType,
updateCustomType,
} from "./prismic";

it("supports --help", async ({ expect, prismic }) => {
const { stdout, stderr, exitCode } = await prismic("dev", ["--help"]);
Expand Down Expand Up @@ -188,6 +195,7 @@ describe("with an isolated repository", () => {

const { proc, output, releaseId } = await startSession(prismic, expect);
expect(output()).toContain(`https://${repo}.${host}/builder/types?r=`);
await expect.poll(output, { timeout: 30_000 }).toContain("prismic preview add");

const releaseTypeIds = (await getCustomTypes({ repo, token, host, releaseId })).map(
(m) => m.id,
Expand Down Expand Up @@ -218,6 +226,31 @@ describe("with an isolated repository", () => {
expect(await readdir(new URL(".config/prismic/dev/", home))).toEqual([]);
}, 120_000);

it("rebuilds the release preview when a model changes", async ({
expect,
prismic,
project,
repo,
token,
host,
}) => {
await addPreview("https://example.com/api/preview", "Test", { repo, token, host });
const customType = buildCustomType();
await writeLocalCustomType(project, customType);

const { output, releaseId } = await startSession(prismic, expect);
await expect
.poll(output, { timeout: 30_000 })
.toContain("Preview: https://example.com/api/preview?token=");
const before = await getReleasePreviewToken(releaseId, { repo, token, host });

await writeLocalCustomType(project, { ...customType, label: "Edited" });
await expect.poll(output, { timeout: 30_000 }).toContain(`Pushed ${customType.id}`);
await expect
.poll(() => getReleasePreviewToken(releaseId, { repo, token, host }), { timeout: 30_000 })
.not.toBe(before);
}, 90_000);

it("keeps syncing after a rejected change", async ({
expect,
prismic,
Expand Down
19 changes: 19 additions & 0 deletions test/prismic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,25 @@ export async function addPreview(
if (!res.ok) throw new Error(`Failed to add preview: ${res.status} ${await res.text()}`);
}

export async function getReleasePreviewToken(
releaseId: string,
config: RepoConfig,
): Promise<string | null> {
const host = config.host ?? DEFAULT_HOST;
const [preview] = await getPreviews(config);
const url = new URL(
`core/previews/session/releases/${releaseId}`,
`https://${config.repo}.${host}/`,
);
url.searchParams.set("previewId", preview.id);
const res = await fetch(url, {
headers: { Cookie: `prismic-auth=${config.token}` },
});
if (!res.ok) throw new Error(`Failed to get release preview: ${res.status} ${await res.text()}`);
const data = await res.json();
return new URL(data.preview_url).searchParams.get("token");
}

export async function getLocales(
config: RepoConfig,
): Promise<{ id: string; label: string; customName: string | null; isMaster: boolean }[]> {
Expand Down
Loading