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
27 changes: 15 additions & 12 deletions src/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { findProjectRoot, getLibraries } from "../project";

const TYPES_FILENAME = "prismicio-types.d.ts";

type CustomTypeMeta = { model: CustomType; directory: URL };
type SharedSliceMeta = { model: SharedSlice; directory: URL; library: URL };
export type CustomTypeMeta = { model: CustomType; file: URL; directory: URL };
export type SharedSliceMeta = { model: SharedSlice; file: URL; directory: URL; library: URL };

export async function getAdapter(): Promise<Adapter> {
const { dependencies, devDependencies, peerDependencies } = await readPackageJson();
Expand Down Expand Up @@ -86,7 +86,7 @@ export abstract class Adapter {
sliceModelPaths.map(async (sliceModelPath) => {
const directory = new URL(".", sliceModelPath);
const model = await readJsonFile<SharedSlice>(sliceModelPath);
return { library, directory, model };
return { library, directory, file: sliceModelPath, model };
}),
);
allSlices.push(...slices);
Expand Down Expand Up @@ -141,7 +141,7 @@ export abstract class Adapter {
customTypeModelPaths.map(async (customTypeModelPath) => {
const directory = new URL(".", customTypeModelPath);
const model = await readJsonFile<CustomType>(customTypeModelPath);
return { directory, model };
return { directory, file: customTypeModelPath, model };
}),
);

Expand Down Expand Up @@ -181,26 +181,28 @@ export abstract class Adapter {
await this.onCustomTypeDeleted(id);
}

async syncModels(config: {
async pullModels(config: {
repo: string;
token: string | undefined;
host: string;
force?: boolean;
}): Promise<void> {
const { repo, token, host } = config;
const { repo, token, host, force } = config;
await Promise.all([
this.syncSlices({ repo, token, host, generateTypes: false }),
this.syncCustomTypes({ repo, token, host, generateTypes: false }),
this.pullSlices({ repo, token, host, generateTypes: false, force }),
this.pullCustomTypes({ repo, token, host, generateTypes: false, force }),
]);
await this.generateTypes();
}

async syncSlices(config: {
async pullSlices(config: {
repo: string;
token: string | undefined;
host: string;
generateTypes?: boolean;
force?: boolean;
}): Promise<void> {
const { repo, token, host, generateTypes = true } = config;
const { repo, token, host, generateTypes = true, force = false } = config;

const remoteSlices = await getSlices({ repo, token, host });
const localSlices = await this.getSlices();
Expand All @@ -226,13 +228,14 @@ export abstract class Adapter {
if (generateTypes) await this.generateTypes();
}

async syncCustomTypes(config: {
async pullCustomTypes(config: {
repo: string;
token: string | undefined;
host: string;
generateTypes?: boolean;
force?: boolean;
}): Promise<void> {
const { repo, token, host, generateTypes = true } = config;
const { repo, token, host, generateTypes = true, force = false } = config;

const remoteCustomTypes = await getCustomTypes({ repo, token, host });
const localCustomTypes = await this.getCustomTypes();
Expand Down
2 changes: 1 addition & 1 deletion src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export default createCommand(config, async ({ values }) => {
}

// Sync models from remote and generate types
await adapter.syncModels({ repo, token, host });
await adapter.pullModels({ repo, token, host });

console.info(`\nInitialized Prismic for repository "${repo}".`);
console.info("Run `prismic type create <name>` to create a content type.");
Expand Down
146 changes: 146 additions & 0 deletions src/commands/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import type { CustomType, SharedSlice } from "@prismicio/types-internal/lib/customtypes";

import { type CustomTypeMeta, getAdapter, type SharedSliceMeta, type Adapter } from "../adapters";
import { getHost, getToken } from "../auth";
import { getCustomTypes, getSlices } from "../clients/custom-types";
import { CommandError, createCommand, type CommandConfig } from "../lib/command";
import { dedent, formatTable } from "../lib/string";
import { relativePathname } from "../lib/url";
import { findProjectRoot, getRepositoryName } from "../project";

const config = {
name: "prismic pull",
description: `
Pull content types and slices from Prismic to local files.

Remote models are the source of truth. Local files are created, updated,
or deleted to match.
`,
options: {
force: { type: "boolean", description: "Force pull remote changes" },
repo: { type: "string", short: "r", description: "Repository domain" },
},
} satisfies CommandConfig;

export default createCommand(config, async ({ values }) => {
const { force = false, repo = await getRepositoryName() } = values;

const token = await getToken();
const host = await getHost();
const adapter = await getAdapter();

console.info(`Pulling from repository: ${repo}`);

const [sliceOperations, customTypeOperations] = await Promise.all([
getSliceOperations({ adapter, repo, token, host }),
getCustomTypeOperations({ adapter, repo, token, host }),
]);

if (
(!force && sliceOperations.update.length > 0) ||
sliceOperations.delete.length > 0 ||
customTypeOperations.update.length > 0 ||
customTypeOperations.delete.length > 0
) {
const projectRoot = await findProjectRoot();
const destructiveOperationRows: string[][] = [
...sliceOperations.update.map((operation) => [
"Overwrite slice",
operation.model.id,
relativePathname(projectRoot, operation.file),
]),
...sliceOperations.delete.map((operation) => [
"Delete slice",
operation.model.id,
relativePathname(projectRoot, operation.file),
]),
];

throw new CommandError(dedent`
The following destructive changes will happen:

${formatTable(destructiveOperationRows, { headers: ["OPERATION", "ID", "FILE"] })}

Re-run the command with \`--force\` to confirm.
`);
}

console.info("Pull complete");
});

type Operations<TInsert, TUpdate = TInsert, TDelete = TInsert> = {
insert: TInsert[];
update: TUpdate[];
delete: TDelete[];
};

async function getSliceOperations(config: {
adapter: Adapter;
repo: string;
token: string | undefined;
host: string;
}): Promise<Operations<SharedSlice, SharedSliceMeta, SharedSliceMeta>> {
const { adapter, repo, token, host } = config;

const operations: Operations<SharedSlice, SharedSliceMeta, SharedSliceMeta> = {
insert: [],
update: [],
delete: [],
};

const remoteSlices = await getSlices({ repo, token, host });
const localSlices = await adapter.getSlices();
for (const remoteSlice of remoteSlices) {
const localSlice = localSlices.find((slice) => slice.model.id === remoteSlice.id);
if (localSlice) {
const isSame = localSlice && JSON.stringify(remoteSlice) === JSON.stringify(localSlice);
if (!isSame) operations.update.push(localSlice);
} else {
operations.insert.push(remoteSlice);
}
}
for (const localSlice of localSlices) {
const remoteSlice = remoteSlices.find((slice) => slice.id === localSlice.model.id);
if (!remoteSlice) operations.delete.push(localSlice);
}

return operations;
}

async function getCustomTypeOperations(config: {
adapter: Adapter;
repo: string;
token: string | undefined;
host: string;
}): Promise<Operations<CustomType, CustomTypeMeta, CustomTypeMeta>> {
const { adapter, repo, token, host } = config;

const operations: Operations<CustomType, CustomTypeMeta, CustomTypeMeta> = {
insert: [],
update: [],
delete: [],
};

const remoteCustomTypes = await getCustomTypes({ repo, token, host });
const localCustomTypes = await adapter.getCustomTypes();
for (const remoteCustomType of remoteCustomTypes) {
const localCustomType = localCustomTypes.find(
(customType) => customType.model.id === remoteCustomType.id,
);
if (localCustomType) {
const isSame =
localCustomType && JSON.stringify(remoteCustomType) === JSON.stringify(localCustomType);
if (!isSame) operations.update.push(localCustomType);
} else {
operations.insert.push(remoteCustomType);
}
}
for (const localCustomType of localCustomTypes) {
const remoteCustomType = remoteCustomTypes.find(
(customType) => customType.id === localCustomType.model.id,
);
if (!remoteCustomType) operations.delete.push(localCustomType);
}

return operations;
}
8 changes: 4 additions & 4 deletions src/commands/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default createCommand(config, async ({ values }) => {
} else {
const token = await getToken();
const host = await getHost();
await adapter.syncModels({ repo, token, host });
await adapter.pullModels({ repo, token, host });
segmentTrackEnd("sync", { watch });

console.info("Sync complete");
Expand All @@ -64,7 +64,7 @@ async function watchForChanges(repo: string, adapter: Adapter) {
const initialRemoteSlices = await getSlices({ repo, token, host });
const initialRemoteCustomTypes = await getCustomTypes({ repo, token, host });

await adapter.syncModels({ repo, token, host });
await adapter.pullModels({ repo, token, host });

console.info(dedent`
Initial sync completed!
Expand Down Expand Up @@ -103,12 +103,12 @@ async function watchForChanges(repo: string, adapter: Adapter) {
const changed = [];

if (slicesChanged) {
await adapter.syncSlices({ repo, token, host, generateTypes: false });
await adapter.pullSlices({ repo, token, host, generateTypes: false });
lastRemoteSlicesHash = remoteSlicesHash;
changed.push("slices");
}
if (customTypesChanged) {
await adapter.syncCustomTypes({ repo, token, host, generateTypes: false });
await adapter.pullCustomTypes({ repo, token, host, generateTypes: false });
lastRemoteCustomTypesHash = remoteCustomTypesHash;
changed.push("custom types");
}
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import locale from "./commands/locale";
import login from "./commands/login";
import logout from "./commands/logout";
import preview from "./commands/preview";
import pull from "./commands/pull";
import repo from "./commands/repo";
import slice from "./commands/slice";
import sync from "./commands/sync";
Expand All @@ -23,6 +24,7 @@ import webhook from "./commands/webhook";
import whoami from "./commands/whoami";
import { UPDATE_NOTIFIER_STATE_PATH } from "./config";
import { CommandError, createCommandRouter } from "./lib/command";
import { decodePayload } from "./lib/jwt";
import {
ForbiddenRequestError,
NotFoundRequestError,
Expand All @@ -42,7 +44,6 @@ import {
sentrySetUser,
setupSentry,
} from "./lib/sentry";
import { decodePayload } from "./lib/jwt";
import { dedent } from "./lib/string";
import { initUpdateNotifier } from "./lib/update-notifier";
import { InvalidPrismicConfigError, MissingPrismicConfigError } from "./project";
Expand Down Expand Up @@ -100,6 +101,10 @@ const router = createCommandRouter({
handler: preview,
description: "Manage preview configurations",
},
pull: {
handler: pull,
description: "Pull types and slices from Prismic",
},
token: {
handler: token,
description: "Manage API tokens",
Expand Down
Loading