@angeloashmore/prismic-cli-poc 0.0.0-canary.2ff9563

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.
Files changed (119) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +98 -0
  3. package/dist/index.mjs +1996 -0
  4. package/package.json +52 -0
  5. package/src/custom-type-add-field-boolean.ts +171 -0
  6. package/src/custom-type-add-field-color.ts +158 -0
  7. package/src/custom-type-add-field-date.ts +161 -0
  8. package/src/custom-type-add-field-embed.ts +158 -0
  9. package/src/custom-type-add-field-geo-point.ts +155 -0
  10. package/src/custom-type-add-field-image.ts +158 -0
  11. package/src/custom-type-add-field-key-text.ts +158 -0
  12. package/src/custom-type-add-field-link.ts +180 -0
  13. package/src/custom-type-add-field-number.ts +190 -0
  14. package/src/custom-type-add-field-rich-text.ts +181 -0
  15. package/src/custom-type-add-field-select.ts +164 -0
  16. package/src/custom-type-add-field-timestamp.ts +161 -0
  17. package/src/custom-type-add-field-uid.ts +158 -0
  18. package/src/custom-type-add-field.ts +111 -0
  19. package/src/custom-type-connect-slice.ts +221 -0
  20. package/src/custom-type-create.ts +92 -0
  21. package/src/custom-type-disconnect-slice.ts +179 -0
  22. package/src/custom-type-list.ts +110 -0
  23. package/src/custom-type-remove-field.ts +161 -0
  24. package/src/custom-type-remove.ts +126 -0
  25. package/src/custom-type-set-name.ts +128 -0
  26. package/src/custom-type-view.ts +118 -0
  27. package/src/custom-type.ts +85 -0
  28. package/src/index.ts +100 -0
  29. package/src/init.ts +62 -0
  30. package/src/lib/auth.ts +60 -0
  31. package/src/lib/config.ts +111 -0
  32. package/src/lib/file.ts +49 -0
  33. package/src/lib/json.ts +3 -0
  34. package/src/lib/request.ts +116 -0
  35. package/src/lib/slice.ts +112 -0
  36. package/src/lib/url.ts +25 -0
  37. package/src/locale-add.ts +116 -0
  38. package/src/locale-list.ts +107 -0
  39. package/src/locale-remove.ts +88 -0
  40. package/src/locale-set-default.ts +131 -0
  41. package/src/locale.ts +60 -0
  42. package/src/login.ts +143 -0
  43. package/src/logout.ts +36 -0
  44. package/src/page-type-add-field-boolean.ts +171 -0
  45. package/src/page-type-add-field-color.ts +158 -0
  46. package/src/page-type-add-field-date.ts +161 -0
  47. package/src/page-type-add-field-embed.ts +158 -0
  48. package/src/page-type-add-field-geo-point.ts +155 -0
  49. package/src/page-type-add-field-image.ts +158 -0
  50. package/src/page-type-add-field-key-text.ts +158 -0
  51. package/src/page-type-add-field-link.ts +180 -0
  52. package/src/page-type-add-field-number.ts +190 -0
  53. package/src/page-type-add-field-rich-text.ts +181 -0
  54. package/src/page-type-add-field-select.ts +164 -0
  55. package/src/page-type-add-field-timestamp.ts +161 -0
  56. package/src/page-type-add-field-uid.ts +158 -0
  57. package/src/page-type-add-field.ts +111 -0
  58. package/src/page-type-connect-slice.ts +221 -0
  59. package/src/page-type-create.ts +93 -0
  60. package/src/page-type-disconnect-slice.ts +179 -0
  61. package/src/page-type-list.ts +109 -0
  62. package/src/page-type-remove-field.ts +161 -0
  63. package/src/page-type-remove.ts +126 -0
  64. package/src/page-type-set-name.ts +128 -0
  65. package/src/page-type-set-repeatable.ts +137 -0
  66. package/src/page-type-view.ts +118 -0
  67. package/src/page-type.ts +90 -0
  68. package/src/preview-add.ts +126 -0
  69. package/src/preview-list.ts +106 -0
  70. package/src/preview-remove.ts +109 -0
  71. package/src/preview-set-name.ts +137 -0
  72. package/src/preview.ts +60 -0
  73. package/src/repo-create.ts +136 -0
  74. package/src/repo-list.ts +100 -0
  75. package/src/repo-set-name.ts +102 -0
  76. package/src/repo-view.ts +113 -0
  77. package/src/repo.ts +60 -0
  78. package/src/slice-add-field-boolean.ts +150 -0
  79. package/src/slice-add-field-color.ts +137 -0
  80. package/src/slice-add-field-date.ts +137 -0
  81. package/src/slice-add-field-embed.ts +137 -0
  82. package/src/slice-add-field-geo-point.ts +134 -0
  83. package/src/slice-add-field-image.ts +134 -0
  84. package/src/slice-add-field-key-text.ts +137 -0
  85. package/src/slice-add-field-link.ts +155 -0
  86. package/src/slice-add-field-number.ts +137 -0
  87. package/src/slice-add-field-rich-text.ts +160 -0
  88. package/src/slice-add-field-select.ts +143 -0
  89. package/src/slice-add-field-timestamp.ts +137 -0
  90. package/src/slice-add-field.ts +106 -0
  91. package/src/slice-add-variation.ts +137 -0
  92. package/src/slice-create.ts +129 -0
  93. package/src/slice-list-variations.ts +67 -0
  94. package/src/slice-list.ts +88 -0
  95. package/src/slice-remove-field.ts +117 -0
  96. package/src/slice-remove-variation.ts +108 -0
  97. package/src/slice-remove.ts +81 -0
  98. package/src/slice-rename.ts +112 -0
  99. package/src/slice-view.ts +77 -0
  100. package/src/slice.ts +90 -0
  101. package/src/sync.ts +309 -0
  102. package/src/token-create.ts +185 -0
  103. package/src/token-delete.ts +161 -0
  104. package/src/token-list.ts +212 -0
  105. package/src/token-set-name.ts +165 -0
  106. package/src/token.ts +60 -0
  107. package/src/webhook-add-header.ts +118 -0
  108. package/src/webhook-create.ts +152 -0
  109. package/src/webhook-disable.ts +109 -0
  110. package/src/webhook-enable.ts +132 -0
  111. package/src/webhook-list.ts +93 -0
  112. package/src/webhook-remove-header.ts +117 -0
  113. package/src/webhook-remove.ts +106 -0
  114. package/src/webhook-set-triggers.ts +148 -0
  115. package/src/webhook-status.ts +90 -0
  116. package/src/webhook-test.ts +106 -0
  117. package/src/webhook-view.ts +147 -0
  118. package/src/webhook.ts +95 -0
  119. package/src/whoami.ts +62 -0
