mechanica 2.1.0 → 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.
@@ -2,7 +2,18 @@ export interface ParsedArgs {
2
2
  command?: string;
3
3
  /** Positional arguments after the command (excluding `--flag` values). */
4
4
  args: string[];
5
+ /** Flag values; a flag given several times keeps its last value (see {@link flagValues}). */
5
6
  flags: Record<string, string | boolean>;
6
7
  }
8
+ export interface ParseOptions {
9
+ /** Flags that never take a value, so `--full hero` keeps `hero` positional. */
10
+ booleans?: string[];
11
+ }
7
12
  /** Parse `argv` (without node/script) into a command, positionals and a `--flag[=value]` map. */
8
- export declare function parseArgs(argv: string[]): ParsedArgs;
13
+ export declare function parseArgs(argv: string[], options?: ParseOptions): ParsedArgs;
14
+ /**
15
+ * Every value a repeatable flag was given, in order — `--section a --section b`
16
+ * → `['a', 'b']`; a bare `--section` contributes `true`. Takes the same `argv`
17
+ * as {@link parseArgs} (command first).
18
+ */
19
+ export declare function flagValues(argv: string[], name: string, options?: ParseOptions): Array<string | boolean>;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * `mechanica/browser` — the headless browser behind `mechanica shot` and
3
+ * `mechanica thumbs`, for a site's own scripts: open a page (a dev-server URL,
4
+ * a design handoff's `file://` URL, any site), resize it, scroll it, measure it
5
+ * with `evaluate`, and screenshot it or one section of it.
6
+ *
7
+ * Drives a Chrome/Edge already installed on the machine over raw CDP
8
+ * (`headless.ts`), so there is nothing to download. Needs a global `WebSocket`
9
+ * (Node 22+ or Bun) and never imports Vite.
10
+ */
11
+ import { type Clip } from './headless';
12
+ export { findDevServer, MAX_SHOT_HEIGHT, type Clip } from './headless';
13
+ /** Readiness of an ordinary page: the load event has fired. */
14
+ export declare const PAGE_LOADED = "document.readyState === 'complete'";
15
+ export interface GotoOptions {
16
+ /** JS expression polled until it is `true`. Default: {@link PAGE_LOADED}. */
17
+ ready?: string;
18
+ /** Then wait for web fonts and images (see {@link BrowserPage.settle}). Default `true`. */
19
+ settle?: boolean;
20
+ /** How long `ready` may take, in ms. Default 15 000. */
21
+ timeout?: number;
22
+ }
23
+ export interface OpenPageOptions extends GotoOptions {
24
+ /** Viewport width in CSS px. Default 1440. */
25
+ width?: number;
26
+ /** Viewport height in CSS px. Default 900. */
27
+ height?: number;
28
+ /** A Chromium-based executable. Default: Edge or Chrome from their usual install paths. */
29
+ browser?: string;
30
+ /**
31
+ * Emulate `prefers-reduced-motion: reduce`, as `mechanica shot` does: pages
32
+ * that honor it skip their entrance animations. Default `true`.
33
+ */
34
+ reducedMotion?: boolean;
35
+ }
36
+ export interface ScreenshotOptions {
37
+ /** An explicit region in document coordinates. Wins over everything else. */
38
+ clip?: Clip;
39
+ /** A CSS selector: the bounding box of the first matching element. */
40
+ section?: string;
41
+ /** The whole document (height capped at {@link MAX_SHOT_HEIGHT}). */
42
+ full?: boolean;
43
+ /** Output scale: `0.5` writes a half-size PNG. Default 1. */
44
+ scale?: number;
45
+ }
46
+ /** `goto` could not open the URL at all (DNS, refused connection, missing file…). */
47
+ export declare class NavigationError extends Error {
48
+ readonly url: string;
49
+ readonly reason: string;
50
+ constructor(url: string, reason: string);
51
+ }
52
+ /** One headless tab in its own browser process. */
53
+ export interface BrowserPage {
54
+ /** Console errors/warnings and uncaught exceptions, as `[console.error] …` / `[pageerror] …` lines. */
55
+ readonly messages: string[];
56
+ /** Navigate, then wait for `ready` and (by default) fonts + images. */
57
+ goto(url: string, options?: GotoOptions): Promise<void>;
58
+ /** Evaluate an expression in the page (promises are awaited) and return its JSON value. Throws on a page exception. */
59
+ evaluate<T = unknown>(expression: string): Promise<T>;
60
+ /** Poll `expression` until it is `true`. */
61
+ waitFor(expression: string, timeout?: number): Promise<void>;
62
+ /** Resize the viewport (height defaults to the current one). */
63
+ setViewport(width: number, height?: number): Promise<void>;
64
+ /** Scroll the window to `y` instantly (ignoring `scroll-behavior: smooth`), wait two frames, return the reached `scrollY`. */
65
+ scrollTo(y: number): Promise<number>;
66
+ /** Wait two animation frames: one full style → layout → paint cycle. */
67
+ nextFrame(): Promise<void>;
68
+ /** Wait for web fonts and images; lazy images are switched to eager. Gives up after 10 s. */
69
+ settle(): Promise<void>;
70
+ /** Turn off CSS animations and transitions (running transitions jump to their end). Idempotent. */
71
+ freezeAnimations(): Promise<void>;
72
+ /** Undo {@link freezeAnimations}. */
73
+ unfreezeAnimations(): Promise<void>;
74
+ /** The document-coordinate box of the first element matching `selector`. Throws when none matches or the box is empty. */
75
+ sectionClip(selector: string): Promise<Clip>;
76
+ /**
77
+ * A PNG of the page: `clip`, else `section`, else `full`, else what is in
78
+ * the viewport right now (at the current scroll position).
79
+ */
80
+ screenshot(options?: ScreenshotOptions): Promise<Buffer>;
81
+ /** A raw CDP command on this tab's session, for anything not covered above. */
82
+ send(method: string, params?: object): Promise<any>;
83
+ /** Close the browser and remove its temporary profile. */
84
+ close(): Promise<void>;
85
+ }
86
+ /**
87
+ * Launch a headless browser with one tab, sized and media-emulated like
88
+ * `mechanica shot`, and open `url` in it (omit `url` for a blank tab to
89
+ * `goto` later). Always `close()` the page — it owns the browser process.
90
+ *
91
+ * ```ts
92
+ * import { openPage } from 'mechanica/browser'
93
+ *
94
+ * const page = await openPage('file:///…/design/index.html', { width: 390 })
95
+ * try {
96
+ * const size = await page.evaluate<string>(`getComputedStyle(document.querySelector('h1')).fontSize`)
97
+ * fs.writeFileSync('hero.png', await page.screenshot({ section: '#hero' }))
98
+ * } finally {
99
+ * await page.close()
100
+ * }
101
+ * ```
102
+ */
103
+ export declare function openPage(url?: string, options?: OpenPageOptions): Promise<BrowserPage>;
104
+ type SectionBox = Clip | {
105
+ error: 'invalid';
106
+ message: string;
107
+ } | {
108
+ error: 'missing';
109
+ ids: string[];
110
+ } | {
111
+ error: 'empty';
112
+ width: number;
113
+ height: number;
114
+ };
115
+ /** The message for a `sectionClip` that found nothing usable. */
116
+ export declare function describeSectionError(selector: string, box: Exclude<SectionBox, Clip>): string;
@@ -0,0 +1,13 @@
1
+ export interface ResolvedServer {
2
+ origin: string;
3
+ close: () => Promise<void>;
4
+ }
5
+ /**
6
+ * Find a dev server to render against: an explicit origin, a running local
7
+ * Mechanica dev server, or an ephemeral one booted from the project's
8
+ * `vite.config.ts` and torn down afterwards.
9
+ *
10
+ * The only browser-side helper that needs Vite, kept out of `headless.ts` so
11
+ * the public `mechanica/browser` entry never pulls it in.
12
+ */
13
+ export declare function resolveServer(flag?: string): Promise<ResolvedServer>;
@@ -1,13 +1,7 @@
1
- export interface ResolvedServer {
2
- origin: string;
3
- close: () => Promise<void>;
4
- }
5
- /**
6
- * Find a dev server to render against: an explicit origin, a running local
7
- * Mechanica dev server, or an ephemeral one booted from the project's
8
- * `vite.config.ts` and torn down afterwards.
9
- */
10
- export declare function resolveServer(flag?: string): Promise<ResolvedServer>;
1
+ /** Where `mechanica shot`/`thumbs` look for a running dev server first. */
2
+ export declare const DEV_SERVER_ORIGINS: string[];
3
+ /** The origin of a running Mechanica dev server on the default port, or null. */
4
+ export declare function findDevServer(origins?: string[]): Promise<string | null>;
11
5
  export declare function setViewport(cdp: CdpClient, sessionId: string, width: number, height?: number): Promise<void>;
