@sandboxsonny/ace-kit 0.3.0 → 0.5.0
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 +32 -0
- package/README.md +4 -2
- package/astro/AceSections.astro +28 -0
- package/dist/astro-integration.d.ts +6 -4
- package/dist/astro-integration.js +21 -6
- package/dist/astro.d.ts +1 -1
- package/dist/chunk-N5DIMG3X.js +16 -0
- package/dist/chunk-WM5NOQE3.js +477 -0
- package/dist/{chunk-2MXND4HG.js → chunk-XS6Z7BMM.js} +15 -1
- package/dist/cli.js +434 -25
- package/dist/index.d.ts +180 -29
- package/dist/index.js +113 -1
- package/dist/react-router-server.d.ts +1 -1
- package/dist/react-router-server.js +3 -2
- package/dist/react-router.d.ts +47 -2
- package/dist/react-router.js +2 -2
- package/dist/react.d.ts +46 -1
- package/dist/react.js +1 -1
- package/dist/vite.d.ts +2 -1
- package/dist/vite.js +22 -2
- package/docs/api-reference.md +13 -2
- package/docs/astro.md +12 -6
- package/docs/command-line.md +5 -4
- package/docs/dev-sites.md +41 -0
- package/docs/react-router.md +7 -5
- package/docs/sections.md +104 -0
- package/docs/typed-content.md +4 -3
- package/package.json +1 -1
|
@@ -55,6 +55,20 @@ function Node({ node, Link }) {
|
|
|
55
55
|
function AceRichText({ document, link = PlainLink2 }) {
|
|
56
56
|
return /* @__PURE__ */ jsx(Fragment, { children: renderNodes(document.content, link) });
|
|
57
57
|
}
|
|
58
|
+
var warned = /* @__PURE__ */ new Set();
|
|
59
|
+
function AceSections({ sections, components, fallback: Fallback }) {
|
|
60
|
+
if (!Array.isArray(sections)) return null;
|
|
61
|
+
return /* @__PURE__ */ jsx(Fragment, { children: sections.map((section) => {
|
|
62
|
+
const Component = Object.hasOwn(components, section.type) ? components[section.type] : void 0;
|
|
63
|
+
if (Component) return /* @__PURE__ */ jsx(Component, { section }, section.id);
|
|
64
|
+
if (Fallback) return /* @__PURE__ */ jsx(Fallback, { section }, section.id);
|
|
65
|
+
if (process.env.NODE_ENV !== "production" && !warned.has(section.type)) {
|
|
66
|
+
warned.add(section.type);
|
|
67
|
+
console.warn(`AceSections: no component draws the section type "${section.type}", so it was left out.`);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}) });
|
|
71
|
+
}
|
|
58
72
|
function AceTextArea({ value, multiline = true }) {
|
|
59
73
|
if (!multiline) return /* @__PURE__ */ jsx(Fragment, { children: textAreaSingleLine(value) });
|
|
60
74
|
return /* @__PURE__ */ jsx(Fragment, { children: textAreaParagraphs(value).map((paragraph, paragraphIndex) => /* @__PURE__ */ jsx("p", { children: textAreaLines(paragraph).map((line, lineIndex) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
@@ -69,4 +83,4 @@ function usePreviewRefresh(adminOrigin, refresh) {
|
|
|
69
83
|
}, [adminOrigin, refresh]);
|
|
70
84
|
}
|
|
71
85
|
|
|
72
|
-
export { AceImage, AceLink, AceRichText, AceTextArea, usePreviewRefresh };
|
|
86
|
+
export { AceImage, AceLink, AceRichText, AceSections, AceTextArea, usePreviewRefresh };
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/cli/environment.ts
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { hostname } from "os";
|
|
6
|
+
import { pathToFileURL } from "url";
|
|
6
7
|
|
|
7
8
|
// src/auth/file-store.ts
|
|
8
9
|
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
@@ -170,7 +171,7 @@ function createLastApiUrl(path) {
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
// src/cli/version.ts
|
|
173
|
-
var aceKitVersion = true ? "0.
|
|
174
|
+
var aceKitVersion = true ? "0.5.0" : "development";
|
|
174
175
|
|
|
175
176
|
// src/cli/environment.ts
|
|
176
177
|
var runCommand = (command, commandArguments, input) => new Promise((resolve2, reject) => {
|
|
@@ -201,6 +202,19 @@ async function openBrowser(platform, address) {
|
|
|
201
202
|
});
|
|
202
203
|
});
|
|
203
204
|
}
|
|
205
|
+
async function importModule(path) {
|
|
206
|
+
try {
|
|
207
|
+
return await import(pathToFileURL(path).href);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error.code === "ERR_UNKNOWN_FILE_EXTENSION") {
|
|
210
|
+
throw new Error(
|
|
211
|
+
"this version of Node can't run TypeScript files. Use Node 22.18 or newer, or write the schema in .js.",
|
|
212
|
+
{ cause: error }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
204
218
|
async function createRealEnvironment() {
|
|
205
219
|
try {
|
|
206
220
|
process.loadEnvFile(".env");
|
|
@@ -219,6 +233,8 @@ async function createRealEnvironment() {
|
|
|
219
233
|
fetchImplementation: (input, init) => fetch(input, init),
|
|
220
234
|
sleep: (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds)),
|
|
221
235
|
now: Date.now,
|
|
236
|
+
workingDirectory: process.cwd(),
|
|
237
|
+
importModule,
|
|
222
238
|
openBrowser: (address) => openBrowser(platform, address),
|
|
223
239
|
store: await chooseLoginStore({ platform, variables, run: runCommand }),
|
|
224
240
|
lastApiUrl: createLastApiUrl(defaultLastApiUrlFile(variables, platform))
|
|
@@ -348,6 +364,8 @@ function typeOfField(field) {
|
|
|
348
364
|
return { type: "string" };
|
|
349
365
|
case "link":
|
|
350
366
|
return { type: "AceLinkData", uses: "AceLinkData" };
|
|
367
|
+
case "sections":
|
|
368
|
+
return { type: "AceSection[]", uses: "AceSection" };
|
|
351
369
|
default:
|
|
352
370
|
return { type: "unknown" };
|
|
353
371
|
}
|
|
@@ -385,15 +403,29 @@ export {};
|
|
|
385
403
|
// src/helpers/package-name.ts
|
|
386
404
|
var packageName = "@sandboxsonny/ace-kit";
|
|
387
405
|
|
|
406
|
+
// src/helpers/platform.ts
|
|
407
|
+
var hostedApiUrl = "https://api.ace-cms.co.uk";
|
|
408
|
+
function domainFromSite(site) {
|
|
409
|
+
const trimmed = site.trim();
|
|
410
|
+
if (trimmed.includes("://")) {
|
|
411
|
+
try {
|
|
412
|
+
return new URL(trimmed).hostname.toLowerCase();
|
|
413
|
+
} catch {
|
|
414
|
+
return trimmed.toLowerCase();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return trimmed.replace(/\/.*$/, "").replace(/:\d+$/, "").toLowerCase();
|
|
418
|
+
}
|
|
419
|
+
|
|
388
420
|
// src/cli/generate-command.ts
|
|
389
421
|
var generateUsage = `ace-kit generate: write TypeScript types for your CMS Ace content types.
|
|
390
422
|
|
|
391
423
|
Usage:
|
|
392
|
-
ace-kit generate [--
|
|
424
|
+
ace-kit generate [--site <domain>] [--api-url <address>] [--out <file>]
|
|
393
425
|
|
|
394
426
|
Options:
|
|
395
|
-
--
|
|
396
|
-
--
|
|
427
|
+
--site The domain your site has in the CMS, e.g. example.com (or set ACE_SITE_DOMAIN). --domain means the same.
|
|
428
|
+
--api-url Where the CMS Ace API lives (or set ACE_API_URL). Only for a CMS on your own machine.
|
|
397
429
|
--out Where to write the types (default: src/ace-kit.generated.d.ts).
|
|
398
430
|
--help Show this message.
|
|
399
431
|
`;
|
|
@@ -403,6 +435,7 @@ async function generateCommand(commandArguments, environment) {
|
|
|
403
435
|
args: commandArguments,
|
|
404
436
|
options: {
|
|
405
437
|
"api-url": { type: "string" },
|
|
438
|
+
site: { type: "string" },
|
|
406
439
|
domain: { type: "string" },
|
|
407
440
|
out: { type: "string" },
|
|
408
441
|
help: { type: "boolean" }
|
|
@@ -412,14 +445,13 @@ async function generateCommand(commandArguments, environment) {
|
|
|
412
445
|
environment.out(generateUsage);
|
|
413
446
|
return 0;
|
|
414
447
|
}
|
|
415
|
-
const apiUrl = values["api-url"] ?? environment.variables.ACE_API_URL;
|
|
416
|
-
const
|
|
417
|
-
if (!
|
|
418
|
-
environment.error(
|
|
419
|
-
"Give the API address and the site's domain: --api-url and --domain, or ACE_API_URL and ACE_SITE_DOMAIN."
|
|
420
|
-
);
|
|
448
|
+
const apiUrl = values["api-url"] ?? environment.variables.ACE_API_URL ?? hostedApiUrl;
|
|
449
|
+
const given = values.site ?? values.domain ?? environment.variables.ACE_SITE_DOMAIN;
|
|
450
|
+
if (!given) {
|
|
451
|
+
environment.error("Give the site's domain: --site, or ACE_SITE_DOMAIN.");
|
|
421
452
|
return 1;
|
|
422
453
|
}
|
|
454
|
+
const siteDomain = domainFromSite(given);
|
|
423
455
|
const client = createAceClient({
|
|
424
456
|
apiUrl,
|
|
425
457
|
siteDomain,
|
|
@@ -566,6 +598,20 @@ async function resolveLogin(options) {
|
|
|
566
598
|
return { token: stored.token, email: stored.email, source: options.store.description };
|
|
567
599
|
}
|
|
568
600
|
|
|
601
|
+
// src/cli/api-url.ts
|
|
602
|
+
async function resolveApiUrl(fromOption, environment) {
|
|
603
|
+
const address = fromOption ?? environment.variables.ACE_API_URL ?? await environment.lastApiUrl.read() ?? hostedApiUrl;
|
|
604
|
+
try {
|
|
605
|
+
return normaliseApiUrl(address);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
environment.error(error instanceof Error ? error.message : String(error));
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function isLocalApiUrl(apiUrl) {
|
|
612
|
+
return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(apiUrl).hostname);
|
|
613
|
+
}
|
|
614
|
+
|
|
569
615
|
// src/cli/login-commands.ts
|
|
570
616
|
var loginUsage = `ace-kit login: sign in to CMS Ace from the command line.
|
|
571
617
|
|
|
@@ -589,19 +635,8 @@ async function readApiUrl(commandArguments, environment) {
|
|
|
589
635
|
options: { "api-url": { type: "string" }, help: { type: "boolean" } }
|
|
590
636
|
});
|
|
591
637
|
if (values.help) return { help: true };
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
environment.error(
|
|
595
|
-
"Give the API address: --api-url, or ACE_API_URL in the environment or a .env file. (Once you have signed in, that address is remembered.)"
|
|
596
|
-
);
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
599
|
-
try {
|
|
600
|
-
return { apiUrl: normaliseApiUrl(address) };
|
|
601
|
-
} catch (error) {
|
|
602
|
-
environment.error(error instanceof Error ? error.message : String(error));
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
638
|
+
const apiUrl = await resolveApiUrl(values["api-url"], environment);
|
|
639
|
+
return apiUrl === null ? null : { apiUrl };
|
|
605
640
|
}
|
|
606
641
|
function webAddress(address) {
|
|
607
642
|
try {
|
|
@@ -611,7 +646,6 @@ function webAddress(address) {
|
|
|
611
646
|
return null;
|
|
612
647
|
}
|
|
613
648
|
}
|
|
614
|
-
var isLocal = (apiUrl) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(apiUrl).hostname);
|
|
615
649
|
async function loginCommand(commandArguments, environment) {
|
|
616
650
|
const read = await readApiUrl(commandArguments, environment);
|
|
617
651
|
if (!read) return 1;
|
|
@@ -620,7 +654,7 @@ async function loginCommand(commandArguments, environment) {
|
|
|
620
654
|
return 0;
|
|
621
655
|
}
|
|
622
656
|
const { apiUrl } = read;
|
|
623
|
-
if (apiUrl.startsWith("http://") && !
|
|
657
|
+
if (apiUrl.startsWith("http://") && !isLocalApiUrl(apiUrl)) {
|
|
624
658
|
environment.error(
|
|
625
659
|
`Warning: ${apiUrl} is not encrypted (http://), so the login could be read on the way. Use https:// if you can.`
|
|
626
660
|
);
|
|
@@ -729,11 +763,384 @@ async function whoamiCommand(commandArguments, environment) {
|
|
|
729
763
|
}
|
|
730
764
|
}
|
|
731
765
|
|
|
766
|
+
// src/cli/sync-command.ts
|
|
767
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
768
|
+
|
|
769
|
+
// src/sections/find-schemas.ts
|
|
770
|
+
import { readdir } from "fs/promises";
|
|
771
|
+
import { join as join3, relative } from "path";
|
|
772
|
+
var schemaFilePattern = /\.schema\.(?:ts|mts|js|mjs)$/;
|
|
773
|
+
var skippedFolders = /* @__PURE__ */ new Set([
|
|
774
|
+
"node_modules",
|
|
775
|
+
".git",
|
|
776
|
+
"dist",
|
|
777
|
+
"build",
|
|
778
|
+
".astro",
|
|
779
|
+
".react-router",
|
|
780
|
+
".output",
|
|
781
|
+
"coverage"
|
|
782
|
+
]);
|
|
783
|
+
async function findSchemaFiles(root, folder = "src") {
|
|
784
|
+
const start = join3(root, folder);
|
|
785
|
+
const found = [];
|
|
786
|
+
async function walk(directory) {
|
|
787
|
+
let entries;
|
|
788
|
+
try {
|
|
789
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
790
|
+
} catch {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
for (const entry of entries) {
|
|
794
|
+
const path = join3(directory, entry.name);
|
|
795
|
+
if (entry.isDirectory()) {
|
|
796
|
+
if (!skippedFolders.has(entry.name)) await walk(path);
|
|
797
|
+
} else if (entry.isFile() && schemaFilePattern.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
798
|
+
found.push(path);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
await walk(start);
|
|
803
|
+
return found.sort((first, second) => {
|
|
804
|
+
const [firstName, secondName] = [relative(root, first), relative(root, second)];
|
|
805
|
+
return firstName < secondName ? -1 : firstName > secondName ? 1 : 0;
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/sections/load-schemas.ts
|
|
810
|
+
import { relative as relative2 } from "path";
|
|
811
|
+
|
|
812
|
+
// src/sections/payload.ts
|
|
813
|
+
var handlePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
814
|
+
var maximumHandleLength = 64;
|
|
815
|
+
var maximumNameLength = 200;
|
|
816
|
+
var fieldTypes = [
|
|
817
|
+
"text",
|
|
818
|
+
"textarea",
|
|
819
|
+
"number",
|
|
820
|
+
"checkbox",
|
|
821
|
+
"select",
|
|
822
|
+
"image",
|
|
823
|
+
"richtext",
|
|
824
|
+
"date",
|
|
825
|
+
"link"
|
|
826
|
+
];
|
|
827
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
828
|
+
var isHandle = (value) => typeof value === "string" && value.length <= maximumHandleLength && handlePattern.test(value);
|
|
829
|
+
var handleRule = 'lowercase letters, numbers and hyphens (such as "call-to-action")';
|
|
830
|
+
function checkFields(fields, where, problems) {
|
|
831
|
+
if (fields === void 0) return;
|
|
832
|
+
if (!Array.isArray(fields)) {
|
|
833
|
+
problems.push(`${where}: fields must be a list.`);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const seen = /* @__PURE__ */ new Set();
|
|
837
|
+
fields.forEach((field, index) => {
|
|
838
|
+
if (!isObject(field)) {
|
|
839
|
+
problems.push(`${where}: field ${index + 1} must be an object with a handle, a label and a type.`);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const name = isHandle(field.handle) ? `field "${field.handle}"` : `field ${index + 1}`;
|
|
843
|
+
if (!isHandle(field.handle)) problems.push(`${where}: ${name} needs a handle of ${handleRule}.`);
|
|
844
|
+
else if (seen.has(field.handle)) problems.push(`${where}: ${name} is defined twice.`);
|
|
845
|
+
else seen.add(field.handle);
|
|
846
|
+
if (typeof field.label !== "string" || field.label.trim() === "" || field.label.length > maximumNameLength) {
|
|
847
|
+
problems.push(`${where}: ${name} needs a label.`);
|
|
848
|
+
}
|
|
849
|
+
if (typeof field.type !== "string" || !fieldTypes.includes(field.type)) {
|
|
850
|
+
const given = typeof field.type === "string" ? ` "${field.type}"` : "";
|
|
851
|
+
problems.push(`${where}: ${name} has a type${given} that isn't one of ${fieldTypes.join(", ")}.`);
|
|
852
|
+
} else if (field.type === "select") {
|
|
853
|
+
const { options } = field;
|
|
854
|
+
if (!Array.isArray(options) || options.length === 0 || options.some((option) => typeof option !== "string" || option === "")) {
|
|
855
|
+
problems.push(`${where}: ${name} is a select, so it needs options: a list of the choices, as text.`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (field.required !== void 0 && typeof field.required !== "boolean") {
|
|
859
|
+
problems.push(`${where}: ${name} has a required that isn't true or false.`);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
function checkSectionDefinition(definition, where = "this section") {
|
|
864
|
+
const problems = [];
|
|
865
|
+
if (!isObject(definition)) {
|
|
866
|
+
return [`${where}: the default export must be the result of defineSection({ ... }).`];
|
|
867
|
+
}
|
|
868
|
+
if (!isHandle(definition.handle)) {
|
|
869
|
+
problems.push(`${where}: needs a handle of ${handleRule}.`);
|
|
870
|
+
}
|
|
871
|
+
if (typeof definition.name !== "string" || definition.name.trim() === "" || definition.name.length > maximumNameLength) {
|
|
872
|
+
problems.push(`${where}: needs a name.`);
|
|
873
|
+
}
|
|
874
|
+
checkFields(definition.fields, where, problems);
|
|
875
|
+
if (definition.blocks !== void 0) {
|
|
876
|
+
if (!isObject(definition.blocks)) {
|
|
877
|
+
problems.push(`${where}: blocks must be an object of the kinds of block, by handle.`);
|
|
878
|
+
} else {
|
|
879
|
+
for (const [handle, block] of Object.entries(definition.blocks)) {
|
|
880
|
+
const blockWhere = `${where}, block "${handle}"`;
|
|
881
|
+
if (!isHandle(handle)) problems.push(`${blockWhere}: the handle must be ${handleRule}.`);
|
|
882
|
+
if (!isObject(block)) {
|
|
883
|
+
problems.push(`${blockWhere}: must be an object with a name and fields.`);
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (typeof block.name !== "string" || block.name.trim() === "") problems.push(`${blockWhere}: needs a name.`);
|
|
887
|
+
checkFields(block.fields, blockWhere, problems);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return problems;
|
|
892
|
+
}
|
|
893
|
+
function apiFields(fields) {
|
|
894
|
+
return (fields ?? []).map((field) => ({
|
|
895
|
+
handle: field.handle,
|
|
896
|
+
label: field.label,
|
|
897
|
+
component_key: field.type,
|
|
898
|
+
config: {
|
|
899
|
+
...field.required ? { required: true } : {},
|
|
900
|
+
...field.type === "select" ? { options: [...field.options] } : {}
|
|
901
|
+
}
|
|
902
|
+
}));
|
|
903
|
+
}
|
|
904
|
+
function sectionTypesPayload(definitions) {
|
|
905
|
+
return {
|
|
906
|
+
sections: definitions.map((definition) => ({
|
|
907
|
+
handle: definition.handle,
|
|
908
|
+
name: definition.name,
|
|
909
|
+
fields: apiFields(definition.fields),
|
|
910
|
+
blocks: Object.entries(definition.blocks ?? {}).map(([handle, block]) => ({
|
|
911
|
+
handle,
|
|
912
|
+
name: block.name,
|
|
913
|
+
fields: apiFields(block.fields)
|
|
914
|
+
}))
|
|
915
|
+
}))
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/sections/load-schemas.ts
|
|
920
|
+
async function loadSchemas(files, root, importModule2) {
|
|
921
|
+
const definitions = [];
|
|
922
|
+
const problems = [];
|
|
923
|
+
const handleToFile = /* @__PURE__ */ new Map();
|
|
924
|
+
for (const file of files) {
|
|
925
|
+
const name = relative2(root, file);
|
|
926
|
+
let exported;
|
|
927
|
+
try {
|
|
928
|
+
const module = await importModule2(file);
|
|
929
|
+
exported = module.default;
|
|
930
|
+
} catch (error) {
|
|
931
|
+
problems.push(`${name}: could not be loaded: ${error instanceof Error ? error.message : String(error)}`);
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
const fileProblems = checkSectionDefinition(exported, name);
|
|
935
|
+
if (fileProblems.length > 0) {
|
|
936
|
+
problems.push(...fileProblems);
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
const definition = exported;
|
|
940
|
+
const earlier = handleToFile.get(definition.handle);
|
|
941
|
+
if (earlier) {
|
|
942
|
+
problems.push(`${name}: the handle "${definition.handle}" is already used by ${earlier}.`);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
handleToFile.set(definition.handle, name);
|
|
946
|
+
definitions.push({ file, definition });
|
|
947
|
+
}
|
|
948
|
+
return { definitions, problems };
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/sections/sync.ts
|
|
952
|
+
var requestTimeoutMilliseconds4 = 2e4;
|
|
953
|
+
async function readMessage2(response, fallback) {
|
|
954
|
+
try {
|
|
955
|
+
const body = await response.json();
|
|
956
|
+
if (typeof body.error === "string") return body.error;
|
|
957
|
+
} catch {
|
|
958
|
+
}
|
|
959
|
+
return fallback;
|
|
960
|
+
}
|
|
961
|
+
async function syncSectionTypes(options) {
|
|
962
|
+
const { apiUrl, siteDomain, token, payload, fetchImplementation } = options;
|
|
963
|
+
const headers = { Accept: "application/json", Authorization: `Bearer ${token}` };
|
|
964
|
+
let sitesResponse;
|
|
965
|
+
try {
|
|
966
|
+
sitesResponse = await fetchImplementation(`${apiUrl}/sites`, {
|
|
967
|
+
headers,
|
|
968
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds4)
|
|
969
|
+
});
|
|
970
|
+
} catch {
|
|
971
|
+
return { kind: "unreachable" };
|
|
972
|
+
}
|
|
973
|
+
if (sitesResponse.status === 401) return { kind: "login-ended" };
|
|
974
|
+
if (!sitesResponse.ok)
|
|
975
|
+
return {
|
|
976
|
+
kind: "rejected",
|
|
977
|
+
message: await readMessage2(sitesResponse, `The CMS answered with an error (${sitesResponse.status}).`)
|
|
978
|
+
};
|
|
979
|
+
const sites = await sitesResponse.json().catch(() => []);
|
|
980
|
+
const wanted = siteDomain.trim().toLowerCase();
|
|
981
|
+
const site = sites.find((candidate) => candidate.domain.toLowerCase() === wanted);
|
|
982
|
+
if (!site)
|
|
983
|
+
return { kind: "site-not-found", domain: siteDomain, available: sites.map((candidate) => candidate.domain) };
|
|
984
|
+
let response;
|
|
985
|
+
try {
|
|
986
|
+
response = await fetchImplementation(`${apiUrl}/sites/${site.id}/section-types`, {
|
|
987
|
+
method: "PUT",
|
|
988
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
989
|
+
body: JSON.stringify(payload),
|
|
990
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds4)
|
|
991
|
+
});
|
|
992
|
+
} catch {
|
|
993
|
+
return { kind: "unreachable" };
|
|
994
|
+
}
|
|
995
|
+
if (response.status === 401) return { kind: "login-ended" };
|
|
996
|
+
if (response.status === 403) {
|
|
997
|
+
return {
|
|
998
|
+
kind: "forbidden",
|
|
999
|
+
message: "Your account can't change this site's sections: only a site's owners and super admins can."
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
if (!response.ok)
|
|
1003
|
+
return {
|
|
1004
|
+
kind: "rejected",
|
|
1005
|
+
message: await readMessage2(response, `The CMS answered with an error (${response.status}).`)
|
|
1006
|
+
};
|
|
1007
|
+
const body = await response.json().catch(() => null);
|
|
1008
|
+
return {
|
|
1009
|
+
kind: "synced",
|
|
1010
|
+
site: { id: site.id, domain: site.domain },
|
|
1011
|
+
sections: (body?.sections ?? []).map((section) => ({
|
|
1012
|
+
handle: section.handle,
|
|
1013
|
+
name: section.name,
|
|
1014
|
+
retired: section.retired_at !== void 0
|
|
1015
|
+
})),
|
|
1016
|
+
warnings: body?.warnings ?? []
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/cli/sync-command.ts
|
|
1021
|
+
var syncUsage = `ace-kit sync: send the section types your code defines to CMS Ace.
|
|
1022
|
+
|
|
1023
|
+
Usage:
|
|
1024
|
+
ace-kit sync [--site <domain>] [--api-url <address>] [--dir <folder>]
|
|
1025
|
+
|
|
1026
|
+
It finds every *.schema.ts file in the folder (src by default), checks them, and makes the CMS's section types for your
|
|
1027
|
+
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
|
|
1028
|
+
build server (with ACE_TOKEN set).
|
|
1029
|
+
|
|
1030
|
+
Nothing is deleted: a section type, block or field you take out of your code is kept in the CMS, hidden, so the content
|
|
1031
|
+
written in it survives. If any schema file can't be loaded, nothing is synced.
|
|
1032
|
+
|
|
1033
|
+
Options:
|
|
1034
|
+
--site The domain your site has in the CMS, e.g. example.com (default: ACE_SITE_DOMAIN, else localhost).
|
|
1035
|
+
(--domain means the same.)
|
|
1036
|
+
--api-url Where the CMS Ace API lives (default: ACE_API_URL, else the hosted CMS Ace). Only for a CMS on your own machine.
|
|
1037
|
+
--dir The folder to look in (default: src).
|
|
1038
|
+
--help Show this message.
|
|
1039
|
+
`;
|
|
1040
|
+
var isSwitchedOff = (value) => ["false", "off", "no", "0"].includes((value ?? "").trim().toLowerCase());
|
|
1041
|
+
async function syncCommand(commandArguments, environment) {
|
|
1042
|
+
const { values } = parseArgs3({
|
|
1043
|
+
// eslint-disable-next-line id-denylist -- "args" is the name node's parseArgs gives this option.
|
|
1044
|
+
args: commandArguments,
|
|
1045
|
+
options: {
|
|
1046
|
+
"api-url": { type: "string" },
|
|
1047
|
+
site: { type: "string" },
|
|
1048
|
+
domain: { type: "string" },
|
|
1049
|
+
dir: { type: "string" },
|
|
1050
|
+
// Set by the dev server, which syncs on its own: it stays quiet when there is nothing to do, and keeps to a
|
|
1051
|
+
// local CMS unless told otherwise.
|
|
1052
|
+
automatic: { type: "boolean" },
|
|
1053
|
+
help: { type: "boolean" }
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
if (values.help) {
|
|
1057
|
+
environment.out(syncUsage);
|
|
1058
|
+
return 0;
|
|
1059
|
+
}
|
|
1060
|
+
const automatic = values.automatic === true;
|
|
1061
|
+
if (automatic && isSwitchedOff(environment.variables.ACE_SYNC_SECTIONS)) return 0;
|
|
1062
|
+
let apiUrl;
|
|
1063
|
+
try {
|
|
1064
|
+
apiUrl = normaliseApiUrl(values["api-url"] ?? environment.variables.ACE_API_URL ?? hostedApiUrl);
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
environment.error(error instanceof Error ? error.message : String(error));
|
|
1067
|
+
return 1;
|
|
1068
|
+
}
|
|
1069
|
+
const given = values.site ?? values.domain ?? environment.variables.ACE_SITE_DOMAIN;
|
|
1070
|
+
const siteDomain = given ? domainFromSite(given) : "localhost";
|
|
1071
|
+
const aimedAtDevSite = environment.variables.ACE_DEV_SITE_ACTIVE === "1";
|
|
1072
|
+
if (automatic && !aimedAtDevSite && !isLocalApiUrl(apiUrl) && environment.variables.ACE_SYNC_SECTIONS?.trim().toLowerCase() !== "remote") {
|
|
1073
|
+
environment.out(
|
|
1074
|
+
`Section sync is off while ${apiUrl} is a hosted CMS, so a half-written change can't reach a live site. To sync on every save to it anyway, set ACE_SYNC_SECTIONS=remote. (npx ace-kit sync always works.)`
|
|
1075
|
+
);
|
|
1076
|
+
return 0;
|
|
1077
|
+
}
|
|
1078
|
+
const files = await findSchemaFiles(environment.workingDirectory, values.dir ?? "src");
|
|
1079
|
+
if (files.length === 0) {
|
|
1080
|
+
if (!automatic) {
|
|
1081
|
+
environment.out(
|
|
1082
|
+
`No *.schema.ts files found in ${values.dir ?? "src"}, so nothing was synced (syncing none would retire every section).`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
return 0;
|
|
1086
|
+
}
|
|
1087
|
+
const login = await resolveLogin({
|
|
1088
|
+
apiUrl,
|
|
1089
|
+
variables: environment.variables,
|
|
1090
|
+
store: environment.store,
|
|
1091
|
+
now: environment.now
|
|
1092
|
+
});
|
|
1093
|
+
if (!login) {
|
|
1094
|
+
environment.out(`Section sync is waiting for you to sign in. Run: npx ace-kit login --api-url ${apiUrl}`);
|
|
1095
|
+
return automatic ? 0 : 1;
|
|
1096
|
+
}
|
|
1097
|
+
const loaded = await loadSchemas(files, environment.workingDirectory, environment.importModule);
|
|
1098
|
+
if (loaded.problems.length > 0) {
|
|
1099
|
+
for (const problem of loaded.problems) environment.error(problem);
|
|
1100
|
+
environment.error("Nothing was synced, because syncing only some of your sections would retire the rest.");
|
|
1101
|
+
return 1;
|
|
1102
|
+
}
|
|
1103
|
+
const outcome = await syncSectionTypes({
|
|
1104
|
+
apiUrl,
|
|
1105
|
+
siteDomain,
|
|
1106
|
+
token: login.token,
|
|
1107
|
+
payload: sectionTypesPayload(loaded.definitions.map((entry) => entry.definition)),
|
|
1108
|
+
fetchImplementation: environment.fetchImplementation
|
|
1109
|
+
});
|
|
1110
|
+
switch (outcome.kind) {
|
|
1111
|
+
case "synced": {
|
|
1112
|
+
const defined = outcome.sections.filter((section) => !section.retired).map((section) => section.handle);
|
|
1113
|
+
const retired = outcome.sections.filter((section) => section.retired).map((section) => section.handle);
|
|
1114
|
+
environment.out(
|
|
1115
|
+
`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(", ")}.` : "")
|
|
1116
|
+
);
|
|
1117
|
+
for (const warning of outcome.warnings) environment.error(`Warning: ${warning}`);
|
|
1118
|
+
return 0;
|
|
1119
|
+
}
|
|
1120
|
+
case "login-ended":
|
|
1121
|
+
environment.out(`Your login has ended. Run: npx ace-kit login --api-url ${apiUrl}`);
|
|
1122
|
+
return automatic ? 0 : 1;
|
|
1123
|
+
case "site-not-found":
|
|
1124
|
+
environment.error(
|
|
1125
|
+
`The CMS at ${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."
|
|
1126
|
+
);
|
|
1127
|
+
return 1;
|
|
1128
|
+
case "forbidden":
|
|
1129
|
+
case "rejected":
|
|
1130
|
+
environment.error(outcome.message);
|
|
1131
|
+
return 1;
|
|
1132
|
+
case "unreachable":
|
|
1133
|
+
environment.error(`Could not reach the CMS at ${apiUrl}.`);
|
|
1134
|
+
return 1;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
732
1138
|
// src/cli/main.ts
|
|
733
1139
|
var usage = `ace-kit: tools for sites built on CMS Ace.
|
|
734
1140
|
|
|
735
1141
|
Commands:
|
|
736
1142
|
ace-kit generate Write TypeScript types for your content types.
|
|
1143
|
+
ace-kit sync Send the section types your code defines to the CMS.
|
|
737
1144
|
ace-kit login Sign in from the command line (opens the admin app).
|
|
738
1145
|
ace-kit logout Sign out.
|
|
739
1146
|
ace-kit whoami Say who you are signed in as.
|
|
@@ -745,6 +1152,8 @@ async function runCli(commandArguments, environment) {
|
|
|
745
1152
|
switch (command) {
|
|
746
1153
|
case "generate":
|
|
747
1154
|
return generateCommand(rest, environment);
|
|
1155
|
+
case "sync":
|
|
1156
|
+
return syncCommand(rest, environment);
|
|
748
1157
|
case "login":
|
|
749
1158
|
return loginCommand(rest, environment);
|
|
750
1159
|
case "logout":
|