mechanica 2.2.0 → 2.2.2
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 +7 -3
- package/dist/browser.js +1 -1
- package/dist/chunks/{browser-BCfAv8bW.js → browser-BV2iB_xD.js} +337 -85
- package/dist/chunks/{create-mechanica-CYaKXuQd.js → create-mechanica-CMcmeOtG.js} +30 -8
- package/dist/chunks/{preview-CbUxh2gL.js → preview-BrtQ0KpO.js} +1 -1
- package/dist/cli.js +10 -3
- package/dist/composer.js +1 -1
- package/dist/editor.js +2 -2
- package/dist/index.js +5 -5
- package/dist/plugin.js +21 -15
- package/dist/types/cli/browser.d.ts +17 -3
- package/dist/types/cli/headless.d.ts +44 -2
- package/dist/types/cli/section-query.d.ts +49 -0
- package/dist/types/cli/shot.d.ts +6 -1
- package/dist/types/core/page-path.d.ts +13 -0
- package/dist/types/core/router.d.ts +7 -0
- package/dist/types/vite/entries.d.ts +9 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -156,6 +156,8 @@ Other `defineBlock` options: `icon`, `description` and `order` for the palette;
|
|
|
156
156
|
|
|
157
157
|
Give every block meaningful `previewData`: it drives the palette's hover preview, the standalone preview route and `mechanica shot`. A `$slots` key fills slots with child blocks, e.g. `$slots: { default: [{ blockId: 'card', data: { title: 'One' } }] }`.
|
|
158
158
|
|
|
159
|
+
The exported site is prerendered, and the browser **hydrates** that HTML rather than rendering it again, so a block must render the same on the server and in the browser: read `window`, `document` or `matchMedia` only in `onMounted` (or behind a client-only flag set there), never in `setup` or the template. The dev server mounts into an empty page and never hydrates, so a mismatch shows only in the built site. To check, build with `MECHANICA_HYDRATION_DEBUG=1 mechanica build` and watch the browser console on the exported pages: each mismatch is logged with the element involved.
|
|
160
|
+
|
|
159
161
|
## Pages
|
|
160
162
|
|
|
161
163
|
Each page is a `.page.md` file under `.mech/pages/`, and its path is the URL: `about.page.md` → `/about`, `blog/index.page.md` → `/blog`, `index.page.md` → `/`.
|
|
@@ -254,7 +256,7 @@ Editors can set a focal point and crop from the image field's **Position / Crop*
|
|
|
254
256
|
|
|
255
257
|
## Links and navigation
|
|
256
258
|
|
|
257
|
-
`<Link :to="…">` takes a path string or a `smartLink` value. Internal links navigate client-side without a full reload, get an `is-active` class on the current page, and add the language prefix on multi-language sites. `useRouter().push(path)` navigates from code, and `useRoute().path` is the current path.
|
|
259
|
+
`<Link :to="…">` takes a path string or a `smartLink` value. Internal links navigate client-side without a full reload, get an `is-active` class on the current page, and add the language prefix on multi-language sites. `useRouter().push(path)` navigates from code, and `useRoute().path` is the current path. A target the client-side navigation cannot take over (an error response, or a page without Mechanica state) is opened as a normal full-page navigation instead.
|
|
258
260
|
|
|
259
261
|
## Listing pages and pagination
|
|
260
262
|
|
|
@@ -339,6 +341,8 @@ Every export also warns about missing titles and descriptions, pages without exa
|
|
|
339
341
|
|
|
340
342
|
By default all blocks share one `blocks` chunk, cached across pages. Mark heavy, rarely used blocks with `chunk: 'charts'` in `defineBlock` to split them into their own chunk, or set the plugin's `blockChunks: 'per-block'`. The export adds per-page `<link rel="modulepreload">` and stylesheet tags, so each page loads exactly the block code it uses.
|
|
341
343
|
|
|
344
|
+
Code outside blocks follows Vite's defaults. To group it yourself, add your own groups under `build.rollupOptions.output.codeSplitting.groups` in `vite.config.ts`: they are kept next to Mechanica's blocks group and listed before it, so at equal `priority` yours win. The plugin sets `includeDependenciesRecursively: false`, so a group holds exactly the modules its `test` matches.
|
|
345
|
+
|
|
342
346
|
## Checking your work visually
|
|
343
347
|
|
|
344
348
|
```bash
|
|
@@ -361,7 +365,7 @@ These drive a locally installed Chrome or Edge (or `--browser <path>`) and need
|
|
|
361
365
|
| --- | --- |
|
|
362
366
|
| `mechanica export` | Build, then render every page to static HTML in `export/`. Flags: `--site-url`, `--site-name`, `--assets-url` (serve `/assets` and `/media` from a CDN) |
|
|
363
367
|
| `mechanica build` | Build the client and server bundles into `dist/` (`export` runs this for you) |
|
|
364
|
-
| `mechanica shot <blockId \| /path \| url \| file.html>` | Screenshot a block, a page, any URL or a local HTML file (a design handoff; no dev server needed). Flags: `--width`, `--section <css>` (repeatable, one PNG each), `--scroll <px[,px…]>` (the viewport after scrolling), `--no-freeze`, `--full`, `--data` (blocks), `--out`, `--server`, `--browser` |
|
|
368
|
+
| `mechanica shot <blockId \| /path \| url \| file.html>` | Screenshot a block, a page, any URL or a local HTML file (a design handoff; no dev server needed). Flags: `--width`, `--section <css>` (repeatable, one PNG each; also inside open shadow roots, `#name` also finds the block `[data-block-id="name"]`), `--scroll <px[,px…]>` (the viewport after scrolling), `--no-freeze`, `--full`, `--data` (blocks), `--out`, `--server`, `--browser` |
|
|
365
369
|
| `mechanica thumbs [--blocks]` | Regenerate page or block thumbnails for the editor |
|
|
366
370
|
| `mechanica images` | Generate image dimensions and blur-up previews for `.mech/assets` (needs `sharp`; `--force` redoes all) |
|
|
367
371
|
| `mechanica login` | Sign in to the Mechanica platform: it prints a link to open and approve in the browser (signing in or registering on the way) and keeps the access token for this machine. In a terminal it waits for the approval; run from a coding agent, it prints the link and returns, and the next `link` or `push` finishes the sign-in (`--wait`, `--no-wait`, `--host`, or `--token` to store a token made in the dashboard) |
|
|
@@ -399,7 +403,7 @@ From `mechanica`:
|
|
|
399
403
|
|
|
400
404
|
From `mechanica/plugin`: `mechanica` and the `PageProvider` type. From `mechanica/widgets`: `defineWidget` and `useWidgetServices` (uploads and the image picker inside widget editors). `mechanica/editor` and `mechanica/composer` are loaded by the plugin in development; you don't import them. From `mechanica/export`: `exportBuilt(projectDir, options)`, which statically renders an already built project (`dist/` + `.mech/`) without running a build and resolves to `{ pages, warnings }` — for render services that receive a prebuilt bundle; `options.outDir` sets where the site is written, and `options.fetchJson` replaces how `useFetch` requests are made.
|
|
401
405
|
|
|
402
|
-
From `mechanica/browser`: the headless browser behind `mechanica shot`, for your own Node 22+ or Bun scripts. It needs no dev server and no Vite. `openPage(url, { width, height, browser })` launches a local Chrome or Edge and resolves to a page with `evaluate(expression)`, `setViewport(width, height)`, `scrollTo(y)`, `settle()` (fonts and images), `freezeAnimations()`, `sectionClip(selector)
|
|
406
|
+
From `mechanica/browser`: the headless browser behind `mechanica shot`, for your own Node 22+ or Bun scripts. It needs no dev server and no Vite. `openPage(url, { width, height, browser })` launches a local Chrome or Edge and resolves to a page with `evaluate(expression)`, `setViewport(width, height)`, `scrollTo(y)`, `settle()` (fonts and images), `freezeAnimations()`, `sectionClip(selector)` (document, then open shadow roots; `#name` falls back to `[data-block-id="name"]`), `screenshot({ clip, section, full, scale })` (a PNG `Buffer`; with no options, what is in the viewport), `goto(url)`, `messages` (console errors and page errors) and `close()`. `findDevServer()` returns the origin of a running dev server, or `null`.
|
|
403
407
|
|
|
404
408
|
```js
|
|
405
409
|
import { openPage } from 'mechanica/browser'
|
package/dist/browser.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as openPage, n as PAGE_LOADED, o as MAX_SHOT_HEIGHT, r as describeSectionError, s as findDevServer, t as NavigationError } from "./chunks/browser-BV2iB_xD.js";
|
|
2
2
|
export { MAX_SHOT_HEIGHT, NavigationError, PAGE_LOADED, describeSectionError, findDevServer, openPage };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, win32 } from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { execFile, spawn } from "node:child_process";
|
|
5
5
|
//#region src/cli/headless.ts
|
|
6
6
|
/**
|
|
7
7
|
* Headless-browser plumbing shared by `mechanica shot`, `mechanica thumbs` and
|
|
@@ -133,13 +133,56 @@ async function viewportClip(cdp, sessionId) {
|
|
|
133
133
|
height: viewport.clientHeight
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
|
+
/** How long a spawned browser may take to print its DevTools endpoint. */
|
|
137
|
+
var STARTUP_TIMEOUT = 2e4;
|
|
136
138
|
/**
|
|
137
139
|
* Spawn a headless Chromium-based browser with a DevTools endpoint and connect
|
|
138
|
-
* to it.
|
|
139
|
-
*
|
|
140
|
+
* to it. With `explicitPath` (`--browser`) only that executable is tried.
|
|
141
|
+
* Otherwise the installed browsers of {@link platformBrowserCandidates} are
|
|
142
|
+
* tried in order until one starts — a browser already on the machine, so
|
|
143
|
+
* nothing needs downloading — and the error names each one that failed.
|
|
140
144
|
*/
|
|
141
145
|
async function launchBrowser(explicitPath) {
|
|
142
|
-
|
|
146
|
+
if (explicitPath) {
|
|
147
|
+
if (!fs.existsSync(explicitPath)) throw new Error(`--browser not found: ${explicitPath}`);
|
|
148
|
+
const executable = {
|
|
149
|
+
label: "browser from --browser",
|
|
150
|
+
path: explicitPath
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
return await startBrowser(executable);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new Error(`Failed to start ${executable.label} (${executable.path}): ${errorMessage(error)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const candidates = platformBrowserCandidates();
|
|
159
|
+
const installed = candidates.filter((candidate, index) => candidates.findIndex((other) => other.path === candidate.path) === index && fs.existsSync(candidate.path));
|
|
160
|
+
if (!installed.length) {
|
|
161
|
+
const looked = candidates.map((c) => ` ${c.label}: ${c.path}`).join("\n");
|
|
162
|
+
throw new Error(`No Chromium-based browser found. Looked for:\n${looked}\nInstall Chrome or Edge, or pass --browser <path-to-executable>.`);
|
|
163
|
+
}
|
|
164
|
+
return startFirstBrowser(installed, startBrowser);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Try `start` on each candidate in order and return the first browser that
|
|
168
|
+
* comes up. When none does, the error lists every candidate and why it failed.
|
|
169
|
+
*/
|
|
170
|
+
async function startFirstBrowser(candidates, start) {
|
|
171
|
+
const failures = [];
|
|
172
|
+
for (const candidate of candidates) try {
|
|
173
|
+
return await start(candidate);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
failures.push(` ${candidate.label} (${candidate.path}): ${errorMessage(error)}`);
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`No browser could be started. Tried:\n${failures.join("\n")}\nPass --browser <path-to-executable> to use another Chromium-based browser.`);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Spawn one browser on a fresh temporary profile and connect to it. When it
|
|
181
|
+
* does not come up (exits early, never prints its endpoint, refuses the
|
|
182
|
+
* connection), everything it started is ended and the profile removed before
|
|
183
|
+
* the error is rethrown.
|
|
184
|
+
*/
|
|
185
|
+
async function startBrowser(executable) {
|
|
143
186
|
const userDataDir = fs.mkdtempSync(join(os.tmpdir(), "mechanica-shot-"));
|
|
144
187
|
const child = spawn(executable.path, [
|
|
145
188
|
"--headless=new",
|
|
@@ -150,55 +193,128 @@ async function launchBrowser(explicitPath) {
|
|
|
150
193
|
"--disable-extensions",
|
|
151
194
|
"--hide-scrollbars",
|
|
152
195
|
"about:blank"
|
|
153
|
-
], {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
setTimeout(resolve, 3e3);
|
|
163
|
-
});
|
|
164
|
-
child.kill();
|
|
165
|
-
await exited;
|
|
166
|
-
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
167
|
-
fs.rmSync(userDataDir, {
|
|
168
|
-
recursive: true,
|
|
169
|
-
force: true
|
|
170
|
-
});
|
|
171
|
-
return;
|
|
172
|
-
} catch {
|
|
173
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
let endpoint;
|
|
196
|
+
], {
|
|
197
|
+
stdio: [
|
|
198
|
+
"ignore",
|
|
199
|
+
"ignore",
|
|
200
|
+
"pipe"
|
|
201
|
+
],
|
|
202
|
+
env: browserEnv()
|
|
203
|
+
});
|
|
204
|
+
let client;
|
|
177
205
|
try {
|
|
178
|
-
endpoint = await waitForDevtoolsEndpoint(child,
|
|
206
|
+
const endpoint = await waitForDevtoolsEndpoint(child, STARTUP_TIMEOUT);
|
|
207
|
+
client = await CdpClient.connect(endpoint);
|
|
179
208
|
} catch (error) {
|
|
180
|
-
await
|
|
181
|
-
|
|
209
|
+
await killBrowserTree(child, userDataDir);
|
|
210
|
+
await removeProfile(userDataDir);
|
|
211
|
+
throw error;
|
|
182
212
|
}
|
|
183
|
-
const client = await CdpClient.connect(endpoint);
|
|
184
213
|
return {
|
|
185
214
|
client,
|
|
186
215
|
close: async () => {
|
|
187
216
|
await client.send("Browser.close").catch(() => {});
|
|
188
217
|
client.dispose();
|
|
189
|
-
await
|
|
218
|
+
await stopProcess(child);
|
|
219
|
+
await removeProfile(userDataDir);
|
|
190
220
|
}
|
|
191
221
|
};
|
|
192
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* The environment a browser starts with. On Windows it leaves out
|
|
225
|
+
* `__COMPAT_LAYER` (Git Bash and some tool runners set it to `RunAsInvoker`):
|
|
226
|
+
* under a compatibility layer Edge relaunches itself without it and exits at
|
|
227
|
+
* once, so the process we spawned never prints its endpoint while the
|
|
228
|
+
* relaunched one keeps running.
|
|
229
|
+
*/
|
|
230
|
+
function browserEnv(env = process.env, platform = process.platform) {
|
|
231
|
+
if (platform !== "win32") return env;
|
|
232
|
+
const result = { ...env };
|
|
233
|
+
for (const key of Object.keys(result)) if (key.toUpperCase() === "__COMPAT_LAYER") delete result[key];
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The Windows commands that end a browser which failed to start, best effort:
|
|
238
|
+
* `taskkill /T /F` on its process tree while the spawned process still runs
|
|
239
|
+
* (`pid`), then — because a browser that relaunched itself has outlived the
|
|
240
|
+
* process we spawned — every process still running on its temporary profile,
|
|
241
|
+
* found by the profile's folder name in the command line.
|
|
242
|
+
*/
|
|
243
|
+
function windowsKillCommands(pid, userDataDir) {
|
|
244
|
+
const commands = [];
|
|
245
|
+
if (pid) commands.push(["taskkill.exe", [
|
|
246
|
+
"/T",
|
|
247
|
+
"/F",
|
|
248
|
+
"/PID",
|
|
249
|
+
String(pid)
|
|
250
|
+
]]);
|
|
251
|
+
const profile = win32.basename(userDataDir);
|
|
252
|
+
if (/^[\w.-]+$/.test(profile)) {
|
|
253
|
+
const script = `Get-CimInstance Win32_Process -Filter "CommandLine LIKE '%${profile}%'" | Where-Object { \$_.ProcessId -ne \$PID } | ForEach-Object { Stop-Process -Id \$_.ProcessId -Force -ErrorAction SilentlyContinue }`;
|
|
254
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
255
|
+
commands.push(["powershell.exe", [
|
|
256
|
+
"-NoProfile",
|
|
257
|
+
"-NonInteractive",
|
|
258
|
+
"-EncodedCommand",
|
|
259
|
+
encoded
|
|
260
|
+
]]);
|
|
261
|
+
}
|
|
262
|
+
return commands;
|
|
263
|
+
}
|
|
264
|
+
/** End a browser that failed to start, with every process it spawned (best effort). */
|
|
265
|
+
async function killBrowserTree(child, userDataDir) {
|
|
266
|
+
if (process.platform === "win32") for (const [command, args] of windowsKillCommands(isRunning(child) ? child.pid : void 0, userDataDir)) await runQuietly(command, args);
|
|
267
|
+
await stopProcess(child);
|
|
268
|
+
}
|
|
269
|
+
function isRunning(child) {
|
|
270
|
+
return child.exitCode === null && child.signalCode === null;
|
|
271
|
+
}
|
|
272
|
+
/** Kill the spawned process (if it still runs) and wait up to 3 s for it to exit. */
|
|
273
|
+
async function stopProcess(child) {
|
|
274
|
+
if (!isRunning(child)) return;
|
|
275
|
+
const exited = new Promise((resolve) => {
|
|
276
|
+
child.once("exit", () => resolve());
|
|
277
|
+
setTimeout(resolve, 3e3);
|
|
278
|
+
});
|
|
279
|
+
child.kill();
|
|
280
|
+
await exited;
|
|
281
|
+
}
|
|
282
|
+
/** Remove a temporary profile; it can stay locked for a moment after exit on Windows. */
|
|
283
|
+
async function removeProfile(userDataDir) {
|
|
284
|
+
for (let attempt = 0; attempt < 5; attempt++) try {
|
|
285
|
+
fs.rmSync(userDataDir, {
|
|
286
|
+
recursive: true,
|
|
287
|
+
force: true
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
} catch {
|
|
291
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** Run a command, ignoring its output and any failure. */
|
|
295
|
+
function runQuietly(command, args) {
|
|
296
|
+
return new Promise((resolve) => {
|
|
297
|
+
execFile(command, args, {
|
|
298
|
+
timeout: 15e3,
|
|
299
|
+
windowsHide: true
|
|
300
|
+
}, () => resolve());
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function errorMessage(error) {
|
|
304
|
+
return error instanceof Error ? error.message : String(error);
|
|
305
|
+
}
|
|
193
306
|
/** Read the spawned browser's stderr until it prints its DevTools WebSocket URL. */
|
|
194
307
|
function waitForDevtoolsEndpoint(child, timeout) {
|
|
195
308
|
return new Promise((resolve, reject) => {
|
|
196
309
|
let buffer = "";
|
|
197
310
|
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`no DevTools endpoint within ${timeout}ms`)), timeout);
|
|
311
|
+
let found = false;
|
|
198
312
|
child.stderr.on("data", (chunk) => {
|
|
313
|
+
if (found) return;
|
|
199
314
|
buffer += chunk.toString();
|
|
200
315
|
const match = buffer.match(/DevTools listening on (ws:\/\/\S+)/);
|
|
201
316
|
if (match) {
|
|
317
|
+
found = true;
|
|
202
318
|
clearTimeout(timer);
|
|
203
319
|
resolve(match[1]);
|
|
204
320
|
}
|
|
@@ -207,54 +323,60 @@ function waitForDevtoolsEndpoint(child, timeout) {
|
|
|
207
323
|
clearTimeout(timer);
|
|
208
324
|
reject(error);
|
|
209
325
|
});
|
|
210
|
-
child.on("exit", (code) => {
|
|
326
|
+
child.on("exit", (code, signal) => {
|
|
211
327
|
clearTimeout(timer);
|
|
212
|
-
|
|
328
|
+
const said = buffer.trim().split(/\r?\n/).pop()?.trim().slice(0, 200);
|
|
329
|
+
reject(/* @__PURE__ */ new Error(`browser exited early (${code !== null ? `code ${code}` : signal})${said ? `: ${said}` : ""}`));
|
|
213
330
|
});
|
|
214
331
|
});
|
|
215
332
|
}
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Known install locations of Chromium-based browsers, in the order they are
|
|
335
|
+
* tried. On Windows Chrome comes first: Edge ships with Windows, so it is the
|
|
336
|
+
* fallback, and it relaunches itself under a compatibility layer (see
|
|
337
|
+
* {@link browserEnv}).
|
|
338
|
+
*/
|
|
339
|
+
function platformBrowserCandidates(platform = process.platform, env = process.env, home = os.homedir()) {
|
|
340
|
+
if (platform === "win32") {
|
|
341
|
+
const programFiles = env["ProgramFiles"] ?? "C:\\Program Files";
|
|
342
|
+
const programFilesX86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
343
|
+
const localAppData = env["LOCALAPPDATA"] ?? win32.join(home, "AppData", "Local");
|
|
344
|
+
const chrome = [
|
|
345
|
+
"Google",
|
|
346
|
+
"Chrome",
|
|
347
|
+
"Application",
|
|
348
|
+
"chrome.exe"
|
|
349
|
+
];
|
|
350
|
+
const edge = [
|
|
351
|
+
"Microsoft",
|
|
352
|
+
"Edge",
|
|
353
|
+
"Application",
|
|
354
|
+
"msedge.exe"
|
|
355
|
+
];
|
|
238
356
|
return [
|
|
239
357
|
{
|
|
240
|
-
label: "
|
|
241
|
-
path: join(
|
|
358
|
+
label: "Google Chrome",
|
|
359
|
+
path: win32.join(programFiles, ...chrome)
|
|
242
360
|
},
|
|
243
361
|
{
|
|
244
|
-
label: "
|
|
245
|
-
path: join(
|
|
362
|
+
label: "Google Chrome",
|
|
363
|
+
path: win32.join(programFilesX86, ...chrome)
|
|
246
364
|
},
|
|
247
365
|
{
|
|
248
366
|
label: "Google Chrome",
|
|
249
|
-
path: join(
|
|
367
|
+
path: win32.join(localAppData, ...chrome)
|
|
250
368
|
},
|
|
251
369
|
{
|
|
252
|
-
label: "
|
|
253
|
-
path: join(
|
|
370
|
+
label: "Microsoft Edge",
|
|
371
|
+
path: win32.join(programFilesX86, ...edge)
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
label: "Microsoft Edge",
|
|
375
|
+
path: win32.join(programFiles, ...edge)
|
|
254
376
|
}
|
|
255
377
|
];
|
|
256
378
|
}
|
|
257
|
-
if (
|
|
379
|
+
if (platform === "darwin") return [
|
|
258
380
|
{
|
|
259
381
|
label: "Google Chrome",
|
|
260
382
|
path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
@@ -353,6 +475,138 @@ var CdpClient = class CdpClient {
|
|
|
353
475
|
if (typeof message.method === "string") for (const listener of this.listeners) listener(message.method, message.params ?? {}, message.sessionId);
|
|
354
476
|
}
|
|
355
477
|
};
|
|
478
|
+
/**
|
|
479
|
+
* Split a selector at `>>>` into trimmed parts (one part when there is none).
|
|
480
|
+
* A `>>>` inside quotes, brackets or parentheses, or after a backslash, is
|
|
481
|
+
* part of the selector. Throws when a part is empty (`>>> .a`, `.a >>>`).
|
|
482
|
+
*/
|
|
483
|
+
function splitShadowSelector(selector) {
|
|
484
|
+
const parts = [];
|
|
485
|
+
let start = 0;
|
|
486
|
+
let depth = 0;
|
|
487
|
+
let quote = "";
|
|
488
|
+
for (let i = 0; i < selector.length; i++) {
|
|
489
|
+
const char = selector[i];
|
|
490
|
+
if (char === "\\") {
|
|
491
|
+
i++;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (quote) {
|
|
495
|
+
if (char === quote) quote = "";
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (char === "\"" || char === "'") quote = char;
|
|
499
|
+
else if (char === "[" || char === "(") depth++;
|
|
500
|
+
else if ((char === "]" || char === ")") && depth > 0) depth--;
|
|
501
|
+
else if (depth === 0 && selector.startsWith(">>>", i)) {
|
|
502
|
+
parts.push(selector.slice(start, i).trim());
|
|
503
|
+
i += 2;
|
|
504
|
+
start = i + 1;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
parts.push(selector.slice(start).trim());
|
|
508
|
+
if (parts.some((part) => !part)) throw new Error(`empty selector around ">>>"`);
|
|
509
|
+
return parts;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* The block-id selector to try when a bare `#name` or `name` matches nothing:
|
|
513
|
+
* `[data-block-id="name"]`. Null for any other selector. A name that is not a
|
|
514
|
+
* valid CSS id (`#1hero`) still gets one, so block ids starting with a digit
|
|
515
|
+
* are reachable too.
|
|
516
|
+
*/
|
|
517
|
+
function blockIdFallback(selector) {
|
|
518
|
+
const match = /^#?([\p{L}\p{N}_-]+)$/u.exec(selector.trim());
|
|
519
|
+
return match ? `[data-block-id="${match[1]}"]` : null;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* In-page source of `(parts, fallback) => result`, where `parts` is a selector
|
|
523
|
+
* split by {@link splitShadowSelector} and `fallback` is
|
|
524
|
+
* {@link blockIdFallback}'s result (or null). It finds the first element
|
|
525
|
+
* matching `parts`, else the first matching `fallback`, in the document and
|
|
526
|
+
* in every open shadow root, and returns:
|
|
527
|
+
*
|
|
528
|
+
* - `{ element }`;
|
|
529
|
+
* - `{ error: 'invalid', message }` when the selector does not parse (and the
|
|
530
|
+
* fallback finds nothing);
|
|
531
|
+
* - `{ error: 'missing', ids, blockIds, fallback }`: the readable ids
|
|
532
|
+
* (`#name`) and the `data-block-id` values on the page, shadow roots
|
|
533
|
+
* included, for the error message.
|
|
534
|
+
*
|
|
535
|
+
* Plain JS with no outside references: it is sent to the page as text.
|
|
536
|
+
*/
|
|
537
|
+
var FIND_SECTION_SOURCE = String.raw`(parts, fallback) => {
|
|
538
|
+
// Every open shadow root under scope, depth first in tree order.
|
|
539
|
+
const shadowRoots = (scope) => {
|
|
540
|
+
const roots = []
|
|
541
|
+
const visit = (root) => {
|
|
542
|
+
for (const el of root.querySelectorAll('*')) {
|
|
543
|
+
if (el.shadowRoot) {
|
|
544
|
+
roots.push(el.shadowRoot)
|
|
545
|
+
visit(el.shadowRoot)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
visit(scope)
|
|
550
|
+
return roots
|
|
551
|
+
}
|
|
552
|
+
// The first match in scope's own tree, else in the shadow roots under it.
|
|
553
|
+
const first = (scope, selector) => {
|
|
554
|
+
const own = scope.querySelector(selector)
|
|
555
|
+
if (own) return own
|
|
556
|
+
for (const root of shadowRoots(scope)) {
|
|
557
|
+
const hit = root.querySelector(selector)
|
|
558
|
+
if (hit) return hit
|
|
559
|
+
}
|
|
560
|
+
return null
|
|
561
|
+
}
|
|
562
|
+
const all = (scope, selector) =>
|
|
563
|
+
[scope, ...shadowRoots(scope)].flatMap((root) => [...root.querySelectorAll(selector)])
|
|
564
|
+
const query = () => {
|
|
565
|
+
let scopes = [document]
|
|
566
|
+
for (const part of parts.slice(0, -1)) {
|
|
567
|
+
const hosts = new Set(scopes.flatMap((scope) => all(scope, part)))
|
|
568
|
+
scopes = [...hosts].map((host) => host.shadowRoot || host)
|
|
569
|
+
}
|
|
570
|
+
for (const scope of scopes) {
|
|
571
|
+
const hit = first(scope, parts[parts.length - 1])
|
|
572
|
+
if (hit) return hit
|
|
573
|
+
}
|
|
574
|
+
return null
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
let element = null
|
|
578
|
+
let invalid = null
|
|
579
|
+
try {
|
|
580
|
+
element = query()
|
|
581
|
+
} catch (error) {
|
|
582
|
+
invalid = String((error && error.message) || error)
|
|
583
|
+
}
|
|
584
|
+
if (!element && fallback) element = first(document, fallback)
|
|
585
|
+
if (element) return { element }
|
|
586
|
+
if (invalid) return { error: 'invalid', message: invalid }
|
|
587
|
+
|
|
588
|
+
// A hint for the next try: readable ids (not SVG internals or generated
|
|
589
|
+
// ids) and block ids, in the document and its shadow roots.
|
|
590
|
+
const ids = new Set()
|
|
591
|
+
const blockIds = new Set()
|
|
592
|
+
for (const root of [document, ...shadowRoots(document)]) {
|
|
593
|
+
for (const el of root.querySelectorAll(root === document ? 'body [id]' : '[id]')) {
|
|
594
|
+
const id = el.id
|
|
595
|
+
if (el.closest('svg') || !/^[A-Za-z][\w-]{0,39}$/.test(id) || /^w-node-|[0-9a-f]{8}/i.test(id)) continue
|
|
596
|
+
ids.add('#' + id)
|
|
597
|
+
}
|
|
598
|
+
for (const el of root.querySelectorAll('[data-block-id]')) {
|
|
599
|
+
const id = el.getAttribute('data-block-id')
|
|
600
|
+
if (id) blockIds.add(id)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
error: 'missing',
|
|
605
|
+
ids: [...ids].slice(0, ${20}),
|
|
606
|
+
blockIds: [...blockIds].slice(0, ${20}),
|
|
607
|
+
fallback,
|
|
608
|
+
}
|
|
609
|
+
}`;
|
|
356
610
|
//#endregion
|
|
357
611
|
//#region src/cli/browser.ts
|
|
358
612
|
/**
|
|
@@ -413,7 +667,7 @@ async function openPage(url, options = {}) {
|
|
|
413
667
|
function describeSectionError(selector, box) {
|
|
414
668
|
switch (box.error) {
|
|
415
669
|
case "invalid": return `"${selector}" is not a valid CSS selector (${box.message})`;
|
|
416
|
-
case "missing": return `No element matches "${selector}"` + (box.ids.length ? `. Ids on the page: ${box.ids.join(", ")}` : "");
|
|
670
|
+
case "missing": return `No element matches "${selector}"` + (box.fallback ? ` (nor ${box.fallback})` : "") + (box.ids.length ? `. Ids on the page: ${box.ids.join(", ")}` : "") + (box.blockIds?.length ? `. Block ids (data-block-id): ${box.blockIds.join(", ")}` : "");
|
|
417
671
|
case "empty": return `"${selector}" matched an element with an empty box (${Math.round(box.width)}×${Math.round(box.height)}) — is it hidden at this width?`;
|
|
418
672
|
}
|
|
419
673
|
}
|
|
@@ -501,25 +755,23 @@ var HeadlessPage = class HeadlessPage {
|
|
|
501
755
|
return unfreezeAnimations(this.browser.client, this.sessionId);
|
|
502
756
|
}
|
|
503
757
|
async sectionClip(selector) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
const rect = element.getBoundingClientRect()
|
|
758
|
+
let parts;
|
|
759
|
+
try {
|
|
760
|
+
parts = splitShadowSelector(selector);
|
|
761
|
+
} catch (error) {
|
|
762
|
+
throw new Error(describeSectionError(selector, {
|
|
763
|
+
error: "invalid",
|
|
764
|
+
message: error.message
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
const fallback = parts.length === 1 ? blockIdFallback(selector) : null;
|
|
768
|
+
const box = await this.evaluate(`((find, parts, fallback) => {
|
|
769
|
+
const found = find(parts, fallback)
|
|
770
|
+
if (!found.element) return found
|
|
771
|
+
const rect = found.element.getBoundingClientRect()
|
|
520
772
|
if (rect.width < 1 || rect.height < 1) return { error: 'empty', width: rect.width, height: rect.height }
|
|
521
773
|
return { x: rect.x + scrollX, y: rect.y + scrollY, width: rect.width, height: rect.height }
|
|
522
|
-
})(${JSON.stringify(
|
|
774
|
+
})(${FIND_SECTION_SOURCE}, ${JSON.stringify(parts)}, ${JSON.stringify(fallback)})`);
|
|
523
775
|
if ("error" in box) throw new Error(describeSectionError(selector, box));
|
|
524
776
|
return box;
|
|
525
777
|
}
|
|
@@ -555,4 +807,4 @@ var HeadlessPage = class HeadlessPage {
|
|
|
555
807
|
}
|
|
556
808
|
};
|
|
557
809
|
//#endregion
|
|
558
|
-
export {
|
|
810
|
+
export { splitShadowSelector as a, openPage as i, PAGE_LOADED as n, MAX_SHOT_HEIGHT as o, describeSectionError as r, findDevServer as s, NavigationError as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computed, defineComponent, h, inject, nextTick, shallowReactive, shallowRef } from "vue";
|
|
2
|
-
import { localePath, resolveComposedTemplate, templateBlockIds, walkTree } from "mechanica-shared";
|
|
2
|
+
import { localePath, paginationVariantPath, resolveComposedTemplate, templateBlockIds, walkTree } from "mechanica-shared";
|
|
3
3
|
//#region src/core/state.ts
|
|
4
4
|
/** Vue injection key for the {@link MechanicaContext}. */
|
|
5
5
|
var mechanicaKey = Symbol("mechanica");
|
|
@@ -36,7 +36,7 @@ function createRouter(content, data, options) {
|
|
|
36
36
|
if (!baseUrl) return path;
|
|
37
37
|
return path.startsWith(baseUrl) ? path.slice(baseUrl.length) : path;
|
|
38
38
|
};
|
|
39
|
-
const initialPath = isBrowser ? normalizePath(stripBase(window.location.pathname)) : "/";
|
|
39
|
+
const initialPath = isBrowser ? normalizePath(stripBase(window.location.pathname)) : normalizePath(options.initialPath ?? "/");
|
|
40
40
|
const currentRoute = shallowReactive({ path: initialPath });
|
|
41
41
|
const cache = /* @__PURE__ */ new Map();
|
|
42
42
|
const inflight = /* @__PURE__ */ new Map();
|
|
@@ -45,12 +45,14 @@ function createRouter(content, data, options) {
|
|
|
45
45
|
const existing = inflight.get(path);
|
|
46
46
|
if (existing) return existing;
|
|
47
47
|
const run = async () => {
|
|
48
|
-
const
|
|
48
|
+
const res = await fetch(path);
|
|
49
|
+
const html = res.ok ? await res.text() : "";
|
|
49
50
|
const match = html.match(/window\.state\s*=\s*(\{.+?\})<\/script>/s)?.[1];
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
51
|
+
if (!match) {
|
|
52
|
+
if (isBrowser) window.location.assign(path);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const state = JSON.parse(match);
|
|
54
56
|
state.page = {
|
|
55
57
|
...state.page,
|
|
56
58
|
title: html.match(/<title>(.*?)<\/title>/)?.[1]
|
|
@@ -869,6 +871,25 @@ function runtimeLayoutNames() {
|
|
|
869
871
|
return window[RUNTIME_KEY]?.layoutNames ?? [];
|
|
870
872
|
}
|
|
871
873
|
//#endregion
|
|
874
|
+
//#region src/core/page-path.ts
|
|
875
|
+
/**
|
|
876
|
+
* The address a page's state is served at: its logical `path` under the
|
|
877
|
+
* locale prefix (`/about` -> `/ru/about`) plus the pagination variant suffix
|
|
878
|
+
* (`/blog` -> `/blog/2`). This is what the browser shows in the address bar for
|
|
879
|
+
* that state, so the server-side router starts there and `<Link>` resolves the
|
|
880
|
+
* same `is-active` on both sides of hydration.
|
|
881
|
+
*
|
|
882
|
+
* The export already serves a variant under its own path (`page.path` is
|
|
883
|
+
* `/blog/2` there, while dev keeps the base `/blog`); a suffix that is
|
|
884
|
+
* already present is kept, as `usePagination().pathFor` does.
|
|
885
|
+
*/
|
|
886
|
+
function servedPagePath(page, locales) {
|
|
887
|
+
const localized = localePath(page?.path || "/", page?.locale, locales);
|
|
888
|
+
const variant = page?.pagination?.page ?? 1;
|
|
889
|
+
if (variant > 1 && localized.endsWith(`/${variant}`)) return localized;
|
|
890
|
+
return paginationVariantPath(localized, variant);
|
|
891
|
+
}
|
|
892
|
+
//#endregion
|
|
872
893
|
//#region src/core/create-mechanica.ts
|
|
873
894
|
/**
|
|
874
895
|
* Vue plugin that wires the Mechanica runtime: page content, shared data, the
|
|
@@ -903,6 +924,7 @@ function createMechanica(options = {}) {
|
|
|
903
924
|
router: createRouter(content, data, {
|
|
904
925
|
mode,
|
|
905
926
|
baseUrl: initial?.baseUrl,
|
|
927
|
+
initialPath: servedPagePath(initial?.page, initial?.locales),
|
|
906
928
|
ensureBlocks: loaders ? async (next) => {
|
|
907
929
|
await loadBlocks(loaders, next, blocks, composedMap);
|
|
908
930
|
} : void 0,
|
|
@@ -927,7 +949,7 @@ function createMechanica(options = {}) {
|
|
|
927
949
|
mergeData: (incoming) => mergeData(context.data, incoming),
|
|
928
950
|
setPage: (next) => {
|
|
929
951
|
applyPage(next);
|
|
930
|
-
if (next.path) context.router.currentRoute.path = context.router.normalizePath(next
|
|
952
|
+
if (next.path) context.router.currentRoute.path = context.router.normalizePath(servedPagePath(next, initial?.locales));
|
|
931
953
|
}
|
|
932
954
|
});
|
|
933
955
|
} };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as renderBlocks, t as createMechanica } from "./create-mechanica-
|
|
1
|
+
import { a as renderBlocks, t as createMechanica } from "./create-mechanica-CMcmeOtG.js";
|
|
2
2
|
import { createApp, defineComponent, h } from "vue";
|
|
3
3
|
import { areFieldSchemasRegistered, buildPreviewData, registerFieldSchemas } from "mechanica-shared";
|
|
4
4
|
import { unfoldSchema } from "compact-json-schema";
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ import { t as buildSiteManifest } from "./chunks/site-BVqx8iKj.js";
|
|
|
2
2
|
import { J as readImageManifest, K as isDerivedAsset, W as fsAssetStore, X as updateImageManifest, et as withRemoteAssets, it as isRasterImage, nt as analyzeImageBuffer, rt as hasSharp } from "./chunks/rich-text-codec-DCv1X0eZ.js";
|
|
3
3
|
import { _ as request, a as git, c as clearPendingLogin, d as hostOrigin, f as savePendingLogin, g as parseRemoteUrl, h as REMOTE_NAME, i as uploadsIgnored, l as getPendingLogin, m as PlatformError, n as remoteAssetsFor, o as gitAuthEnv, p as saveToken, r as runAssetsPull, s as gitOk, t as pushUploads, u as getToken } from "./chunks/assets-5a2WdmFg.js";
|
|
4
4
|
import { exportBuilt } from "./export.js";
|
|
5
|
-
import { i as openPage,
|
|
5
|
+
import { a as splitShadowSelector, i as openPage, s as findDevServer, t as NavigationError } from "./chunks/browser-BV2iB_xD.js";
|
|
6
6
|
import fs, { readFileSync } from "node:fs";
|
|
7
7
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
8
8
|
import { EDITOR_DIST_DIR, LEGACY_UPLOADS_PREFIX, SITE_MANIFEST_FILE, UPLOADS_PREFIX } from "mechanica-shared";
|
|
@@ -679,7 +679,7 @@ async function resolveServer(flag) {
|
|
|
679
679
|
}
|
|
680
680
|
//#endregion
|
|
681
681
|
//#region src/cli/shot.ts
|
|
682
|
-
var USAGE = "Usage: mechanica shot <blockId | /page/path | URL | file.html> [--page </path>] [--data <json|@file>] [--width 1440,768] [--section <css>]… [--scroll <px[,px…]>] [--no-freeze] [--full] [--out <path>] [--server <url>] [--browser <path>]";
|
|
682
|
+
var USAGE = "Usage: mechanica shot <blockId | /page/path | URL | file.html> [--page </path>] [--data <json|@file>] [--width 1440,768] [--section <css>]… [--scroll <px[,px…]>] [--no-freeze] [--full] [--out <path>] [--server <url>] [--browser <path>]\n--section also looks inside open shadow roots (\"host >>> inner\" crosses one explicitly); a bare #name (or name) that matches nothing finds the Mechanica block [data-block-id=\"name\"].";
|
|
683
683
|
/** Readiness flag of the standalone block preview route. */
|
|
684
684
|
var PREVIEW_READY = "window.__MECHANICA_PREVIEW_READY__ === true";
|
|
685
685
|
/** Parse a `--width` value into a list of positive integers. Defaults to `[1440]`. */
|
|
@@ -863,6 +863,11 @@ function planShot(target, options = {}, cwd = process.cwd()) {
|
|
|
863
863
|
const selectors = options.section ?? [];
|
|
864
864
|
if (selectors.length && isBlock) throw new Error("--section applies to page, URL and file shots — a block shot is already clipped to its block");
|
|
865
865
|
if (selectors.some((selector) => !selector.trim())) throw new Error("--section needs a CSS selector, e.g. --section \"#hero\"");
|
|
866
|
+
for (const selector of selectors) try {
|
|
867
|
+
splitShadowSelector(selector);
|
|
868
|
+
} catch (error) {
|
|
869
|
+
throw new Error(`--section "${selector}": ${error instanceof Error ? error.message : error}`);
|
|
870
|
+
}
|
|
866
871
|
const slugs = sectionSlugs(selectors);
|
|
867
872
|
const scrolls = parseScrolls(options.scroll);
|
|
868
873
|
return {
|
|
@@ -1193,7 +1198,9 @@ Usage:
|
|
|
1193
1198
|
Screenshot any URL or local HTML file (a design
|
|
1194
1199
|
handoff) the same way, without a dev server
|
|
1195
1200
|
(--page </path>, --data <json|@file>, --width 1440,768,
|
|
1196
|
-
--section <css> (repeatable: one PNG per section
|
|
1201
|
+
--section <css> (repeatable: one PNG per section;
|
|
1202
|
+
looks inside open shadow roots, "host >>> inner"
|
|
1203
|
+
crosses one; #name also finds [data-block-id=name]),
|
|
1197
1204
|
--scroll <px[,px…]> (viewport after scrolling),
|
|
1198
1205
|
--no-freeze, --full, --out <path>, --server <url>,
|
|
1199
1206
|
--browser <path>)
|
package/dist/composer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import './editor.css';
|
|
2
|
-
import { a as renderBlocks, l as ANCHOR_H, t as createMechanica, u as ANCHOR_V } from "./chunks/create-mechanica-
|
|
2
|
+
import { a as renderBlocks, l as ANCHOR_H, t as createMechanica, u as ANCHOR_V } from "./chunks/create-mechanica-CMcmeOtG.js";
|
|
3
3
|
import { A as SmartLinkField_default, C as removeBlock, I as editorBackend, L as createSaveQueue, R as VSelect_default, S as relocateBlock, T as registerBuiltinFieldEditors, _ as findBlock, a as FieldControl_default, b as moveBlock, f as toBlockMeta, g as ensureSlotList, h as duplicateBlock, i as SchemaField_default, m as cloneBlock, n as normalizeTemplate, o as shortcutChar, p as humanize, v as findParentList, w as uid, x as placeBlock, y as findParentSlot, z as VPopover_default } from "./chunks/editor-DoYHG0mS.js";
|
|
4
4
|
import { d as _plugin_vue_export_helper_default, l as VIcon_default, u as contents } from "./chunks/ImagePickerDialog-DJGhtW3r.js";
|
|
5
5
|
import { Fragment, Teleport, computed, createApp, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, onScopeDispose, openBlock, provide, reactive, ref, renderList, renderSlot, resolveComponent, toDisplayString, unref, vModelText, watch, withCtx, withDirectives, withKeys, withModifiers } from "vue";
|
package/dist/editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import './editor.css';
|
|
2
|
-
import { _ as mechanicaKey, a as renderBlocks, c as registerElements, i as createComposedComponent, m as createRouter, n as pushStateUpdate, r as runtimeLayoutNames } from "./chunks/create-mechanica-
|
|
3
|
-
import { i as getDataEntries, t as buildPreviewContent } from "./chunks/preview-
|
|
2
|
+
import { _ as mechanicaKey, a as renderBlocks, c as registerElements, i as createComposedComponent, m as createRouter, n as pushStateUpdate, r as runtimeLayoutNames } from "./chunks/create-mechanica-CMcmeOtG.js";
|
|
3
|
+
import { i as getDataEntries, t as buildPreviewContent } from "./chunks/preview-BrtQ0KpO.js";
|
|
4
4
|
import { C as removeBlock, D as contextMenuKey, E as registerRichTextWidgets, F as pathFromName, I as editorBackend, L as createSaveQueue, M as filterPages, N as groupPagesByFolder, O as createContextMenu, P as pageThumbUrl, R as VSelect_default, S as relocateBlock, T as registerBuiltinFieldEditors, _ as findBlock, b as moveBlock, c as blockAvailableIn, d as isUnderFolder, f as toBlockMeta, h as duplicateBlock, j as fetchPages, k as VSegmented_default, l as compareBlocks, m as cloneBlock, o as shortcutChar, p as humanize, r as SchemaForm_default, s as blockAvailableForLayout, t as createRootFrame, u as createContentBlock, w as uid, x as placeBlock, y as findParentSlot, z as VPopover_default } from "./chunks/editor-DoYHG0mS.js";
|
|
5
5
|
import { c as useDialog, d as _plugin_vue_export_helper_default, l as VIcon_default, n as VDialog_default, o as createDialogStore, s as dialogKey } from "./chunks/ImagePickerDialog-DJGhtW3r.js";
|
|
6
6
|
import { Fragment, Teleport, Transition, TransitionGroup, computed, createApp, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, markRaw, mergeProps, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onErrorCaptured, onMounted, onScopeDispose, openBlock, provide, reactive, ref, renderList, resolveComponent, resolveDynamicComponent, shallowRef, toDisplayString, unref, useTemplateRef, vModelCheckbox, vModelText, watch, withCtx, withDirectives, withModifiers } from "vue";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as mechanicaKey, a as renderBlocks, c as registerElements, d as Link, f as loadBlocks, g as useRouter, h as useRoute, i as createComposedComponent, m as createRouter, o as elements, p as usedBlockIds, s as isElementBlock, t as createMechanica } from "./chunks/create-mechanica-
|
|
2
|
-
import { a as registerDataEntry, i as getDataEntries, n as mountPreviewApp, r as clearDataEntries, t as buildPreviewContent } from "./chunks/preview-
|
|
1
|
+
import { _ as mechanicaKey, a as renderBlocks, c as registerElements, d as Link, f as loadBlocks, g as useRouter, h as useRoute, i as createComposedComponent, m as createRouter, o as elements, p as usedBlockIds, s as isElementBlock, t as createMechanica } from "./chunks/create-mechanica-CMcmeOtG.js";
|
|
2
|
+
import { a as registerDataEntry, i as getDataEntries, n as mountPreviewApp, r as clearDataEntries, t as buildPreviewContent } from "./chunks/preview-BrtQ0KpO.js";
|
|
3
3
|
import { computed, createApp, createSSRApp, defineComponent, h, inject, isReactive, onMounted, onServerPrefetch, reactive, ref, shallowReactive } from "vue";
|
|
4
4
|
import { localePath, normalizeClassManifest, passDefaultValue } from "mechanica-shared";
|
|
5
5
|
import { unfoldSchema } from "compact-json-schema";
|
|
@@ -205,13 +205,13 @@ var Image = defineComponent({
|
|
|
205
205
|
const cropped = !!image.croppedSrc;
|
|
206
206
|
return h("img", {
|
|
207
207
|
ref: el,
|
|
208
|
+
loading: props.eager ? "eager" : "lazy",
|
|
209
|
+
fetchpriority: props.eager ? "high" : void 0,
|
|
210
|
+
decoding: "async",
|
|
208
211
|
src: cropped ? image.croppedSrc : image.src,
|
|
209
212
|
alt: image.alt ?? "",
|
|
210
213
|
width: (cropped ? image.croppedWidth : image.width) ?? void 0,
|
|
211
214
|
height: (cropped ? image.croppedHeight : image.height) ?? void 0,
|
|
212
|
-
loading: props.eager ? "eager" : "lazy",
|
|
213
|
-
fetchpriority: props.eager ? "high" : void 0,
|
|
214
|
-
decoding: "async",
|
|
215
215
|
style: lqipStyle.value,
|
|
216
216
|
onLoad: () => {
|
|
217
217
|
loaded.value = true;
|
package/dist/plugin.js
CHANGED
|
@@ -660,6 +660,7 @@ function generateComponentsModule(manifestSpecifier) {
|
|
|
660
660
|
* `createApp().mount()` with an explicit, generated entry.
|
|
661
661
|
*/
|
|
662
662
|
function generateClientEntry(options) {
|
|
663
|
+
const hydrate = options.hydrate ? "ssr: true, " : "";
|
|
663
664
|
if (options.lazy) return [
|
|
664
665
|
`import 'virtual:mechanica/elements.css'`,
|
|
665
666
|
`import definition from ${JSON.stringify(options.userEntry)}`,
|
|
@@ -672,7 +673,7 @@ function generateClientEntry(options) {
|
|
|
672
673
|
`const composedMap = new Map(composedList.map((def) => [def.id, def]))`,
|
|
673
674
|
`loadBlocks(blockLoaders, state.content ?? [], undefined, composedMap).then((blocks) => {`,
|
|
674
675
|
` registerComponents(blocks)`,
|
|
675
|
-
` createMechanicaApp(definition, { mode: ${JSON.stringify(options.mode)}, state, blocks, blockLoaders, composed: composedList })`,
|
|
676
|
+
` createMechanicaApp(definition, { ${hydrate}mode: ${JSON.stringify(options.mode)}, state, blocks, blockLoaders, composed: composedList })`,
|
|
676
677
|
` .mount(${JSON.stringify(options.mount)})`,
|
|
677
678
|
`})`,
|
|
678
679
|
``
|
|
@@ -697,7 +698,7 @@ function generateClientEntry(options) {
|
|
|
697
698
|
`const state = window.state ?? { content: [], data: {} }`,
|
|
698
699
|
`registerComponents(blocksMap)`,
|
|
699
700
|
...resolveQuery,
|
|
700
|
-
`createMechanicaApp(definition, { mode: ${JSON.stringify(options.mode)}, state, blocks: blocksMap, composed: composedList${options.mode === "dev" ? ", resolveQuery" : ""} })`,
|
|
701
|
+
`createMechanicaApp(definition, { ${hydrate}mode: ${JSON.stringify(options.mode)}, state, blocks: blocksMap, composed: composedList${options.mode === "dev" ? ", resolveQuery" : ""} })`,
|
|
701
702
|
` .mount(${JSON.stringify(options.mount)})`,
|
|
702
703
|
``
|
|
703
704
|
].join("\n");
|
|
@@ -1118,18 +1119,22 @@ function mechanica(options = {}) {
|
|
|
1118
1119
|
config(_config, env) {
|
|
1119
1120
|
if (env.command !== "build" || env.isSsrBuild) return;
|
|
1120
1121
|
if (process.env.MECHANICA_EDITOR_BUILD) return;
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1122
|
+
const label = supportsGroupDebugName(this.meta?.rolldownVersion) ? { debugName: "mechanica:blocks" } : {};
|
|
1123
|
+
return {
|
|
1124
|
+
...process.env.MECHANICA_HYDRATION_DEBUG ? { define: { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "true" } } : {},
|
|
1125
|
+
build: { rollupOptions: { output: { codeSplitting: {
|
|
1126
|
+
includeDependenciesRecursively: false,
|
|
1127
|
+
groups: [{
|
|
1128
|
+
...label,
|
|
1129
|
+
name: (id, ctx) => {
|
|
1130
|
+
const own = blockChunkNames.get(slash(id.split("?")[0]));
|
|
1131
|
+
if (own !== void 0) return groupName(own);
|
|
1132
|
+
const g = sharedGroupOf(id, ctx, /* @__PURE__ */ new Set());
|
|
1133
|
+
return g === "skip" ? null : g;
|
|
1134
|
+
}
|
|
1135
|
+
}]
|
|
1136
|
+
} } } }
|
|
1137
|
+
};
|
|
1133
1138
|
},
|
|
1134
1139
|
configResolved(config) {
|
|
1135
1140
|
blocksDir = join(config.root, options.blocksDir ?? "src/blocks");
|
|
@@ -1196,7 +1201,8 @@ function mechanica(options = {}) {
|
|
|
1196
1201
|
userEntry,
|
|
1197
1202
|
mount,
|
|
1198
1203
|
mode: isDev || isEditorBuild ? "dev" : "client",
|
|
1199
|
-
lazy: isClientBuild
|
|
1204
|
+
lazy: isClientBuild,
|
|
1205
|
+
hydrate: isClientBuild
|
|
1200
1206
|
});
|
|
1201
1207
|
if (id === RESOLVED_SSR_ID) return generateSsrEntry({
|
|
1202
1208
|
userEntry,
|
|
@@ -25,7 +25,10 @@ export interface OpenPageOptions extends GotoOptions {
|
|
|
25
25
|
width?: number;
|
|
26
26
|
/** Viewport height in CSS px. Default 900. */
|
|
27
27
|
height?: number;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* A Chromium-based executable. Default: the first installed browser that
|
|
30
|
+
* starts, from the usual install paths (on Windows Chrome, then Edge).
|
|
31
|
+
*/
|
|
29
32
|
browser?: string;
|
|
30
33
|
/**
|
|
31
34
|
* Emulate `prefers-reduced-motion: reduce`, as `mechanica shot` does: pages
|
|
@@ -36,7 +39,7 @@ export interface OpenPageOptions extends GotoOptions {
|
|
|
36
39
|
export interface ScreenshotOptions {
|
|
37
40
|
/** An explicit region in document coordinates. Wins over everything else. */
|
|
38
41
|
clip?: Clip;
|
|
39
|
-
/** A CSS selector: the bounding box of the first matching element. */
|
|
42
|
+
/** A CSS selector: the bounding box of the first matching element (see {@link BrowserPage.sectionClip}). */
|
|
40
43
|
section?: string;
|
|
41
44
|
/** The whole document (height capped at {@link MAX_SHOT_HEIGHT}). */
|
|
42
45
|
full?: boolean;
|
|
@@ -71,7 +74,13 @@ export interface BrowserPage {
|
|
|
71
74
|
freezeAnimations(): Promise<void>;
|
|
72
75
|
/** Undo {@link freezeAnimations}. */
|
|
73
76
|
unfreezeAnimations(): Promise<void>;
|
|
74
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* The document-coordinate box of the first element matching `selector`.
|
|
79
|
+
* Looks in the document, then in every open shadow root (`host >>> inner`
|
|
80
|
+
* crosses into `host`'s shadow root explicitly). A bare `#name` or `name`
|
|
81
|
+
* that matches nothing falls back to `[data-block-id="name"]`, a Mechanica
|
|
82
|
+
* block. Throws when nothing matches or the box is empty.
|
|
83
|
+
*/
|
|
75
84
|
sectionClip(selector: string): Promise<Clip>;
|
|
76
85
|
/**
|
|
77
86
|
* A PNG of the page: `clip`, else `section`, else `full`, else what is in
|
|
@@ -106,7 +115,12 @@ type SectionBox = Clip | {
|
|
|
106
115
|
message: string;
|
|
107
116
|
} | {
|
|
108
117
|
error: 'missing';
|
|
118
|
+
/** Readable ids on the page (`#name`), shadow roots included. */
|
|
109
119
|
ids: string[];
|
|
120
|
+
/** `data-block-id` values on the page. */
|
|
121
|
+
blockIds?: string[];
|
|
122
|
+
/** The block-id selector that was tried as well. */
|
|
123
|
+
fallback?: string | null;
|
|
110
124
|
} | {
|
|
111
125
|
error: 'empty';
|
|
112
126
|
width: number;
|
|
@@ -42,12 +42,54 @@ export interface LaunchedBrowser {
|
|
|
42
42
|
client: CdpClient;
|
|
43
43
|
close: () => Promise<void>;
|
|
44
44
|
}
|
|
45
|
+
/** A Chromium-based browser executable to try. */
|
|
46
|
+
export interface BrowserExecutable {
|
|
47
|
+
label: string;
|
|
48
|
+
path: string;
|
|
49
|
+
}
|
|
45
50
|
/**
|
|
46
51
|
* Spawn a headless Chromium-based browser with a DevTools endpoint and connect
|
|
47
|
-
* to it.
|
|
48
|
-
*
|
|
52
|
+
* to it. With `explicitPath` (`--browser`) only that executable is tried.
|
|
53
|
+
* Otherwise the installed browsers of {@link platformBrowserCandidates} are
|
|
54
|
+
* tried in order until one starts — a browser already on the machine, so
|
|
55
|
+
* nothing needs downloading — and the error names each one that failed.
|
|
49
56
|
*/
|
|
50
57
|
export declare function launchBrowser(explicitPath?: string): Promise<LaunchedBrowser>;
|
|
58
|
+
/**
|
|
59
|
+
* Try `start` on each candidate in order and return the first browser that
|
|
60
|
+
* comes up. When none does, the error lists every candidate and why it failed.
|
|
61
|
+
*/
|
|
62
|
+
export declare function startFirstBrowser<T>(candidates: BrowserExecutable[], start: (candidate: BrowserExecutable) => Promise<T>): Promise<T>;
|
|
63
|
+
/**
|
|
64
|
+
* Spawn one browser on a fresh temporary profile and connect to it. When it
|
|
65
|
+
* does not come up (exits early, never prints its endpoint, refuses the
|
|
66
|
+
* connection), everything it started is ended and the profile removed before
|
|
67
|
+
* the error is rethrown.
|
|
68
|
+
*/
|
|
69
|
+
export declare function startBrowser(executable: BrowserExecutable): Promise<LaunchedBrowser>;
|
|
70
|
+
/**
|
|
71
|
+
* The environment a browser starts with. On Windows it leaves out
|
|
72
|
+
* `__COMPAT_LAYER` (Git Bash and some tool runners set it to `RunAsInvoker`):
|
|
73
|
+
* under a compatibility layer Edge relaunches itself without it and exits at
|
|
74
|
+
* once, so the process we spawned never prints its endpoint while the
|
|
75
|
+
* relaunched one keeps running.
|
|
76
|
+
*/
|
|
77
|
+
export declare function browserEnv(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
|
|
78
|
+
/**
|
|
79
|
+
* The Windows commands that end a browser which failed to start, best effort:
|
|
80
|
+
* `taskkill /T /F` on its process tree while the spawned process still runs
|
|
81
|
+
* (`pid`), then — because a browser that relaunched itself has outlived the
|
|
82
|
+
* process we spawned — every process still running on its temporary profile,
|
|
83
|
+
* found by the profile's folder name in the command line.
|
|
84
|
+
*/
|
|
85
|
+
export declare function windowsKillCommands(pid: number | undefined, userDataDir: string): Array<[string, string[]]>;
|
|
86
|
+
/**
|
|
87
|
+
* Known install locations of Chromium-based browsers, in the order they are
|
|
88
|
+
* tried. On Windows Chrome comes first: Edge ships with Windows, so it is the
|
|
89
|
+
* fallback, and it relaunches itself under a compatibility layer (see
|
|
90
|
+
* {@link browserEnv}).
|
|
91
|
+
*/
|
|
92
|
+
export declare function platformBrowserCandidates(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, home?: string): BrowserExecutable[];
|
|
51
93
|
type CdpEventListener = (method: string, params: any, sessionId?: string) => void;
|
|
52
94
|
/**
|
|
53
95
|
* A minimal Chrome DevTools Protocol client over the platform `WebSocket`
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding the element behind a section selector (`mechanica shot --section`,
|
|
3
|
+
* `BrowserPage.sectionClip`): the lookup that runs in the page, and the pure
|
|
4
|
+
* helpers that prepare its arguments.
|
|
5
|
+
*
|
|
6
|
+
* - **Shadow DOM.** Design handoffs render their sections inside custom
|
|
7
|
+
* elements with open shadow roots, where `document.querySelector` sees
|
|
8
|
+
* nothing. The lookup tries the document first, then every open shadow root
|
|
9
|
+
* in tree order (depth first), matching the selector within each root.
|
|
10
|
+
* Slotted content belongs to its host's tree, so that tree's query covers
|
|
11
|
+
* it. `host >>> inner` crosses into the shadow root of `host` explicitly.
|
|
12
|
+
* - **Block ids.** Mechanica renders a block with `data-block-id="<node id>"`
|
|
13
|
+
* and no `id`, so a bare `#hero` (or `hero`) that matches nothing falls back
|
|
14
|
+
* to `[data-block-id="hero"]`.
|
|
15
|
+
*/
|
|
16
|
+
/** Crosses into the shadow root of the elements matched by the part before it. */
|
|
17
|
+
export declare const SHADOW_COMBINATOR = ">>>";
|
|
18
|
+
/**
|
|
19
|
+
* Split a selector at `>>>` into trimmed parts (one part when there is none).
|
|
20
|
+
* A `>>>` inside quotes, brackets or parentheses, or after a backslash, is
|
|
21
|
+
* part of the selector. Throws when a part is empty (`>>> .a`, `.a >>>`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function splitShadowSelector(selector: string): string[];
|
|
24
|
+
/**
|
|
25
|
+
* The block-id selector to try when a bare `#name` or `name` matches nothing:
|
|
26
|
+
* `[data-block-id="name"]`. Null for any other selector. A name that is not a
|
|
27
|
+
* valid CSS id (`#1hero`) still gets one, so block ids starting with a digit
|
|
28
|
+
* are reachable too.
|
|
29
|
+
*/
|
|
30
|
+
export declare function blockIdFallback(selector: string): string | null;
|
|
31
|
+
/** Readable ids and block ids listed in a "no match" error, at most this many of each. */
|
|
32
|
+
export declare const MAX_LISTED_IDS = 20;
|
|
33
|
+
/**
|
|
34
|
+
* In-page source of `(parts, fallback) => result`, where `parts` is a selector
|
|
35
|
+
* split by {@link splitShadowSelector} and `fallback` is
|
|
36
|
+
* {@link blockIdFallback}'s result (or null). It finds the first element
|
|
37
|
+
* matching `parts`, else the first matching `fallback`, in the document and
|
|
38
|
+
* in every open shadow root, and returns:
|
|
39
|
+
*
|
|
40
|
+
* - `{ element }`;
|
|
41
|
+
* - `{ error: 'invalid', message }` when the selector does not parse (and the
|
|
42
|
+
* fallback finds nothing);
|
|
43
|
+
* - `{ error: 'missing', ids, blockIds, fallback }`: the readable ids
|
|
44
|
+
* (`#name`) and the `data-block-id` values on the page, shadow roots
|
|
45
|
+
* included, for the error message.
|
|
46
|
+
*
|
|
47
|
+
* Plain JS with no outside references: it is sent to the page as text.
|
|
48
|
+
*/
|
|
49
|
+
export declare const FIND_SECTION_SOURCE: string;
|
package/dist/types/cli/shot.d.ts
CHANGED
|
@@ -13,7 +13,12 @@ export interface ShotOptions {
|
|
|
13
13
|
browser?: string;
|
|
14
14
|
/** Screenshot the full page instead of clipping to the block element (or to the viewport with `scroll`). */
|
|
15
15
|
full?: boolean;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* CSS selectors (`--section`, repeatable): one PNG each, clipped to the first
|
|
18
|
+
* match — in the document or an open shadow root (`host >>> inner` crosses
|
|
19
|
+
* one explicitly); a bare `#name` that matches nothing finds
|
|
20
|
+
* `[data-block-id="name"]`. Page/URL/file shots only.
|
|
21
|
+
*/
|
|
17
22
|
section?: string[];
|
|
18
23
|
/** Scroll offset(s) in px, comma-separated: capture the viewport scrolled there. */
|
|
19
24
|
scroll?: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type LocalesConfig, type PageMeta } from 'mechanica-shared';
|
|
2
|
+
/**
|
|
3
|
+
* The address a page's state is served at: its logical `path` under the
|
|
4
|
+
* locale prefix (`/about` -> `/ru/about`) plus the pagination variant suffix
|
|
5
|
+
* (`/blog` -> `/blog/2`). This is what the browser shows in the address bar for
|
|
6
|
+
* that state, so the server-side router starts there and `<Link>` resolves the
|
|
7
|
+
* same `is-active` on both sides of hydration.
|
|
8
|
+
*
|
|
9
|
+
* The export already serves a variant under its own path (`page.path` is
|
|
10
|
+
* `/blog/2` there, while dev keeps the base `/blog`); a suffix that is
|
|
11
|
+
* already present is kept, as `usePagination().pathFor` does.
|
|
12
|
+
*/
|
|
13
|
+
export declare function servedPagePath(page: PageMeta | undefined, locales?: LocalesConfig | null): string;
|
|
@@ -27,6 +27,13 @@ export interface CreateRouterOptions {
|
|
|
27
27
|
* runtime context.
|
|
28
28
|
*/
|
|
29
29
|
applyState?: (state: State) => void;
|
|
30
|
+
/**
|
|
31
|
+
* The address the rendered page is served at (locale prefix and pagination
|
|
32
|
+
* variant included). The server has no address bar, so this seeds its
|
|
33
|
+
* current route: `<Link>` then computes the same `is-active` the client
|
|
34
|
+
* will on hydration. The client reads `window.location` instead.
|
|
35
|
+
*/
|
|
36
|
+
initialPath?: string;
|
|
30
37
|
}
|
|
31
38
|
/**
|
|
32
39
|
* Create the client-side router. On the server it is inert (no listeners, no
|
|
@@ -12,6 +12,15 @@ export interface ClientEntryOptions {
|
|
|
12
12
|
* components while unused blocks stay in their own chunks, never downloaded.
|
|
13
13
|
*/
|
|
14
14
|
lazy?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Hydrate the server-rendered HTML (`createSSRApp`) instead of mounting
|
|
17
|
+
* into an empty container. The production client build: the export
|
|
18
|
+
* prerenders every page, and re-rendering would throw that DOM away and
|
|
19
|
+
* refetch every image at once (elements the client creates start their
|
|
20
|
+
* fetch before `loading="lazy"` applies). Dev and the editor build mount
|
|
21
|
+
* into an empty `#app`, so they keep `createApp`.
|
|
22
|
+
*/
|
|
23
|
+
hydrate?: boolean;
|
|
15
24
|
}
|
|
16
25
|
/**
|
|
17
26
|
* Generate the client entry that mounts the user's app from their
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mechanica",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Build Vue 3 websites with a visual block editor",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"prepublishOnly": "bun ../../scripts/ensure-bun-publish.js && bun run build"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"mechanica-shared": "2.2.
|
|
73
|
+
"mechanica-shared": "2.2.2",
|
|
74
74
|
"@vue/compiler-sfc": "^3.5.38",
|
|
75
75
|
"compact-json-schema": "^0.1.5",
|
|
76
76
|
"estree-walker": "^3.0.3",
|