mechanica 2.1.0-beta.7 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -344,25 +344,31 @@ By default all blocks share one `blocks` chunk, cached across pages. Mark heavy,
344
344
  ```bash
345
345
  npx mechanica shot hero # screenshot one block, from its previewData
346
346
  npx mechanica shot /about --width 1440,390 # a whole page, at two widths
347
+ npx mechanica shot design/landing/index.html --width 1440,390 # a design handoff, the same way
348
+ npx mechanica shot /about --section "#pricing" --section "footer" # one PNG per section
349
+ npx mechanica shot /about --scroll 800,1600 # the viewport after scrolling (sticky, scroll-driven states)
347
350
  npx mechanica thumbs # page thumbnails for the editor's page browser
348
351
  npx mechanica thumbs --blocks # block thumbnails for the palette
349
352
  ```
350
353
 
351
354
  These drive a locally installed Chrome or Edge (or `--browser <path>`) and need Node.js 22+ or Bun. They reuse a dev server running on port 5173 and start one if there isn't. Screenshots go to `.mech/shots/`, which is handy for reviewing a block or letting an AI assistant check its own work.
352
355
 
356
+ `shot` also takes any `http(s)://` URL, a `file://` URL or a path to a local `.html` file, and renders it with the same browser, widths, waits and animation freeze as your own pages, without a dev server. That lets you compare a design handoff with the page you built from it, section by section: run both with the same `--width` and `--section` flags. `--scroll <px>` scrolls first and captures what is on screen, so sticky stages and scroll-revealed content show their later states. `--no-freeze` leaves animations and transitions running. For your own scripts (for example, measuring computed styles in the design and on the site), `mechanica/browser` exposes the same headless browser (see [API](#api)).
357
+
353
358
  ## CLI
354
359
 
355
360
  | Command | What it does |
356
361
  | --- | --- |
357
362
  | `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) |
358
363
  | `mechanica build` | Build the client and server bundles into `dist/` (`export` runs this for you) |
359
- | `mechanica shot <blockId \| /path>` | Screenshot a block or a page. Flags: `--width`, `--data`, `--out`, `--full`, `--server`, `--browser` |
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` |
360
365
  | `mechanica thumbs [--blocks]` | Regenerate page or block thumbnails for the editor |
361
366
  | `mechanica images` | Generate image dimensions and blur-up previews for `.mech/assets` (needs `sharp`; `--force` redoes all) |
362
- | `mechanica login` | Sign in to the Mechanica platform and save an access token for this machine (`--host`, `--token`) |
367
+ | `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) |
363
368
  | `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
369
  | `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. It pushes the branch that is checked out, under its own name: the first branch a site is pushed is the site itself, any other is deployed to an address of its own (`<slug>--<branch>`) with its own online editor — a branch name is lowercase letters, digits and single hyphens |
365
370
  | `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) |
371
+ | `mechanica migrate` | Bring `.mech` content written by an older version up to the current format: uploads are addressed as `/media/<file>` (2.0 wrote `/@mechanica/assets/<file>`) |
366
372
 
367
373
  ## Plugin options
368
374
 
@@ -393,6 +399,27 @@ From `mechanica`:
393
399
 
394
400
  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.
395
401
 
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)`, `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
+
404
+ ```js
405
+ import { openPage } from 'mechanica/browser'
406
+
407
+ const page = await openPage(new URL('design/landing/index.html', import.meta.url).href, { width: 390 })
408
+ try {
409
+ const size = await page.evaluate(`getComputedStyle(document.querySelector('#hero h1')).fontSize`)
410
+ await page.scrollTo(1200)
411
+ const png = await page.screenshot({ section: '#hero' })
412
+ } finally {
413
+ await page.close()
414
+ }
415
+ ```
416
+
417
+ ## Upgrading from 2.0
418
+
419
+ - Uploaded files are addressed as `/media/<file>` in page files, shared data and rich text; 2.0 wrote `/@mechanica/assets/<file>`. The old address still works everywhere, so nothing breaks — run `npx mechanica migrate` once to rewrite it, and don't serve anything of your own under `/media/`.
420
+ - A file uploaded through the editor now gets a short content hash in its name (`photo-9f2c1a7b.jpg`). Files already in `.mech/assets/` keep their names.
421
+ - In the page browser, "Mark as draft" / "Publish" became "Hide from site" / "Show on site". `draft: true` in the page file means the same as before.
422
+
396
423
  ## Upgrading from 1.x
397
424
 
398
425
  Mechanica 2 is a rewrite, and a 1.x project has to be ported by hand; there is no automatic migration. What changes:
@@ -0,0 +1,2 @@
1
+ import { a as MAX_SHOT_HEIGHT, i as openPage, n as PAGE_LOADED, o as findDevServer, r as describeSectionError, t as NavigationError } from "./chunks/browser-BCfAv8bW.js";
2
+ export { MAX_SHOT_HEIGHT, NavigationError, PAGE_LOADED, describeSectionError, findDevServer, openPage };
@@ -1,44 +1,8 @@
1
- import { G as isAssetName, J as readImageManifest, X as updateImageManifest } from "./rich-text-codec-BSD35k2U.js";
1
+ import { G as isAssetName, J as readImageManifest, X as updateImageManifest } from "./rich-text-codec-DCv1X0eZ.js";
2
2
  import { join } from "node:path";
