prismic 0.0.0-canary.169f5fa

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ import * as v from "valibot";
2
+
3
+ import { env } from "../env";
4
+ import { request } from "../lib/request";
5
+
6
+ export async function validateToken(
7
+ token: string,
8
+ config: { host: string | undefined },
9
+ ): Promise<boolean> {
10
+ const { host } = config;
11
+ const authServiceUrl = getAuthServiceUrl(host);
12
+ const url = new URL("validate", authServiceUrl);
13
+ url.searchParams.set("token", token);
14
+ try {
15
+ await request(url);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ export async function refreshToken(
23
+ token: string,
24
+ config: { host: string | undefined },
25
+ ): Promise<string> {
26
+ const { host } = config;
27
+ const authServiceUrl = getAuthServiceUrl(host);
28
+ const url = new URL("refreshtoken", authServiceUrl);
29
+ url.searchParams.set("token", token);
30
+ const refreshedToken = await request(url, { schema: v.string() });
31
+ return refreshedToken;
32
+ }
33
+
34
+ function getAuthServiceUrl(host = env.PRISMIC_HOST): URL {
35
+ return new URL(`https://auth.${host}/`);
36
+ }
@@ -0,0 +1,36 @@
1
+ import type { CustomType, SharedSlice } from "@prismicio/types-internal/lib/customtypes";
2
+
3
+ import { env } from "../env";
4
+ import { request } from "../lib/request";
5
+
6
+ export async function getCustomTypes(config: {
7
+ repo: string;
8
+ token: string | undefined;
9
+ host: string | undefined;
10
+ }): Promise<CustomType[]> {
11
+ const { repo, token, host } = config;
12
+ const customTypesServiceUrl = getCustomTypesServiceUrl(host);
13
+ const url = new URL("customtypes", customTypesServiceUrl);
14
+ const response = await request<CustomType[]>(url, {
15
+ headers: { repository: repo, Authorization: `Bearer ${token}` },
16
+ });
17
+ return response;
18
+ }
19
+
20
+ export async function getSlices(config: {
21
+ repo: string;
22
+ token: string | undefined;
23
+ host: string | undefined;
24
+ }): Promise<SharedSlice[]> {
25
+ const { repo, token, host } = config;
26
+ const customTypesServiceUrl = getCustomTypesServiceUrl(host);
27
+ const url = new URL("slices", customTypesServiceUrl);
28
+ const response = await request<SharedSlice[]>(url, {
29
+ headers: { repository: repo, Authorization: `Bearer ${token}` },
30
+ });
31
+ return response;
32
+ }
33
+
34
+ function getCustomTypesServiceUrl(host = env.PRISMIC_HOST): URL {
35
+ return new URL(`https://customtypes.${host}/`);
36
+ }
@@ -0,0 +1,35 @@
1
+ import * as v from "valibot";
2
+
3
+ import { env } from "../env";
4
+ import { request } from "../lib/request";
5
+
6
+ const ProfileSchema = v.object({
7
+ email: v.string(),
8
+ shortId: v.string(),
9
+ intercomHash: v.string(),
10
+ repositories: v.array(
11
+ v.object({
12
+ domain: v.string(),
13
+ name: v.optional(v.string()),
14
+ }),
15
+ ),
16
+ });
17
+ export type Profile = v.InferOutput<typeof ProfileSchema>;
18
+
19
+ export async function getProfile(config: {
20
+ token: string | undefined;
21
+ host: string | undefined;
22
+ }): Promise<Profile> {
23
+ const { token, host } = config;
24
+ const userServiceUrl = getUserServiceUrl(host);
25
+ const url = new URL("profile", userServiceUrl);
26
+ const response = await request(url, {
27
+ credentials: { "prismic-auth": token },
28
+ schema: ProfileSchema,
29
+ });
30
+ return response;
31
+ }
32
+
33
+ function getUserServiceUrl(host = env.PRISMIC_HOST): URL {
34
+ return new URL(`https://user-service.${host}/`);
35
+ }
@@ -0,0 +1,162 @@
1
+ import { readFile, rm } from "node:fs/promises";
2
+ import { parseArgs } from "node:util";
3
+
4
+ import type { Profile } from "../clients/user";
5
+ import type { FrameworkAdapter } from "../frameworks";
6
+ import type { Config } from "../lib/config";
7
+
8
+ import { getProfile } from "../clients/user";
9
+ import { NoSupportedFrameworkError, requireFramework } from "../frameworks";
10
+ import { createLoginSession, getHost, getToken } from "../lib/auth";
11
+ import { openBrowser } from "../lib/browser";
12
+ import { createConfig, readConfig, UnknownProjectRoot } from "../lib/config";
13
+ import { findUpward } from "../lib/file";
14
+ import { ForbiddenRequestError, UnauthorizedRequestError } from "../lib/request";
15
+ import { syncCustomTypes, syncSlices } from "./sync";
16
+
17
+ const HELP = `
18
+ Initialize a Prismic project by creating a prismic.config.json file.
19
+
20
+ Detects the project framework, installs dependencies, and syncs models
21
+ from Prismic. If a slicemachine.config.json exists, it will be migrated.
22
+
23
+ USAGE
24
+ prismic init [flags]
25
+
26
+ FLAGS
27
+ -r, --repo string Repository name
28
+ -h, --help Show help for command
29
+
30
+ EXAMPLES
31
+ prismic init --repo my-repo
32
+
33
+ LEARN MORE
34
+ Use \`prismic <command> --help\` for more information about a command.
35
+ `.trim();
36
+
37
+ export async function init(): Promise<void> {
38
+ const { values } = parseArgs({
39
+ args: process.argv.slice(3),
40
+ options: {
41
+ help: { type: "boolean", short: "h" },
42
+ repo: { type: "string", short: "r" },
43
+ },
44
+ });
45
+
46
+ if (values.help) {
47
+ console.info(HELP);
48
+ return;
49
+ }
50
+
51
+ // Check for existing prismic.config.json
52
+ const existingConfig = await readConfig();
53
+ if (existingConfig.ok) {
54
+ console.error("A prismic.config.json file already exists.");
55
+ process.exitCode = 1;
56
+ return;
57
+ }
58
+
59
+ // Check for legacy slicemachine.config.json
60
+ const legacyConfigPath = await findUpward("slicemachine.config.json", {
61
+ stop: "package.json",
62
+ });
63
+ let legacyRepoName: string | undefined;
64
+ let legacyLibraries: string[] | undefined;
65
+
66
+ if (legacyConfigPath) {
67
+ try {
68
+ const contents = await readFile(legacyConfigPath, "utf8");
69
+ const legacyConfig = JSON.parse(contents);
70
+ legacyRepoName = legacyConfig.repositoryName;
71
+ legacyLibraries = legacyConfig.libraries;
72
+ } catch {
73
+ console.warn("Could not read slicemachine.config.json, ignoring.");
74
+ }
75
+ }
76
+
77
+ // Determine repo name: --repo flag > legacy config > error
78
+ const repo = values.repo ?? legacyRepoName;
79
+ if (!repo) {
80
+ console.error("Missing required flag: --repo");
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+
85
+ // Validate repo membership
86
+ const token = await getToken();
87
+ const host = await getHost();
88
+ let profile: Profile;
89
+ try {
90
+ profile = await getProfile({ token, host });
91
+ } catch (error) {
92
+ if (error instanceof UnauthorizedRequestError || error instanceof ForbiddenRequestError) {
93
+ console.info("Not logged in. Starting login...");
94
+ const { email } = await createLoginSession({
95
+ onReady: (url) => {
96
+ console.info("Opening browser to complete login...");
97
+ console.info(`If the browser doesn't open, visit: ${url}`);
98
+ openBrowser(url);
99
+ },
100
+ });
101
+ console.info(`Logged in as ${email}`);
102
+ }
103
+ throw error;
104
+ }
105
+
106
+ const repoMeta = profile.repositories.find((repository) => repository.domain === repo);
107
+ if (!repoMeta) {
108
+ console.error(
109
+ `Repository "${repo}" not found in your account. Check the name or create it with \`prismic repo create\`.`,
110
+ );
111
+ process.exitCode = 1;
112
+ return;
113
+ }
114
+
115
+ let framework: FrameworkAdapter;
116
+ try {
117
+ framework = await requireFramework();
118
+ } catch (error) {
119
+ if (error instanceof NoSupportedFrameworkError) {
120
+ console.error(error.message);
121
+ process.exitCode = 1;
122
+ return;
123
+ }
124
+ throw error;
125
+ }
126
+
127
+ // Create prismic.config.json
128
+ const configData: Config = { repositoryName: repo };
129
+ if (legacyLibraries?.length) {
130
+ configData.libraries = legacyLibraries;
131
+ }
132
+
133
+ const configResult = await createConfig(configData);
134
+ if (!configResult.ok) {
135
+ if (configResult.error instanceof UnknownProjectRoot) {
136
+ console.error(
137
+ "Could not find a package.json file. Run this command from a project directory.",
138
+ );
139
+ } else {
140
+ console.error("Failed to create config file.");
141
+ }
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+
146
+ // Delete legacy config after new config is created
147
+ if (legacyConfigPath) {
148
+ await rm(legacyConfigPath);
149
+ console.info("Migrated slicemachine.config.json to prismic.config.json");
150
+ }
151
+
152
+ // Install dependencies and create framework files
153
+ await framework.initProject();
154
+
155
+ // Sync models from remote
156
+ await syncSlices(repo, framework);
157
+ await syncCustomTypes(repo, framework);
158
+
159
+ console.info(
160
+ `Initialized Prismic for repository "${repo}". Run \`npm install\` to install new dependencies.`,
161
+ );
162
+ }
@@ -0,0 +1,45 @@
1
+ import { exec } from "node:child_process";
2
+ import { parseArgs } from "node:util";
3
+
4
+ import { createLoginSession } from "../lib/auth";
5
+
6
+ const HELP = `
7
+ Log in to Prismic via browser.
8
+
9
+ USAGE
10
+ prismic login [flags]
11
+
12
+ FLAGS
13
+ -h, --help Show help for command
14
+
15
+ LEARN MORE
16
+ Use \`prismic <command> --help\` for more information about a command.
17
+ `.trim();
18
+
19
+ export async function login(): Promise<void> {
20
+ const { values } = parseArgs({
21
+ args: process.argv.slice(3),
22
+ options: { help: { type: "boolean", short: "h" } },
23
+ });
24
+
25
+ if (values.help) {
26
+ console.info(HELP);
27
+ return;
28
+ }
29
+
30
+ const { email } = await createLoginSession({
31
+ onReady: (url) => {
32
+ console.info("Opening browser to complete login...");
33
+ console.info(`If the browser doesn't open, visit: ${url}`);
34
+ openBrowser(url);
35
+ },
36
+ });
37
+
38
+ console.info(`Logged in to Prismic as ${email}`);
39
+ }
40
+
41
+ function openBrowser(url: URL): void {
42
+ const cmd =
43
+ process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
44
+ exec(`${cmd} "${url.toString()}"`);
45
+ }
@@ -0,0 +1,36 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ import { logout as baseLogout } from "../lib/auth";
4
+
5
+ const HELP = `
6
+ Log out of Prismic.
7
+
8
+ USAGE
9
+ prismic logout [flags]
10
+
11
+ FLAGS
12
+ -h, --help Show help for command
13
+
14
+ LEARN MORE
15
+ Use \`prismic <command> --help\` for more information about a command.
16
+ `.trim();
17
+
18
+ export async function logout(): Promise<void> {
19
+ const { values } = parseArgs({
20
+ args: process.argv.slice(3),
21
+ options: { help: { type: "boolean", short: "h" } },
22
+ });
23
+
24
+ if (values.help) {
25
+ console.info(HELP);
26
+ return;
27
+ }
28
+
29
+ const ok = await baseLogout();
30
+ if (ok) {
31
+ console.info("Logged out of Prismic");
32
+ } else {
33
+ console.error("Logout failed. You can log out manually by deleting the file.");
34
+ process.exitCode = 1;
35
+ }
36
+ }
@@ -0,0 +1,256 @@
1
+ import { createHash } from "node:crypto";
2
+ import { setTimeout } from "node:timers/promises";
3
+ import { parseArgs } from "node:util";
4
+
5
+ import { getCustomTypes, getSlices } from "../clients/custom-types";
6
+ import { type FrameworkAdapter, NoSupportedFrameworkError, requireFramework } from "../frameworks";
7
+ import { getHost, getToken } from "../lib/auth";
8
+ import { safeGetRepositoryFromConfig } from "../lib/config";
9
+ import { segmentSetRepository, segmentTrackEnd, segmentTrackStart } from "../lib/segment";
10
+ import { sentrySetContext, sentrySetTag } from "../lib/sentry";
11
+ import { dedent } from "../lib/string";
12
+
13
+ const HELP = `
14
+ Sync slices, page types, and custom types from Prismic to local files.
15
+
16
+ Remote models are the source of truth. Local files are created, updated,
17
+ or deleted to match.
18
+
19
+ USAGE
20
+ prismic sync [flags]
21
+
22
+ FLAGS
23
+ -r, --repo string Repository domain
24
+ -w, --watch Watch for changes and sync continuously
25
+ -h, --help Show help for command
26
+ `.trim();
27
+
28
+ // 5 seconds balances responsiveness with API load
29
+ const POLL_INTERVAL_MS = 5000;
30
+ const MAX_BACKOFF_MS = 60000; // Cap backoff at 1 minute
31
+ const MAX_CONSECUTIVE_ERRORS = 10;
32
+
33
+ export async function sync(): Promise<void> {
34
+ const {
35
+ values: { help, repo = await safeGetRepositoryFromConfig(), watch },
36
+ } = parseArgs({
37
+ args: process.argv.slice(3), // skip: node, script, "sync"
38
+ options: {
39
+ repo: { type: "string", short: "r" },
40
+ watch: { type: "boolean", short: "w" },
41
+ help: { type: "boolean", short: "h" },
42
+ },
43
+ allowPositionals: false,
44
+ });
45
+
46
+ if (help) {
47
+ console.info(HELP);
48
+ return;
49
+ }
50
+
51
+ if (!repo) {
52
+ console.error("Missing prismic.config.json or --repo option");
53
+ process.exitCode = 1;
54
+ return;
55
+ }
56
+
57
+ // Override analytics repository context with the resolved repo
58
+ segmentSetRepository(repo);
59
+ sentrySetTag("repository", repo);
60
+ sentrySetContext("Repository Data", { name: repo });
61
+
62
+ let framework: FrameworkAdapter;
63
+ try {
64
+ framework = await requireFramework();
65
+ } catch (error) {
66
+ if (error instanceof NoSupportedFrameworkError) {
67
+ console.error(error.message);
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ throw error;
72
+ }
73
+
74
+ console.info(`Syncing from repository: ${repo}`);
75
+
76
+ segmentTrackStart("sync", { repository: repo });
77
+
78
+ if (watch) {
79
+ await watchForChanges(repo, framework);
80
+ } else {
81
+ await syncSlices(repo, framework);
82
+ await syncCustomTypes(repo, framework);
83
+ segmentTrackEnd("sync", true, undefined, { watch: false });
84
+
85
+ console.info("Sync complete");
86
+ }
87
+ }
88
+
89
+ async function watchForChanges(repo: string, framework: FrameworkAdapter) {
90
+ const token = await getToken();
91
+ const host = await getHost();
92
+
93
+ const initialRemoteSlices = await getSlices({ repo, token, host });
94
+ const initialRemoteCustomTypes = await getCustomTypes({ repo, token, host });
95
+
96
+ await syncSlices(repo, framework);
97
+ await syncCustomTypes(repo, framework);
98
+
99
+ console.info(dedent`
100
+ Initial sync completed!
101
+
102
+ Watching for changes (polling every ${POLL_INTERVAL_MS / 1000}s),
103
+ Press Ctrl+C to stop\n
104
+ `);
105
+
106
+ let lastRemoteSlicesHash = hash(initialRemoteSlices);
107
+ let lastRemoteCustomTypesHash = hash(initialRemoteCustomTypes);
108
+
109
+ let consecutiveErrors = 0;
110
+
111
+ // Handle all common termination signals
112
+ process.on("SIGINT", shutdown); // Ctrl+C
113
+ process.on("SIGTERM", shutdown); // kill command
114
+ process.on("SIGHUP", shutdown); // terminal closed
115
+ process.on("SIGQUIT", shutdown); // Ctrl+\
116
+ if (process.platform === "win32") {
117
+ process.on("SIGBREAK", shutdown); // Windows Ctrl+Break
118
+ }
119
+
120
+ while (true) {
121
+ await setTimeout(exponentialMs(consecutiveErrors));
122
+
123
+ try {
124
+ const remoteSlicesResult = await getSlices({ repo, token, host });
125
+ const remoteSlicesHash = hash(remoteSlicesResult);
126
+ const slicesChanged = remoteSlicesHash !== lastRemoteSlicesHash;
127
+
128
+ const remoteCustomTypesResult = await getCustomTypes({ repo, token, host });
129
+ const remoteCustomTypesHash = hash(remoteCustomTypesResult);
130
+ const customTypesChanged = remoteCustomTypesHash !== lastRemoteCustomTypesHash;
131
+
132
+ if (slicesChanged || customTypesChanged) {
133
+ const changed = [];
134
+
135
+ if (slicesChanged) {
136
+ await syncSlices(repo, framework);
137
+ lastRemoteSlicesHash = remoteSlicesHash;
138
+ changed.push("slices");
139
+ }
140
+ if (customTypesChanged) {
141
+ await syncCustomTypes(repo, framework);
142
+ lastRemoteCustomTypesHash = remoteCustomTypesHash;
143
+ changed.push("custom types");
144
+ }
145
+
146
+ const timestamp = new Date().toLocaleTimeString();
147
+ console.info(`[${timestamp}] Changes detected in ${changed.join(" and ")}`);
148
+ }
149
+
150
+ // Reset error count on success
151
+ consecutiveErrors = 0;
152
+ } catch (error) {
153
+ consecutiveErrors++;
154
+
155
+ const message = error instanceof Error ? error.message : "Unknown error";
156
+
157
+ const nextDelay = Math.min(
158
+ POLL_INTERVAL_MS * Math.pow(2, consecutiveErrors - 1),
159
+ MAX_BACKOFF_MS,
160
+ );
161
+
162
+ console.error(`Error checking for changes: ${message}. Retrying in ${nextDelay / 1000}s...`);
163
+
164
+ if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
165
+ throw new Error(`Too many consecutive errors (${MAX_CONSECUTIVE_ERRORS}), stopping watch.`);
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ export async function syncSlices(repo: string, framework: FrameworkAdapter): Promise<void> {
172
+ const token = await getToken();
173
+ const host = await getHost();
174
+
175
+ const remoteSlices = await getSlices({ repo, token, host });
176
+ const localSlices = await framework.getSlices();
177
+
178
+ // Handle slices update
179
+ for (const remoteSlice of remoteSlices) {
180
+ const localSlice = localSlices.find((slice) => slice.model.id === remoteSlice.id);
181
+ if (localSlice) {
182
+ await framework.updateSlice(remoteSlice);
183
+ }
184
+ }
185
+
186
+ // Handle slices deletion
187
+ for (const localSlice of localSlices) {
188
+ const existsRemotely = remoteSlices.some((slice) => slice.id === localSlice.model.id);
189
+ if (!existsRemotely) {
190
+ await framework.deleteSlice(localSlice.model.id);
191
+ }
192
+ }
193
+
194
+ // Handle slices creation
195
+ const defaultLibrary = await framework.getDefaultSliceLibrary();
196
+ for (const remoteSlice of remoteSlices) {
197
+ const existsLocally = localSlices.some((slice) => slice.model.id === remoteSlice.id);
198
+ if (!existsLocally) {
199
+ await framework.createSlice(remoteSlice, defaultLibrary);
200
+ }
201
+ }
202
+ }
203
+
204
+ export async function syncCustomTypes(repo: string, framework: FrameworkAdapter): Promise<void> {
205
+ const token = await getToken();
206
+ const host = await getHost();
207
+
208
+ const remoteCustomTypes = await getCustomTypes({ repo, token, host });
209
+ const localCustomTypes = await framework.getCustomTypes();
210
+
211
+ // Handle custom types update
212
+ for (const remoteCustomType of remoteCustomTypes) {
213
+ const localCustomType = localCustomTypes.find(
214
+ (customType) => customType.model.id === remoteCustomType.id,
215
+ );
216
+ if (localCustomType) {
217
+ await framework.updateCustomType(remoteCustomType);
218
+ }
219
+ }
220
+
221
+ // Handle custom types deletion
222
+ for (const localCustomType of localCustomTypes) {
223
+ const existsRemotely = remoteCustomTypes.some(
224
+ (customType) => customType.id === localCustomType.model.id,
225
+ );
226
+ if (!existsRemotely) {
227
+ await framework.deleteCustomType(localCustomType.model.id);
228
+ }
229
+ }
230
+
231
+ // Handle custom types creation
232
+ for (const remoteCustomType of remoteCustomTypes) {
233
+ const existsLocally = localCustomTypes.some(
234
+ (customType) => customType.model.id === remoteCustomType.id,
235
+ );
236
+ if (!existsLocally) {
237
+ await framework.createCustomType(remoteCustomType);
238
+ }
239
+ }
240
+ }
241
+
242
+ function shutdown(): void {
243
+ console.info("Watch stopped. Goodbye!");
244
+ segmentTrackEnd("sync", true, undefined, { watch: true });
245
+ process.exit(0);
246
+ }
247
+
248
+ // Exponential backoff: 5s, 10s, 20s, 40s, 60s (capped)
249
+ function exponentialMs(base: number): number {
250
+ if (base === 0) return POLL_INTERVAL_MS;
251
+ return Math.min(POLL_INTERVAL_MS * Math.pow(2, base - 1), MAX_BACKOFF_MS);
252
+ }
253
+
254
+ function hash(data: unknown): string {
255
+ return createHash("sha256").update(JSON.stringify(data)).digest("hex");
256
+ }
@@ -0,0 +1,37 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ import { getProfile } from "../clients/user";
4
+ import { getHost, getToken } from "../lib/auth";
5
+
6
+ const HELP = `
7
+ Show the currently logged in user.
8
+
9
+ USAGE
10
+ prismic whoami [flags]
11
+
12
+ FLAGS
13
+ -h, --help Show help for command
14
+
15
+ LEARN MORE
16
+ Use \`prismic <command> --help\` for more information about a command.
17
+ `.trim();
18
+
19
+ export async function whoami(): Promise<void> {
20
+ const {
21
+ values: { help },
22
+ } = parseArgs({
23
+ args: process.argv.slice(3),
24
+ options: { help: { type: "boolean", short: "h" } },
25
+ });
26
+
27
+ if (help) {
28
+ console.info(HELP);
29
+ return;
30
+ }
31
+
32
+ const token = await getToken();
33
+ const host = await getHost();
34
+ const profile = await getProfile({ token, host });
35
+
36
+ console.info(profile.email);
37
+ }
package/src/env.ts ADDED
@@ -0,0 +1,23 @@
1
+ import * as v from "valibot";
2
+
3
+ const Env = v.object({
4
+ MODE: v.string(),
5
+ DEV: v.boolean(),
6
+ PROD: v.boolean(),
7
+ PRISMIC_SENTRY_DSN: v.optional(v.string()),
8
+ PRISMIC_SENTRY_ENVIRONMENT: v.optional(v.string()),
9
+ PRISMIC_SENTRY_ENABLED: v.optional(
10
+ v.pipe(
11
+ v.picklist(["true", "false"]),
12
+ v.transform((input) => input === "true"),
13
+ ),
14
+ ),
15
+ PRISMIC_HOST: v.optional(v.string(), "prismic.io"),
16
+ });
17
+
18
+ export const env = v.parse(Env, {
19
+ MODE: process.env.MODE,
20
+ DEV: process.env.MODE !== "production",
21
+ PROD: process.env.MODE === "production",
22
+ ...process.env,
23
+ });