mechanica 2.1.0-beta.3 → 2.1.0-beta.5
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/README.md +3 -2
- package/dist/chunks/assets-C7yHGJBU.js +322 -0
- package/dist/chunks/{node-adapter-CSZY47h6.js → node-adapter-GZZrfGSz.js} +2 -2
- package/dist/chunks/{rich-text-codec-D8JAuJIS.js → rich-text-codec-BXOKV94u.js} +70 -1
- package/dist/chunks/{site-ONN3eU9_.js → site-Co04D3rB.js} +1 -1
- package/dist/cli.js +56 -157
- package/dist/editor.js +4 -0
- package/dist/export.js +15 -1
- package/dist/plugin.js +15 -5
- package/dist/server.js +4 -4
- package/dist/types/cli/platform/assets.d.ts +51 -0
- package/dist/types/cli/platform/link.d.ts +8 -0
- package/dist/types/cli/platform/push.d.ts +2 -1
- package/dist/types/server/assets-store.d.ts +31 -0
- package/dist/types/server/index.d.ts +1 -1
- package/package.json +2 -2
- package/dist/types/vite/dev/middleware.d.ts +0 -11
package/README.md
CHANGED
|
@@ -360,8 +360,9 @@ These drive a locally installed Chrome or Edge (or `--browser <path>`) and need
|
|
|
360
360
|
| `mechanica thumbs [--blocks]` | Regenerate page or block thumbnails for the editor |
|
|
361
361
|
| `mechanica images` | Generate image dimensions and blur-up previews for `.mech/assets` (needs `sharp`; `--force` redoes all) |
|
|
362
362
|
| `mechanica login` | Sign in to the Mechanica platform and save an access token for this machine (`--host`, `--token`) |
|
|
363
|
-
| `mechanica link <slug>` | Point this repository at a site on the platform (`--create` makes the site) |
|
|
364
|
-
| `mechanica push` | Publish the committed project to the platform: pull what was edited online, `git push`, build and upload the bundle when the code changed, wait for the deploy |
|
|
363
|
+
| `mechanica link <slug>` | Point this repository at a site on the platform (`--create` makes the site); uploads are then kept by the platform, so `.mech/assets/` goes into `.gitignore` |
|
|
364
|
+
| `mechanica push` | Publish the committed project to the platform: pull what was edited online, send the uploads it lacks, `git push`, build and upload the bundle when the code changed, wait for the deploy |
|
|
365
|
+
| `mechanica assets pull` | Download the uploads made in the platform's online editor that this project lacks (the dev server fetches them one by one on demand) |
|
|
365
366
|
|
|
366
367
|
## Plugin options
|
|
367
368
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { W as isAssetName, Y as updateImageManifest, q as readImageManifest } from "./rich-text-codec-BXOKV94u.js";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { chmod, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
/** Normalize a host argument to an origin (`https://example.com`, no trailing slash or path). */
|
|
7
|
+
function hostOrigin(host) {
|
|
8
|
+
const value = host ?? process.env.MECHANICA_HOST ?? "https://mechanica.jt3.ru";
|
|
9
|
+
return new URL(/^https?:\/\//.test(value) ? value : `https://${value}`).origin;
|
|
10
|
+
}
|
|
11
|
+
/** Where access tokens are kept: one file per user, outside any project. */
|
|
12
|
+
function credentialsFile() {
|
|
13
|
+
return join(process.env.MECHANICA_CONFIG_DIR ?? join(os.homedir(), ".mechanica"), "credentials.json");
|
|
14
|
+
}
|
|
15
|
+
async function readAll() {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(await readFile(credentialsFile(), "utf-8"));
|
|
18
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The access token for a platform origin: `MECHANICA_TOKEN` (CI) wins over the
|
|
25
|
+
* one `mechanica login` saved.
|
|
26
|
+
*/
|
|
27
|
+
async function getToken(origin) {
|
|
28
|
+
if (process.env.MECHANICA_TOKEN) return process.env.MECHANICA_TOKEN;
|
|
29
|
+
return (await readAll())[origin] ?? null;
|
|
30
|
+
}
|
|
31
|
+
/** Remember the token for an origin, in a file only the user can read. */
|
|
32
|
+
async function saveToken(origin, token) {
|
|
33
|
+
const file = credentialsFile();
|
|
34
|
+
const all = await readAll();
|
|
35
|
+
all[origin] = token;
|
|
36
|
+
await mkdir(join(file, ".."), { recursive: true });
|
|
37
|
+
await writeFile(file, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
38
|
+
await chmod(file, 384).catch(() => {});
|
|
39
|
+
return file;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/cli/platform/api.ts
|
|
43
|
+
/** A failed platform request: the HTTP status plus the message the platform gave. */
|
|
44
|
+
var PlatformError = class extends Error {
|
|
45
|
+
status;
|
|
46
|
+
constructor(status, message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.status = status;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/** The platform answers errors as plain text or as `{ error: { field: { message } } }`. */
|
|
52
|
+
function errorMessage(status, text) {
|
|
53
|
+
try {
|
|
54
|
+
const error = JSON.parse(text)?.error;
|
|
55
|
+
if (typeof error === "string") return error;
|
|
56
|
+
if (error && typeof error === "object") {
|
|
57
|
+
const messages = Object.values(error).map((item) => item?.message).filter((message) => typeof message === "string");
|
|
58
|
+
if (messages.length) return messages.join("; ");
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
return text.trim() || `Request failed (${status})`;
|
|
62
|
+
}
|
|
63
|
+
/** One request to the platform API; resolves with the parsed JSON body (or null). */
|
|
64
|
+
async function request(origin, method, path, options = {}) {
|
|
65
|
+
const headers = {};
|
|
66
|
+
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
67
|
+
if (options.raw) headers["content-type"] = "application/octet-stream";
|
|
68
|
+
else if (options.body !== void 0) headers["content-type"] = "application/json";
|
|
69
|
+
let response;
|
|
70
|
+
try {
|
|
71
|
+
response = await fetch(origin + path, {
|
|
72
|
+
method,
|
|
73
|
+
headers,
|
|
74
|
+
body: options.raw ?? (options.body !== void 0 ? JSON.stringify(options.body) : void 0)
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new PlatformError(0, `Could not reach ${origin} (${error.message})`);
|
|
78
|
+
}
|
|
79
|
+
const text = await response.text();
|
|
80
|
+
if (!response.ok) throw new PlatformError(response.status, errorMessage(response.status, text));
|
|
81
|
+
return text ? JSON.parse(text) : null;
|
|
82
|
+
}
|
|
83
|
+
/** The saved token for `origin`, or an error telling the user to log in. */
|
|
84
|
+
async function requireToken(origin) {
|
|
85
|
+
const token = await getToken(origin);
|
|
86
|
+
if (!token) throw new Error(`Not logged in to ${origin} — run \`mechanica login\` first`);
|
|
87
|
+
return token;
|
|
88
|
+
}
|
|
89
|
+
/** The name of the git remote `mechanica link` manages. */
|
|
90
|
+
var REMOTE_NAME = "mechanica";
|
|
91
|
+
/**
|
|
92
|
+
* Split a site's git remote URL (`https://host/git/<slug>.git`) into the
|
|
93
|
+
* platform origin and the slug; null for a URL that is not one of ours.
|
|
94
|
+
*/
|
|
95
|
+
function parseRemoteUrl(url) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = new URL(url);
|
|
98
|
+
const match = /^\/git\/([a-z0-9][a-z0-9-]*[a-z0-9])\.git\/?$/.exec(parsed.pathname);
|
|
99
|
+
if (!match || !/^https?:$/.test(parsed.protocol)) return null;
|
|
100
|
+
return {
|
|
101
|
+
origin: parsed.origin,
|
|
102
|
+
slug: match[1]
|
|
103
|
+
};
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/cli/platform/git.ts
|
|
110
|
+
/**
|
|
111
|
+
* Environment that makes git send the access token to one origin only, as HTTP
|
|
112
|
+
* Basic credentials. Passed through `GIT_CONFIG_*` variables so the token never
|
|
113
|
+
* appears on a command line (visible to other processes) or in the repo config.
|
|
114
|
+
*/
|
|
115
|
+
function gitAuthEnv(origin, token) {
|
|
116
|
+
const basic = Buffer.from(`mechanica:${token}`).toString("base64");
|
|
117
|
+
return {
|
|
118
|
+
GIT_CONFIG_COUNT: "1",
|
|
119
|
+
GIT_CONFIG_KEY_0: `http.${origin}/.extraHeader`,
|
|
120
|
+
GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`,
|
|
121
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Run git in `cwd` and collect its output; never throws on a non-zero exit. */
|
|
125
|
+
function git(cwd, args, env = {}) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const child = spawn("git", args, {
|
|
128
|
+
cwd,
|
|
129
|
+
env: {
|
|
130
|
+
...process.env,
|
|
131
|
+
...env
|
|
132
|
+
},
|
|
133
|
+
windowsHide: true
|
|
134
|
+
});
|
|
135
|
+
let stdout = "";
|
|
136
|
+
let stderr = "";
|
|
137
|
+
child.stdout.on("data", (chunk) => stdout += chunk);
|
|
138
|
+
child.stderr.on("data", (chunk) => stderr += chunk);
|
|
139
|
+
child.on("error", (error) => reject(error.code === "ENOENT" ? /* @__PURE__ */ new Error("git is not installed (or not on PATH)") : error));
|
|
140
|
+
child.on("close", (code) => resolve({
|
|
141
|
+
code: code ?? 1,
|
|
142
|
+
stdout: stdout.trim(),
|
|
143
|
+
stderr: stderr.trim()
|
|
144
|
+
}));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Like {@link git}, but a non-zero exit throws with git's own message. */
|
|
148
|
+
async function gitOk(cwd, args, env = {}) {
|
|
149
|
+
const result = await git(cwd, args, env);
|
|
150
|
+
if (result.code !== 0) throw new Error(result.stderr || result.stdout || `git ${args[0]} failed`);
|
|
151
|
+
return result.stdout;
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/cli/platform/assets.ts
|
|
155
|
+
var ASSETS_DIR = ".mech/assets";
|
|
156
|
+
/**
|
|
157
|
+
* The site this project is linked to (the `mechanica` git remote) with the
|
|
158
|
+
* saved token for it — or null when it is not linked, not logged in, or the
|
|
159
|
+
* account has no access to the site. Never throws: an unlinked project is the
|
|
160
|
+
* normal case for the dev server.
|
|
161
|
+
*/
|
|
162
|
+
async function resolveLink(cwd) {
|
|
163
|
+
try {
|
|
164
|
+
const remote = await git(cwd, [
|
|
165
|
+
"remote",
|
|
166
|
+
"get-url",
|
|
167
|
+
REMOTE_NAME
|
|
168
|
+
]);
|
|
169
|
+
const link = remote.code === 0 ? parseRemoteUrl(remote.stdout) : null;
|
|
170
|
+
if (!link) return null;
|
|
171
|
+
const token = await getToken(link.origin);
|
|
172
|
+
if (!token) return null;
|
|
173
|
+
const site = (await request(link.origin, "GET", "/api/sites", { token })).find((item) => item.slug === link.slug);
|
|
174
|
+
return site ? {
|
|
175
|
+
...link,
|
|
176
|
+
uuid: site.uuid,
|
|
177
|
+
token
|
|
178
|
+
} : null;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** The uploads the platform's store holds for the site. */
|
|
184
|
+
var listRemoteAssets = (link) => request(link.origin, "GET", `/api/sites/${link.uuid}/assets`, { token: link.token });
|
|
185
|
+
var fileUrl = (link, name) => `${link.origin}/api/sites/${link.uuid}/assets/file?name=${encodeURIComponent(name)}`;
|
|
186
|
+
/** One upload's bytes from the platform, or null when it has none of that name. */
|
|
187
|
+
async function downloadRemoteAsset(link, name) {
|
|
188
|
+
const response = await fetch(fileUrl(link, name), { headers: { authorization: `Bearer ${link.token}` } });
|
|
189
|
+
if (response.status === 404) return null;
|
|
190
|
+
if (!response.ok) throw new Error(`Could not download ${name} (${response.status})`);
|
|
191
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
192
|
+
}
|
|
193
|
+
var imageInfoOf = ({ width, height, previewSrc }) => ({
|
|
194
|
+
...width ? { width } : {},
|
|
195
|
+
...height ? { height } : {},
|
|
196
|
+
...previewSrc ? { previewSrc } : {}
|
|
197
|
+
});
|
|
198
|
+
/** The files in the project's uploads directory (flat: an upload is one file name). */
|
|
199
|
+
async function localAssets(cwd) {
|
|
200
|
+
const dir = join(cwd, ASSETS_DIR);
|
|
201
|
+
return (await readdir(dir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile() && isAssetName(entry.name)).map((entry) => entry.name);
|
|
202
|
+
}
|
|
203
|
+
/** The uploads git already carries: those reach the platform with the push itself. */
|
|
204
|
+
async function trackedAssets(cwd) {
|
|
205
|
+
const listed = await git(cwd, [
|
|
206
|
+
"ls-files",
|
|
207
|
+
"-z",
|
|
208
|
+
"--",
|
|
209
|
+
ASSETS_DIR
|
|
210
|
+
]);
|
|
211
|
+
if (listed.code !== 0) return /* @__PURE__ */ new Set();
|
|
212
|
+
return new Set(listed.stdout.split("\0").filter(Boolean).map((path) => path.slice(13)));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Send the platform the uploads this project has and it lacks — the files in
|
|
216
|
+
* `.mech/assets` that are not committed (committed ones travel with git) — with
|
|
217
|
+
* what `.mech/images.json` knows about them. Returns the names sent.
|
|
218
|
+
*/
|
|
219
|
+
async function pushUploads(cwd, link, log = console.info) {
|
|
220
|
+
const tracked = await trackedAssets(cwd);
|
|
221
|
+
const candidates = (await localAssets(cwd)).filter((name) => !tracked.has(name));
|
|
222
|
+
if (!candidates.length) return [];
|
|
223
|
+
const remote = new Set((await listRemoteAssets(link)).map((asset) => asset.name));
|
|
224
|
+
const missing = candidates.filter((name) => !remote.has(name));
|
|
225
|
+
if (!missing.length) return [];
|
|
226
|
+
log(`Uploading ${missing.length} file(s)…`);
|
|
227
|
+
for (const name of missing) {
|
|
228
|
+
const data = await readFile(join(cwd, ASSETS_DIR, name));
|
|
229
|
+
await request(link.origin, "POST", `/api/sites/${link.uuid}/assets/file?name=${encodeURIComponent(name)}`, {
|
|
230
|
+
token: link.token,
|
|
231
|
+
raw: new Uint8Array(data)
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const manifest = readImageManifest(join(cwd, ".mech"));
|
|
235
|
+
const info = {};
|
|
236
|
+
for (const name of missing) if (manifest[name]) info[name] = manifest[name];
|
|
237
|
+
if (Object.keys(info).length) await request(link.origin, "POST", `/api/sites/${link.uuid}/assets/info`, {
|
|
238
|
+
token: link.token,
|
|
239
|
+
body: info
|
|
240
|
+
});
|
|
241
|
+
return missing;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Whether git ignores the project's uploads directory — i.e. whether a file
|
|
245
|
+
* fetched into it stays out of `git status`. Decides if the dev server may
|
|
246
|
+
* keep what it fetches (see `withRemoteAssets`).
|
|
247
|
+
*/
|
|
248
|
+
async function uploadsIgnored(cwd) {
|
|
249
|
+
return (await git(cwd, [
|
|
250
|
+
"check-ignore",
|
|
251
|
+
"-q",
|
|
252
|
+
`${ASSETS_DIR}/probe.bin`
|
|
253
|
+
]).catch(() => ({ code: 1 }))).code === 0;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The platform's uploads as the dev server sees them: resolved lazily, once —
|
|
257
|
+
* a project that is not linked (or is offline) simply has none.
|
|
258
|
+
*/
|
|
259
|
+
function remoteAssetsFor(cwd) {
|
|
260
|
+
let link = null;
|
|
261
|
+
const linked = () => link ??= resolveLink(cwd);
|
|
262
|
+
let listing = null;
|
|
263
|
+
const assets = async () => {
|
|
264
|
+
const site = await linked();
|
|
265
|
+
if (!site) return [];
|
|
266
|
+
if (!listing || Date.now() - listing.at > 3e4) listing = {
|
|
267
|
+
at: Date.now(),
|
|
268
|
+
assets: listRemoteAssets(site).catch(() => [])
|
|
269
|
+
};
|
|
270
|
+
return listing.assets;
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
async fetch(name) {
|
|
274
|
+
const site = await linked();
|
|
275
|
+
if (!site) return null;
|
|
276
|
+
const data = await downloadRemoteAsset(site, name);
|
|
277
|
+
if (!data) return null;
|
|
278
|
+
const entry = (await assets()).find((asset) => asset.name === name);
|
|
279
|
+
return {
|
|
280
|
+
data,
|
|
281
|
+
...entry ? { info: imageInfoOf(entry) } : {}
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
list: async () => (await assets()).map((asset) => asset.name)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* `mechanica assets pull` — download every upload the platform has and this
|
|
289
|
+
* project lacks into `.mech/assets`, with its image info. What the dev server
|
|
290
|
+
* does one file at a time, for all of them: before an offline session, a local
|
|
291
|
+
* `mechanica export`, or leaving the platform. Returns the names downloaded.
|
|
292
|
+
*/
|
|
293
|
+
async function runAssetsPull(options = {}) {
|
|
294
|
+
const cwd = options.cwd ?? process.cwd();
|
|
295
|
+
const link = await resolveLink(cwd);
|
|
296
|
+
if (!link) throw new Error("This project is not linked to a site you can reach — run `mechanica login` and `mechanica link <slug>`");
|
|
297
|
+
const dir = join(cwd, ASSETS_DIR);
|
|
298
|
+
const have = new Set(await localAssets(cwd));
|
|
299
|
+
const missing = (await listRemoteAssets(link)).filter((asset) => isAssetName(asset.name) && !have.has(asset.name));
|
|
300
|
+
if (!missing.length) {
|
|
301
|
+
console.info("Nothing to download — every upload of the site is already here");
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
await mkdir(dir, { recursive: true });
|
|
305
|
+
const info = {};
|
|
306
|
+
const done = [];
|
|
307
|
+
for (const asset of missing) {
|
|
308
|
+
const data = await downloadRemoteAsset(link, asset.name);
|
|
309
|
+
if (!data) continue;
|
|
310
|
+
await writeFile(join(dir, asset.name), data);
|
|
311
|
+
const entry = imageInfoOf(asset);
|
|
312
|
+
if (Object.keys(entry).length) info[asset.name] = entry;
|
|
313
|
+
done.push(asset.name);
|
|
314
|
+
console.info(`✓ ${asset.name}`);
|
|
315
|
+
}
|
|
316
|
+
updateImageManifest(join(cwd, ".mech"), info);
|
|
317
|
+
console.info(`Downloaded ${done.length} file(s) into ${ASSETS_DIR}`);
|
|
318
|
+
if (!await uploadsIgnored(cwd) && await stat(join(cwd, ".git")).catch(() => null)) console.info(`Note: ${ASSETS_DIR} is not ignored by git, so these files show as changes. Uploads live on the platform — add \`.mech/assets/\` and \`.mech/images.json\` to .gitignore.`);
|
|
319
|
+
return done;
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
export { git as a, PlatformError as c, request as d, requireToken as f, uploadsIgnored as i, REMOTE_NAME as l, saveToken as m, remoteAssetsFor as n, gitAuthEnv as o, hostOrigin as p, runAssetsPull as r, gitOk as s, pushUploads as t, parseRemoteUrl as u };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as configureSite, r as indexGeneratedPages } from "./site-
|
|
2
|
-
import { A as pageVersion, C as harvestPageImages, D as movePage, E as listPages,
|
|
1
|
+
import { n as configureSite, r as indexGeneratedPages } from "./site-Co04D3rB.js";
|
|
2
|
+
import { A as pageVersion, C as harvestPageImages, D as movePage, E as listPages, I as setPageDraft, J as storeUpload, K as listUploads, L as translationsOf, M as renamePage, N as savePage, R as contentFilesOf, T as listFolders, U as fsAssetStore, W as isAssetName, _ as deletePage, a as folderOf, b as fillContentDefaults, c as mergeLocaleSiteData, f as readSiteData, g as createTranslation, h as createPage, j as readPage, l as mergeSiteData, m as PageExistsError, o as mergeFolderData, s as mergeLocaleFolderData, u as readFolderData, v as deleteTranslation, w as imageNamesOf, x as fillImageMeta, y as duplicatePage } from "./rich-text-codec-BXOKV94u.js";
|
|
3
3
|
import fs, { readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { basename, extname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { EDITOR_DIST_DIR, mergeTranslation, parseLocalePath, passDataToHTML, resolveQueryKey, serializeState } from "mechanica-shared";
|
|
@@ -273,6 +273,75 @@ async function listUploads(assets) {
|
|
|
273
273
|
src: assetUrl(name)
|
|
274
274
|
}));
|
|
275
275
|
}
|
|
276
|
+
var MISS_TTL_MS = 3e4;
|
|
277
|
+
var MEMORY_LIMIT = 67108864;
|
|
278
|
+
/**
|
|
279
|
+
* A store that falls back to {@link RemoteAssets} for what the local one lacks:
|
|
280
|
+
* an upload made in a hosted editor shows up in the dev server the first time a
|
|
281
|
+
* page asks for it, and (when `cache` allows) stays in the project's directory.
|
|
282
|
+
* Everything written goes to the local store; a remote failure is a miss, never
|
|
283
|
+
* an error — the dev server works offline with what it has.
|
|
284
|
+
*/
|
|
285
|
+
function withRemoteAssets(local, remote, options = {}) {
|
|
286
|
+
const fetched = /* @__PURE__ */ new Map();
|
|
287
|
+
let memory = 0;
|
|
288
|
+
const caching = async () => typeof options.cache === "function" ? await options.cache() : options.cache !== false;
|
|
289
|
+
const pull = (name) => {
|
|
290
|
+
let pending = fetched.get(name);
|
|
291
|
+
if (!pending) {
|
|
292
|
+
pending = (async () => {
|
|
293
|
+
const result = await remote.fetch(name).catch(() => null);
|
|
294
|
+
if (!result) {
|
|
295
|
+
setTimeout(() => fetched.delete(name), MISS_TTL_MS).unref?.();
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
if (await caching()) {
|
|
299
|
+
await local.write(name, result.data);
|
|
300
|
+
if (result.info && Object.keys(result.info).length) await local.saveInfo({ [name]: result.info });
|
|
301
|
+
fetched.delete(name);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
memory += result.data.byteLength;
|
|
305
|
+
if (memory > MEMORY_LIMIT) {
|
|
306
|
+
memory -= result.data.byteLength;
|
|
307
|
+
fetched.delete(name);
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
})();
|
|
311
|
+
fetched.set(name, pending);
|
|
312
|
+
}
|
|
313
|
+
return pending;
|
|
314
|
+
};
|
|
315
|
+
let remoteNames = null;
|
|
316
|
+
const listRemote = () => {
|
|
317
|
+
if (!remoteNames || Date.now() - remoteNames.at > 3e4) remoteNames = {
|
|
318
|
+
at: Date.now(),
|
|
319
|
+
names: remote.list().catch(() => [])
|
|
320
|
+
};
|
|
321
|
+
return remoteNames.names;
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
write: (name, data) => local.write(name, data),
|
|
325
|
+
saveInfo: (entries) => local.saveInfo(entries),
|
|
326
|
+
async open(name) {
|
|
327
|
+
return await local.open(name) ?? (await pull(name))?.data ?? null;
|
|
328
|
+
},
|
|
329
|
+
async list() {
|
|
330
|
+
return [.../* @__PURE__ */ new Set([...await local.list(), ...await listRemote()])];
|
|
331
|
+
},
|
|
332
|
+
async info(names) {
|
|
333
|
+
const known = await local.info(names);
|
|
334
|
+
const unknown = names.filter((name) => !known[name]);
|
|
335
|
+
if (!unknown.length) return known;
|
|
336
|
+
const here = new Set(await local.list());
|
|
337
|
+
await Promise.all(unknown.filter((name) => !here.has(name)).map(async (name) => {
|
|
338
|
+
const info = (await pull(name))?.info;
|
|
339
|
+
if (info && Object.keys(info).length) known[name] = info;
|
|
340
|
+
}));
|
|
341
|
+
return known;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
276
345
|
//#endregion
|
|
277
346
|
//#region src/server/content-files.ts
|
|
278
347
|
var slash = (path) => path.replace(/\\/g, "/");
|
|
@@ -1134,4 +1203,4 @@ function findRichTextProps(props) {
|
|
|
1134
1203
|
return found;
|
|
1135
1204
|
}
|
|
1136
1205
|
//#endregion
|
|
1137
|
-
export {
|
|
1206
|
+
export { analyzeImageBuffer as $, pageVersion as A, memoryContentFiles as B, harvestPageImages as C, movePage as D, listPages as E, setPageCodec as F, isDerivedAsset as G, assetFileOf as H, setPageDraft as I, storeUpload as J, listUploads as K, translationsOf as L, renamePage as M, savePage as N, pageOfFile as O, setPageBlocks as P, wasRecentlyMutated as Q, contentFilesOf as R, getPagePath as S, listFolders as T, fsAssetStore as U, UPLOADS_PREFIX$1 as V, isAssetName as W, uploadName as X, updateImageManifest as Y, withRemoteAssets as Z, deletePage as _, folderOf as a, fillContentDefaults as b, mergeLocaleSiteData as c, readFoldersData as d, hasSharp as et, readSiteData as f, createTranslation as g, createPage as h, toBlockMeta as i, readPage as j, pageUrlOf as k, mergeSiteData as l, PageExistsError as m, richTextCodecFromBlocks as n, mergeFolderData as o, readSiteLocaleOverride as p, readImageManifest as q, composedBlockMeta as r, mergeLocaleFolderData as s, buildRichTextCodec as t, isRasterImage as tt, readFolderData as u, deleteTranslation as v, imageNamesOf as w, fillImageMeta as x, duplicatePage as y, fsContentFiles as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as setPageCodec, P as setPageBlocks, i as toBlockMeta, n as richTextCodecFromBlocks, r as composedBlockMeta } from "./rich-text-codec-
|
|
1
|
+
import { F as setPageCodec, P as setPageBlocks, i as toBlockMeta, n as richTextCodecFromBlocks, r as composedBlockMeta } from "./rich-text-codec-BXOKV94u.js";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { SITE_MANIFEST_FILE, areFieldSchemasRegistered, localePath, registerFieldSchemas } from "mechanica-shared";
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { t as buildSiteManifest } from "./chunks/site-
|
|
2
|
-
import { $ as
|
|
1
|
+
import { t as buildSiteManifest } from "./chunks/site-Co04D3rB.js";
|
|
2
|
+
import { $ as analyzeImageBuffer, G as isDerivedAsset, Y as updateImageManifest, et as hasSharp, q as readImageManifest, tt as isRasterImage } from "./chunks/rich-text-codec-BXOKV94u.js";
|
|
3
|
+
import { a as git, c as PlatformError, d as request, f as requireToken, l as REMOTE_NAME, m as saveToken, o as gitAuthEnv, p as hostOrigin, r as runAssetsPull, s as gitOk, t as pushUploads, u as parseRemoteUrl } from "./chunks/assets-C7yHGJBU.js";
|
|
3
4
|
import { exportBuilt } from "./export.js";
|
|
4
5
|
import fs, { readFileSync } from "node:fs";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
7
|
import { EDITOR_DIST_DIR, LEGACY_UPLOADS_PREFIX, SITE_MANIFEST_FILE, UPLOADS_PREFIX } from "mechanica-shared";
|
|
7
|
-
import {
|
|
8
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
-
import { build } from "vite";
|
|
8
|
+
import { appendFile, mkdir, readFile, readdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
10
9
|
import os from "node:os";
|
|
11
10
|
import { spawn } from "node:child_process";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
+
import { build } from "vite";
|
|
12
13
|
import readline from "node:readline";
|
|
13
14
|
//#region src/cli/args.ts
|
|
14
15
|
/** Parse `argv` (without node/script) into a command, positionals and a `--flag[=value]` map. */
|
|
@@ -214,153 +215,6 @@ async function runMigrate(options = {}) {
|
|
|
214
215
|
console.info(changed.length ? `Updated ${changed.length} file(s): uploads are now referenced as ${UPLOADS_PREFIX}<file>` : "Nothing to migrate — the content is already in the current format");
|
|
215
216
|
return changed;
|
|
216
217
|
}
|
|
217
|
-
/** Normalize a host argument to an origin (`https://example.com`, no trailing slash or path). */
|
|
218
|
-
function hostOrigin(host) {
|
|
219
|
-
const value = host ?? process.env.MECHANICA_HOST ?? "https://mechanica.jt3.ru";
|
|
220
|
-
return new URL(/^https?:\/\//.test(value) ? value : `https://${value}`).origin;
|
|
221
|
-
}
|
|
222
|
-
/** Where access tokens are kept: one file per user, outside any project. */
|
|
223
|
-
function credentialsFile() {
|
|
224
|
-
return join(process.env.MECHANICA_CONFIG_DIR ?? join(os.homedir(), ".mechanica"), "credentials.json");
|
|
225
|
-
}
|
|
226
|
-
async function readAll() {
|
|
227
|
-
try {
|
|
228
|
-
const parsed = JSON.parse(await readFile(credentialsFile(), "utf-8"));
|
|
229
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
230
|
-
} catch {
|
|
231
|
-
return {};
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* The access token for a platform origin: `MECHANICA_TOKEN` (CI) wins over the
|
|
236
|
-
* one `mechanica login` saved.
|
|
237
|
-
*/
|
|
238
|
-
async function getToken(origin) {
|
|
239
|
-
if (process.env.MECHANICA_TOKEN) return process.env.MECHANICA_TOKEN;
|
|
240
|
-
return (await readAll())[origin] ?? null;
|
|
241
|
-
}
|
|
242
|
-
/** Remember the token for an origin, in a file only the user can read. */
|
|
243
|
-
async function saveToken(origin, token) {
|
|
244
|
-
const file = credentialsFile();
|
|
245
|
-
const all = await readAll();
|
|
246
|
-
all[origin] = token;
|
|
247
|
-
await mkdir(join(file, ".."), { recursive: true });
|
|
248
|
-
await writeFile(file, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
249
|
-
await chmod(file, 384).catch(() => {});
|
|
250
|
-
return file;
|
|
251
|
-
}
|
|
252
|
-
//#endregion
|
|
253
|
-
//#region src/cli/platform/api.ts
|
|
254
|
-
/** A failed platform request: the HTTP status plus the message the platform gave. */
|
|
255
|
-
var PlatformError = class extends Error {
|
|
256
|
-
status;
|
|
257
|
-
constructor(status, message) {
|
|
258
|
-
super(message);
|
|
259
|
-
this.status = status;
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
/** The platform answers errors as plain text or as `{ error: { field: { message } } }`. */
|
|
263
|
-
function errorMessage(status, text) {
|
|
264
|
-
try {
|
|
265
|
-
const error = JSON.parse(text)?.error;
|
|
266
|
-
if (typeof error === "string") return error;
|
|
267
|
-
if (error && typeof error === "object") {
|
|
268
|
-
const messages = Object.values(error).map((item) => item?.message).filter((message) => typeof message === "string");
|
|
269
|
-
if (messages.length) return messages.join("; ");
|
|
270
|
-
}
|
|
271
|
-
} catch {}
|
|
272
|
-
return text.trim() || `Request failed (${status})`;
|
|
273
|
-
}
|
|
274
|
-
/** One request to the platform API; resolves with the parsed JSON body (or null). */
|
|
275
|
-
async function request(origin, method, path, options = {}) {
|
|
276
|
-
const headers = {};
|
|
277
|
-
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
278
|
-
if (options.raw) headers["content-type"] = "application/octet-stream";
|
|
279
|
-
else if (options.body !== void 0) headers["content-type"] = "application/json";
|
|
280
|
-
let response;
|
|
281
|
-
try {
|
|
282
|
-
response = await fetch(origin + path, {
|
|
283
|
-
method,
|
|
284
|
-
headers,
|
|
285
|
-
body: options.raw ?? (options.body !== void 0 ? JSON.stringify(options.body) : void 0)
|
|
286
|
-
});
|
|
287
|
-
} catch (error) {
|
|
288
|
-
throw new PlatformError(0, `Could not reach ${origin} (${error.message})`);
|
|
289
|
-
}
|
|
290
|
-
const text = await response.text();
|
|
291
|
-
if (!response.ok) throw new PlatformError(response.status, errorMessage(response.status, text));
|
|
292
|
-
return text ? JSON.parse(text) : null;
|
|
293
|
-
}
|
|
294
|
-
/** The saved token for `origin`, or an error telling the user to log in. */
|
|
295
|
-
async function requireToken(origin) {
|
|
296
|
-
const token = await getToken(origin);
|
|
297
|
-
if (!token) throw new Error(`Not logged in to ${origin} — run \`mechanica login\` first`);
|
|
298
|
-
return token;
|
|
299
|
-
}
|
|
300
|
-
/** The name of the git remote `mechanica link` manages. */
|
|
301
|
-
var REMOTE_NAME = "mechanica";
|
|
302
|
-
/**
|
|
303
|
-
* Split a site's git remote URL (`https://host/git/<slug>.git`) into the
|
|
304
|
-
* platform origin and the slug; null for a URL that is not one of ours.
|
|
305
|
-
*/
|
|
306
|
-
function parseRemoteUrl(url) {
|
|
307
|
-
try {
|
|
308
|
-
const parsed = new URL(url);
|
|
309
|
-
const match = /^\/git\/([a-z0-9][a-z0-9-]*[a-z0-9])\.git\/?$/.exec(parsed.pathname);
|
|
310
|
-
if (!match || !/^https?:$/.test(parsed.protocol)) return null;
|
|
311
|
-
return {
|
|
312
|
-
origin: parsed.origin,
|
|
313
|
-
slug: match[1]
|
|
314
|
-
};
|
|
315
|
-
} catch {
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
//#endregion
|
|
320
|
-
//#region src/cli/platform/git.ts
|
|
321
|
-
/**
|
|
322
|
-
* Environment that makes git send the access token to one origin only, as HTTP
|
|
323
|
-
* Basic credentials. Passed through `GIT_CONFIG_*` variables so the token never
|
|
324
|
-
* appears on a command line (visible to other processes) or in the repo config.
|
|
325
|
-
*/
|
|
326
|
-
function gitAuthEnv(origin, token) {
|
|
327
|
-
const basic = Buffer.from(`mechanica:${token}`).toString("base64");
|
|
328
|
-
return {
|
|
329
|
-
GIT_CONFIG_COUNT: "1",
|
|
330
|
-
GIT_CONFIG_KEY_0: `http.${origin}/.extraHeader`,
|
|
331
|
-
GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`,
|
|
332
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
/** Run git in `cwd` and collect its output; never throws on a non-zero exit. */
|
|
336
|
-
function git(cwd, args, env = {}) {
|
|
337
|
-
return new Promise((resolve, reject) => {
|
|
338
|
-
const child = spawn("git", args, {
|
|
339
|
-
cwd,
|
|
340
|
-
env: {
|
|
341
|
-
...process.env,
|
|
342
|
-
...env
|
|
343
|
-
},
|
|
344
|
-
windowsHide: true
|
|
345
|
-
});
|
|
346
|
-
let stdout = "";
|
|
347
|
-
let stderr = "";
|
|
348
|
-
child.stdout.on("data", (chunk) => stdout += chunk);
|
|
349
|
-
child.stderr.on("data", (chunk) => stderr += chunk);
|
|
350
|
-
child.on("error", (error) => reject(error.code === "ENOENT" ? /* @__PURE__ */ new Error("git is not installed (or not on PATH)") : error));
|
|
351
|
-
child.on("close", (code) => resolve({
|
|
352
|
-
code: code ?? 1,
|
|
353
|
-
stdout: stdout.trim(),
|
|
354
|
-
stderr: stderr.trim()
|
|
355
|
-
}));
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
/** Like {@link git}, but a non-zero exit throws with git's own message. */
|
|
359
|
-
async function gitOk(cwd, args, env = {}) {
|
|
360
|
-
const result = await git(cwd, args, env);
|
|
361
|
-
if (result.code !== 0) throw new Error(result.stderr || result.stdout || `git ${args[0]} failed`);
|
|
362
|
-
return result.stdout;
|
|
363
|
-
}
|
|
364
218
|
//#endregion
|
|
365
219
|
//#region src/cli/platform/link.ts
|
|
366
220
|
/**
|
|
@@ -413,8 +267,36 @@ async function runLink(slug, options) {
|
|
|
413
267
|
site.gitUrl
|
|
414
268
|
]);
|
|
415
269
|
console.info(`Linked to ${site.slug} (${site.gitUrl})`);
|
|
270
|
+
await ignoreUploads(cwd);
|
|
416
271
|
console.info("Commit your work, then run `mechanica push` to deploy it.");
|
|
417
272
|
}
|
|
273
|
+
/** What a linked project keeps out of git: uploads live on the platform. */
|
|
274
|
+
var UPLOAD_IGNORES = [".mech/assets/", ".mech/images.json"];
|
|
275
|
+
/**
|
|
276
|
+
* A linked project's uploads live on the platform, not in git: `push` sends
|
|
277
|
+
* the files, the dev server fetches the ones made online. Make git ignore the
|
|
278
|
+
* local copies, so a fetched file is never an uncommitted change. Uploads that
|
|
279
|
+
* are already committed stay committed (git keeps tracking them) and keep
|
|
280
|
+
* working — the hint says how to move them over.
|
|
281
|
+
*/
|
|
282
|
+
async function ignoreUploads(cwd) {
|
|
283
|
+
const file = join(cwd, ".gitignore");
|
|
284
|
+
const current = await readFile(file, "utf-8").catch(() => "");
|
|
285
|
+
const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
286
|
+
const missing = UPLOAD_IGNORES.filter((entry) => !lines.has(entry) && !lines.has(entry.replace(/\/$/, "")));
|
|
287
|
+
if (missing.length) {
|
|
288
|
+
const block = `${current && !current.endsWith("\n") ? "\n" : ""}\n# Uploads live on the platform (mechanica push sends them, the dev server fetches them)\n${missing.join("\n")}\n`;
|
|
289
|
+
await appendFile(file, block);
|
|
290
|
+
console.info(`Added ${missing.join(" and ")} to .gitignore — uploads are kept by the platform, not by git.`);
|
|
291
|
+
}
|
|
292
|
+
const tracked = await git(cwd, [
|
|
293
|
+
"ls-files",
|
|
294
|
+
"--",
|
|
295
|
+
".mech/assets",
|
|
296
|
+
".mech/images.json"
|
|
297
|
+
]);
|
|
298
|
+
if (tracked.code === 0 && tracked.stdout) console.info("Uploads already committed in this repository keep working. To move them to the platform:\n git rm -r --cached .mech/assets .mech/images.json && git commit -m \"Uploads live on the platform\"");
|
|
299
|
+
}
|
|
418
300
|
//#endregion
|
|
419
301
|
//#region src/cli/platform/login.ts
|
|
420
302
|
/** Ask one line on the terminal; with `hidden`, the typed characters are not echoed. */
|
|
@@ -451,11 +333,14 @@ async function runLogin(options) {
|
|
|
451
333
|
if (!token) {
|
|
452
334
|
if (!process.stdin.isTTY) throw new Error("No terminal to ask for credentials — pass --token <access token>");
|
|
453
335
|
console.info(`Signing in to ${origin}`);
|
|
336
|
+
const login = await prompt("Email: ");
|
|
337
|
+
const password = await prompt("Password: ", true);
|
|
338
|
+
const session = await request(origin, "POST", "/api/account/login", { body: {
|
|
339
|
+
login,
|
|
340
|
+
password
|
|
341
|
+
} });
|
|
454
342
|
token = (await request(origin, "POST", "/api/account/tokens", {
|
|
455
|
-
token:
|
|
456
|
-
login: await prompt("Email: "),
|
|
457
|
-
password: await prompt("Password: ", true)
|
|
458
|
-
} })).accessToken,
|
|
343
|
+
token: session.accessToken,
|
|
459
344
|
body: { name: `CLI on ${os.hostname()}` }
|
|
460
345
|
})).token;
|
|
461
346
|
}
|
|
@@ -528,7 +413,8 @@ var AUTO_DEPLOY_WAIT_MS = 1e4;
|
|
|
528
413
|
* `mechanica push`: publish the committed project to the platform.
|
|
529
414
|
*
|
|
530
415
|
* 1. bring in what was published online since the last pull (`pull --rebase`),
|
|
531
|
-
* 2.
|
|
416
|
+
* 2. send the uploads the platform lacks (they are not in git), then `git push`
|
|
417
|
+
* the commits to the site's repository,
|
|
532
418
|
* 3. if the code changed, build locally and upload the bundle for that commit
|
|
533
419
|
* (content-only pushes reuse the bundle already there — no build at all),
|
|
534
420
|
* 4. wait for the deploy and report it.
|
|
@@ -574,6 +460,11 @@ async function runPush() {
|
|
|
574
460
|
], auth);
|
|
575
461
|
if (pulled.code !== 0) throw new Error(`Could not rebase onto the platform's changes:\n${pulled.stderr || pulled.stdout}\nResolve the conflict (\`git rebase --continue\`), then run \`mechanica push\` again.`);
|
|
576
462
|
}
|
|
463
|
+
await pushUploads(cwd, {
|
|
464
|
+
...link,
|
|
465
|
+
uuid: site.uuid,
|
|
466
|
+
token
|
|
467
|
+
});
|
|
577
468
|
const head = await gitOk(cwd, ["rev-parse", "HEAD"]);
|
|
578
469
|
console.info(`Pushing ${head.slice(0, 8)} to ${link.slug}…`);
|
|
579
470
|
const pushed = await git(cwd, [
|
|
@@ -1324,6 +1215,10 @@ Usage:
|
|
|
1324
1215
|
previews) for .mech/assets into .mech/images.json —
|
|
1325
1216
|
for pages authored without the browser editor
|
|
1326
1217
|
(needs the optional sharp dependency; --force redoes all)
|
|
1218
|
+
mechanica assets pull Download the site's uploads this project lacks into
|
|
1219
|
+
.mech/assets (the dev server fetches them one by one
|
|
1220
|
+
on demand; this gets all of them — for working offline
|
|
1221
|
+
or a local export)
|
|
1327
1222
|
mechanica migrate Bring .mech content up to the current format: uploads
|
|
1328
1223
|
are referenced as /media/<file> (was /@mechanica/assets/)
|
|
1329
1224
|
`;
|
|
@@ -1339,6 +1234,10 @@ async function run(argv) {
|
|
|
1339
1234
|
assetsUrl: str(flags["assets-url"])
|
|
1340
1235
|
});
|
|
1341
1236
|
case "images": return runImages({ force: flags.force === true });
|
|
1237
|
+
case "assets":
|
|
1238
|
+
if (args[0] !== "pull") throw new Error("Usage: mechanica assets pull");
|
|
1239
|
+
await runAssetsPull();
|
|
1240
|
+
return;
|
|
1342
1241
|
case "migrate":
|
|
1343
1242
|
await runMigrate();
|
|
1344
1243
|
return;
|
package/dist/editor.js
CHANGED
|
@@ -3355,6 +3355,10 @@ var saveQueue = createSaveQueue({
|
|
|
3355
3355
|
version: pageVersion
|
|
3356
3356
|
})
|
|
3357
3357
|
});
|
|
3358
|
+
window.__MECHANICA_EDITOR_API__ = {
|
|
3359
|
+
hasUnsaved: () => saveQueue.hasUnsaved(),
|
|
3360
|
+
flush: () => saveQueue.flush()
|
|
3361
|
+
};
|
|
3358
3362
|
window.addEventListener("beforeunload", (event) => {
|
|
3359
3363
|
if (saveQueue.flushOnUnload()) return;
|
|
3360
3364
|
event.preventDefault();
|
package/dist/export.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as analyzeImageBuffer, E as listPages, F as setPageCodec, G as isDerivedAsset, H as assetFileOf, S as getPagePath, V as UPLOADS_PREFIX$1, Y as updateImageManifest, d as readFoldersData, et as hasSharp, f as readSiteData, i as toBlockMeta, p as readSiteLocaleOverride, q as readImageManifest, r as composedBlockMeta, t as buildRichTextCodec, u as readFolderData } from "./chunks/rich-text-codec-BXOKV94u.js";
|
|
2
2
|
import { n as blockAssetLinks, t as BLOCKS_MANIFEST_FILE } from "./chunks/page-assets-DsJXmoDs.js";
|
|
3
3
|
import { dirname, join, parse, relative, resolve } from "node:path";
|
|
4
4
|
import { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, applySeoTags, auditPageHtml, buildRobotsTxt, buildSitemap, findUnknownBlocks, generateProject, isPaginatedQuery, localePath, mergeTranslation, migrateContent, paginationVariantPath, registerFieldSchemas, resolveQueryKey, templateBlockIds, validateLinks, walkSchema, walkTree } from "mechanica-shared";
|
|
@@ -435,12 +435,26 @@ async function exportProject(cwd, ssr, options = {}) {
|
|
|
435
435
|
onFile,
|
|
436
436
|
pageLinks
|
|
437
437
|
};
|
|
438
|
+
const unclaimed = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const name of await readdir(join(cwd, ".mech/assets")).catch(() => [])) {
|
|
440
|
+
const spellings = [
|
|
441
|
+
name,
|
|
442
|
+
encodeURI(name),
|
|
443
|
+
encodeURIComponent(name)
|
|
444
|
+
].map((spelled) => UPLOADS_PREFIX$1 + spelled);
|
|
445
|
+
unclaimed.set(name, [...new Set(spellings)]);
|
|
446
|
+
}
|
|
438
447
|
const written = [];
|
|
439
448
|
const writePage = async (path, html) => {
|
|
440
449
|
const dir = path === "/" ? exportDir : join(exportDir, path);
|
|
441
450
|
await mkdir(dir, { recursive: true });
|
|
442
451
|
await writeFile(join(dir, "index.html"), html);
|
|
443
452
|
written.push(path);
|
|
453
|
+
for (const [name, addresses] of unclaimed) {
|
|
454
|
+
if (!addresses.some((address) => html.includes(address))) continue;
|
|
455
|
+
referenced.add(name);
|
|
456
|
+
unclaimed.delete(name);
|
|
457
|
+
}
|
|
444
458
|
};
|
|
445
459
|
const templateHandlesPagination = index.includes("page.pagination");
|
|
446
460
|
const pageNames = new Map(defaultPages.map((page) => [page.logicalPath, page.name ?? (page.logicalPath === "/" ? "Home" : page.logicalPath.split("/").pop())]));
|
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as collectGeneratedPages, n as configureSite, r as indexGeneratedPages, t as buildSiteManifest } from "./chunks/site-
|
|
2
|
-
import { a as renderEditablePage, c as fillStateImages, d as loadComposedDefinitions, n as toNodeMiddleware, o as buildGeneratedState, r as createEditorService, s as buildPageState, t as toAssetMiddleware, u as collectComposed } from "./chunks/node-adapter-
|
|
3
|
-
import {
|
|
1
|
+
import { a as collectGeneratedPages, n as configureSite, r as indexGeneratedPages, t as buildSiteManifest } from "./chunks/site-Co04D3rB.js";
|
|
2
|
+
import { a as renderEditablePage, c as fillStateImages, d as loadComposedDefinitions, n as toNodeMiddleware, o as buildGeneratedState, r as createEditorService, s as buildPageState, t as toAssetMiddleware, u as collectComposed } from "./chunks/node-adapter-GZZrfGSz.js";
|
|
3
|
+
import { Q as wasRecentlyMutated, U as fsAssetStore, Z as withRemoteAssets, k as pageUrlOf } from "./chunks/rich-text-codec-BXOKV94u.js";
|
|
4
|
+
import { i as uploadsIgnored, n as remoteAssetsFor } from "./chunks/assets-C7yHGJBU.js";
|
|
4
5
|
import { t as BLOCKS_MANIFEST_FILE } from "./chunks/page-assets-DsJXmoDs.js";
|
|
5
6
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
6
7
|
import { join, relative } from "node:path";
|
|
@@ -955,6 +956,12 @@ function mechanica(options = {}) {
|
|
|
955
956
|
let blocksDir = "";
|
|
956
957
|
let widgetsDir = "";
|
|
957
958
|
let mechDir = "";
|
|
959
|
+
let uploadStore = null;
|
|
960
|
+
let uploadsCached = null;
|
|
961
|
+
const uploads = () => uploadStore ??= withRemoteAssets(fsAssetStore(mechDir), remoteAssetsFor(root), { cache: () => uploadsCached ??= uploadsIgnored(root).then((ignored) => {
|
|
962
|
+
if (!ignored) console.info("[mechanica] Uploads made online are fetched from the platform on every start: add `.mech/assets/` and `.mech/images.json` to .gitignore (`mechanica link` does it) to keep them locally.");
|
|
963
|
+
return ignored;
|
|
964
|
+
}) });
|
|
958
965
|
let composedDir = "";
|
|
959
966
|
let composerFilePath = "";
|
|
960
967
|
let composerSpecifier = "";
|
|
@@ -1231,7 +1238,10 @@ function mechanica(options = {}) {
|
|
|
1231
1238
|
configureServer(server) {
|
|
1232
1239
|
server.middlewares.use("/@mechanica/preview", createPreviewMiddleware());
|
|
1233
1240
|
server.middlewares.use("/@mechanica/composer", createComposerMiddleware());
|
|
1234
|
-
const service = createEditorService(mechDir, {
|
|
1241
|
+
const service = createEditorService(mechDir, {
|
|
1242
|
+
site: () => loadSite(server),
|
|
1243
|
+
assets: uploads()
|
|
1244
|
+
});
|
|
1235
1245
|
server.middlewares.use("/@mechanica", toNodeMiddleware(service));
|
|
1236
1246
|
server.middlewares.use(UPLOADS_PREFIX.replace(/\/$/, ""), toAssetMiddleware(service));
|
|
1237
1247
|
if (server.httpServer && !server.httpServer.listening) server.httpServer.once("listening", () => void loadSite(server));
|
|
@@ -1307,7 +1317,7 @@ function mechanica(options = {}) {
|
|
|
1307
1317
|
const withEditor = !url.searchParams.has("mechanica-shot");
|
|
1308
1318
|
if (ctx.server) await loadSite(ctx.server);
|
|
1309
1319
|
const generated = (await loadGeneratedIndex()).byServed.get(urlPath);
|
|
1310
|
-
const state = await fillStateImages(mechDir,
|
|
1320
|
+
const state = await fillStateImages(mechDir, uploads(), generated ? buildGeneratedState(mechDir, generated, locales) : buildPageState(mechDir, urlPath, locales));
|
|
1311
1321
|
return renderEditablePage(html, state, {
|
|
1312
1322
|
site: {
|
|
1313
1323
|
url: options.siteUrl,
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as readSiteManifest, n as configureSite, t as buildSiteManifest } from "./chunks/site-
|
|
2
|
-
import { a as renderEditablePage, i as readEditorTemplate, n as toNodeMiddleware, r as createEditorService, t as toAssetMiddleware } from "./chunks/node-adapter-
|
|
3
|
-
import { B as memoryContentFiles,
|
|
1
|
+
import { i as readSiteManifest, n as configureSite, t as buildSiteManifest } from "./chunks/site-Co04D3rB.js";
|
|
2
|
+
import { a as renderEditablePage, i as readEditorTemplate, n as toNodeMiddleware, r as createEditorService, t as toAssetMiddleware } from "./chunks/node-adapter-GZZrfGSz.js";
|
|
3
|
+
import { B as memoryContentFiles, G as isDerivedAsset, O as pageOfFile, U as fsAssetStore, X as uploadName, Z as withRemoteAssets, z as fsContentFiles } from "./chunks/rich-text-codec-BXOKV94u.js";
|
|
4
4
|
import { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, UPLOADS_PREFIX, uploadNameOf } from "mechanica-shared";
|
|
5
|
-
export { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, UPLOADS_PREFIX, buildSiteManifest, configureSite, createEditorService, fsAssetStore, fsContentFiles, isDerivedAsset, memoryContentFiles, pageOfFile, readEditorTemplate, readSiteManifest, renderEditablePage, toAssetMiddleware, toNodeMiddleware, uploadName, uploadNameOf };
|
|
5
|
+
export { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, UPLOADS_PREFIX, buildSiteManifest, configureSite, createEditorService, fsAssetStore, fsContentFiles, isDerivedAsset, memoryContentFiles, pageOfFile, readEditorTemplate, readSiteManifest, renderEditablePage, toAssetMiddleware, toNodeMiddleware, uploadName, uploadNameOf, withRemoteAssets };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ImageManifestEntry, type RemoteAssets } from '../../server/assets-store';
|
|
2
|
+
/** A site this project can talk to: where it is, and as whom. */
|
|
3
|
+
export interface SiteLink {
|
|
4
|
+
origin: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
uuid: string;
|
|
7
|
+
token: string;
|
|
8
|
+
}
|
|
9
|
+
/** An upload as the platform lists it. */
|
|
10
|
+
export interface RemoteAsset extends ImageManifestEntry {
|
|
11
|
+
name: string;
|
|
12
|
+
size: number;
|
|
13
|
+
hash: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The site this project is linked to (the `mechanica` git remote) with the
|
|
17
|
+
* saved token for it — or null when it is not linked, not logged in, or the
|
|
18
|
+
* account has no access to the site. Never throws: an unlinked project is the
|
|
19
|
+
* normal case for the dev server.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveLink(cwd: string): Promise<SiteLink | null>;
|
|
22
|
+
/** The uploads the platform's store holds for the site. */
|
|
23
|
+
export declare const listRemoteAssets: (link: SiteLink) => Promise<RemoteAsset[]>;
|
|
24
|
+
/** One upload's bytes from the platform, or null when it has none of that name. */
|
|
25
|
+
export declare function downloadRemoteAsset(link: SiteLink, name: string): Promise<Uint8Array | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Send the platform the uploads this project has and it lacks — the files in
|
|
28
|
+
* `.mech/assets` that are not committed (committed ones travel with git) — with
|
|
29
|
+
* what `.mech/images.json` knows about them. Returns the names sent.
|
|
30
|
+
*/
|
|
31
|
+
export declare function pushUploads(cwd: string, link: SiteLink, log?: (message: string) => void): Promise<string[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Whether git ignores the project's uploads directory — i.e. whether a file
|
|
34
|
+
* fetched into it stays out of `git status`. Decides if the dev server may
|
|
35
|
+
* keep what it fetches (see `withRemoteAssets`).
|
|
36
|
+
*/
|
|
37
|
+
export declare function uploadsIgnored(cwd: string): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* The platform's uploads as the dev server sees them: resolved lazily, once —
|
|
40
|
+
* a project that is not linked (or is offline) simply has none.
|
|
41
|
+
*/
|
|
42
|
+
export declare function remoteAssetsFor(cwd: string): RemoteAssets;
|
|
43
|
+
/**
|
|
44
|
+
* `mechanica assets pull` — download every upload the platform has and this
|
|
45
|
+
* project lacks into `.mech/assets`, with its image info. What the dev server
|
|
46
|
+
* does one file at a time, for all of them: before an offline session, a local
|
|
47
|
+
* `mechanica export`, or leaving the platform. Returns the names downloaded.
|
|
48
|
+
*/
|
|
49
|
+
export declare function runAssetsPull(options?: {
|
|
50
|
+
cwd?: string;
|
|
51
|
+
}): Promise<string[]>;
|
|
@@ -9,3 +9,11 @@ export declare function runLink(slug: string | undefined, options: {
|
|
|
9
9
|
create?: boolean;
|
|
10
10
|
name?: string;
|
|
11
11
|
}): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* A linked project's uploads live on the platform, not in git: `push` sends
|
|
14
|
+
* the files, the dev server fetches the ones made online. Make git ignore the
|
|
15
|
+
* local copies, so a fetched file is never an uncommitted change. Uploads that
|
|
16
|
+
* are already committed stay committed (git keeps tracking them) and keep
|
|
17
|
+
* working — the hint says how to move them over.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ignoreUploads(cwd: string): Promise<void>;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* `mechanica push`: publish the committed project to the platform.
|
|
3
3
|
*
|
|
4
4
|
* 1. bring in what was published online since the last pull (`pull --rebase`),
|
|
5
|
-
* 2.
|
|
5
|
+
* 2. send the uploads the platform lacks (they are not in git), then `git push`
|
|
6
|
+
* the commits to the site's repository,
|
|
6
7
|
* 3. if the code changed, build locally and upload the bundle for that commit
|
|
7
8
|
* (content-only pushes reuse the bundle already there — no build at all),
|
|
8
9
|
* 4. wait for the deploy and report it.
|
|
@@ -84,3 +84,34 @@ export declare function listUploads(assets: AssetStore): Promise<{
|
|
|
84
84
|
name: string;
|
|
85
85
|
src: string;
|
|
86
86
|
}[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Where a project's uploads also live — a hosted platform the project is linked
|
|
89
|
+
* to. Uploads made in its online editor are not in the project's directory
|
|
90
|
+
* until something asks for them.
|
|
91
|
+
*/
|
|
92
|
+
export interface RemoteAssets {
|
|
93
|
+
/** An upload's bytes with what is known about it as an image; null when there is no such upload. */
|
|
94
|
+
fetch(name: string): Promise<{
|
|
95
|
+
data: Uint8Array;
|
|
96
|
+
info?: ImageManifestEntry;
|
|
97
|
+
} | null>;
|
|
98
|
+
/** The names of the uploads kept there. */
|
|
99
|
+
list(): Promise<string[]>;
|
|
100
|
+
}
|
|
101
|
+
export interface RemoteAssetsOptions {
|
|
102
|
+
/**
|
|
103
|
+
* Whether a fetched upload is written into the local store. When it is not,
|
|
104
|
+
* the upload is fetched once per process and served from memory — for a
|
|
105
|
+
* project whose uploads directory is under version control, where a file
|
|
106
|
+
* appearing in it would be an uncommitted change.
|
|
107
|
+
*/
|
|
108
|
+
cache?: boolean | (() => boolean | Promise<boolean>);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* A store that falls back to {@link RemoteAssets} for what the local one lacks:
|
|
112
|
+
* an upload made in a hosted editor shows up in the dev server the first time a
|
|
113
|
+
* page asks for it, and (when `cache` allows) stays in the project's directory.
|
|
114
|
+
* Everything written goes to the local store; a remote failure is a miss, never
|
|
115
|
+
* an error — the dev server works offline with what it has.
|
|
116
|
+
*/
|
|
117
|
+
export declare function withRemoteAssets(local: AssetStore, remote: RemoteAssets, options?: RemoteAssetsOptions): AssetStore;
|
|
@@ -8,7 +8,7 @@ export { createEditorService, type EditorService, type EditorServiceOptions, typ
|
|
|
8
8
|
export { toNodeMiddleware, toAssetMiddleware, type NodeMiddleware } from './node-adapter';
|
|
9
9
|
export { fsContentFiles, memoryContentFiles, type ContentFiles, type ContentChanges, type MemoryContentFiles, type Mech, } from './content-files';
|
|
10
10
|
export { pageOfFile } from './pages-store';
|
|
11
|
-
export { fsAssetStore, uploadName, isDerivedAsset, type AssetStore, type ImageManifest, type ImageManifestEntry, } from './assets-store';
|
|
11
|
+
export { fsAssetStore, withRemoteAssets, uploadName, isDerivedAsset, type AssetStore, type RemoteAssets, type RemoteAssetsOptions, type ImageManifest, type ImageManifestEntry, } from './assets-store';
|
|
12
12
|
export { buildSiteManifest, readSiteManifest, configureSite, type SiteManifestInput } from './site';
|
|
13
13
|
export { renderEditablePage, readEditorTemplate, type EditablePageOptions } from './editor-page';
|
|
14
14
|
export { SITE_MANIFEST_FILE, EDITOR_DIST_DIR, UPLOADS_PREFIX, uploadNameOf, type SiteManifest, type EditorHostConfig, } from 'mechanica-shared';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mechanica",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.5",
|
|
4
4
|
"description": "Build Vue 3 websites with a visual block editor",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"prepublishOnly": "bun ../../scripts/ensure-bun-publish.js && bun run build"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"mechanica-shared": "2.1.0-beta.
|
|
69
|
+
"mechanica-shared": "2.1.0-beta.5",
|
|
70
70
|
"@vue/compiler-sfc": "^3.5.38",
|
|
71
71
|
"compact-json-schema": "^0.1.5",
|
|
72
72
|
"estree-walker": "^3.0.3",
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Connect } from 'vite';
|
|
2
|
-
import { type EditorServiceOptions } from '../../server/service';
|
|
3
|
-
export type { BlockListing } from '../../server/service';
|
|
4
|
-
export type DevMiddlewareOptions = EditorServiceOptions;
|
|
5
|
-
/**
|
|
6
|
-
* The `/@mechanica` dev middleware: the site's editor API (`src/server` —
|
|
7
|
-
* page CRUD, asset upload/serve, folder list, query resolution) over the local
|
|
8
|
-
* `.mech` store, mounted on Vite's connect stack. Mounted under the
|
|
9
|
-
* `/@mechanica` prefix, so `req.url` here is already prefix-stripped.
|
|
10
|
-
*/
|
|
11
|
-
export declare function createDevMiddleware(mechDir: string, options?: DevMiddlewareOptions): Connect.NextHandleFunction;
|