@sandboxsonny/ace-kit 0.6.0 → 0.7.1
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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/dist/astro-integration.js +1 -1
- package/dist/{chunk-G5FGBOFT.js → chunk-2W2NWDKV.js} +12 -9
- package/dist/cli.js +2 -2
- package/dist/{environment-T5K5ZJ2M.js → environment-TGMGSRJ3.js} +2 -1
- package/dist/{main-YRQQWLHX.js → main-IAQ2IL3M.js} +497 -231
- package/dist/react-router-server.d.ts +1 -1
- package/dist/vite.js +1 -1
- package/docs/astro.md +6 -8
- package/docs/command-line.md +32 -10
- package/docs/dev-sites.md +1 -1
- package/docs/react-router.md +36 -9
- package/docs/react.md +5 -2
- package/docs/typed-content.md +1 -1
- package/package.json +1 -1
- package/templates/react-router/app/ace-kit.generated.d.ts +7 -1
- package/templates/react-router/app/app.css +85 -0
- package/templates/react-router/app/components/page-content.tsx +47 -24
- package/templates/react-router/app/components/sections/faq.schema.ts +17 -0
- package/templates/react-router/app/components/sections/faq.tsx +26 -0
- package/templates/react-router/app/components/sections/gallery.schema.ts +20 -0
- package/templates/react-router/app/components/sections/gallery.tsx +39 -0
- package/templates/react-router/app/components/sections/hero.schema.ts +14 -0
- package/templates/react-router/app/components/sections/hero.tsx +25 -0
- package/templates/react-router/app/routes/page.test.tsx +54 -0
- package/templates/react-router/app/routes.ts +7 -3
- package/templates/react-router.section-types.json +109 -0
|
@@ -126,12 +126,6 @@ async function createSite(options) {
|
|
|
126
126
|
text = replaceOnce(file, text, /site: "https?:\/\/[^"]+"/, `site: "https://${domain}"`);
|
|
127
127
|
if (text !== original) await writeFile(path, text);
|
|
128
128
|
}
|
|
129
|
-
const settings = [
|
|
130
|
-
"# Where CMS Ace runs. Leave these out for the hosted CMS Ace; they are for a CMS on your own machine.",
|
|
131
|
-
"# ACE_API_URL=http://localhost:8080",
|
|
132
|
-
"# ACE_PREVIEW_ADMIN_ORIGIN=http://localhost:5173",
|
|
133
|
-
""
|
|
134
|
-
];
|
|
135
129
|
const extraSettings = Object.entries(options.environmentSettings ?? {}).map(([name2, value]) => `${name2}=${value}`);
|
|
136
130
|
if (options.framework === "astro") {
|
|
137
131
|
await writeNew(
|
|
@@ -140,8 +134,7 @@ async function createSite(options) {
|
|
|
140
134
|
[
|
|
141
135
|
"# The site's domain in CMS Ace is the `site` setting in astro.config.mjs. Set this only to override it.",
|
|
142
136
|
"# ACE_SITE_DOMAIN=" + domain,
|
|
143
|
-
""
|
|
144
|
-
...settings
|
|
137
|
+
""
|
|
145
138
|
].join("\n")
|
|
146
139
|
);
|
|
147
140
|
if (extraSettings.length > 0) await writeNew(folder, ".env", `${extraSettings.join("\n")}
|
|
@@ -149,7 +142,7 @@ async function createSite(options) {
|
|
|
149
142
|
} else {
|
|
150
143
|
const domainLine = `ACE_SITE_DOMAIN=${domain}
|
|
151
144
|
`;
|
|
152
|
-
await writeNew(folder, ".env.example", [`# The site's domain in CMS Ace.`, domainLine
|
|
145
|
+
await writeNew(folder, ".env.example", [`# The site's domain in CMS Ace.`, domainLine].join("\n"));
|
|
153
146
|
await writeNew(folder, ".env", [domainLine.trim(), ...extraSettings].join("\n") + "\n");
|
|
154
147
|
}
|
|
155
148
|
await writeNew(folder, "README.md", readme({ name, domain, framework: options.framework }));
|
|
@@ -182,7 +175,7 @@ var richText = (text) => ({
|
|
|
182
175
|
type: "doc",
|
|
183
176
|
content: text.split("\n\n").map((paragraph) => ({ type: "paragraph", content: [{ type: "text", text: paragraph }] }))
|
|
184
177
|
});
|
|
185
|
-
var frameworksWithSections = ["astro"];
|
|
178
|
+
var frameworksWithSections = ["astro", "react-router"];
|
|
186
179
|
function pageFields(framework) {
|
|
187
180
|
const fields = [
|
|
188
181
|
{ componentKey: "text", label: "Headline", handle: "headline", config: { required: true } },
|
|
@@ -396,6 +389,9 @@ async function resolveApiUrl(fromOption, environment) {
|
|
|
396
389
|
return null;
|
|
397
390
|
}
|
|
398
391
|
}
|
|
392
|
+
var cmsName = (apiUrl) => apiUrl === hostedApiUrl ? "CMS Ace" : `the CMS at ${apiUrl}`;
|
|
393
|
+
var cmsLabel = (apiUrl) => apiUrl === hostedApiUrl ? "CMS Ace" : apiUrl;
|
|
394
|
+
var loginCommandFor = (apiUrl) => apiUrl === hostedApiUrl ? "npx ace-kit login" : `npx ace-kit login --api-url ${apiUrl}`;
|
|
399
395
|
function isLocalApiUrl(apiUrl) {
|
|
400
396
|
return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(apiUrl).hostname);
|
|
401
397
|
}
|
|
@@ -525,13 +521,12 @@ async function waitForApproval(dependencies) {
|
|
|
525
521
|
var loginUsage = `ace-kit login: sign in to CMS Ace from the command line.
|
|
526
522
|
|
|
527
523
|
Usage:
|
|
528
|
-
ace-kit login
|
|
529
|
-
ace-kit logout
|
|
530
|
-
ace-kit whoami
|
|
524
|
+
ace-kit login Sign in. Opens the admin app, where you type the code shown here.
|
|
525
|
+
ace-kit logout Sign out, and forget the login kept on this machine.
|
|
526
|
+
ace-kit whoami Say who you are signed in as.
|
|
531
527
|
|
|
532
528
|
Options:
|
|
533
|
-
--
|
|
534
|
-
--help Show this message.
|
|
529
|
+
--help Show this message.
|
|
535
530
|
|
|
536
531
|
The login is kept in the macOS Keychain, or in a file only you can read on other systems. On a build server,
|
|
537
532
|
where nobody can approve a login in a browser, set ACE_TOKEN instead.
|
|
@@ -565,7 +560,7 @@ async function loginCommand(commandArguments, environment) {
|
|
|
565
560
|
const { apiUrl } = read;
|
|
566
561
|
if (apiUrl.startsWith("http://") && !isLocalApiUrl(apiUrl)) {
|
|
567
562
|
environment.error(
|
|
568
|
-
`Warning: ${apiUrl} is not encrypted (http://), so the login could be read on the way. Use https:// if you can.`
|
|
563
|
+
`Warning: ${cmsName(apiUrl)} is not encrypted (http://), so the login could be read on the way. Use https:// if you can.`
|
|
569
564
|
);
|
|
570
565
|
}
|
|
571
566
|
try {
|
|
@@ -589,7 +584,7 @@ async function signIn(apiUrl, environment) {
|
|
|
589
584
|
"unsupported",
|
|
590
585
|
"The CMS sent an approval address that isn't a web address, so it was not opened."
|
|
591
586
|
);
|
|
592
|
-
environment.out(`Signing in to ${apiUrl}
|
|
587
|
+
environment.out(`Signing in to ${cmsLabel(apiUrl)}
|
|
593
588
|
`);
|
|
594
589
|
environment.out(` 1. Go to ${page}`);
|
|
595
590
|
environment.out(` 2. Type ${request.userCode}
|
|
@@ -619,7 +614,7 @@ async function logoutCommand(commandArguments, environment) {
|
|
|
619
614
|
const { apiUrl } = read;
|
|
620
615
|
const stored = await environment.store.read(apiUrl);
|
|
621
616
|
if (!stored) {
|
|
622
|
-
environment.out(`Not signed in to ${apiUrl}.`);
|
|
617
|
+
environment.out(`Not signed in to ${cmsLabel(apiUrl)}.`);
|
|
623
618
|
return 0;
|
|
624
619
|
}
|
|
625
620
|
const ended = await environment.fetchImplementation(`${apiUrl}/auth/logout`, {
|
|
@@ -632,7 +627,7 @@ async function logoutCommand(commandArguments, environment) {
|
|
|
632
627
|
);
|
|
633
628
|
await environment.store.remove(apiUrl);
|
|
634
629
|
environment.out(
|
|
635
|
-
ended ? `Signed out of ${apiUrl}.` : `Forgot the login for ${apiUrl} on this machine, but could not reach the CMS to end it there. It ends by itself after 30 days unused; a super admin can also end it from the Sessions screen.`
|
|
630
|
+
ended ? `Signed out of ${cmsLabel(apiUrl)}.` : `Forgot the login for ${cmsLabel(apiUrl)} on this machine, but could not reach the CMS to end it there. It ends by itself after 30 days unused; a super admin can also end it from the Sessions screen.`
|
|
636
631
|
);
|
|
637
632
|
return 0;
|
|
638
633
|
}
|
|
@@ -651,7 +646,7 @@ async function whoamiCommand(commandArguments, environment) {
|
|
|
651
646
|
now: environment.now
|
|
652
647
|
});
|
|
653
648
|
if (!login) {
|
|
654
|
-
environment.out(`Not signed in to ${apiUrl}. Run:
|
|
649
|
+
environment.out(`Not signed in to ${cmsLabel(apiUrl)}. Run: ${loginCommandFor(apiUrl).replace("npx ", "")}`);
|
|
655
650
|
return 1;
|
|
656
651
|
}
|
|
657
652
|
try {
|
|
@@ -668,10 +663,10 @@ async function whoamiCommand(commandArguments, environment) {
|
|
|
668
663
|
return 1;
|
|
669
664
|
}
|
|
670
665
|
const me = await response.json();
|
|
671
|
-
environment.out(`Signed in to ${apiUrl} as ${String(me.email)}, using the login in ${login.source}.`);
|
|
666
|
+
environment.out(`Signed in to ${cmsLabel(apiUrl)} as ${String(me.email)}, using the login in ${login.source}.`);
|
|
672
667
|
return 0;
|
|
673
668
|
} catch {
|
|
674
|
-
environment.error(`Could not reach
|
|
669
|
+
environment.error(`Could not reach ${cmsName(apiUrl)}.`);
|
|
675
670
|
return 1;
|
|
676
671
|
}
|
|
677
672
|
}
|
|
@@ -680,7 +675,7 @@ async function whoamiCommand(commandArguments, environment) {
|
|
|
680
675
|
var createUsage = `ace-kit create: make a new site that uses CMS Ace.
|
|
681
676
|
|
|
682
677
|
Usage:
|
|
683
|
-
ace-kit create <folder> <domain> [--framework astro|react-router] [--name <name>]
|
|
678
|
+
ace-kit create <folder> <domain> [--framework astro|react-router] [--name <name>]
|
|
684
679
|
|
|
685
680
|
It signs you in if you aren't already (your browser opens to approve it), makes the site in CMS Ace with starter
|
|
686
681
|
content (a Pages type, Home, About and Contact, a main menu), and copies a starter site into the folder with your
|
|
@@ -691,7 +686,6 @@ Options:
|
|
|
691
686
|
<domain> The domain the site has in CMS Ace, e.g. example.com. (--site <domain> means the same.)
|
|
692
687
|
--framework astro (the default) or react-router.
|
|
693
688
|
--name The name shown to visitors (default: made from the folder's name, so my-site is "My Site").
|
|
694
|
-
--api-url Where the CMS Ace API lives (or set ACE_API_URL). Only for a CMS on your own machine.
|
|
695
689
|
--help Show this message.
|
|
696
690
|
`;
|
|
697
691
|
async function createCommand(commandArguments, environment) {
|
|
@@ -742,7 +736,7 @@ ${createUsage}`);
|
|
|
742
736
|
}
|
|
743
737
|
if (apiUrl.startsWith("http://") && !isLocalApiUrl(apiUrl)) {
|
|
744
738
|
environment.error(
|
|
745
|
-
`Warning: ${apiUrl} is not encrypted (http://), so your login could be read on the way. Use https:// if you can.`
|
|
739
|
+
`Warning: ${cmsName(apiUrl)} is not encrypted (http://), so your login could be read on the way. Use https:// if you can.`
|
|
746
740
|
);
|
|
747
741
|
}
|
|
748
742
|
let login = await resolveLogin({
|
|
@@ -797,7 +791,7 @@ Nothing was made.`);
|
|
|
797
791
|
switch (seeded.kind) {
|
|
798
792
|
case "seeded":
|
|
799
793
|
environment.out(
|
|
800
|
-
`Made ${created.domain} in CMS Ace (${apiUrl}) with a Pages type, ${seeded.entries} entries and a main menu.`
|
|
794
|
+
`Made ${created.domain} in ${apiUrl === hostedApiUrl ? "CMS Ace" : `CMS Ace (${apiUrl})`} with a Pages type, ${seeded.entries} entries and a main menu.`
|
|
801
795
|
);
|
|
802
796
|
break;
|
|
803
797
|
case "exists":
|
|
@@ -822,10 +816,462 @@ Next:
|
|
|
822
816
|
return seeded.kind === "failed" ? 1 : 0;
|
|
823
817
|
}
|
|
824
818
|
|
|
819
|
+
// src/cli/doctor-command.ts
|
|
820
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
821
|
+
import { join as join4 } from "path";
|
|
822
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
823
|
+
|
|
824
|
+
// src/sections/find-schemas.ts
|
|
825
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
826
|
+
import { join as join3, relative as relative2 } from "path";
|
|
827
|
+
var schemaFilePattern = /\.schema\.(?:ts|mts|js|mjs)$/;
|
|
828
|
+
var skippedFolders = /* @__PURE__ */ new Set([
|
|
829
|
+
"node_modules",
|
|
830
|
+
".git",
|
|
831
|
+
"dist",
|
|
832
|
+
"build",
|
|
833
|
+
".astro",
|
|
834
|
+
".react-router",
|
|
835
|
+
".output",
|
|
836
|
+
"coverage"
|
|
837
|
+
]);
|
|
838
|
+
var defaultSchemaFolders = ["src", "app"];
|
|
839
|
+
async function findSchemaFiles(root, folder) {
|
|
840
|
+
const folders = folder === void 0 ? defaultSchemaFolders : [folder];
|
|
841
|
+
const found = [];
|
|
842
|
+
async function walk(directory) {
|
|
843
|
+
let entries;
|
|
844
|
+
try {
|
|
845
|
+
entries = await readdir2(directory, { withFileTypes: true });
|
|
846
|
+
} catch {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
for (const entry of entries) {
|
|
850
|
+
const path = join3(directory, entry.name);
|
|
851
|
+
if (entry.isDirectory()) {
|
|
852
|
+
if (!skippedFolders.has(entry.name)) await walk(path);
|
|
853
|
+
} else if (entry.isFile() && schemaFilePattern.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
854
|
+
found.push(path);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
for (const name of folders) await walk(join3(root, name));
|
|
859
|
+
return found.sort((first, second) => {
|
|
860
|
+
const [firstName, secondName] = [relative2(root, first), relative2(root, second)];
|
|
861
|
+
return firstName < secondName ? -1 : firstName > secondName ? 1 : 0;
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/sections/load-schemas.ts
|
|
866
|
+
import { relative as relative3 } from "path";
|
|
867
|
+
|
|
868
|
+
// src/sections/payload.ts
|
|
869
|
+
var handlePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
870
|
+
var maximumHandleLength = 64;
|
|
871
|
+
var maximumNameLength = 200;
|
|
872
|
+
var fieldTypes = [
|
|
873
|
+
"text",
|
|
874
|
+
"textarea",
|
|
875
|
+
"number",
|
|
876
|
+
"checkbox",
|
|
877
|
+
"select",
|
|
878
|
+
"image",
|
|
879
|
+
"richtext",
|
|
880
|
+
"date",
|
|
881
|
+
"link"
|
|
882
|
+
];
|
|
883
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
884
|
+
var isHandle = (value) => typeof value === "string" && value.length <= maximumHandleLength && handlePattern.test(value);
|
|
885
|
+
var handleRule = 'lowercase letters, numbers and hyphens (such as "call-to-action")';
|
|
886
|
+
function checkFields(fields, where, problems) {
|
|
887
|
+
if (fields === void 0) return;
|
|
888
|
+
if (!Array.isArray(fields)) {
|
|
889
|
+
problems.push(`${where}: fields must be a list.`);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const seen = /* @__PURE__ */ new Set();
|
|
893
|
+
fields.forEach((field, index) => {
|
|
894
|
+
if (!isObject(field)) {
|
|
895
|
+
problems.push(`${where}: field ${index + 1} must be an object with a handle, a label and a type.`);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const name = isHandle(field.handle) ? `field "${field.handle}"` : `field ${index + 1}`;
|
|
899
|
+
if (!isHandle(field.handle)) problems.push(`${where}: ${name} needs a handle of ${handleRule}.`);
|
|
900
|
+
else if (seen.has(field.handle)) problems.push(`${where}: ${name} is defined twice.`);
|
|
901
|
+
else seen.add(field.handle);
|
|
902
|
+
if (typeof field.label !== "string" || field.label.trim() === "" || field.label.length > maximumNameLength) {
|
|
903
|
+
problems.push(`${where}: ${name} needs a label.`);
|
|
904
|
+
}
|
|
905
|
+
if (typeof field.type !== "string" || !fieldTypes.includes(field.type)) {
|
|
906
|
+
const given = typeof field.type === "string" ? ` "${field.type}"` : "";
|
|
907
|
+
problems.push(`${where}: ${name} has a type${given} that isn't one of ${fieldTypes.join(", ")}.`);
|
|
908
|
+
} else if (field.type === "select") {
|
|
909
|
+
const { options } = field;
|
|
910
|
+
if (!Array.isArray(options) || options.length === 0 || options.some((option) => typeof option !== "string" || option === "")) {
|
|
911
|
+
problems.push(`${where}: ${name} is a select, so it needs options: a list of the choices, as text.`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (field.required !== void 0 && typeof field.required !== "boolean") {
|
|
915
|
+
problems.push(`${where}: ${name} has a required that isn't true or false.`);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
function checkSectionDefinition(definition, where = "this section") {
|
|
920
|
+
const problems = [];
|
|
921
|
+
if (!isObject(definition)) {
|
|
922
|
+
return [`${where}: the default export must be the result of defineSection({ ... }).`];
|
|
923
|
+
}
|
|
924
|
+
if (!isHandle(definition.handle)) {
|
|
925
|
+
problems.push(`${where}: needs a handle of ${handleRule}.`);
|
|
926
|
+
}
|
|
927
|
+
if (typeof definition.name !== "string" || definition.name.trim() === "" || definition.name.length > maximumNameLength) {
|
|
928
|
+
problems.push(`${where}: needs a name.`);
|
|
929
|
+
}
|
|
930
|
+
checkFields(definition.fields, where, problems);
|
|
931
|
+
if (definition.blocks !== void 0) {
|
|
932
|
+
if (!isObject(definition.blocks)) {
|
|
933
|
+
problems.push(`${where}: blocks must be an object of the kinds of block, by handle.`);
|
|
934
|
+
} else {
|
|
935
|
+
for (const [handle, block] of Object.entries(definition.blocks)) {
|
|
936
|
+
const blockWhere = `${where}, block "${handle}"`;
|
|
937
|
+
if (!isHandle(handle)) problems.push(`${blockWhere}: the handle must be ${handleRule}.`);
|
|
938
|
+
if (!isObject(block)) {
|
|
939
|
+
problems.push(`${blockWhere}: must be an object with a name and fields.`);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (typeof block.name !== "string" || block.name.trim() === "") problems.push(`${blockWhere}: needs a name.`);
|
|
943
|
+
checkFields(block.fields, blockWhere, problems);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return problems;
|
|
948
|
+
}
|
|
949
|
+
function apiFields(fields) {
|
|
950
|
+
return (fields ?? []).map((field) => ({
|
|
951
|
+
handle: field.handle,
|
|
952
|
+
label: field.label,
|
|
953
|
+
component_key: field.type,
|
|
954
|
+
config: {
|
|
955
|
+
...field.required ? { required: true } : {},
|
|
956
|
+
...field.type === "select" ? { options: [...field.options] } : {}
|
|
957
|
+
}
|
|
958
|
+
}));
|
|
959
|
+
}
|
|
960
|
+
function sectionTypesPayload(definitions) {
|
|
961
|
+
return {
|
|
962
|
+
sections: definitions.map((definition) => ({
|
|
963
|
+
handle: definition.handle,
|
|
964
|
+
name: definition.name,
|
|
965
|
+
fields: apiFields(definition.fields),
|
|
966
|
+
blocks: Object.entries(definition.blocks ?? {}).map(([handle, block]) => ({
|
|
967
|
+
handle,
|
|
968
|
+
name: block.name,
|
|
969
|
+
fields: apiFields(block.fields)
|
|
970
|
+
}))
|
|
971
|
+
}))
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// src/sections/load-schemas.ts
|
|
976
|
+
async function loadSchemas(files, root, importModule) {
|
|
977
|
+
const definitions = [];
|
|
978
|
+
const problems = [];
|
|
979
|
+
const handleToFile = /* @__PURE__ */ new Map();
|
|
980
|
+
for (const file of files) {
|
|
981
|
+
const name = relative3(root, file);
|
|
982
|
+
let exported;
|
|
983
|
+
try {
|
|
984
|
+
const module = await importModule(file);
|
|
985
|
+
exported = module.default;
|
|
986
|
+
} catch (error) {
|
|
987
|
+
problems.push(`${name}: could not be loaded: ${error instanceof Error ? error.message : String(error)}`);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const fileProblems = checkSectionDefinition(exported, name);
|
|
991
|
+
if (fileProblems.length > 0) {
|
|
992
|
+
problems.push(...fileProblems);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
const definition = exported;
|
|
996
|
+
const earlier = handleToFile.get(definition.handle);
|
|
997
|
+
if (earlier) {
|
|
998
|
+
problems.push(`${name}: the handle "${definition.handle}" is already used by ${earlier}.`);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
handleToFile.set(definition.handle, name);
|
|
1002
|
+
definitions.push({ file, definition });
|
|
1003
|
+
}
|
|
1004
|
+
return { definitions, problems };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/cli/doctor-command.ts
|
|
1008
|
+
var doctorUsage = `ace-kit doctor: check that this site is set up to work with CMS Ace.
|
|
1009
|
+
|
|
1010
|
+
Usage:
|
|
1011
|
+
ace-kit doctor [--site <domain>] [--dir <folder>]
|
|
1012
|
+
|
|
1013
|
+
Run it in your site's folder. It changes nothing and never prints your login. It checks your Node version, that Ace Kit
|
|
1014
|
+
is installed, which CMS and site the settings point at, that the CMS can be reached and knows the site, that you are
|
|
1015
|
+
signed in, that the site has its Pages content, and that the section types in your code match the CMS. Each problem
|
|
1016
|
+
says what to do. It exits with 1 if anything is wrong.
|
|
1017
|
+
|
|
1018
|
+
Options:
|
|
1019
|
+
--site The domain your site has in the CMS (default: ACE_SITE_DOMAIN, else Astro's \`site\` setting).
|
|
1020
|
+
--dir The folder holding your *.schema.ts files (default: src and app).
|
|
1021
|
+
--help Show this message.
|
|
1022
|
+
`;
|
|
1023
|
+
var symbols = { ok: "\u2713", warn: "!", fail: "\u2717", info: "-" };
|
|
1024
|
+
var requestTimeoutMilliseconds4 = 15e3;
|
|
1025
|
+
var plain = (text) => String(text).replace(/\p{Cc}/gu, "").slice(0, 200);
|
|
1026
|
+
var isOlderThan = (version, major, minor) => {
|
|
1027
|
+
const [actualMajor = 0, actualMinor = 0] = version.split(".").map(Number);
|
|
1028
|
+
return actualMajor < major || actualMajor === major && actualMinor < minor;
|
|
1029
|
+
};
|
|
1030
|
+
async function readText(path) {
|
|
1031
|
+
try {
|
|
1032
|
+
return await readFile3(path, "utf8");
|
|
1033
|
+
} catch {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
async function domainFromAstroConfig(root) {
|
|
1038
|
+
for (const name of ["astro.config.mjs", "astro.config.ts", "astro.config.js", "astro.config.mts"]) {
|
|
1039
|
+
const text = await readText(join4(root, name));
|
|
1040
|
+
const found = text?.match(/\bsite\s*:\s*["']([^"']+)["']/);
|
|
1041
|
+
if (found?.[1]) return domainFromSite(found[1]);
|
|
1042
|
+
}
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
async function doctorCommand(commandArguments, environment) {
|
|
1046
|
+
const { values } = parseArgs3({
|
|
1047
|
+
// eslint-disable-next-line id-denylist -- "args" is the name node's parseArgs gives this option.
|
|
1048
|
+
args: commandArguments,
|
|
1049
|
+
options: {
|
|
1050
|
+
"api-url": { type: "string" },
|
|
1051
|
+
site: { type: "string" },
|
|
1052
|
+
dir: { type: "string" },
|
|
1053
|
+
help: { type: "boolean" }
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
if (values.help) {
|
|
1057
|
+
environment.out(doctorUsage);
|
|
1058
|
+
return 0;
|
|
1059
|
+
}
|
|
1060
|
+
const checks = [];
|
|
1061
|
+
const add = (status, label, hint) => checks.push({ status, label, hint });
|
|
1062
|
+
const root = environment.workingDirectory;
|
|
1063
|
+
if (isOlderThan(environment.nodeVersion, 22, 12))
|
|
1064
|
+
add(
|
|
1065
|
+
"fail",
|
|
1066
|
+
`Node ${environment.nodeVersion} is too old`,
|
|
1067
|
+
"Ace Kit needs Node 22.12 or newer. If you use mise, nvm or fnm, check which version it picks in this folder."
|
|
1068
|
+
);
|
|
1069
|
+
else add("ok", `Node ${environment.nodeVersion}`);
|
|
1070
|
+
const manifestText = await readText(join4(root, "package.json"));
|
|
1071
|
+
let framework = null;
|
|
1072
|
+
if (manifestText === null) {
|
|
1073
|
+
add("fail", "No package.json in this folder", "Run this in your site's folder.");
|
|
1074
|
+
} else {
|
|
1075
|
+
let manifest = {};
|
|
1076
|
+
try {
|
|
1077
|
+
manifest = JSON.parse(manifestText);
|
|
1078
|
+
} catch {
|
|
1079
|
+
add("fail", "package.json isn't valid JSON");
|
|
1080
|
+
}
|
|
1081
|
+
const dependencies = { ...manifest.devDependencies, ...manifest.dependencies };
|
|
1082
|
+
if ("astro" in dependencies) framework = "astro";
|
|
1083
|
+
else if ("react-router" in dependencies) framework = "react-router";
|
|
1084
|
+
add(
|
|
1085
|
+
framework ? "ok" : "info",
|
|
1086
|
+
framework ? framework === "astro" ? "An Astro site" : "A React Router site" : "Not an Astro or React Router site"
|
|
1087
|
+
);
|
|
1088
|
+
const installed = await readText(join4(root, "node_modules", "@sandboxsonny", "ace-kit", "package.json"));
|
|
1089
|
+
if (!("@sandboxsonny/ace-kit" in dependencies)) {
|
|
1090
|
+
add("fail", "Ace Kit isn't a dependency of this site", "Run: npm install @sandboxsonny/ace-kit");
|
|
1091
|
+
} else if (installed === null) {
|
|
1092
|
+
add("fail", "Ace Kit isn't installed", "Run: npm install");
|
|
1093
|
+
} else {
|
|
1094
|
+
let version = "unknown";
|
|
1095
|
+
try {
|
|
1096
|
+
version = String(JSON.parse(installed).version ?? "unknown");
|
|
1097
|
+
} catch {
|
|
1098
|
+
}
|
|
1099
|
+
add("ok", `Ace Kit ${version} is installed`);
|
|
1100
|
+
if (version !== environment.version && environment.version !== "development")
|
|
1101
|
+
add("info", `This command is Ace Kit ${environment.version}; the site uses ${version}`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
let apiUrl = null;
|
|
1105
|
+
try {
|
|
1106
|
+
apiUrl = normaliseApiUrl(values["api-url"] ?? environment.variables.ACE_API_URL ?? hostedApiUrl);
|
|
1107
|
+
add("ok", apiUrl === hostedApiUrl ? "Connected to CMS Ace" : `CMS: ${apiUrl}`);
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
add("fail", `The CMS address is wrong: ${error instanceof Error ? error.message : String(error)}`);
|
|
1110
|
+
}
|
|
1111
|
+
const given = values.site ?? environment.variables.ACE_SITE_DOMAIN;
|
|
1112
|
+
const domain = given ? domainFromSite(given) : framework === "astro" ? await domainFromAstroConfig(root) : null;
|
|
1113
|
+
if (domain) add("ok", `Site: ${domain}`);
|
|
1114
|
+
else
|
|
1115
|
+
add(
|
|
1116
|
+
"fail",
|
|
1117
|
+
"Can't tell which site this is",
|
|
1118
|
+
framework === "astro" ? 'Set `site: "https://your-domain.com"` in astro.config.mjs, or ACE_SITE_DOMAIN in .env.' : "Set ACE_SITE_DOMAIN=your-domain.com in .env."
|
|
1119
|
+
);
|
|
1120
|
+
if (apiUrl) await checkTheCms(apiUrl, domain);
|
|
1121
|
+
return finish();
|
|
1122
|
+
async function request(path, headers = {}) {
|
|
1123
|
+
try {
|
|
1124
|
+
const response = await environment.fetchImplementation(`${apiUrl}${path}`, {
|
|
1125
|
+
headers: { Accept: "application/json", ...headers },
|
|
1126
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds4)
|
|
1127
|
+
});
|
|
1128
|
+
return { status: response.status, body: await response.json().catch(() => null) };
|
|
1129
|
+
} catch {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
async function checkTheCms(address, siteDomain) {
|
|
1134
|
+
const health = await request("/health");
|
|
1135
|
+
if (!health || health.status >= 500 || health.status === 404) {
|
|
1136
|
+
add(
|
|
1137
|
+
"fail",
|
|
1138
|
+
`Can't reach the CMS at ${address}`,
|
|
1139
|
+
isLocalApiUrl(address) ? "Is your local CMS running? (docker compose -p cms-backend up -d)" : "Check the address and your connection."
|
|
1140
|
+
);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
add("ok", "CMS Ace answers");
|
|
1144
|
+
if (!siteDomain) return;
|
|
1145
|
+
const asSite = { "X-Ace-Site-Domain": siteDomain };
|
|
1146
|
+
const site = await request("/public/site", asSite);
|
|
1147
|
+
if (site?.status === 404) {
|
|
1148
|
+
add(
|
|
1149
|
+
"fail",
|
|
1150
|
+
`The CMS has no site with the domain ${siteDomain}`,
|
|
1151
|
+
"Make it with `npx @sandboxsonny/ace-kit create`, or in the admin app, or fix the domain."
|
|
1152
|
+
);
|
|
1153
|
+
} else if (site?.status === 200) {
|
|
1154
|
+
add("ok", `The CMS knows the site: "${plain(site.body?.name)}"`);
|
|
1155
|
+
await checkContent(asSite);
|
|
1156
|
+
} else {
|
|
1157
|
+
add("fail", `The CMS answered with an error (${site?.status ?? "no answer"}) when asked about the site`);
|
|
1158
|
+
}
|
|
1159
|
+
await checkLogin(address, siteDomain);
|
|
1160
|
+
}
|
|
1161
|
+
async function checkContent(asSite) {
|
|
1162
|
+
const types = await request("/public/content-types", asSite);
|
|
1163
|
+
const typeList = Array.isArray(types?.body) ? types.body : [];
|
|
1164
|
+
if (!typeList.some((type) => type.handle === "pages")) {
|
|
1165
|
+
add(
|
|
1166
|
+
"warn",
|
|
1167
|
+
"The site has no Pages content type",
|
|
1168
|
+
"The starter's pages read one. Add it in the admin app (Content types)."
|
|
1169
|
+
);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const entries = await request("/public/entries?content_type=pages&limit=100", asSite);
|
|
1173
|
+
const list = Array.isArray(entries?.body) ? entries.body : [];
|
|
1174
|
+
if (list.some((entry) => entry.handle === "home"))
|
|
1175
|
+
add("ok", `Pages has ${list.length} published ${list.length === 1 ? "entry" : "entries"}, including home`);
|
|
1176
|
+
else
|
|
1177
|
+
add(
|
|
1178
|
+
"warn",
|
|
1179
|
+
"Pages has no published entry called home",
|
|
1180
|
+
"The site's front page (/) reads it. Add one in the admin app and publish it."
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
async function checkLogin(address, siteDomain) {
|
|
1184
|
+
const login = await resolveLogin({
|
|
1185
|
+
apiUrl: address,
|
|
1186
|
+
variables: environment.variables,
|
|
1187
|
+
store: environment.store,
|
|
1188
|
+
now: environment.now
|
|
1189
|
+
});
|
|
1190
|
+
if (!login) {
|
|
1191
|
+
add("warn", "Not signed in", `Run: ${loginCommandFor(address)}. Section sync and dev sites need it.`);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const authorised = { Authorization: `Bearer ${login.token}` };
|
|
1195
|
+
const me = await request("/auth/me", authorised);
|
|
1196
|
+
if (me?.status === 401) {
|
|
1197
|
+
add("warn", "Your login has ended", "Run: npx ace-kit login");
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (me?.status !== 200) {
|
|
1201
|
+
add("warn", `Couldn't check your login (${me?.status ?? "no answer"})`);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
add("ok", `Signed in as ${plain(me.body?.email)}`);
|
|
1205
|
+
const sites = await request("/sites", authorised);
|
|
1206
|
+
const visible = Array.isArray(sites?.body) ? sites.body : [];
|
|
1207
|
+
const mine = visible.find((candidate) => candidate.kind !== "dev" && candidate.domain.toLowerCase() === siteDomain);
|
|
1208
|
+
if (!mine) {
|
|
1209
|
+
add("info", "This login can't see that site, so its section types and preview address weren't checked");
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (mine.preview_url) add("ok", `Preview address: ${plain(mine.preview_url)}`);
|
|
1213
|
+
else if (isLocalApiUrl(address))
|
|
1214
|
+
add(
|
|
1215
|
+
"warn",
|
|
1216
|
+
"The site has no Preview address",
|
|
1217
|
+
"Set it in the site's Settings (for example http://localhost:4321) so the customiser can show your site."
|
|
1218
|
+
);
|
|
1219
|
+
await checkSections(address, mine.id, authorised);
|
|
1220
|
+
}
|
|
1221
|
+
async function checkSections(address, siteId, authorised) {
|
|
1222
|
+
const files = await findSchemaFiles(root, values.dir);
|
|
1223
|
+
if (files.length === 0) {
|
|
1224
|
+
add("info", "No section schemas (*.schema.ts) in your code");
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const loaded = await loadSchemas(files, root, environment.importModule);
|
|
1228
|
+
if (loaded.problems.length > 0) {
|
|
1229
|
+
for (const problem of loaded.problems) add("fail", `Section schema problem: ${plain(problem)}`);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const inCode = loaded.definitions.map(({ definition }) => definition.handle);
|
|
1233
|
+
add("ok", `${inCode.length} section type${inCode.length === 1 ? "" : "s"} in your code (${inCode.join(", ")})`);
|
|
1234
|
+
const cms = await request(`/sites/${siteId}/section-types`, authorised);
|
|
1235
|
+
if (!Array.isArray(cms?.body)) return;
|
|
1236
|
+
const active = new Set(
|
|
1237
|
+
cms.body.filter((type) => !type.retired_at).map((type) => type.handle)
|
|
1238
|
+
);
|
|
1239
|
+
const missing = inCode.filter((handle) => !active.has(handle));
|
|
1240
|
+
const unused = [...active].filter((handle) => !inCode.includes(handle));
|
|
1241
|
+
if (missing.length > 0)
|
|
1242
|
+
add(
|
|
1243
|
+
"warn",
|
|
1244
|
+
`Not in the CMS yet: ${missing.join(", ")}`,
|
|
1245
|
+
`Run: npx ace-kit sync${isLocalApiUrl(address) ? ` --api-url ${address}` : ""} (the dev server does this on save)`
|
|
1246
|
+
);
|
|
1247
|
+
else add("ok", "The CMS has all of them");
|
|
1248
|
+
if (unused.length > 0)
|
|
1249
|
+
add(
|
|
1250
|
+
"info",
|
|
1251
|
+
`In the CMS but not in your code: ${unused.join(", ")} (a sync would retire ${unused.length === 1 ? "it" : "them"})`
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
function finish() {
|
|
1255
|
+
environment.out("ace-kit doctor\n");
|
|
1256
|
+
for (const check of checks) {
|
|
1257
|
+
environment.out(` ${symbols[check.status]} ${check.label}`);
|
|
1258
|
+
if (check.hint) environment.out(` ${check.hint}`);
|
|
1259
|
+
}
|
|
1260
|
+
const failed = checks.filter((check) => check.status === "fail").length;
|
|
1261
|
+
const warned = checks.filter((check) => check.status === "warn").length;
|
|
1262
|
+
environment.out(
|
|
1263
|
+
failed > 0 ? `
|
|
1264
|
+
${failed} problem${failed === 1 ? "" : "s"} to fix${warned > 0 ? `, and ${warned} warning${warned === 1 ? "" : "s"}` : ""}.` : warned > 0 ? `
|
|
1265
|
+
Nothing is broken; ${warned} warning${warned === 1 ? "" : "s"}.` : "\nAll good."
|
|
1266
|
+
);
|
|
1267
|
+
return failed > 0 ? 1 : 0;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
825
1271
|
// src/cli/generate-command.ts
|
|
826
1272
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
827
1273
|
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
828
|
-
import { parseArgs as
|
|
1274
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
829
1275
|
|
|
830
1276
|
// src/client/errors.ts
|
|
831
1277
|
var AcePreviewExpiredError = class extends Error {
|
|
@@ -842,7 +1288,7 @@ var AceUnavailableError = class extends Error {
|
|
|
842
1288
|
};
|
|
843
1289
|
|
|
844
1290
|
// src/client/create-client.ts
|
|
845
|
-
var
|
|
1291
|
+
var requestTimeoutMilliseconds5 = 5e3;
|
|
846
1292
|
var maximumCachedAnswers = 500;
|
|
847
1293
|
function createAceClient({
|
|
848
1294
|
apiUrl,
|
|
@@ -866,7 +1312,7 @@ function createAceClient({
|
|
|
866
1312
|
try {
|
|
867
1313
|
response = await fetchImplementation(`${apiUrl.replace(/\/+$/, "")}${path}`, {
|
|
868
1314
|
headers: { Accept: "application/json", "X-Ace-Site-Domain": siteDomain, "X-Forwarded-Host": siteDomain },
|
|
869
|
-
signal: AbortSignal.timeout(
|
|
1315
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds5)
|
|
870
1316
|
});
|
|
871
1317
|
} catch {
|
|
872
1318
|
throw new AceUnavailableError("Could not reach the CMS.");
|
|
@@ -985,16 +1431,15 @@ export {};
|
|
|
985
1431
|
var generateUsage = `ace-kit generate: write TypeScript types for your CMS Ace content types.
|
|
986
1432
|
|
|
987
1433
|
Usage:
|
|
988
|
-
ace-kit generate [--site <domain>] [--
|
|
1434
|
+
ace-kit generate [--site <domain>] [--out <file>]
|
|
989
1435
|
|
|
990
1436
|
Options:
|
|
991
1437
|
--site The domain your site has in the CMS, e.g. example.com (or set ACE_SITE_DOMAIN). --domain means the same.
|
|
992
|
-
--api-url Where the CMS Ace API lives (or set ACE_API_URL). Only for a CMS on your own machine.
|
|
993
1438
|
--out Where to write the types (default: src/ace-kit.generated.d.ts).
|
|
994
1439
|
--help Show this message.
|
|
995
1440
|
`;
|
|
996
1441
|
async function generateCommand(commandArguments, environment) {
|
|
997
|
-
const { values } =
|
|
1442
|
+
const { values } = parseArgs4({
|
|
998
1443
|
// eslint-disable-next-line id-denylist -- "args" is the name node's parseArgs gives this option.
|
|
999
1444
|
args: commandArguments,
|
|
1000
1445
|
options: {
|
|
@@ -1033,192 +1478,10 @@ async function generateCommand(commandArguments, environment) {
|
|
|
1033
1478
|
}
|
|
1034
1479
|
|
|
1035
1480
|
// src/cli/sync-command.ts
|
|
1036
|
-
import { parseArgs as
|
|
1037
|
-
|
|
1038
|
-
// src/sections/find-schemas.ts
|
|
1039
|
-
import { readdir as readdir2 } from "fs/promises";
|
|
1040
|
-
import { join as join3, relative as relative2 } from "path";
|
|
1041
|
-
var schemaFilePattern = /\.schema\.(?:ts|mts|js|mjs)$/;
|
|
1042
|
-
var skippedFolders = /* @__PURE__ */ new Set([
|
|
1043
|
-
"node_modules",
|
|
1044
|
-
".git",
|
|
1045
|
-
"dist",
|
|
1046
|
-
"build",
|
|
1047
|
-
".astro",
|
|
1048
|
-
".react-router",
|
|
1049
|
-
".output",
|
|
1050
|
-
"coverage"
|
|
1051
|
-
]);
|
|
1052
|
-
async function findSchemaFiles(root, folder = "src") {
|
|
1053
|
-
const start = join3(root, folder);
|
|
1054
|
-
const found = [];
|
|
1055
|
-
async function walk(directory) {
|
|
1056
|
-
let entries;
|
|
1057
|
-
try {
|
|
1058
|
-
entries = await readdir2(directory, { withFileTypes: true });
|
|
1059
|
-
} catch {
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
for (const entry of entries) {
|
|
1063
|
-
const path = join3(directory, entry.name);
|
|
1064
|
-
if (entry.isDirectory()) {
|
|
1065
|
-
if (!skippedFolders.has(entry.name)) await walk(path);
|
|
1066
|
-
} else if (entry.isFile() && schemaFilePattern.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
1067
|
-
found.push(path);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
await walk(start);
|
|
1072
|
-
return found.sort((first, second) => {
|
|
1073
|
-
const [firstName, secondName] = [relative2(root, first), relative2(root, second)];
|
|
1074
|
-
return firstName < secondName ? -1 : firstName > secondName ? 1 : 0;
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// src/sections/load-schemas.ts
|
|
1079
|
-
import { relative as relative3 } from "path";
|
|
1080
|
-
|
|
1081
|
-
// src/sections/payload.ts
|
|
1082
|
-
var handlePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
1083
|
-
var maximumHandleLength = 64;
|
|
1084
|
-
var maximumNameLength = 200;
|
|
1085
|
-
var fieldTypes = [
|
|
1086
|
-
"text",
|
|
1087
|
-
"textarea",
|
|
1088
|
-
"number",
|
|
1089
|
-
"checkbox",
|
|
1090
|
-
"select",
|
|
1091
|
-
"image",
|
|
1092
|
-
"richtext",
|
|
1093
|
-
"date",
|
|
1094
|
-
"link"
|
|
1095
|
-
];
|
|
1096
|
-
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1097
|
-
var isHandle = (value) => typeof value === "string" && value.length <= maximumHandleLength && handlePattern.test(value);
|
|
1098
|
-
var handleRule = 'lowercase letters, numbers and hyphens (such as "call-to-action")';
|
|
1099
|
-
function checkFields(fields, where, problems) {
|
|
1100
|
-
if (fields === void 0) return;
|
|
1101
|
-
if (!Array.isArray(fields)) {
|
|
1102
|
-
problems.push(`${where}: fields must be a list.`);
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1106
|
-
fields.forEach((field, index) => {
|
|
1107
|
-
if (!isObject(field)) {
|
|
1108
|
-
problems.push(`${where}: field ${index + 1} must be an object with a handle, a label and a type.`);
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
const name = isHandle(field.handle) ? `field "${field.handle}"` : `field ${index + 1}`;
|
|
1112
|
-
if (!isHandle(field.handle)) problems.push(`${where}: ${name} needs a handle of ${handleRule}.`);
|
|
1113
|
-
else if (seen.has(field.handle)) problems.push(`${where}: ${name} is defined twice.`);
|
|
1114
|
-
else seen.add(field.handle);
|
|
1115
|
-
if (typeof field.label !== "string" || field.label.trim() === "" || field.label.length > maximumNameLength) {
|
|
1116
|
-
problems.push(`${where}: ${name} needs a label.`);
|
|
1117
|
-
}
|
|
1118
|
-
if (typeof field.type !== "string" || !fieldTypes.includes(field.type)) {
|
|
1119
|
-
const given = typeof field.type === "string" ? ` "${field.type}"` : "";
|
|
1120
|
-
problems.push(`${where}: ${name} has a type${given} that isn't one of ${fieldTypes.join(", ")}.`);
|
|
1121
|
-
} else if (field.type === "select") {
|
|
1122
|
-
const { options } = field;
|
|
1123
|
-
if (!Array.isArray(options) || options.length === 0 || options.some((option) => typeof option !== "string" || option === "")) {
|
|
1124
|
-
problems.push(`${where}: ${name} is a select, so it needs options: a list of the choices, as text.`);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
if (field.required !== void 0 && typeof field.required !== "boolean") {
|
|
1128
|
-
problems.push(`${where}: ${name} has a required that isn't true or false.`);
|
|
1129
|
-
}
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
function checkSectionDefinition(definition, where = "this section") {
|
|
1133
|
-
const problems = [];
|
|
1134
|
-
if (!isObject(definition)) {
|
|
1135
|
-
return [`${where}: the default export must be the result of defineSection({ ... }).`];
|
|
1136
|
-
}
|
|
1137
|
-
if (!isHandle(definition.handle)) {
|
|
1138
|
-
problems.push(`${where}: needs a handle of ${handleRule}.`);
|
|
1139
|
-
}
|
|
1140
|
-
if (typeof definition.name !== "string" || definition.name.trim() === "" || definition.name.length > maximumNameLength) {
|
|
1141
|
-
problems.push(`${where}: needs a name.`);
|
|
1142
|
-
}
|
|
1143
|
-
checkFields(definition.fields, where, problems);
|
|
1144
|
-
if (definition.blocks !== void 0) {
|
|
1145
|
-
if (!isObject(definition.blocks)) {
|
|
1146
|
-
problems.push(`${where}: blocks must be an object of the kinds of block, by handle.`);
|
|
1147
|
-
} else {
|
|
1148
|
-
for (const [handle, block] of Object.entries(definition.blocks)) {
|
|
1149
|
-
const blockWhere = `${where}, block "${handle}"`;
|
|
1150
|
-
if (!isHandle(handle)) problems.push(`${blockWhere}: the handle must be ${handleRule}.`);
|
|
1151
|
-
if (!isObject(block)) {
|
|
1152
|
-
problems.push(`${blockWhere}: must be an object with a name and fields.`);
|
|
1153
|
-
continue;
|
|
1154
|
-
}
|
|
1155
|
-
if (typeof block.name !== "string" || block.name.trim() === "") problems.push(`${blockWhere}: needs a name.`);
|
|
1156
|
-
checkFields(block.fields, blockWhere, problems);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
return problems;
|
|
1161
|
-
}
|
|
1162
|
-
function apiFields(fields) {
|
|
1163
|
-
return (fields ?? []).map((field) => ({
|
|
1164
|
-
handle: field.handle,
|
|
1165
|
-
label: field.label,
|
|
1166
|
-
component_key: field.type,
|
|
1167
|
-
config: {
|
|
1168
|
-
...field.required ? { required: true } : {},
|
|
1169
|
-
...field.type === "select" ? { options: [...field.options] } : {}
|
|
1170
|
-
}
|
|
1171
|
-
}));
|
|
1172
|
-
}
|
|
1173
|
-
function sectionTypesPayload(definitions) {
|
|
1174
|
-
return {
|
|
1175
|
-
sections: definitions.map((definition) => ({
|
|
1176
|
-
handle: definition.handle,
|
|
1177
|
-
name: definition.name,
|
|
1178
|
-
fields: apiFields(definition.fields),
|
|
1179
|
-
blocks: Object.entries(definition.blocks ?? {}).map(([handle, block]) => ({
|
|
1180
|
-
handle,
|
|
1181
|
-
name: block.name,
|
|
1182
|
-
fields: apiFields(block.fields)
|
|
1183
|
-
}))
|
|
1184
|
-
}))
|
|
1185
|
-
};
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// src/sections/load-schemas.ts
|
|
1189
|
-
async function loadSchemas(files, root, importModule) {
|
|
1190
|
-
const definitions = [];
|
|
1191
|
-
const problems = [];
|
|
1192
|
-
const handleToFile = /* @__PURE__ */ new Map();
|
|
1193
|
-
for (const file of files) {
|
|
1194
|
-
const name = relative3(root, file);
|
|
1195
|
-
let exported;
|
|
1196
|
-
try {
|
|
1197
|
-
const module = await importModule(file);
|
|
1198
|
-
exported = module.default;
|
|
1199
|
-
} catch (error) {
|
|
1200
|
-
problems.push(`${name}: could not be loaded: ${error instanceof Error ? error.message : String(error)}`);
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
const fileProblems = checkSectionDefinition(exported, name);
|
|
1204
|
-
if (fileProblems.length > 0) {
|
|
1205
|
-
problems.push(...fileProblems);
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
const definition = exported;
|
|
1209
|
-
const earlier = handleToFile.get(definition.handle);
|
|
1210
|
-
if (earlier) {
|
|
1211
|
-
problems.push(`${name}: the handle "${definition.handle}" is already used by ${earlier}.`);
|
|
1212
|
-
continue;
|
|
1213
|
-
}
|
|
1214
|
-
handleToFile.set(definition.handle, name);
|
|
1215
|
-
definitions.push({ file, definition });
|
|
1216
|
-
}
|
|
1217
|
-
return { definitions, problems };
|
|
1218
|
-
}
|
|
1481
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
1219
1482
|
|
|
1220
1483
|
// src/sections/sync.ts
|
|
1221
|
-
var
|
|
1484
|
+
var requestTimeoutMilliseconds6 = 2e4;
|
|
1222
1485
|
async function readMessage2(response, fallback) {
|
|
1223
1486
|
try {
|
|
1224
1487
|
const body = await response.json();
|
|
@@ -1234,7 +1497,7 @@ async function syncSectionTypes(options) {
|
|
|
1234
1497
|
try {
|
|
1235
1498
|
sitesResponse = await fetchImplementation(`${apiUrl}/sites`, {
|
|
1236
1499
|
headers,
|
|
1237
|
-
signal: AbortSignal.timeout(
|
|
1500
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds6)
|
|
1238
1501
|
});
|
|
1239
1502
|
} catch {
|
|
1240
1503
|
return { kind: "unreachable" };
|
|
@@ -1256,7 +1519,7 @@ async function syncSectionTypes(options) {
|
|
|
1256
1519
|
method: "PUT",
|
|
1257
1520
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
1258
1521
|
body: JSON.stringify(payload),
|
|
1259
|
-
signal: AbortSignal.timeout(
|
|
1522
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds6)
|
|
1260
1523
|
});
|
|
1261
1524
|
} catch {
|
|
1262
1525
|
return { kind: "unreachable" };
|
|
@@ -1290,9 +1553,9 @@ async function syncSectionTypes(options) {
|
|
|
1290
1553
|
var syncUsage = `ace-kit sync: send the section types your code defines to CMS Ace.
|
|
1291
1554
|
|
|
1292
1555
|
Usage:
|
|
1293
|
-
ace-kit sync [--site <domain>] [--
|
|
1556
|
+
ace-kit sync [--site <domain>] [--dir <folder>]
|
|
1294
1557
|
|
|
1295
|
-
It finds every *.schema.ts file in the folder (src by default), checks them, and makes the CMS's section types for your
|
|
1558
|
+
It finds every *.schema.ts file in the folder (src and app by default), checks them, and makes the CMS's section types for your
|
|
1296
1559
|
site match. The dev server does this for you whenever a schema file is saved; run it yourself for a one-off sync, or on a
|
|
1297
1560
|
build server (with ACE_TOKEN set).
|
|
1298
1561
|
|
|
@@ -1300,15 +1563,14 @@ Nothing is deleted: a section type, block or field you take out of your code is
|
|
|
1300
1563
|
written in it survives. If any schema file can't be loaded, nothing is synced.
|
|
1301
1564
|
|
|
1302
1565
|
Options:
|
|
1303
|
-
--site The domain your site has in the CMS, e.g. example.com (
|
|
1566
|
+
--site The domain your site has in the CMS, e.g. example.com (or set ACE_SITE_DOMAIN).
|
|
1304
1567
|
(--domain means the same.)
|
|
1305
|
-
--
|
|
1306
|
-
--dir The folder to look in (default: src).
|
|
1568
|
+
--dir The folder to look in (default: src and app).
|
|
1307
1569
|
--help Show this message.
|
|
1308
1570
|
`;
|
|
1309
1571
|
var isSwitchedOff = (value) => ["false", "off", "no", "0"].includes((value ?? "").trim().toLowerCase());
|
|
1310
1572
|
async function syncCommand(commandArguments, environment) {
|
|
1311
|
-
const { values } =
|
|
1573
|
+
const { values } = parseArgs5({
|
|
1312
1574
|
// eslint-disable-next-line id-denylist -- "args" is the name node's parseArgs gives this option.
|
|
1313
1575
|
args: commandArguments,
|
|
1314
1576
|
options: {
|
|
@@ -1344,11 +1606,11 @@ async function syncCommand(commandArguments, environment) {
|
|
|
1344
1606
|
);
|
|
1345
1607
|
return 0;
|
|
1346
1608
|
}
|
|
1347
|
-
const files = await findSchemaFiles(environment.workingDirectory, values.dir
|
|
1609
|
+
const files = await findSchemaFiles(environment.workingDirectory, values.dir);
|
|
1348
1610
|
if (files.length === 0) {
|
|
1349
1611
|
if (!automatic) {
|
|
1350
1612
|
environment.out(
|
|
1351
|
-
`No *.schema.ts files found in ${values.dir ?? "src"}, so nothing was synced (syncing none would retire every section).`
|
|
1613
|
+
`No *.schema.ts files found in ${values.dir ?? "src or app"}, so nothing was synced (syncing none would retire every section).`
|
|
1352
1614
|
);
|
|
1353
1615
|
}
|
|
1354
1616
|
return 0;
|
|
@@ -1360,7 +1622,7 @@ async function syncCommand(commandArguments, environment) {
|
|
|
1360
1622
|
now: environment.now
|
|
1361
1623
|
});
|
|
1362
1624
|
if (!login) {
|
|
1363
|
-
environment.out(`Section sync is waiting for you to sign in. Run:
|
|
1625
|
+
environment.out(`Section sync is waiting for you to sign in. Run: ${loginCommandFor(apiUrl)}`);
|
|
1364
1626
|
return automatic ? 0 : 1;
|
|
1365
1627
|
}
|
|
1366
1628
|
const loaded = await loadSchemas(files, environment.workingDirectory, environment.importModule);
|
|
@@ -1381,17 +1643,17 @@ async function syncCommand(commandArguments, environment) {
|
|
|
1381
1643
|
const defined = outcome.sections.filter((section) => !section.retired).map((section) => section.handle);
|
|
1382
1644
|
const retired = outcome.sections.filter((section) => section.retired).map((section) => section.handle);
|
|
1383
1645
|
environment.out(
|
|
1384
|
-
`Synced ${defined.length} section type${defined.length === 1 ? "" : "s"} (${defined.join(", ")}) to ${outcome.site.domain} (${apiUrl}).` + (retired.length > 0 ? ` No longer in your code, kept but hidden: ${retired.join(", ")}.` : "")
|
|
1646
|
+
`Synced ${defined.length} section type${defined.length === 1 ? "" : "s"} (${defined.join(", ")}) to ${outcome.site.domain}${apiUrl === hostedApiUrl ? "" : ` (${apiUrl})`}.` + (retired.length > 0 ? ` No longer in your code, kept but hidden: ${retired.join(", ")}.` : "")
|
|
1385
1647
|
);
|
|
1386
1648
|
for (const warning of outcome.warnings) environment.error(`Warning: ${warning}`);
|
|
1387
1649
|
return 0;
|
|
1388
1650
|
}
|
|
1389
1651
|
case "login-ended":
|
|
1390
|
-
environment.out(`Your login has ended. Run:
|
|
1652
|
+
environment.out(`Your login has ended. Run: ${loginCommandFor(apiUrl)}`);
|
|
1391
1653
|
return automatic ? 0 : 1;
|
|
1392
1654
|
case "site-not-found":
|
|
1393
1655
|
environment.error(
|
|
1394
|
-
|
|
1656
|
+
`${cmsName(apiUrl)} has no site with the domain "${outcome.domain}" that you can see` + (outcome.available.length > 0 ? ` (your sites: ${outcome.available.join(", ")})` : "") + ". Set ACE_SITE_DOMAIN or pass --site."
|
|
1395
1657
|
);
|
|
1396
1658
|
return 1;
|
|
1397
1659
|
case "forbidden":
|
|
@@ -1399,7 +1661,7 @@ async function syncCommand(commandArguments, environment) {
|
|
|
1399
1661
|
environment.error(outcome.message);
|
|
1400
1662
|
return 1;
|
|
1401
1663
|
case "unreachable":
|
|
1402
|
-
environment.error(`Could not reach
|
|
1664
|
+
environment.error(`Could not reach ${cmsName(apiUrl)}.`);
|
|
1403
1665
|
return 1;
|
|
1404
1666
|
}
|
|
1405
1667
|
}
|
|
@@ -1409,6 +1671,7 @@ var usage = `ace-kit: tools for sites built on CMS Ace.
|
|
|
1409
1671
|
|
|
1410
1672
|
Commands:
|
|
1411
1673
|
ace-kit create Make a new site from a starter.
|
|
1674
|
+
ace-kit doctor Check this site is set up to work with CMS Ace.
|
|
1412
1675
|
ace-kit generate Write TypeScript types for your content types.
|
|
1413
1676
|
ace-kit sync Send the section types your code defines to the CMS.
|
|
1414
1677
|
ace-kit login Sign in from the command line (opens the admin app).
|
|
@@ -1422,6 +1685,8 @@ async function runCli(commandArguments, environment) {
|
|
|
1422
1685
|
switch (command) {
|
|
1423
1686
|
case "create":
|
|
1424
1687
|
return createCommand(rest, environment);
|
|
1688
|
+
case "doctor":
|
|
1689
|
+
return doctorCommand(rest, environment);
|
|
1425
1690
|
case "generate":
|
|
1426
1691
|
return generateCommand(rest, environment);
|
|
1427
1692
|
case "sync":
|
|
@@ -1447,6 +1712,7 @@ ${usage}`);
|
|
|
1447
1712
|
}
|
|
1448
1713
|
export {
|
|
1449
1714
|
createUsage,
|
|
1715
|
+
doctorUsage,
|
|
1450
1716
|
generateUsage,
|
|
1451
1717
|
loginUsage,
|
|
1452
1718
|
runCli,
|