Skip to content
Merged
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
13 changes: 11 additions & 2 deletions src/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { generateTypes } from "prismic-ts-codegen";
import { glob } from "tinyglobby";

import { getCredentials } from "../auth";
import { CommandError } from "../lib/command";
import {
exists,
readEnvFile,
Expand All @@ -28,6 +29,7 @@ import {
import {
canonicalizeCustomType,
canonicalizeSlice,
getLegacySlices,
type Models,
type ModelsDiff,
} from "../lib/prismic/models";
Expand Down Expand Up @@ -181,8 +183,15 @@ export abstract class Adapter {

async getSlice(id: string): Promise<ModelMeta<SharedSliceModel>> {
const slice = (await this.getSlices()).find((s) => s.model.id === id);
if (!slice) throw new Error(`No slice found with ID: ${id}`);
return slice;
if (slice) return slice;
const customTypes = (await this.getCustomTypes()).map((customType) => customType.model);
const legacySlice = getLegacySlices(customTypes).find((s) => s.id === id);
if (legacySlice) {
throw new CommandError(
`"${id}" is a legacy slice in "${legacySlice.customTypeId}". The CLI cannot edit legacy slices.`,
);
}
throw new Error(`No slice found with ID: ${id}`);
}

async createSlice(model: SharedSliceModel): Promise<void> {
Expand Down
48 changes: 40 additions & 8 deletions src/commands/slice-list.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,64 @@
import { getAdapter } from "../adapters";
import { createCommand, type CommandConfig } from "../lib/command";
import { stringify } from "../lib/json";
import { getLegacySlices } from "../lib/prismic/models";
import { formatTable } from "../lib/string";

const config = {
name: "prismic slice list",
description: "List all slices.",
description: `
List all slices.

Legacy slices from the Legacy Builder are marked with * and show the type
that defines them.
`,
options: {
legacy: { type: "boolean", description: "List only legacy slices" },
json: { type: "boolean", description: "Output as JSON" },
},
} satisfies CommandConfig;

export default createCommand(config, async ({ values }) => {
const { json } = values;
const { legacy, json } = values;

const adapter = await getAdapter();
const slices = await adapter.getSlices();
const slices = legacy ? [] : (await adapter.getSlices()).map((slice) => slice.model);
const customTypes = (await adapter.getCustomTypes()).map((customType) => customType.model);
const legacySlices = getLegacySlices(customTypes);

if (json && legacy) {
const rows = legacySlices.map((s) => ({
id: s.id,
definedIn: s.customTypeId,
sliceZone: s.sliceZoneId,
}));
console.info(stringify(rows));
return;
}

if (json) {
console.info(stringify(slices.map((s) => s.model)));
console.info(stringify(slices));
return;
}

if (slices.length === 0) {
console.info("No slices found.");
if (slices.length === 0 && legacySlices.length === 0) {
console.info(legacy ? "No legacy slices found." : "No slices found.");
return;
}

const rows = slices.map(({ model }) => [model.name, model.id]);
console.info(formatTable(rows, { headers: ["NAME", "ID"] }));
const rows = [
...slices.map((slice) => [slice.name, slice.id, ""]),
...legacySlices.map(({ id, model, customTypeId, sliceZoneId }) => {
const name = "fieldset" in model ? model.fieldset : model.config?.label;
return [
`${name || id} *`,
id,
sliceZoneId === "body" ? customTypeId : `${customTypeId} (${sliceZoneId} slice zone)`,
];
}),
];
console.info(formatTable(rows, { headers: ["NAME", "ID", "DEFINED IN"] }));
if (legacySlices.length > 0) {
console.info("\n* Legacy slice. The CLI cannot edit legacy slices.");
}
});
12 changes: 12 additions & 0 deletions src/commands/type-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getAdapter } from "../adapters";
import { formatFieldTable } from "../fields";
import { createCommand, type CommandConfig } from "../lib/command";
import { stringify } from "../lib/json";
import { getLegacySlices } from "../lib/prismic/models";
import { readConfig } from "../project";

const config = {
Expand Down Expand Up @@ -39,5 +40,16 @@ export default createCommand(config, async ({ positionals, values }) => {
console.info("");
console.info(`${tabName}:`);
console.info(formatFieldTable(fields));
for (const [fieldId, field] of Object.entries(fields)) {
if (field.type !== "Slices") continue;
const sliceIds = Object.entries(field.config?.choices ?? {}).map(([sliceId, choice]) =>
choice.type === "SharedSlice" ? sliceId : `${sliceId} *`,
);
console.info(`\n ${fieldId} slices: ${sliceIds.join(", ") || "(none)"}`);
}
}

if (getLegacySlices([type]).length > 0) {
console.info("\n* Legacy slice. The CLI cannot edit legacy slices.");
}
});
25 changes: 25 additions & 0 deletions src/lib/prismic/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,31 @@ function resolveNestedFieldContainer(
return resolveNestedFieldContainer(remaining.join("."), field.config.fields);
}

type SliceChoice = NonNullable<NonNullable<DynamicSlicesModel["config"]>["choices"]>[string];

export type LegacySlice = {
id: string;
customTypeId: string;
sliceZoneId: string;
model: Exclude<SliceChoice, { type: "SharedSlice" }>;
};

/** Slices defined inside a slice zone by the Legacy Builder. */
export function getLegacySlices(customTypes: DynamicCustomTypeModel[]): LegacySlice[] {
return customTypes.flatMap((customType) =>
Object.values(customType.json).flatMap((tab) =>
Object.entries(tab).flatMap(([sliceZoneId, sliceZone]) => {
if (sliceZone.type !== "Slices") return [];
return Object.entries(sliceZone.config?.choices ?? {}).flatMap(([id, model]) =>
model.type === "SharedSlice"
? []
: [{ id, customTypeId: customType.id, sliceZoneId, model }],
);
}),
),
);
}

export class FieldExistsError extends Error {
name = "FieldExistsError";

Expand Down
31 changes: 31 additions & 0 deletions test/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,37 @@ export function buildCustomType(
};
}

/** A type whose "body" slice zone holds a Slice, a Group, and a field legacy slice. */
export function buildLegacyCustomType(): DynamicCustomTypeModel {
return buildCustomType({
json: {
Main: {
body: {
type: "Slices",
config: {
choices: {
hero: {
type: "Slice",
fieldset: "Hero",
"non-repeat": {
title: { type: "StructuredText", config: { label: "Title", single: "heading1" } },
},
repeat: { label: { type: "Text", config: { label: "Label" } } },
},
gallery: {
type: "Group",
fieldset: "Gallery",
config: { fields: { caption: { type: "Text", config: { label: "Caption" } } } },
},
quote: { type: "Text", config: { label: "Quote" } },
},
},
},
},
},
});
}

export function buildSlice(overrides?: Partial<SharedSliceModel>): SharedSliceModel {
const id = crypto.randomUUID().split("-")[0];
return {
Expand Down
24 changes: 23 additions & 1 deletion test/slice-list.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildSlice, it, writeLocalSlice } from "./it";
import { buildLegacyCustomType, buildSlice, it, writeLocalCustomType, writeLocalSlice } from "./it";

it("supports --help", async ({ expect, prismic }) => {
const { stdout, stderr, exitCode } = await prismic("slice", ["list", "--help"]);
Expand All @@ -24,3 +24,25 @@ it("lists slices as JSON", async ({ expect, prismic, project }) => {
const parsed = JSON.parse(stdout);
expect(parsed).toEqual(expect.arrayContaining([expect.objectContaining({ id: slice.id })]));
});

it("marks legacy slices", async ({ expect, prismic, project }) => {
const customType = buildLegacyCustomType();
await writeLocalCustomType(project, customType);

const { stdout, stderr, exitCode } = await prismic("slice", ["list"]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toMatch(new RegExp(`Hero \\*\\s+hero\\s+${customType.id}`));
expect(stdout).toContain("* Legacy slice. The CLI cannot edit legacy slices.");
});

it("lists only legacy slices as JSON", async ({ expect, prismic, project }) => {
await writeLocalSlice(project, buildSlice());
const customType = buildLegacyCustomType();
await writeLocalCustomType(project, customType);

const { stdout, stderr, exitCode } = await prismic("slice", ["list", "--legacy", "--json"]);
expect(exitCode, stderr).toBe(0);
const parsed = JSON.parse(stdout);
expect(parsed).toHaveLength(3);
expect(parsed).toContainEqual({ id: "hero", definedIn: customType.id, sliceZone: "body" });
});
13 changes: 12 additions & 1 deletion test/slice-view.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildSlice, it, writeLocalSlice } from "./it";
import { buildLegacyCustomType, buildSlice, it, writeLocalCustomType, writeLocalSlice } from "./it";

it("supports --help", async ({ expect, prismic }) => {
const { stdout, stderr, exitCode } = await prismic("slice", ["view", "--help"]);
Expand Down Expand Up @@ -65,3 +65,14 @@ it("views a slice as JSON", async ({ expect, prismic, project }) => {
const parsed = JSON.parse(stdout);
expect(parsed).toMatchObject({ id: slice.id, name: slice.name });
});

it("fails for a legacy slice", async ({ expect, prismic, project }) => {
const customType = buildLegacyCustomType();
await writeLocalCustomType(project, customType);

const { stderr, exitCode } = await prismic("slice", ["view", "hero"]);
expect(exitCode).toBe(1);
expect(stderr).toContain(
`"hero" is a legacy slice in "${customType.id}". The CLI cannot edit legacy slices.`,
);
});
12 changes: 11 additions & 1 deletion test/type-view.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildCustomType, it, writeLocalCustomType } from "./it";
import { buildCustomType, buildLegacyCustomType, it, writeLocalCustomType } from "./it";

it("supports --help", async ({ expect, prismic }) => {
const { stdout, stderr, exitCode } = await prismic("type", ["view", "--help"]);
Expand Down Expand Up @@ -52,3 +52,13 @@ it("views a type as JSON", async ({ expect, prismic, project }) => {
const parsed = JSON.parse(stdout);
expect(parsed).toMatchObject({ id: customType.id, label: customType.label, format: "custom" });
});

it("marks legacy slices", async ({ expect, prismic, project }) => {
const customType = buildLegacyCustomType();
await writeLocalCustomType(project, customType);

const { stdout, stderr, exitCode } = await prismic("type", ["view", customType.id]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("body slices: hero *, gallery *, quote *");
expect(stdout).toContain("* Legacy slice.");
});
Loading