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.
- package/LICENSE +202 -0
- package/README.md +69 -0
- package/dist/builders-hKD4IrLX-CLndfeUt.mjs +97 -0
- package/dist/frameworks-CrDiIk_N.mjs +17 -0
- package/dist/index.mjs +90 -0
- package/dist/nextjs-CkWxsKc9.mjs +312 -0
- package/dist/nuxt-B4ZFmGzN.mjs +59 -0
- package/dist/sveltekit-DQPylEC2.mjs +226 -0
- package/package.json +58 -0
- package/src/clients/auth.ts +36 -0
- package/src/clients/custom-types.ts +36 -0
- package/src/clients/user.ts +35 -0
- package/src/commands/init.ts +162 -0
- package/src/commands/login.ts +45 -0
- package/src/commands/logout.ts +36 -0
- package/src/commands/sync.ts +256 -0
- package/src/commands/whoami.ts +37 -0
- package/src/env.ts +23 -0
- package/src/frameworks/index.ts +402 -0
- package/src/frameworks/nextjs.templates.ts +426 -0
- package/src/frameworks/nextjs.ts +216 -0
- package/src/frameworks/nuxt.templates.ts +74 -0
- package/src/frameworks/nuxt.ts +250 -0
- package/src/frameworks/sveltekit.templates.ts +278 -0
- package/src/frameworks/sveltekit.ts +241 -0
- package/src/index.ts +146 -0
- package/src/lib/auth.ts +176 -0
- package/src/lib/browser.ts +11 -0
- package/src/lib/config.ts +111 -0
- package/src/lib/error.ts +3 -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/request.ts +100 -0
- package/src/lib/segment.ts +197 -0
- package/src/lib/sentry.ts +67 -0
- package/src/lib/string.ts +10 -0
- package/src/lib/url.ts +12 -0
|
@@ -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
|
+
});
|