12
6
  export declare function evaluateValue<T>(cdp: CdpClient, sessionId: string, expression: string): Promise<T>;
13
7
  /**
@@ -16,19 +10,34 @@ export declare function evaluateValue<T>(cdp: CdpClient, sessionId: string, expr
16
10
  * ready yet" rather than failing the wait.
17
11
  */
18
12
  export declare function waitForCondition(cdp: CdpClient, sessionId: string, expression: string, timeout: number): Promise<void>;
19
- /** Wait for web fonts and all images — normal pages have no injected ready flag. */
20
- export declare function settlePageMedia(cdp: CdpClient, sessionId: string): Promise<void>;
21
- /** Freeze animations/transitions so captures are deterministic across runs. */
13
+ /** Upper bound for {@link settlePageMedia}: a stalled image must not hang a shot. */
14
+ export declare const SETTLE_TIMEOUT = 10000;
15
+ /**
16
+ * Wait for web fonts and all images — normal pages have no injected ready flag.
17
+ * Lazy images are switched to eager first: below the fold they would never
18
+ * start loading in a headless tab (the wait used to hang on them), and a
19
+ * full-page capture should show them anyway. Gives up after `timeout` ms.
20
+ */
21
+ export declare function settlePageMedia(cdp: CdpClient, sessionId: string, timeout?: number): Promise<void>;
22
+ /** Freeze animations/transitions so captures are deterministic across runs. Idempotent. */
22
23
  export declare function freezeAnimations(cdp: CdpClient, sessionId: string): Promise<void>;