3
- import { chmod, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import { chmod, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
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
6
  //#region src/cli/platform/api.ts
43
7
  /** A failed platform request: the HTTP status plus the message the platform gave. */
44
8
  var PlatformError = class extends Error {
@@ -80,12 +44,6 @@ async function request(origin, method, path, options = {}) {
80
44
  if (!response.ok) throw new PlatformError(response.status, errorMessage(response.status, text));
81
45
  return text ? JSON.parse(text) : null;
82
46
  }
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
47
  /** The name of the git remote `mechanica link` manages. */
90
48
  var REMOTE_NAME = "mechanica";
91
49
  /**
@@ -105,6 +63,70 @@ function parseRemoteUrl(url) {
105
63
  return null;
106
64
  }
107
65
  }
66
+ /** Normalize a host argument to an origin (`https://example.com`, no trailing slash or path). */
67
+ function hostOrigin(host) {
68
+ const value = host ?? process.env.MECHANICA_HOST ?? "https://mechanica.jt3.ru";
69
+ return new URL(/^https?:\/\//.test(value) ? value : `https://${value}`).origin;
70
+ }
71
+ /** Where the CLI keeps what it knows about the user: one folder per user, outside any project. */
72
+ function configDir() {
73
+ return process.env.MECHANICA_CONFIG_DIR ?? join(os.homedir(), ".mechanica");
74
+ }
75
+ /** Access tokens, by platform origin. */
76
+ var credentialsFile = () => join(configDir(), "credentials.json");
77
+ /** Sign-ins started by `mechanica login` that wait for their approval, by platform origin. */
78
+ var pendingFile = () => join(configDir(), "pending-logins.json");
79
+ async function readJson(file) {
80
+ try {
81
+ const parsed = JSON.parse(await readFile(file, "utf-8"));
82
+ return parsed && typeof parsed === "object" ? parsed : {};
83
+ } catch {
84
+ return {};
85
+ }
86
+ }
87
+ /** Write one of the files, readable by the user alone. */
88
+ async function writeJson(file, value) {
89
+ await mkdir(join(file, ".."), { recursive: true });
90
+ await writeFile(file, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
91
+ await chmod(file, 384).catch(() => {});
92
+ }
93
+ /**
94
+ * The access token for a platform origin: `MECHANICA_TOKEN` (CI) wins over the
95
+ * one `mechanica login` saved.
96
+ */
97
+ async function getToken(origin) {
98
+ if (process.env.MECHANICA_TOKEN) return process.env.MECHANICA_TOKEN;
99
+ return (await readJson(credentialsFile()))[origin] ?? null;
100
+ }
101
+ /** Remember the token for an origin, in a file only the user can read. */
102
+ async function saveToken(origin, token) {
103
+ const file = credentialsFile();
104
+ const all = await readJson(file);
105
+ all[origin] = token;
106
+ await writeJson(file, all);
107
+ return file;
108
+ }
109
+ /** The sign-in waiting for an origin, or null. An expired one is reported too: whoever reads it says so. */
110
+ async function getPendingLogin(origin) {
111
+ const entry = (await readJson(pendingFile()))[origin];
112
+ return entry && typeof entry.deviceCode === "string" && typeof entry.verificationUrl === "string" ? entry : null;
113
+ }
114
+ /** Keep a started sign-in for an origin (replacing an earlier one). */
115
+ async function savePendingLogin(origin, login) {
116
+ const file = pendingFile();
117
+ const all = await readJson(file);
118
+ all[origin] = login;
119
+ await writeJson(file, all);
120
+ }
121
+ /** Forget the sign-in waiting for an origin — it was finished, denied or has expired. */
122
+ async function clearPendingLogin(origin) {
123
+ const file = pendingFile();
124
+ const all = await readJson(file);
125
+ if (!(origin in all)) return;
126
+ delete all[origin];
127
+ if (Object.keys(all).length) await writeJson(file, all);
128
+ else await rm(file, { force: true });
129
+ }
108
130
  //#endregion
109
131
  //#region src/cli/platform/git.ts
110
132
  /**
@@ -319,4 +341,4 @@ async function runAssetsPull(options = {}) {
319
341
  return done;
320
342
  }
321
343
  //#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 };
344
+ export { request as _, git as a, clearPendingLogin as c, hostOrigin as d, savePendingLogin as f, parseRemoteUrl as g, REMOTE_NAME as h, uploadsIgnored as i, getPendingLogin as l, PlatformError as m, remoteAssetsFor as n, gitAuthEnv as o, saveToken as p, runAssetsPull as r, gitOk as s, pushUploads as t, getToken as u };