prismic 0.0.0-canary.2cfb4a8
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.
- package/LICENSE +202 -0
- package/README.md +69 -0
- package/dist/builders-hKD4IrLX-CSAFppyU.mjs +97 -0
- package/dist/framework-AxGiKSX9.mjs +17 -0
- package/dist/index.mjs +87 -0
- package/dist/nextjs-D4viImqE.mjs +312 -0
- package/dist/nuxt-CbBJIJw1.mjs +59 -0
- package/dist/sveltekit-BuNy6sKm.mjs +226 -0
- package/package.json +58 -0
- package/src/env.d.ts +12 -0
- package/src/framework/index.ts +399 -0
- package/src/framework/nextjs.templates.ts +426 -0
- package/src/framework/nextjs.ts +216 -0
- package/src/framework/nuxt.templates.ts +74 -0
- package/src/framework/nuxt.ts +250 -0
- package/src/framework/sveltekit.templates.ts +278 -0
- package/src/framework/sveltekit.ts +241 -0
- package/src/index.ts +90 -0
- package/src/init.ts +173 -0
- package/src/lib/auth.ts +200 -0
- package/src/lib/browser.ts +11 -0
- package/src/lib/config.ts +111 -0
- package/src/lib/custom-types-api.ts +385 -0
- package/src/lib/field-path.ts +81 -0
- package/src/lib/file.ts +49 -0
- package/src/lib/json.ts +3 -0
- package/src/lib/packageJson.ts +35 -0
- package/src/lib/profile.ts +39 -0
- package/src/lib/request.ts +116 -0
- package/src/lib/segment.ts +145 -0
- package/src/lib/sentry.ts +63 -0
- package/src/lib/string.ts +10 -0
- package/src/lib/url.ts +31 -0
- package/src/login.ts +45 -0
- package/src/logout.ts +36 -0
- package/src/sync.ts +259 -0
- package/src/whoami.ts +62 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
|
|
3
|
+
import { readToken } from "./auth";
|
|
4
|
+
|
|
5
|
+
type CustomRequestInit = Omit<RequestInit, "body"> & {
|
|
6
|
+
body?: RequestInit["body"] | Record<PropertyKey, unknown>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type RequestResponse<T> = SuccessfulRequestResponse<T> | FailedRequestResponse;
|
|
10
|
+
export type ParsedRequestResponse<T> =
|
|
11
|
+
| RequestResponse<T>
|
|
12
|
+
| { ok: false; value: unknown; error: v.ValiError<v.GenericSchema<T>> };
|
|
13
|
+
export type SuccessfulRequestResponse<T> = { ok: true; value: T };
|
|
14
|
+
export type FailedRequestResponse = {
|
|
15
|
+
ok: false;
|
|
16
|
+
value: unknown;
|
|
17
|
+
error: RequestError | ForbiddenRequestError | UnauthorizedRequestError;
|
|
18
|
+
};
|
|
19
|
+
export type FailedParsedRequestResponse<T> =
|
|
20
|
+
| FailedRequestResponse
|
|
21
|
+
| { ok: false; value: unknown; error: v.ValiError<v.GenericSchema<T>> };
|
|
22
|
+
|
|
23
|
+
export async function request<T>(
|
|
24
|
+
input: RequestInfo | URL,
|
|
25
|
+
init?: CustomRequestInit,
|
|
26
|
+
): Promise<RequestResponse<T>>;
|
|
27
|
+
export async function request<T>(
|
|
28
|
+
input: RequestInfo | URL,
|
|
29
|
+
init: CustomRequestInit & { schema: v.GenericSchema<T> },
|
|
30
|
+
): Promise<ParsedRequestResponse<T>>;
|
|
31
|
+
export async function request<T>(
|
|
32
|
+
input: RequestInfo | URL,
|
|
33
|
+
init: CustomRequestInit & { schema?: v.GenericSchema<T> } = {},
|
|
34
|
+
): Promise<ParsedRequestResponse<T>> {
|
|
35
|
+
const { credentials = "include" } = init;
|
|
36
|
+
|
|
37
|
+
const headers = new Headers(init.headers);
|
|
38
|
+
headers.set("Accept", "application/json");
|
|
39
|
+
if (credentials === "include") {
|
|
40
|
+
const token = await readToken();
|
|
41
|
+
if (token) headers.set("Cookie", `SESSION=fake_session; prismic-auth=${token}`);
|
|
42
|
+
}
|
|
43
|
+
if (!headers.has("Content-Type") && init.body) {
|
|
44
|
+
headers.set("Content-Type", "application/json");
|
|
45
|
+
}
|
|
46
|
+
if (init.body instanceof FormData) {
|
|
47
|
+
headers.delete("Content-Type");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const body =
|
|
51
|
+
headers.get("Content-Type") === "application/json"
|
|
52
|
+
? JSON.stringify(init.body)
|
|
53
|
+
: (init.body as RequestInit["body"]);
|
|
54
|
+
|
|
55
|
+
const response = await fetch(input, { ...init, body, headers });
|
|
56
|
+
|
|
57
|
+
const value = await response
|
|
58
|
+
.clone()
|
|
59
|
+
.json()
|
|
60
|
+
.catch(() => response.clone().text());
|
|
61
|
+
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
if (!init?.schema) return { ok: true, value };
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const parsed = v.parse(init.schema, value);
|
|
67
|
+
return { ok: true, value: parsed };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (v.isValiError<v.GenericSchema<T>>(error)) {
|
|
70
|
+
return { ok: false, value, error };
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
if (response.status === 401) {
|
|
76
|
+
const error = new UnauthorizedRequestError(response);
|
|
77
|
+
return { ok: false, value, error };
|
|
78
|
+
} else if (response.status === 403) {
|
|
79
|
+
const error = new ForbiddenRequestError(response);
|
|
80
|
+
return { ok: false, value, error };
|
|
81
|
+
} else {
|
|
82
|
+
const error = new RequestError(response);
|
|
83
|
+
return { ok: false, value, error };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class RequestError extends Error {
|
|
89
|
+
name = "RequestError" as const;
|
|
90
|
+
response: Response;
|
|
91
|
+
|
|
92
|
+
constructor(response: Response) {
|
|
93
|
+
super(`fetch failed: ${response.url}`);
|
|
94
|
+
this.response = response;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async text(): Promise<string> {
|
|
98
|
+
return this.response.clone().text();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async json(): Promise<unknown> {
|
|
102
|
+
return this.response.clone().json();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get status(): number {
|
|
106
|
+
return this.response.status;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get statusText(): string {
|
|
110
|
+
return this.response.statusText;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class ForbiddenRequestError extends RequestError {}
|
|
115
|
+
|
|
116
|
+
export class UnauthorizedRequestError extends RequestError {}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
8
|
+
|
|
9
|
+
const SEGMENT_WRITE_KEY =
|
|
10
|
+
process.env.PRISMIC_ENV && process.env.PRISMIC_ENV !== "production"
|
|
11
|
+
? "Ng5oKJHCGpSWplZ9ymB7Pu7rm0sTDeiG"
|
|
12
|
+
: "cGjidifKefYb6EPaGaqpt8rQXkv5TD6P";
|
|
13
|
+
const SEGMENT_TRACK_URL = "https://api.segment.io/v1/track";
|
|
14
|
+
|
|
15
|
+
let enabled = false;
|
|
16
|
+
let anonymousId = "";
|
|
17
|
+
let authorization = "";
|
|
18
|
+
const appContext = { app: { name: packageJson.name, version: packageJson.version } };
|
|
19
|
+
const eventQueue: Array<{ event: string; properties: Record<string, unknown> }> = [];
|
|
20
|
+
|
|
21
|
+
export async function initSegment(): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
enabled = await isTelemetryEnabled();
|
|
24
|
+
if (!enabled) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
anonymousId = randomUUID();
|
|
29
|
+
authorization = `Basic ${btoa(SEGMENT_WRITE_KEY + ":")}`;
|
|
30
|
+
process.on("exit", flushTelemetry);
|
|
31
|
+
} catch {
|
|
32
|
+
enabled = false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function trackStart(command: string): void {
|
|
37
|
+
if (!enabled) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
eventQueue.push({
|
|
42
|
+
event: "Prismic CLI Start",
|
|
43
|
+
properties: {
|
|
44
|
+
commandType: command,
|
|
45
|
+
fullCommand: process.argv.join(" "),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function trackEnd(
|
|
51
|
+
command: string,
|
|
52
|
+
success: boolean,
|
|
53
|
+
error?: unknown,
|
|
54
|
+
): void {
|
|
55
|
+
if (!enabled) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const properties: Record<string, unknown> = {
|
|
60
|
+
commandType: command,
|
|
61
|
+
fullCommand: process.argv.join(" "),
|
|
62
|
+
success,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (error !== undefined) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
properties.error = message.slice(0, 512);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
eventQueue.push({ event: "Prismic CLI End", properties });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Spawns a detached subprocess to send queued telemetry events.
|
|
75
|
+
* The main process exits immediately; the subprocess handles HTTP delivery.
|
|
76
|
+
*/
|
|
77
|
+
function flushTelemetry(): void {
|
|
78
|
+
if (eventQueue.length === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const payload = Buffer.from(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
url: SEGMENT_TRACK_URL,
|
|
86
|
+
authorization,
|
|
87
|
+
events: eventQueue.map((e) => ({
|
|
88
|
+
anonymousId,
|
|
89
|
+
event: e.event,
|
|
90
|
+
properties: { nodeVersion: process.versions.node, ...e.properties },
|
|
91
|
+
context: appContext,
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
})),
|
|
94
|
+
}),
|
|
95
|
+
).toString("base64");
|
|
96
|
+
|
|
97
|
+
const child = spawn(
|
|
98
|
+
process.execPath,
|
|
99
|
+
["--input-type=module", "-e", FLUSH_SCRIPT, payload],
|
|
100
|
+
{ detached: true, stdio: "ignore" },
|
|
101
|
+
);
|
|
102
|
+
child.unref();
|
|
103
|
+
} catch {
|
|
104
|
+
// Silent failure — never breaks the CLI
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
eventQueue.length = 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const FLUSH_SCRIPT = `
|
|
111
|
+
const {url, authorization, events} = JSON.parse(Buffer.from(process.argv[1], "base64"));
|
|
112
|
+
const h = {"Content-Type": "application/json", Authorization: authorization};
|
|
113
|
+
await Promise.allSettled(events.map(e => fetch(url, {method: "POST", headers: h, body: JSON.stringify(e)}).catch(() => {})));
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
async function isTelemetryEnabled(): Promise<boolean> {
|
|
117
|
+
try {
|
|
118
|
+
// Check user-level .prismicrc
|
|
119
|
+
const userRc = await readJsonFile(join(homedir(), ".prismicrc"));
|
|
120
|
+
if (userRc?.telemetry === false) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check project-level .prismicrc
|
|
125
|
+
const projectRc = await readJsonFile(join(process.cwd(), ".prismicrc"));
|
|
126
|
+
if (projectRc?.telemetry === false) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function readJsonFile(
|
|
137
|
+
path: string,
|
|
138
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
139
|
+
try {
|
|
140
|
+
const contents = await readFile(path, "utf8");
|
|
141
|
+
return JSON.parse(contents);
|
|
142
|
+
} catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/node-core/light";
|
|
2
|
+
|
|
3
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
4
|
+
|
|
5
|
+
const SENTRY_DSN =
|
|
6
|
+
import.meta.env.PRISMIC_SENTRY_DSN ||
|
|
7
|
+
"https://e1886b1775bd397cd1afc60bfd2ebfc8@o146123.ingest.us.sentry.io/4510445143588864";
|
|
8
|
+
|
|
9
|
+
function isSentryEnabled(): boolean {
|
|
10
|
+
if (import.meta.env.PRISMIC_SENTRY_ENABLED === undefined) {
|
|
11
|
+
return import.meta.env.PROD;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return import.meta.env.PRISMIC_SENTRY_ENABLED === "true";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setupSentry(): void {
|
|
18
|
+
try {
|
|
19
|
+
if (!isSentryEnabled()) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Sentry.init({
|
|
24
|
+
dsn: SENTRY_DSN,
|
|
25
|
+
release: packageJson.version,
|
|
26
|
+
environment: import.meta.env.PRISMIC_SENTRY_ENVIRONMENT ?? "production",
|
|
27
|
+
defaultIntegrations: false,
|
|
28
|
+
integrations: [],
|
|
29
|
+
maxValueLength: 2_500,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
Sentry.setContext("Process", {
|
|
33
|
+
command: process.argv.join(" "),
|
|
34
|
+
cwd: process.cwd(),
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
// Silent failure — never breaks the CLI
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function captureError(error: unknown): Promise<void> {
|
|
42
|
+
try {
|
|
43
|
+
if (!isSentryEnabled()) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Sentry.captureException(
|
|
48
|
+
error,
|
|
49
|
+
error instanceof Error
|
|
50
|
+
? { extra: { cause: error.cause, fullCommand: process.argv.join(" ") } }
|
|
51
|
+
: {},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await Sentry.flush();
|
|
55
|
+
} catch {
|
|
56
|
+
// Silent failure — never breaks the CLI
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Re-exports for future devtools-parity integration points
|
|
61
|
+
export const setUser = Sentry.setUser;
|
|
62
|
+
export const setTag = Sentry.setTag;
|
|
63
|
+
export const setContext = Sentry.setContext;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import baseDedent from "dedent";
|
|
2
|
+
|
|
3
|
+
export function humanReadable(id: string): string {
|
|
4
|
+
return id
|
|
5
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
6
|
+
.replace(/[_-]+/g, " ")
|
|
7
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const dedent = baseDedent.withOptions({ alignValues: true });
|
package/src/lib/url.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readHost } from "./auth";
|
|
2
|
+
|
|
3
|
+
export async function getRepoUrl(repo: string): Promise<URL> {
|
|
4
|
+
const host = await readHost();
|
|
5
|
+
host.hostname = `${repo}.${host.hostname}`;
|
|
6
|
+
return appendTrailingSlash(host);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function getInternalApiUrl(): Promise<URL> {
|
|
10
|
+
const host = await readHost();
|
|
11
|
+
host.hostname = `api.internal.${host.hostname}`;
|
|
12
|
+
return appendTrailingSlash(host);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function getUserServiceUrl(): Promise<URL> {
|
|
16
|
+
const host = await readHost();
|
|
17
|
+
host.hostname = `user-service.${host.hostname}`;
|
|
18
|
+
return appendTrailingSlash(host);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getAuthUrl(): Promise<URL> {
|
|
22
|
+
const host = await readHost();
|
|
23
|
+
host.hostname = `auth.${host.hostname}`;
|
|
24
|
+
return appendTrailingSlash(host);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function appendTrailingSlash(url: string | URL): URL {
|
|
28
|
+
const newURL = new URL(url);
|
|
29
|
+
if (!newURL.pathname.endsWith("/")) newURL.pathname += "/";
|
|
30
|
+
return newURL;
|
|
31
|
+
}
|
package/src/login.ts
ADDED
|
@@ -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
|
+
}
|
package/src/logout.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
|
|
3
|
+
import { removeToken } 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 removeToken();
|
|
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
|
+
}
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { setTimeout } from "node:timers/promises";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
|
|
5
|
+
import { isAuthenticated } from "./lib/auth";
|
|
6
|
+
import { safeGetRepositoryFromConfig } from "./lib/config";
|
|
7
|
+
import { fetchRemoteCustomTypes, fetchRemoteSlices } from "./lib/custom-types-api";
|
|
8
|
+
import { type FrameworkAdapter, getFramework } from "./framework";
|
|
9
|
+
import { trackEnd } from "./lib/segment";
|
|
10
|
+
import { dedent } from "./lib/string";
|
|
11
|
+
|
|
12
|
+
const HELP = `
|
|
13
|
+
Sync slices, page types, and custom types from Prismic to local files.
|
|
14
|
+
|
|
15
|
+
Remote models are the source of truth. Local files are created, updated,
|
|
16
|
+
or deleted to match.
|
|
17
|
+
|
|
18
|
+
USAGE
|
|
19
|
+
prismic sync [flags]
|
|
20
|
+
|
|
21
|
+
FLAGS
|
|
22
|
+
-r, --repo string Repository domain
|
|
23
|
+
-w, --watch Watch for changes and sync continuously
|
|
24
|
+
-h, --help Show help for command
|
|
25
|
+
`.trim();
|
|
26
|
+
|
|
27
|
+
// 5 seconds balances responsiveness with API load
|
|
28
|
+
const POLL_INTERVAL_MS = 5000;
|
|
29
|
+
const MAX_BACKOFF_MS = 60000; // Cap backoff at 1 minute
|
|
30
|
+
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
31
|
+
|
|
32
|
+
export async function sync(): Promise<void> {
|
|
33
|
+
const {
|
|
34
|
+
values: { help, repo = await safeGetRepositoryFromConfig(), watch },
|
|
35
|
+
} = parseArgs({
|
|
36
|
+
args: process.argv.slice(3), // skip: node, script, "sync"
|
|
37
|
+
options: {
|
|
38
|
+
repo: { type: "string", short: "r" },
|
|
39
|
+
watch: { type: "boolean", short: "w" },
|
|
40
|
+
help: { type: "boolean", short: "h" },
|
|
41
|
+
},
|
|
42
|
+
allowPositionals: false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (help) {
|
|
46
|
+
console.info(HELP);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!repo) {
|
|
51
|
+
console.error("Missing prismic.config.json or --repo option");
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!(await isAuthenticated())) {
|
|
57
|
+
console.error("Not logged in. Run `prismic login` first.");
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const framework = await getFramework();
|
|
63
|
+
if (!framework) {
|
|
64
|
+
console.error("Could not detect a supported framework (Next.js, Nuxt, or SvelteKit).");
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.info(`Syncing from repository: ${repo}`);
|
|
70
|
+
|
|
71
|
+
if (watch) {
|
|
72
|
+
await watchForChanges(repo, framework);
|
|
73
|
+
} else {
|
|
74
|
+
await syncSlices(repo, framework);
|
|
75
|
+
await syncCustomTypes(repo, framework);
|
|
76
|
+
|
|
77
|
+
console.info("Sync complete");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function watchForChanges(repo: string, framework: FrameworkAdapter) {
|
|
82
|
+
const remoteSlicesResult = await fetchRemoteSlices(repo);
|
|
83
|
+
if (!remoteSlicesResult.ok) {
|
|
84
|
+
console.error(`Failed to fetch remote slices: ${remoteSlicesResult.error}`);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const initialRemoteSlices = remoteSlicesResult.value;
|
|
89
|
+
|
|
90
|
+
const remoteCustomTypesResult = await fetchRemoteCustomTypes(repo);
|
|
91
|
+
if (!remoteCustomTypesResult.ok) {
|
|
92
|
+
console.error(`Failed to fetch remote custom types: ${remoteCustomTypesResult.error}`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const initialRemoteCustomTypes = remoteCustomTypesResult.value;
|
|
97
|
+
|
|
98
|
+
await syncSlices(repo, framework);
|
|
99
|
+
await syncCustomTypes(repo, framework);
|
|
100
|
+
|
|
101
|
+
console.info(dedent`
|
|
102
|
+
Initial sync completed!
|
|
103
|
+
|
|
104
|
+
Watching for changes (polling every ${POLL_INTERVAL_MS / 1000}s),
|
|
105
|
+
Press Ctrl+C to stop
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
let lastRemoteSlicesHash = hash(initialRemoteSlices);
|
|
109
|
+
let lastRemoteCustomTypesHash = hash(initialRemoteCustomTypes);
|
|
110
|
+
|
|
111
|
+
let consecutiveErrors = 0;
|
|
112
|
+
|
|
113
|
+
// Handle all common termination signals
|
|
114
|
+
process.on("SIGINT", shutdown); // Ctrl+C
|
|
115
|
+
process.on("SIGTERM", shutdown); // kill command
|
|
116
|
+
process.on("SIGHUP", shutdown); // terminal closed
|
|
117
|
+
process.on("SIGQUIT", shutdown); // Ctrl+\
|
|
118
|
+
if (process.platform === "win32") {
|
|
119
|
+
process.on("SIGBREAK", shutdown); // Windows Ctrl+Break
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
while (true) {
|
|
123
|
+
await setTimeout(exponentialMs(consecutiveErrors));
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const remoteSlicesResult = await fetchRemoteSlices(repo);
|
|
127
|
+
if (!remoteSlicesResult.ok) continue;
|
|
128
|
+
const remoteSlicesHash = hash(remoteSlicesResult.value);
|
|
129
|
+
const slicesChanged = remoteSlicesHash !== lastRemoteSlicesHash;
|
|
130
|
+
|
|
131
|
+
const remoteCustomTypesResult = await fetchRemoteCustomTypes(repo);
|
|
132
|
+
if (!remoteCustomTypesResult.ok) continue;
|
|
133
|
+
const remoteCustomTypesHash = hash(remoteCustomTypesResult.value);
|
|
134
|
+
const customTypesChanged = remoteCustomTypesHash !== lastRemoteCustomTypesHash;
|
|
135
|
+
|
|
136
|
+
if (slicesChanged || customTypesChanged) {
|
|
137
|
+
if (slicesChanged) {
|
|
138
|
+
await syncSlices(repo, framework);
|
|
139
|
+
lastRemoteSlicesHash = remoteSlicesHash;
|
|
140
|
+
}
|
|
141
|
+
if (customTypesChanged) {
|
|
142
|
+
await syncCustomTypes(repo, framework);
|
|
143
|
+
lastRemoteCustomTypesHash = remoteCustomTypesHash;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Reset error count on success
|
|
148
|
+
consecutiveErrors = 0;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
consecutiveErrors++;
|
|
151
|
+
|
|
152
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
153
|
+
|
|
154
|
+
const nextDelay = Math.min(
|
|
155
|
+
POLL_INTERVAL_MS * Math.pow(2, consecutiveErrors - 1),
|
|
156
|
+
MAX_BACKOFF_MS,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
console.error(`Error checking for changes: ${message}. Retrying in ${nextDelay / 1000}s...`);
|
|
160
|
+
|
|
161
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
162
|
+
throw new Error(`Too many consecutive errors (${MAX_CONSECUTIVE_ERRORS}), stopping watch.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function syncSlices(repo: string, framework: FrameworkAdapter): Promise<void> {
|
|
169
|
+
const remoteSlicesResult = await fetchRemoteSlices(repo);
|
|
170
|
+
if (!remoteSlicesResult.ok) {
|
|
171
|
+
console.error(`Failed to fetch remote slices: ${remoteSlicesResult.error}`);
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const remoteSlices = remoteSlicesResult.value;
|
|
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 remoteCustomTypesResult = await fetchRemoteCustomTypes(repo);
|
|
206
|
+
if (!remoteCustomTypesResult.ok) {
|
|
207
|
+
console.error(`Failed to fetch remote custom types: ${remoteCustomTypesResult.error}`);
|
|
208
|
+
process.exitCode = 1;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const remoteCustomTypes = remoteCustomTypesResult.value;
|
|
212
|
+
const localCustomTypes = await framework.getCustomTypes();
|
|
213
|
+
|
|
214
|
+
// Handle custom types update
|
|
215
|
+
for (const remoteCustomType of remoteCustomTypes) {
|
|
216
|
+
const localCustomType = localCustomTypes.find(
|
|
217
|
+
(customType) => customType.model.id === remoteCustomType.id,
|
|
218
|
+
);
|
|
219
|
+
if (localCustomType) {
|
|
220
|
+
await framework.updateCustomType(remoteCustomType);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle custom types deletion
|
|
225
|
+
for (const localCustomType of localCustomTypes) {
|
|
226
|
+
const existsRemotely = remoteCustomTypes.some(
|
|
227
|
+
(customType) => customType.id === localCustomType.model.id,
|
|
228
|
+
);
|
|
229
|
+
if (!existsRemotely) {
|
|
230
|
+
await framework.deleteCustomType(localCustomType.model.id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Handle custom types creation
|
|
235
|
+
for (const remoteCustomType of remoteCustomTypes) {
|
|
236
|
+
const existsLocally = localCustomTypes.some(
|
|
237
|
+
(customType) => customType.model.id === remoteCustomType.id,
|
|
238
|
+
);
|
|
239
|
+
if (!existsLocally) {
|
|
240
|
+
await framework.createCustomType(remoteCustomType);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function shutdown(): void {
|
|
246
|
+
console.info("Watch stopped. Goodbye!");
|
|
247
|
+
trackEnd("sync", true);
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Exponential backoff: 5s, 10s, 20s, 40s, 60s (capped)
|
|
252
|
+
function exponentialMs(base: number): number {
|
|
253
|
+
if (base === 0) return POLL_INTERVAL_MS;
|
|
254
|
+
return Math.min(POLL_INTERVAL_MS * Math.pow(2, base - 1), MAX_BACKOFF_MS);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function hash(data: unknown): string {
|
|
258
|
+
return createHash("sha256").update(JSON.stringify(data)).digest("hex");
|
|
259
|
+
}
|