24
+ /** Undo {@link freezeAnimations}: transitions and animations run again. */
25
+ export declare function unfreezeAnimations(cdp: CdpClient, sessionId: string): Promise<void>;
26
+ /** Wait two animation frames: a full style → layout → paint cycle has run. */
27
+ export declare function nextFrames(cdp: CdpClient, sessionId: string): Promise<void>;
23
28
  /** Chromium refuses to encode arbitrarily tall captures — cap and say so. */
24
29
  export declare const MAX_SHOT_HEIGHT = 12000;
25
- /** The full document size as a screenshot clip. */
26
- export declare function fullPageClip(cdp: CdpClient, sessionId: string): Promise<{
30
+ /** A screenshot region in CSS pixels, relative to the document (not the viewport). */
31
+ export interface Clip {
27
32
  x: number;
28
33
  y: number;
29
34
  width: number;
30
35
  height: number;
31
- }>;
36
+ }
37
+ /** The full document size as a screenshot clip. */
38
+ export declare function fullPageClip(cdp: CdpClient, sessionId: string): Promise<Clip>;
39
+ /** What is on screen right now (the visual viewport at its scroll offset), as a clip. */
40
+ export declare function viewportClip(cdp: CdpClient, sessionId: string): Promise<Clip>;
32
41
  export interface LaunchedBrowser {
33
42
  client: CdpClient;
34
43
  close: () => Promise<void>;
@@ -11,26 +11,123 @@ export interface ShotOptions {
11
11
  server?: string;
12
12
  /** Explicit Chromium-based browser executable path. */
13
13
  browser?: string;
14
- /** Screenshot the full page instead of clipping to the block element. */
14
+ /** Screenshot the full page instead of clipping to the block element (or to the viewport with `scroll`). */
15
15
  full?: boolean;
16
+ /** CSS selectors (`--section`, repeatable): one PNG each, clipped to the first match. Page/URL/file shots only. */
17
+ section?: string[];
18
+ /** Scroll offset(s) in px, comma-separated: capture the viewport scrolled there. */
19
+ scroll?: string;
20
+ /** Leave CSS animations and transitions running (`--no-freeze`). */
21
+ noFreeze?: boolean;
16
22
  }
23
+ /**
24
+ * What `mechanica shot` renders: a block through the preview route, a page of
25
+ * this project on the dev server, or a foreign document — any `http(s)://` URL
26
+ * or a local file (a design handoff's `index.html`) — opened as is.
27
+ */
28
+ export type ShotTarget = {
29
+ kind: 'block';
30
+ blockId: string;
31
+ } | {
32
+ kind: 'page';
33
+ path: string;
34
+ } | {
35
+ kind: 'url';
36
+ url: string;
37
+ }
38
+ /** `file` is the path on disk when the URL maps to one (checked before launching). */
39
+ | {
40
+ kind: 'file';
41
+ url: string;
42
+ file?: string;
43
+ };
44
+ /** How one capture is framed. */
45
+ export type CaptureMode = 'block' | 'section' | 'full' | 'viewport';
46
+ /** Everything `runShot` needs, resolved and validated before any browser starts. */
47
+ export interface ShotPlan {
48
+ target: ShotTarget;
49
+ /** Base output name (without section/scroll/width suffixes). */
50
+ name: string;
51
+ widths: number[];
52
+ /** Scroll offsets to capture at, in order; undefined = no scrolling. */
53
+ scrolls?: number[];
54
+ sections: Array<{
55
+ selector: string;
56
+ slug: string;
57
+ }>;
58
+ data?: Record<string, unknown>;
59
+ mode: CaptureMode;
60
+ freeze: boolean;
61
+ }
62
+ /** Readiness flag of the standalone block preview route. */
63
+ export declare const PREVIEW_READY = "window.__MECHANICA_PREVIEW_READY__ === true";
17
64
  /** Parse a `--width` value into a list of positive integers. Defaults to `[1440]`. */
18
65
  export declare function parseWidths(value: string | undefined): number[];
66
+ /** Parse a `--scroll` value (px from the top, comma-separated). Undefined when absent. */
67
+ export declare function parseScrolls(value: string | undefined): number[] | undefined;
19
68
  /** Resolve `--data`: inline JSON or `@file`, validated to a JSON object. */
20
69
  export declare function resolveDataArg(value: string | undefined): Record<string, unknown> | undefined;
21
70
  /** Build the preview-route URL for a block on a given server origin. */
22
71
  export declare function previewUrl(origin: string, blockId: string, data?: Record<string, unknown>): string;
23
- /** Resolve the output PNG path for one width. */
24
- export declare function outputPath(out: string | undefined, name: string, width: number, multi: boolean): string;
72
+ /**
73
+ * Resolve the output PNG path for one capture. `variant` (`-<section>`,
74
+ * `-y<scroll>`) is always part of the name; the `-w<width>` suffix only when
75
+ * shooting several widths. An `--out` ending in `.png` is a file name, any
76
+ * other value a directory.
77
+ */
78
+ export declare function outputPath(out: string | undefined, name: string, width: number, multi: boolean, variant?: string): string;
79
+ /** The name suffix of one capture: `-<section slug>` and/or `-y<scroll>`. */
80
+ export declare function shotVariant(sectionSlug?: string, scroll?: number): string;
25
81
  /** `/` → `index`, `/docs/getting-started` → `docs-getting-started`. */
26
82
  export declare function pageSlug(path: string): string;
27
83
  /** Normalize a page path: leading slash, no trailing slash (except the root). */
28
84
  export declare function normalizePagePath(path: string): string;
85
+ /** Lowercase letters and digits (any script) joined by single dashes. */
86
+ export declare function slugify(value: string): string;
87
+ /**
88
+ * One file-name slug per `--section` selector: `#hero` → `hero`,
89
+ * `.stage > .pin` → `stage-pin`. A selector with no letters or digits becomes
90
+ * `section`; repeats get `-2`, `-3`…
91
+ */
92
+ export declare function sectionSlugs(selectors: string[]): string[];
93
+ /**
94
+ * Classify a `shot` target, in this order:
95
+ * - `--page </path>` → that page;
96
+ * - `http://…` / `https://…` → a URL, opened as is;
97
+ * - `file://…`, a Windows drive path (`C:\…`), a `./` or `../` path, or any
98
+ * path ending in `.html`/`.htm` → a local file (relative to `cwd`);
99
+ * - anything else starting with `/` → a page of this project;
100
+ * - anything else → a block id.
101
+ */
102
+ export declare function classifyTarget(target: string | undefined, page?: string, cwd?: string): ShotTarget;
103
+ /**
104
+ * Default output name of a URL or file shot:
105
+ * - a URL → its host (no `www.`, port appended) and path, extension dropped:
106
+ * `https://example.com/business/evolve` → `example-com-business-evolve`;
107
+ * - a file → its path relative to `cwd` (or its last three path segments when
108
+ * it lies outside), extension dropped: `design/evolve-level/index.html` →
109
+ * `design-evolve-level-index`.
110
+ */
111
+ export declare function foreignShotName(target: ShotTarget, cwd?: string): string;
112
+ /**
113
+ * How each capture is framed: `--section` boxes, else `--full` (whole
114
+ * document), else the viewport when scrolling (so sticky/pinned states are
115
+ * what's in the picture), else the block element for block shots and the
116
+ * whole document for pages, URLs and files.
117
+ */
118
+ export declare function captureMode(kind: ShotTarget['kind'], flags: {
119
+ section?: boolean;
120
+ full?: boolean;
121
+ scroll?: boolean;
122
+ }): CaptureMode;
123
+ /** Resolve and validate a `shot` invocation without touching a browser or a server. */
124
+ export declare function planShot(target: string | undefined, options?: ShotOptions, cwd?: string): ShotPlan;
29
125
  /**
30
- * `mechanica shot <blockId>` / `mechanica shot /page/path` — render one block
31
- * through the dev preview route (or a whole page, editor overlay stripped) in
32
- * a headless Chromium and write PNG screenshot(s). Made for agent loops:
33
- * prints the image paths plus any console/render errors, and fails loudly when
34
- * the block or page failed to render.
126
+ * `mechanica shot <target>` — render one block through the dev preview route,
127
+ * a whole page (editor overlay stripped), or a foreign document (any URL, or
128
+ * a local `.html` file such as a design handoff) in a headless Chromium and
129
+ * write PNG screenshot(s): per width, per scroll offset, per `--section`.
130
+ * Made for agent loops: prints the image paths plus any console/render errors,
131
+ * and fails loudly when the block or page failed to render.
35
132
  */
36
133
  export declare function runShot(target: string | undefined, options?: ShotOptions): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mechanica",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Build Vue 3 websites with a visual block editor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,6 +45,10 @@
45
45
  "types": "./dist/types/server/index.d.ts",
46
46
  "import": "./dist/server.js"
47
47
  },
48
+ "./browser": {
49
+ "types": "./dist/types/cli/browser.d.ts",
50
+ "import": "./dist/browser.js"
51
+ },
48
52
  "./editor": {
49
53
  "import": "./dist/editor.js"
50
54
  },
@@ -66,7 +70,7 @@
66
70
  "prepublishOnly": "bun ../../scripts/ensure-bun-publish.js && bun run build"
67
71
  },
68
72
  "dependencies": {
69
- "mechanica-shared": "2.1.0",
73
+ "mechanica-shared": "2.2.0",
70
74
  "@vue/compiler-sfc": "^3.5.38",
71
75
  "compact-json-schema": "^0.1.5",
72
76
  "estree-walker": "^3.0.3",