@@ -0,0 +1,118 @@
1
+ import type { CustomType } from "@prismicio/types-internal/lib/customtypes";
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import { parseArgs } from "node:util";
5
+ import * as v from "valibot";
6
+
7
+ import { findUpward } from "./lib/file";
8
+
9
+ const HELP = `
10
+ View details of a specific custom type.
11
+
12
+ USAGE
13
+ prismic custom-type view <type-id> [flags]
14
+
15
+ ARGUMENTS
16
+ type-id Custom type identifier (required)
17
+
18
+ FLAGS
19
+ --json Output as JSON
20
+ -h, --help Show help for command
21
+
22
+ EXAMPLES
23
+ prismic custom-type view settings
24
+ prismic custom-type view settings --json
25
+ `.trim();
26
+
27
+ const CustomTypeSchema = v.object({
28
+ id: v.string(),
29
+ label: v.string(),
30
+ repeatable: v.boolean(),
31
+ status: v.boolean(),
32
+ format: v.optional(v.string()),
33
+ json: v.record(v.string(), v.record(v.string(), v.unknown())),
34
+ });
35
+
36
+ export async function customTypeView(): Promise<void> {
37
+ const {
38
+ values: { help, json },
39
+ positionals: [typeId],
40
+ } = parseArgs({
41
+ args: process.argv.slice(4), // skip: node, script, "custom-type", "view"
42
+ options: {
43
+ json: { type: "boolean" },
44
+ help: { type: "boolean", short: "h" },
45
+ },
46
+ allowPositionals: true,
47
+ });
48
+
49
+ if (help) {
50
+ console.info(HELP);
51
+ return;
52
+ }
53
+
54
+ if (!typeId) {
55
+ console.error("Missing required argument: type-id\n");
56
+ console.error("Usage: prismic custom-type view <type-id>");
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+
61
+ const projectRoot = await findUpward("package.json");
62
+ if (!projectRoot) {
63
+ console.error("Could not find project root (no package.json found)");
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+
68
+ const modelPath = new URL(`customtypes/${typeId}/index.json`, projectRoot);
69
+
70
+ let model: CustomType;
71
+ try {
72
+ const contents = await readFile(modelPath, "utf8");
73
+ const result = v.safeParse(CustomTypeSchema, JSON.parse(contents));
74
+ if (!result.success) {
75
+ console.error(`Invalid custom type model: ${modelPath.href}`);
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ model = result.output as CustomType;
80
+ } catch (error) {
81
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
82
+ console.error(`Custom type not found: ${typeId}\n`);
83
+ console.error(`Create it first with: prismic custom-type create ${typeId}`);
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ if (error instanceof Error) {
88
+ console.error(`Failed to read custom type: ${error.message}`);
89
+ } else {
90
+ console.error("Failed to read custom type");
91
+ }
92
+ process.exitCode = 1;
93
+ return;
94
+ }
95
+
96
+ // Check if this is actually a custom type (not a page type)
97
+ if (model.format === "page") {
98
+ console.error(`"${typeId}" is not a custom type (format: page)`);
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+
103
+ if (json) {
104
+ console.info(JSON.stringify(model, null, 2));
105
+ return;
106
+ }
107
+
108
+ console.info(`ID: ${model.id}`);
109
+ console.info(`Label: ${model.label}`);
110
+ console.info(`Repeatable: ${model.repeatable}`);
111
+
112
+ const tabs = Object.entries(model.json);
113
+ console.info(`\nTabs (${tabs.length}):`);
114
+ for (const [tabName, tabFields] of tabs) {
115
+ const fieldCount = Object.keys(tabFields).length;
116
+ console.info(` - ${tabName}: ${fieldCount} fields`);
117
+ }
118
+ }
@@ -0,0 +1,85 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ import { customTypeAddField } from "./custom-type-add-field";
4
+ import { customTypeConnectSlice } from "./custom-type-connect-slice";
5
+ import { customTypeCreate } from "./custom-type-create";
6
+ import { customTypeDisconnectSlice } from "./custom-type-disconnect-slice";
7
+ import { customTypeList } from "./custom-type-list";
8
+ import { customTypeRemove } from "./custom-type-remove";
9
+ import { customTypeRemoveField } from "./custom-type-remove-field";
10
+ import { customTypeSetName } from "./custom-type-set-name";
11
+ import { customTypeView } from "./custom-type-view";
12
+
13
+ const HELP = `
14
+ Manage custom types in a Prismic repository.
15
+
16
+ USAGE
17
+ prismic custom-type <command> [flags]
18
+
19
+ COMMANDS
20
+ create Create a new custom type
21
+ list List all custom types
22
+ view View details of a custom type
23
+ remove Remove a custom type
24
+ set-name Change a custom type's display name
25
+ add-field Add a field to a custom type
26
+ remove-field Remove a field from a custom type
27
+ connect-slice Connect a shared slice to a custom type
28
+ disconnect-slice Disconnect a shared slice from a custom type
29
+
30
+ FLAGS
31
+ -h, --help Show help for command
32
+
33
+ LEARN MORE
34
+ Use \`prismic custom-type <command> --help\` for more information about a command.
35
+ `.trim();
36
+
37
+ export async function customType(): Promise<void> {
38
+ const {
39
+ positionals: [subcommand],
40
+ } = parseArgs({
41
+ args: process.argv.slice(3), // skip: node, script, "custom-type"
42
+ options: {
43
+ help: { type: "boolean", short: "h" },
44
+ },
45
+ allowPositionals: true,
46
+ strict: false,
47
+ });
48
+
49
+ switch (subcommand) {
50
+ case "create":
51
+ await customTypeCreate();
52
+ break;
53
+ case "list":
54
+ await customTypeList();
55
+ break;
56
+ case "view":
57
+ await customTypeView();
58
+ break;
59
+ case "remove":
60
+ await customTypeRemove();
61
+ break;
62
+ case "set-name":
63
+ await customTypeSetName();
64
+ break;
65
+ case "add-field":
66
+ await customTypeAddField();
67
+ break;
68
+ case "remove-field":
69
+ await customTypeRemoveField();
70
+ break;
71
+ case "connect-slice":
72
+ await customTypeConnectSlice();
73
+ break;
74
+ case "disconnect-slice":
75
+ await customTypeDisconnectSlice();
76
+ break;
77
+ default: {
78
+ if (subcommand) {
79
+ console.error(`Unknown custom-type subcommand: ${subcommand}\n`);
80
+ process.exitCode = 1;
81
+ }
82
+ console.info(HELP);
83
+ }
84
+ }
85
+ }
package/src/index.ts ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from "node:util";
4
+
5
+ import { customType } from "./custom-type";
6
+ import { init } from "./init";
7
+ import { locale } from "./locale";
8
+ import { login } from "./login";
9
+ import { logout } from "./logout";
10
+ import { pageType } from "./page-type";
11
+ import { preview } from "./preview";
12
+ import { repo } from "./repo";
13
+ import { slice } from "./slice";
14
+ import { sync } from "./sync";
15
+ import { token } from "./token";
16
+ import { webhook } from "./webhook";
17
+ import { whoami } from "./whoami";
18
+
19
+ const HELP = `
20
+ Prismic CLI for managing repositories and configurations.
21
+
22
+ USAGE
23
+ prismic <command> [flags]
24
+
25
+ COMMANDS
26
+ init Initialize a Prismic project
27
+ login Log in to Prismic
28
+ logout Log out of Prismic
29
+ whoami Show the currently logged in user
30
+ repo Manage Prismic repositories
31
+ locale Manage locales in a repository
32
+ page-type Manage page types in a repository
33
+ custom-type Manage custom types in a repository
34
+ slice Manage slices in a project
35
+ sync Sync types and slices from Prismic
36
+ preview Manage preview configurations
37
+ token Manage API tokens in a repository
38
+ webhook Manage webhooks in a repository
39
+
40
+ FLAGS
41
+ -h, --help Show help for command
42
+
43
+ LEARN MORE
44
+ Use \`prismic <command> --help\` for more information about a command.
45
+ `.trim();
46
+
47
+ const { positionals } = parseArgs({
48
+ options: { help: { type: "boolean", short: "h" } },
49
+ allowPositionals: true,
50
+ strict: false,
51
+ });
52
+
53
+ switch (positionals[0]) {
54
+ case "init":
55
+ await init();
56
+ break;
57
+ case "login":
58
+ await login();
59
+ break;
60
+ case "logout":
61
+ await logout();
62
+ break;
63
+ case "whoami":
64
+ await whoami();
65
+ break;
66
+ case "repo":
67
+ await repo();
68
+ break;
69
+ case "locale":
70
+ await locale();
71
+ break;
72
+ case "page-type":
73
+ await pageType();
74
+ break;
75
+ case "custom-type":
76
+ await customType();
77
+ break;
78
+ case "slice":
79
+ await slice();
80
+ break;
81
+ case "sync":
82
+ await sync();
83
+ break;
84
+ case "preview":
85
+ await preview();
86
+ break;
87
+ case "token":
88
+ await token();
89
+ break;
90
+ case "webhook":
91
+ await webhook();
92
+ break;
93
+ default: {
94
+ if (positionals[0]) {
95
+ console.error(`Unknown command: ${positionals[0]}`);
96
+ process.exitCode = 1;
97
+ }
98
+ console.info(HELP);
99
+ }
100
+ }
package/src/init.ts ADDED
@@ -0,0 +1,62 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ import { createConfig, readConfig, UnknownProjectRoot } from "./lib/config";
4
+
5
+ const HELP = `
6
+ Initialize a Prismic project by creating a prismic.config.json file.
7
+
8
+ Use this command to connect an existing Prismic repository to your project.
9
+ To create a new repository, use \`prismic repo create\` instead.
10
+
11
+ USAGE
12
+ prismic init [flags]
13
+
14
+ FLAGS
15
+ -r, --repo Repository name (required)
16
+ -h, --help Show help for command
17
+
18
+ LEARN MORE
19
+ Use \`prismic <command> --help\` for more information about a command.
20
+ `.trim();
21
+
22
+ export async function init(): Promise<void> {
23
+ const { values } = parseArgs({
24
+ args: process.argv.slice(3),
25
+ options: {
26
+ help: { type: "boolean", short: "h" },
27
+ repo: { type: "string", short: "r" },
28
+ },
29
+ });
30
+
31
+ if (values.help) {
32
+ console.info(HELP);
33
+ return;
34
+ }
35
+
36
+ if (!values.repo) {
37
+ console.error("Missing required flag: --repo");
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+
42
+ const existingConfig = await readConfig();
43
+ if (existingConfig.ok) {
44
+ console.error("A prismic.config.json file already exists.");
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+
49
+ const result = await createConfig({ repositoryName: values.repo });
50
+
51
+ if (!result.ok) {
52
+ if (result.error instanceof UnknownProjectRoot) {
53
+ console.error("Could not find a package.json file. Run this command from a project directory.");
54
+ } else {
55
+ console.error("Failed to create config file.");
56
+ }
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+
61
+ console.info(`Created prismic.config.json for repository "${values.repo}"`);
62
+ }
@@ -0,0 +1,60 @@
1
+ import { access, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ import { appendTrailingSlash } from "./url";
6
+
7
+ const AUTH_FILE_PATH = new URL(".prismic", appendTrailingSlash(pathToFileURL(homedir())));
8
+ const DEFAULT_HOST = "https://prismic.io";
9
+
10
+ type AuthContents = {
11
+ token?: string;
12
+ host?: string;
13
+ };
14
+
15
+ export async function saveToken(token: string, options?: { host?: string }): Promise<void> {
16
+ const contents: AuthContents = { token, host: options?.host };
17
+ await writeFile(AUTH_FILE_PATH, JSON.stringify(contents, null, 2));
18
+ }
19
+
20
+ export async function isAuthenticated(): Promise<boolean> {
21
+ const token = await readToken();
22
+ return Boolean(token);
23
+ }
24
+
25
+ export async function readToken(): Promise<string | undefined> {
26
+ const auth = await readAuthFile();
27
+ return auth?.token;
28
+ }
29
+
30
+ export async function readHost(): Promise<URL> {
31
+ try {
32
+ const auth = await readAuthFile();
33
+ if (!auth?.host) return new URL(DEFAULT_HOST);
34
+ return new URL(auth.host);
35
+ } catch {
36
+ return new URL(DEFAULT_HOST);
37
+ }
38
+ }
39
+
40
+ async function readAuthFile(): Promise<AuthContents | undefined> {
41
+ try {
42
+ const contents = await readFile(AUTH_FILE_PATH, "utf-8");
43
+ return JSON.parse(contents);
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ export async function removeToken(): Promise<boolean> {
50
+ try {
51
+ await access(AUTH_FILE_PATH);
52
+ } catch {
53
+ return true;
54
+ }
55
+
56
+ const auth = await readAuthFile();
57
+ if (!auth) return false;
58
+ await rm(AUTH_FILE_PATH);
59
+ return true;
60
+ }
@@ -0,0 +1,111 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { readFile } from "node:fs/promises";
3
+ import { pathToFileURL } from "node:url";
4
+ import * as v from "valibot";
5
+
6
+ import { findUpward } from "./file";
7
+ import { stringify } from "./json";
8
+
9
+ const CONFIG_FILENAME = "prismic.config.json";
10
+
11
+ const ConfigSchema = v.object({
12
+ repositoryName: v.string(),
13
+ apiEndpoint: v.optional(v.pipe(v.string(), v.url())),
14
+ localSliceMachineSimulatorURL: v.optional(v.pipe(v.string(), v.url())),
15
+ libraries: v.optional(v.array(v.string())),
16
+ adapter: v.optional(v.string()),
17
+ labs: v.optional(
18
+ v.object({
19
+ legacySliceUpgrader: v.optional(v.boolean()),
20
+ }),
21
+ ),
22
+ });
23
+ type Config = v.InferOutput<typeof ConfigSchema>;
24
+
25
+ export type ConfigResult = SuccessfulConfigResult | FailedConfigResult;
26
+ export type SuccessfulConfigResult = { ok: true; config: Config };
27
+ export type FailedConfigResult = {
28
+ ok: false;
29
+ error: InvalidPrismicConfig | MissingPrismicConfig;
30
+ };
31
+ export type UnknownProjectRootConfigResult = {
32
+ ok: false;
33
+ error: UnknownProjectRoot;
34
+ };
35
+
36
+ export async function createConfig(
37
+ config: Config,
38
+ cwd = pathToFileURL(process.cwd()),
39
+ ): Promise<ConfigResult | UnknownProjectRootConfigResult> {
40
+ const result = await findSuggestedConfigPath(cwd);
41
+ if (!result.ok) return result;
42
+ await writeFile(result.path, stringify(config));
43
+ return { ok: true, config };
44
+ }
45
+
46
+ export async function safeGetRepositoryFromConfig(
47
+ cwd = pathToFileURL(process.cwd()),
48
+ ): Promise<string | undefined> {
49
+ const result = await readConfig(cwd);
50
+ if (result.ok) return result.config.repositoryName;
51
+ }
52
+
53
+ export async function readConfig(cwd = pathToFileURL(process.cwd())): Promise<ConfigResult> {
54
+ const findResult = await findConfig(cwd);
55
+ if (!findResult.ok) return findResult;
56
+
57
+ try {
58
+ const contents = await readFile(findResult.path, "utf8");
59
+ const result = v.safeParse(ConfigSchema, JSON.parse(contents));
60
+ if (!result.success) {
61
+ return { ok: false, error: new InvalidPrismicConfig(result.issues) };
62
+ }
63
+ return { ok: true, config: result.output };
64
+ } catch {
65
+ return { ok: false, error: new InvalidPrismicConfig() };
66
+ }
67
+ }
68
+
69
+ export class InvalidPrismicConfig extends Error {
70
+ issues: v.InferIssue<typeof ConfigSchema>[];
71
+
72
+ constructor(issues: v.InferIssue<typeof ConfigSchema>[] = []) {
73
+ super("prismic.config.json is invalid.");
74
+ this.issues = issues;
75
+ }
76
+ }
77
+
78
+ export async function updateConfig(
79
+ config: Partial<Config>,
80
+ cwd = pathToFileURL(process.cwd()),
81
+ ): Promise<ConfigResult> {
82
+ const findResult = await findConfig(cwd);
83
+ if (!findResult.ok) return findResult;
84
+
85
+ const readResult = await readConfig(cwd);
86
+ if (!readResult.ok) return readResult;
87
+
88
+ const updatedConfig = { ...readResult.config, ...config };
89
+ await writeFile(findResult.path, stringify(updatedConfig));
90
+ return { ok: true, config: updatedConfig };
91
+ }
92
+
93
+ async function findConfig(cwd = pathToFileURL(process.cwd())) {
94
+ const path = await findUpward(CONFIG_FILENAME, { start: cwd, stop: "package.json" });
95
+ if (!path) return { ok: false, error: new MissingPrismicConfig() } as const;
96
+ return { ok: true, path } as const;
97
+ }
98
+
99
+ async function findSuggestedConfigPath(cwd = pathToFileURL(process.cwd())) {
100
+ const path = await findUpward("package.json", { start: cwd });
101
+ if (!path) return { ok: false, error: new UnknownProjectRoot() } as const;
102
+ return { ok: true, path: new URL(CONFIG_FILENAME, path) } as const;
103
+ }
104
+
105
+ export class MissingPrismicConfig extends Error {
106
+ message = "Could not find a prismic.config.json file.";
107
+ }
108
+
109
+ export class UnknownProjectRoot extends Error {
110
+ message = "Could not find a package.json file.";
111
+ }
@@ -0,0 +1,49 @@
1
+ import { access } from "node:fs/promises";
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ import { appendTrailingSlash } from "./url";
5
+
6
+ export async function findUpward(
7
+ name: string,
8
+ config: { start?: URL; stop?: URL | string } = {},
9
+ ): Promise<URL | undefined> {
10
+ const { start = pathToFileURL(process.cwd()), stop } = config;
11
+
12
+ let dir = appendTrailingSlash(start);
13
+
14
+ while (true) {
15
+ const path = new URL(name, dir);
16
+ try {
17
+ await access(path);
18
+ return path;
19
+ } catch {}
20
+
21
+ if (typeof stop === "string") {
22
+ const stopPath = new URL(stop, dir);
23
+ try {
24
+ await access(stopPath);
25
+ return;
26
+ } catch {}
27
+ } else if (stop instanceof URL) {
28
+ if (stop.href === dir.href) {
29
+ return;
30
+ }
31
+ }
32
+
33
+ const parent = new URL("..", dir);
34
+ if (parent.href === dir.href) {
35
+ return undefined;
36
+ }
37
+
38
+ dir = parent;
39
+ }
40
+ }
41
+
42
+ export async function exists(path: URL): Promise<boolean> {
43
+ try {
44
+ await access(path);
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
@@ -0,0 +1,3 @@
1
+ export function stringify(input: unknown): string {
2
+ return JSON.stringify(input, null, 2);
3
+ }
@@ -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 {}