@sandboxsonny/ace-kit 0.3.0 → 0.4.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 +21 -0
- package/README.md +3 -1
- package/astro/AceSections.astro +28 -0
- package/dist/astro-integration.d.ts +3 -1
- package/dist/astro-integration.js +15 -3
- package/dist/astro.d.ts +1 -1
- package/dist/chunk-O3NAJNUN.js +477 -0
- package/dist/{chunk-2MXND4HG.js → chunk-XS6Z7BMM.js} +15 -1
- package/dist/cli.js +415 -16
- 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.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 +21 -2
- package/docs/api-reference.md +13 -2
- package/docs/command-line.md +1 -0
- package/docs/dev-sites.md +41 -0
- package/docs/sections.md +104 -0
- package/docs/typed-content.md +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @sandboxsonny/ace-kit
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7efae5a: Dev sites: against a hosted CMS, the dev server now works on your own private copy of the site instead of the live one.
|
|
8
|
+
|
|
9
|
+
- On start (the Astro integration, and the `ace()` Vite plugin), the dev server finds or makes your dev site of the site `ACE_SITE_DOMAIN` names, points the site at it for as long as the server runs (`.env` is unchanged), and tells the CMS where the dev server runs so the live preview shows it.
|
|
10
|
+
- Section sync goes to the dev site, so it is now allowed automatically against a hosted CMS without `ACE_SYNC_SECTIONS=remote`. The live site only changes when you run `npx ace-kit sync` yourself.
|
|
11
|
+
- Without a login, or with an account that can't have a dev site, the dev server says why and shows the live site read-only, with section sync off.
|
|
12
|
+
- `ACE_DEV_SITE=false` turns it off; `ACE_DEV_SITE=true` uses one even against a CMS on your own machine.
|
|
13
|
+
|
|
14
|
+
Needs a CMS Ace API with dev sites (migration 00022), and a partner or super admin account.
|
|
15
|
+
- 7efae5a: Sections defined in code, and synced to the CMS as you work.
|
|
16
|
+
|
|
17
|
+
- `defineSection` describes a kind of section (its fields, and the blocks it can hold) in a `*.schema.ts` file next to the component that draws it. `SectionOf<typeof schema>` gives the type of such a section, so a component's props follow its schema: a required field is always there, a select is the union of its options, an image is `AceImageData`, and a block's values depend on its kind.
|
|
18
|
+
- While the dev server runs (the Astro integration, and the `ace()` Vite plugin on React Router), saving a schema file syncs the site's section types to the CMS. `ace-kit sync` does the same once, for a build server (with `ACE_TOKEN`). Nothing is deleted: what you take out of your code is kept in the CMS, hidden. If any schema file can't be loaded, or none are found, nothing is synced. Automatic sync only runs against a local CMS unless `ACE_SYNC_SECTIONS=remote`.
|
|
19
|
+
- `AceSections` draws a sections field, giving each section to the component for its kind: `AceSections.astro`, and `AceSections` from `/react` and `/react-router`.
|
|
20
|
+
- `ace-kit generate` types a sections field as `AceSection[]`.
|
|
21
|
+
|
|
22
|
+
Needs a CMS Ace API with sections (migration 00019) and command-line sign-in (00020). Schema files are TypeScript: Node 22.18 or newer runs them as they are.
|
|
23
|
+
|
|
3
24
|
## 0.3.0
|
|
4
25
|
|
|
5
26
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -51,7 +51,9 @@ const about = await getEntry("pages", "about");
|
|
|
51
51
|
| [React](docs/react.md) | Components for any React framework |
|
|
52
52
|
| [Live preview](docs/live-preview.md) | What a site must do for the editor's preview |
|
|
53
53
|
| [Typed content](docs/typed-content.md) | `ace-kit generate` |
|
|
54
|
-
| [The command line](docs/command-line.md) | `ace-kit login`, `logout`, `whoami`
|
|
54
|
+
| [The command line](docs/command-line.md) | `ace-kit login`, `logout`, `whoami`, `sync` |
|
|
55
|
+
| [Sections](docs/sections.md) | `defineSection`, `AceSections`, the sync |
|
|
56
|
+
| [Dev sites](docs/dev-sites.md) | your private copy of a site to build against |
|
|
55
57
|
| [Rich text](docs/rich-text.md) | The document format and how it is drawn |
|
|
56
58
|
| [API reference](docs/api-reference.md) | Every export |
|
|
57
59
|
| [Releasing](docs/releasing.md) | Versions, the changelog and publishing |
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { AceSection } from "../dist/index.js";
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
/** An entry's sections field, as the CMS gives it: a list of sections, or nothing. */
|
|
6
|
+
sections: readonly AceSection[] | null | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* The component that draws each kind of section, by the section's `type` (the handle in its schema). Each one is
|
|
9
|
+
* given the section as its `section` prop. A kind with no component is left out (and noted in development).
|
|
10
|
+
*/
|
|
11
|
+
components: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const { sections, components } = Astro.props;
|
|
15
|
+
const list = Array.isArray(sections) ? sections : [];
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
{list.map((section) => {
|
|
19
|
+
// An Astro component's props are checked where it is written (its own schema types them), not here.
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
const Component = Object.hasOwn(components, section.type) ? (components[section.type] as any) : undefined;
|
|
22
|
+
if (!Component) {
|
|
23
|
+
if (import.meta.env.DEV)
|
|
24
|
+
console.warn(`AceSections: no component draws the section type "${section.type}", so it was left out.`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return <Component section={section} />;
|
|
28
|
+
})}
|
|
@@ -7,7 +7,9 @@ import { AstroIntegration } from 'astro';
|
|
|
7
7
|
* export default defineConfig({ output: "server", integrations: [ace()] });
|
|
8
8
|
*
|
|
9
9
|
* It declares the three settings below, and lets the CMS's admin app (and only it) frame the site for live preview.
|
|
10
|
-
* The settings are read when the server starts, not baked into the build, so one build runs anywhere.
|
|
10
|
+
* The settings are read when the server starts, not baked into the build, so one build runs anywhere. While the dev
|
|
11
|
+
* server runs it also syncs the site's section types (its `*.schema.ts` files) to the CMS whenever they change; see
|
|
12
|
+
* `ace-kit sync`.
|
|
11
13
|
*
|
|
12
14
|
* | Setting | Default | Meaning |
|
|
13
15
|
* | -------------------------- | ----------------------- | -------------------------------------------------------- |
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
import { startDevSite, sectionSync } from './chunk-O3NAJNUN.js';
|
|
1
2
|
import { envField } from 'astro/config';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
2
4
|
|
|
3
|
-
// src/astro/integration.ts
|
|
4
5
|
function ace() {
|
|
6
|
+
let devSite = null;
|
|
5
7
|
return {
|
|
6
8
|
name: "ace-kit",
|
|
7
9
|
hooks: {
|
|
8
|
-
"astro:config:setup": ({ updateConfig, addMiddleware }) => {
|
|
10
|
+
"astro:config:setup": async ({ command, config, logger, updateConfig, addMiddleware }) => {
|
|
11
|
+
if (command === "dev") {
|
|
12
|
+
devSite = await startDevSite({
|
|
13
|
+
root: fileURLToPath(config.root),
|
|
14
|
+
mode: "development",
|
|
15
|
+
log: { info: (text) => logger.info(text), warn: (text) => logger.warn(text) }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
9
18
|
updateConfig({
|
|
10
19
|
env: {
|
|
11
20
|
schema: {
|
|
@@ -21,9 +30,12 @@ function ace() {
|
|
|
21
30
|
},
|
|
22
31
|
// The kit reads those settings through `astro:env`, which only Vite understands, so Vite must process the
|
|
23
32
|
// kit itself rather than leave it to Node.
|
|
24
|
-
vite: { ssr: { noExternal: ["@sandboxsonny/ace-kit"] } }
|
|
33
|
+
vite: { ssr: { noExternal: ["@sandboxsonny/ace-kit"] }, plugins: [sectionSync()] }
|
|
25
34
|
});
|
|
26
35
|
addMiddleware({ entrypoint: "@sandboxsonny/ace-kit/astro/middleware", order: "pre" });
|
|
36
|
+
},
|
|
37
|
+
"astro:server:start": async ({ address }) => {
|
|
38
|
+
if (devSite) await devSite.setPreviewUrl(`http://localhost:${address.port}`);
|
|
27
39
|
}
|
|
28
40
|
}
|
|
29
41
|
};
|
package/dist/astro.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type AceFieldSchema = {
|
|
3
3
|
handle: string;
|
|
4
4
|
label: string;
|
|
5
|
-
/** `text`, `textarea`, `select`, `number`, `checkbox`, `image`, `richtext`, `date` or `
|
|
5
|
+
/** `text`, `textarea`, `select`, `number`, `checkbox`, `image`, `richtext`, `date`, `link` or `sections`. */
|
|
6
6
|
component_key: string;
|
|
7
7
|
config: { required?: boolean; options?: string[] } | null;
|
|
8
8
|
};
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { loadEnv } from 'vite';
|
|
2
|
+
import { readdir, readFile, mkdir, writeFile, rename } from 'fs/promises';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join, relative, dirname } from 'path';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
// src/dev-site/start-dev-site.ts
|
|
9
|
+
|
|
10
|
+
// src/auth/login-store.ts
|
|
11
|
+
function normaliseApiUrl(address) {
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = new URL(address.trim());
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(`"${address}" is not a web address. Write the API address like http://localhost:8080.`);
|
|
17
|
+
}
|
|
18
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
19
|
+
throw new Error(`The API address must start with http:// or https:// (it was "${address}").`);
|
|
20
|
+
}
|
|
21
|
+
if (parsed.username || parsed.password) {
|
|
22
|
+
throw new Error("The API address must not contain a username or password.");
|
|
23
|
+
}
|
|
24
|
+
return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
25
|
+
}
|
|
26
|
+
function hasExpired(login, now) {
|
|
27
|
+
const endsAt = Date.parse(login.expiresAt);
|
|
28
|
+
return Number.isNaN(endsAt) || endsAt <= now;
|
|
29
|
+
}
|
|
30
|
+
function parseStoredLogin(value) {
|
|
31
|
+
if (typeof value !== "object" || value === null) return null;
|
|
32
|
+
const { token, expiresAt, email } = value;
|
|
33
|
+
if (typeof token !== "string" || token === "" || typeof expiresAt !== "string" || typeof email !== "string")
|
|
34
|
+
return null;
|
|
35
|
+
return { token, expiresAt, email };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/auth/file-store.ts
|
|
39
|
+
function defaultLoginFile(variables, platform) {
|
|
40
|
+
if (platform === "win32" && variables.APPDATA) return join(variables.APPDATA, "ace-kit", "credentials.json");
|
|
41
|
+
const configFolder = variables.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
42
|
+
return join(configFolder, "ace-kit", "credentials.json");
|
|
43
|
+
}
|
|
44
|
+
function createFileStore(path) {
|
|
45
|
+
async function readAll() {
|
|
46
|
+
let text;
|
|
47
|
+
try {
|
|
48
|
+
text = await readFile(path, "utf8");
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(text);
|
|
55
|
+
} catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
const found = {};
|
|
59
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
60
|
+
for (const [address, value] of Object.entries(parsed)) {
|
|
61
|
+
const login = parseStoredLogin(value);
|
|
62
|
+
if (login) found[address] = login;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return found;
|
|
66
|
+
}
|
|
67
|
+
async function writeAll(logins) {
|
|
68
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
69
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
70
|
+
await writeFile(temporary, `${JSON.stringify(logins, null, 2)}
|
|
71
|
+
`, { mode: 384 });
|
|
72
|
+
await rename(temporary, path);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
description: path,
|
|
76
|
+
async read(apiUrl) {
|
|
77
|
+
return (await readAll())[normaliseApiUrl(apiUrl)] ?? null;
|
|
78
|
+
},
|
|
79
|
+
async write(apiUrl, login) {
|
|
80
|
+
const logins = await readAll();
|
|
81
|
+
logins[normaliseApiUrl(apiUrl)] = login;
|
|
82
|
+
await writeAll(logins);
|
|
83
|
+
},
|
|
84
|
+
async remove(apiUrl) {
|
|
85
|
+
const logins = await readAll();
|
|
86
|
+
delete logins[normaliseApiUrl(apiUrl)];
|
|
87
|
+
await writeAll(logins);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/auth/keychain-store.ts
|
|
93
|
+
var keychainService = "ace-kit";
|
|
94
|
+
function createKeychainStore(run) {
|
|
95
|
+
async function read(apiUrl) {
|
|
96
|
+
const found = await run("security", [
|
|
97
|
+
"find-generic-password",
|
|
98
|
+
"-s",
|
|
99
|
+
keychainService,
|
|
100
|
+
"-a",
|
|
101
|
+
normaliseApiUrl(apiUrl),
|
|
102
|
+
"-w"
|
|
103
|
+
]);
|
|
104
|
+
if (found.exitCode !== 0) return null;
|
|
105
|
+
try {
|
|
106
|
+
return parseStoredLogin(JSON.parse(Buffer.from(found.stdout.trim(), "base64").toString("utf8")));
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
description: "the macOS Keychain",
|
|
113
|
+
read,
|
|
114
|
+
async write(apiUrl, login) {
|
|
115
|
+
const account = normaliseApiUrl(apiUrl);
|
|
116
|
+
if (/["\\\s]/.test(account)) throw new Error("That API address can't be kept in the Keychain.");
|
|
117
|
+
const secret = Buffer.from(JSON.stringify(login), "utf8").toString("base64");
|
|
118
|
+
await run(
|
|
119
|
+
"security",
|
|
120
|
+
["-i"],
|
|
121
|
+
`add-generic-password -s "${keychainService}" -a "${account}" -l "Ace Kit login" -w "${secret}" -U
|
|
122
|
+
`
|
|
123
|
+
);
|
|
124
|
+
const kept = await read(apiUrl);
|
|
125
|
+
if (kept?.token !== login.token) throw new Error("The login could not be saved in the macOS Keychain.");
|
|
126
|
+
},
|
|
127
|
+
async remove(apiUrl) {
|
|
128
|
+
await run("security", ["delete-generic-password", "-s", keychainService, "-a", normaliseApiUrl(apiUrl)]);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/auth/choose-store.ts
|
|
134
|
+
async function chooseLoginStore(options) {
|
|
135
|
+
const file = createFileStore(defaultLoginFile(options.variables, options.platform));
|
|
136
|
+
if (options.platform !== "darwin" || options.variables.ACE_KIT_CREDENTIALS === "file") return file;
|
|
137
|
+
try {
|
|
138
|
+
await options.run("security", ["help"]);
|
|
139
|
+
return createKeychainStore(options.run);
|
|
140
|
+
} catch {
|
|
141
|
+
return file;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
var runCommand = (command, commandArguments, input) => new Promise((resolve, reject) => {
|
|
145
|
+
const child = spawn(command, commandArguments, { stdio: ["pipe", "pipe", "ignore"] });
|
|
146
|
+
let stdout = "";
|
|
147
|
+
child.stdout.on("data", (chunk) => {
|
|
148
|
+
stdout += chunk.toString("utf8");
|
|
149
|
+
});
|
|
150
|
+
child.on("error", reject);
|
|
151
|
+
child.on("close", (exitCode) => resolve({ exitCode: exitCode ?? 1, stdout }));
|
|
152
|
+
child.stdin.on("error", () => {
|
|
153
|
+
});
|
|
154
|
+
child.stdin.end(input ?? "");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// src/dev-site/find-or-create.ts
|
|
158
|
+
var requestTimeoutMilliseconds = 6e4;
|
|
159
|
+
async function readMessage(response, fallback) {
|
|
160
|
+
try {
|
|
161
|
+
const body = await response.json();
|
|
162
|
+
if (typeof body.error === "string") return body.error;
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
return fallback;
|
|
166
|
+
}
|
|
167
|
+
async function findOrCreateDevSite(options) {
|
|
168
|
+
const { apiUrl, siteDomain, token, previewUrl, fetchImplementation } = options;
|
|
169
|
+
const headers = { Accept: "application/json", Authorization: `Bearer ${token}` };
|
|
170
|
+
let sitesResponse;
|
|
171
|
+
try {
|
|
172
|
+
sitesResponse = await fetchImplementation(`${apiUrl}/sites`, {
|
|
173
|
+
headers,
|
|
174
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds)
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
return { kind: "unreachable" };
|
|
178
|
+
}
|
|
179
|
+
if (sitesResponse.status === 401) return { kind: "login-ended" };
|
|
180
|
+
if (!sitesResponse.ok) {
|
|
181
|
+
return {
|
|
182
|
+
kind: "rejected",
|
|
183
|
+
message: await readMessage(sitesResponse, `The CMS answered with an error (${sitesResponse.status}).`)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const sites = await sitesResponse.json().catch(() => []);
|
|
187
|
+
const wanted = siteDomain.trim().toLowerCase();
|
|
188
|
+
const site = sites.find((candidate) => candidate.domain.toLowerCase() === wanted);
|
|
189
|
+
if (!site) {
|
|
190
|
+
return {
|
|
191
|
+
kind: "site-not-found",
|
|
192
|
+
domain: siteDomain,
|
|
193
|
+
available: sites.filter((candidate) => candidate.kind !== "dev").map((candidate) => candidate.domain)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (site.kind === "dev") return { kind: "found", site };
|
|
197
|
+
let response;
|
|
198
|
+
try {
|
|
199
|
+
response = await fetchImplementation(`${apiUrl}/sites/${site.id}/dev-site`, {
|
|
200
|
+
method: "PUT",
|
|
201
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
202
|
+
body: previewUrl === void 0 ? void 0 : JSON.stringify({ preview_url: previewUrl }),
|
|
203
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds)
|
|
204
|
+
});
|
|
205
|
+
} catch {
|
|
206
|
+
return { kind: "unreachable" };
|
|
207
|
+
}
|
|
208
|
+
if (response.status === 401) return { kind: "login-ended" };
|
|
209
|
+
if (response.status === 403) {
|
|
210
|
+
return {
|
|
211
|
+
kind: "forbidden",
|
|
212
|
+
message: await readMessage(response, "Your account can't have a dev site of this site.")
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
return {
|
|
217
|
+
kind: "rejected",
|
|
218
|
+
message: await readMessage(response, `The CMS answered with an error (${response.status}).`)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const made = await response.json().catch(() => null);
|
|
222
|
+
if (!made || typeof made.id !== "number" || typeof made.domain !== "string") {
|
|
223
|
+
return { kind: "rejected", message: "The CMS sent an unexpected reply." };
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
kind: response.status === 201 ? "created" : "found",
|
|
227
|
+
site: { id: made.id, name: made.name, domain: made.domain }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/auth/resolve-login.ts
|
|
232
|
+
async function resolveLogin(options) {
|
|
233
|
+
const fromEnvironment = options.variables.ACE_TOKEN?.trim();
|
|
234
|
+
if (fromEnvironment) return { token: fromEnvironment, source: "the ACE_TOKEN setting" };
|
|
235
|
+
const stored = await options.store.read(options.apiUrl);
|
|
236
|
+
if (!stored || hasExpired(stored, options.now())) return null;
|
|
237
|
+
return { token: stored.token, email: stored.email, source: options.store.description };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/cli/api-url.ts
|
|
241
|
+
function isLocalApiUrl(apiUrl) {
|
|
242
|
+
return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(apiUrl).hostname);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/dev-site/prepare-dev-site.ts
|
|
246
|
+
var off = ["false", "off", "no", "0"];
|
|
247
|
+
var on = ["true", "on", "yes", "1"];
|
|
248
|
+
function wantsDevSite(variables, apiUrl) {
|
|
249
|
+
const setting = (variables.ACE_DEV_SITE ?? "").trim().toLowerCase();
|
|
250
|
+
if (off.includes(setting)) return false;
|
|
251
|
+
if (on.includes(setting)) return true;
|
|
252
|
+
return !isLocalApiUrl(apiUrl);
|
|
253
|
+
}
|
|
254
|
+
async function prepareDevSite(options) {
|
|
255
|
+
const { apiUrl, variables, store, now, fetchImplementation } = options;
|
|
256
|
+
if (!wantsDevSite(variables, apiUrl)) return { kind: "off" };
|
|
257
|
+
const login = await resolveLogin({ apiUrl, variables, store, now });
|
|
258
|
+
if (!login) return { kind: "waiting-for-login", apiUrl };
|
|
259
|
+
const siteDomain = variables.ACE_SITE_DOMAIN?.trim() || "localhost";
|
|
260
|
+
const outcome = await findOrCreateDevSite({ apiUrl, siteDomain, token: login.token, fetchImplementation });
|
|
261
|
+
switch (outcome.kind) {
|
|
262
|
+
case "created":
|
|
263
|
+
case "found":
|
|
264
|
+
return { kind: "ready", site: outcome.site, created: outcome.kind === "created" };
|
|
265
|
+
case "login-ended":
|
|
266
|
+
return { kind: "waiting-for-login", apiUrl };
|
|
267
|
+
case "site-not-found":
|
|
268
|
+
return {
|
|
269
|
+
kind: "problem",
|
|
270
|
+
message: `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."
|
|
271
|
+
};
|
|
272
|
+
case "forbidden":
|
|
273
|
+
return {
|
|
274
|
+
kind: "problem",
|
|
275
|
+
message: `${outcome.message[0].toUpperCase()}${outcome.message.slice(1)}. Dev sites are for partner accounts and super admins. Ask for a partner account, or set ACE_DEV_SITE=false to stop trying.`
|
|
276
|
+
};
|
|
277
|
+
case "rejected":
|
|
278
|
+
return { kind: "problem", message: outcome.message };
|
|
279
|
+
case "unreachable":
|
|
280
|
+
return { kind: "problem", message: `Could not reach the CMS at ${apiUrl}.` };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/dev-site/start-dev-site.ts
|
|
285
|
+
var defaultApiUrl = "http://localhost:8080";
|
|
286
|
+
async function startDevSite(options) {
|
|
287
|
+
const { root, mode, log } = options;
|
|
288
|
+
if (mode === "test" || process.env.VITEST) return null;
|
|
289
|
+
const variables = options.environment?.variables ?? { ...loadEnv(mode, root, "ACE_"), ...process.env };
|
|
290
|
+
let apiUrl;
|
|
291
|
+
try {
|
|
292
|
+
apiUrl = normaliseApiUrl(variables.ACE_API_URL ?? defaultApiUrl);
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const environment = options.environment ?? {
|
|
297
|
+
variables,
|
|
298
|
+
store: await chooseLoginStore({ platform: process.platform, variables, run: runCommand }),
|
|
299
|
+
now: Date.now,
|
|
300
|
+
fetchImplementation: (input, init) => fetch(input, init)
|
|
301
|
+
};
|
|
302
|
+
const outcome = await prepareDevSite({ apiUrl, ...environment });
|
|
303
|
+
switch (outcome.kind) {
|
|
304
|
+
case "off":
|
|
305
|
+
return null;
|
|
306
|
+
case "waiting-for-login":
|
|
307
|
+
log.warn(
|
|
308
|
+
`You are not signed in to ${outcome.apiUrl}, so this is showing the live site and section sync is off. Run: npx ace-kit login --api-url ${outcome.apiUrl}`
|
|
309
|
+
);
|
|
310
|
+
return null;
|
|
311
|
+
case "problem":
|
|
312
|
+
log.warn(`No dev site: ${outcome.message} Showing the live site, and section sync is off.`);
|
|
313
|
+
return null;
|
|
314
|
+
case "ready": {
|
|
315
|
+
process.env.ACE_SITE_DOMAIN = outcome.site.domain;
|
|
316
|
+
process.env.ACE_DEV_SITE_ACTIVE = "1";
|
|
317
|
+
log.info(
|
|
318
|
+
outcome.created ? `Made you a dev site, "${outcome.site.name}", as a copy of the live site (${outcome.site.domain}). This dev server reads and syncs to it; the live site is not touched.` : `Using your dev site, "${outcome.site.name}" (${outcome.site.domain}). The live site is not touched.`
|
|
319
|
+
);
|
|
320
|
+
const siteDomain = variables.ACE_SITE_DOMAIN?.trim() || "localhost";
|
|
321
|
+
return {
|
|
322
|
+
domain: outcome.site.domain,
|
|
323
|
+
setPreviewUrl: async (previewUrl) => {
|
|
324
|
+
const login = environment.variables.ACE_TOKEN?.trim() ? { token: environment.variables.ACE_TOKEN.trim() } : await environment.store.read(apiUrl);
|
|
325
|
+
if (!login) return;
|
|
326
|
+
const result = await findOrCreateDevSite({
|
|
327
|
+
apiUrl,
|
|
328
|
+
siteDomain,
|
|
329
|
+
token: login.token,
|
|
330
|
+
previewUrl,
|
|
331
|
+
fetchImplementation: environment.fetchImplementation
|
|
332
|
+
});
|
|
333
|
+
if (result.kind !== "found" && result.kind !== "created") {
|
|
334
|
+
log.warn(
|
|
335
|
+
"Could not tell the CMS where this dev server runs, so its live preview may show another address."
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
var schemaFilePattern = /\.schema\.(?:ts|mts|js|mjs)$/;
|
|
344
|
+
var skippedFolders = /* @__PURE__ */ new Set([
|
|
345
|
+
"node_modules",
|
|
346
|
+
".git",
|
|
347
|
+
"dist",
|
|
348
|
+
"build",
|
|
349
|
+
".astro",
|
|
350
|
+
".react-router",
|
|
351
|
+
".output",
|
|
352
|
+
"coverage"
|
|
353
|
+
]);
|
|
354
|
+
async function findSchemaFiles(root, folder = "src") {
|
|
355
|
+
const start = join(root, folder);
|
|
356
|
+
const found = [];
|
|
357
|
+
async function walk(directory) {
|
|
358
|
+
let entries;
|
|
359
|
+
try {
|
|
360
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
361
|
+
} catch {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
for (const entry of entries) {
|
|
365
|
+
const path = join(directory, entry.name);
|
|
366
|
+
if (entry.isDirectory()) {
|
|
367
|
+
if (!skippedFolders.has(entry.name)) await walk(path);
|
|
368
|
+
} else if (entry.isFile() && schemaFilePattern.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
369
|
+
found.push(path);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
await walk(start);
|
|
374
|
+
return found.sort((first, second) => {
|
|
375
|
+
const [firstName, secondName] = [relative(root, first), relative(root, second)];
|
|
376
|
+
return firstName < secondName ? -1 : firstName > secondName ? 1 : 0;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/vite/section-sync.ts
|
|
381
|
+
function nodeFlags(version) {
|
|
382
|
+
const [major, minor] = version.split(".").map(Number);
|
|
383
|
+
return major === 22 && minor < 18 ? ["--experimental-strip-types", "--no-warnings"] : [];
|
|
384
|
+
}
|
|
385
|
+
function runSyncProgram(root) {
|
|
386
|
+
return new Promise((resolve) => {
|
|
387
|
+
const program = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
388
|
+
const child = spawn(process.execPath, [...nodeFlags(process.versions.node), program, "sync", "--automatic"], {
|
|
389
|
+
cwd: root,
|
|
390
|
+
env: process.env,
|
|
391
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
392
|
+
});
|
|
393
|
+
let standardOutput = "";
|
|
394
|
+
let standardError = "";
|
|
395
|
+
child.stdout.on("data", (chunk) => standardOutput += chunk.toString("utf8"));
|
|
396
|
+
child.stderr.on("data", (chunk) => standardError += chunk.toString("utf8"));
|
|
397
|
+
const lines = (text) => text.split("\n").map((line) => line.trimEnd()).filter((line) => line !== "");
|
|
398
|
+
child.on(
|
|
399
|
+
"error",
|
|
400
|
+
(error) => resolve({ output: [], errors: [`Could not run ace-kit sync: ${error.message}`], failed: true })
|
|
401
|
+
);
|
|
402
|
+
child.on(
|
|
403
|
+
"close",
|
|
404
|
+
(exitCode) => resolve({ output: lines(standardOutput), errors: lines(standardError), failed: exitCode !== 0 })
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function startSectionSync(server, options = {}) {
|
|
409
|
+
if (server.config.mode === "test" || process.env.VITEST) return () => void 0;
|
|
410
|
+
const root = server.config.root;
|
|
411
|
+
const logger = server.config.logger;
|
|
412
|
+
const runSync = options.runSync ?? runSyncProgram;
|
|
413
|
+
const debounceMilliseconds = options.debounceMilliseconds ?? 250;
|
|
414
|
+
let timer;
|
|
415
|
+
let running = false;
|
|
416
|
+
let again = false;
|
|
417
|
+
let stopped = false;
|
|
418
|
+
async function run() {
|
|
419
|
+
if (stopped) return;
|
|
420
|
+
if (running) {
|
|
421
|
+
again = true;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
running = true;
|
|
425
|
+
try {
|
|
426
|
+
const result = await runSync(root);
|
|
427
|
+
for (const line of result.output) logger.info(`[ace-kit] ${line}`, { timestamp: true });
|
|
428
|
+
for (const line of result.errors) logger.warn(`[ace-kit] ${line}`, { timestamp: true });
|
|
429
|
+
if (result.failed && result.errors.length === 0)
|
|
430
|
+
logger.warn("[ace-kit] The section sync failed.", { timestamp: true });
|
|
431
|
+
} catch (error) {
|
|
432
|
+
logger.warn(`[ace-kit] The section sync failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
433
|
+
timestamp: true
|
|
434
|
+
});
|
|
435
|
+
} finally {
|
|
436
|
+
running = false;
|
|
437
|
+
}
|
|
438
|
+
if (again && !stopped) {
|
|
439
|
+
again = false;
|
|
440
|
+
await run();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function schedule() {
|
|
444
|
+
if (stopped) return;
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
timer = setTimeout(() => void run(), debounceMilliseconds);
|
|
447
|
+
}
|
|
448
|
+
const onFileEvent = (path) => {
|
|
449
|
+
if (schemaFilePattern.test(path) && !path.endsWith(".d.ts")) schedule();
|
|
450
|
+
};
|
|
451
|
+
for (const event of ["add", "change", "unlink"]) server.watcher.on(event, onFileEvent);
|
|
452
|
+
const start = () => {
|
|
453
|
+
void findSchemaFiles(root).then((files) => {
|
|
454
|
+
if (files.length > 0) schedule();
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
if (server.httpServer && !server.httpServer.listening) server.httpServer.once("listening", start);
|
|
458
|
+
else start();
|
|
459
|
+
const stop = () => {
|
|
460
|
+
stopped = true;
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
for (const event of ["add", "change", "unlink"]) server.watcher.off(event, onFileEvent);
|
|
463
|
+
};
|
|
464
|
+
server.httpServer?.once("close", stop);
|
|
465
|
+
return stop;
|
|
466
|
+
}
|
|
467
|
+
function sectionSync(options) {
|
|
468
|
+
return {
|
|
469
|
+
name: "ace-kit:section-sync",
|
|
470
|
+
apply: "serve",
|
|
471
|
+
configureServer(server) {
|
|
472
|
+
startSectionSync(server, options);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export { sectionSync, startDevSite, startSectionSync };
|
|
@@ -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 };
|