Pages as one HTML file

Experimental. Render a page to one self-contained HTML file for sharing or embedding, and get the page back from it.

Experimental

This command is experimental, as everything under npx @astryxdesign/cli docs cli/experimental is: its options, its output file and its JSON may still change. Nothing else in the CLI changes because it exists.

Make a file to share

Build the page with Astryx components as usual, then render it. The page module needs a default export; it renders on your machine with the React and Astryx packages your project installed. Outside a project (no Astryx Core or CLI installed or declared for the folder), it renders with the Astryx, React and StyleX that the CLI itself runs with, so a single page file renders anywhere, a React app included.

bash
npx @astryxdesign/cli experimental render src/app/page.tsx --out page.html

The result is ONE file with nothing beside it: the markup, only the CSS rules the page uses (from Core and from any stylesheet it imports, such as a theme), the page’s own StyleX styles, and local images as data: URIs. It runs no script and loads nothing from the network, so it opens from disk, attaches to a message, and displays inside a sandboxed frame that blocks network and script. Colors follow the viewer’s light or dark setting, and a data-theme of light or dark on the root element picks one. The file also carries the source of the page and of every local module, stylesheet, JSON file and image it imports, so whoever you share it with can read that code; keep secrets out of what the page imports.

What it cannot keep: anything that needs a browser while running. State, effects, click handlers and data loaded after the first paint are gone. Charts made with recharts are the exception: the render draws each chart and ResponsiveContainer a module of the page (not an installed package) imports from recharts by name (import {LineChart} from 'recharts'), inside the page, so a custom tick, label or shape sees the page's React context. A chart sized by its container scales to the width the page gives it, and its legend stays out of the picture: a horizontal legend takes its own row under it (above it when the legend asks for the top), and a vertical legend on one side sits beside it, on that side; in a chart of fixed size, a legend keeps the place its props give it. Charts show the data and props of the page's first render, before its effects run: when the page cannot be drawn again without a browser, or its effects change its charts or re-render the component that makes them, each chart is drawn by itself and its parts see React context defaults. A value a chart's parts read from a React context that an effect changes may still show its later value. A chart-size warning counts the charts the render cannot draw (one used through import * as R, say), and says when charts were drawn by themselves. Read browser-only values such as window inside an effect, or the render stops with ERR_RENDER_BROWSER_API.

Read the warnings

Each warning names something that may look different from what you meant, and what to change. The most common is a component that ignores its children: <ListItem>Save</ListItem> renders an empty row because ListItem takes label="Save". Others flag remote images (a sandboxed viewer cannot load them; save the image next to the page and import it), a <script>, <link> or <iframe> the page itself renders (the file promises to run and load nothing, so remove them), missing images, and output the page printed while rendering.

Get the page back

bash
npx @astryxdesign/cli experimental render --reverse page.html --out page.tsx

Every rendered file carries its source, so this writes the page back byte for byte; edit it and render again. A file whose source block was removed but whose markup came from Astryx components is rebuilt from that markup instead: the components, their recorded props and the text come back, while styling, icons and behavior do not, so treat it as a starting point. HTML that was never an Astryx page is refused with ERR_RENDER_NOT_ASTRYX; describe the page to npx @astryxdesign/cli build to start one.

It writes the page, the modules, stylesheets and JSON files the page reaches through its own imports, and the images, fonts and media the render carried (however the page named them, as long as their bytes really are one): each goes where the page's relative imports find it from wherever you put the page, and a file the page imports through a path alias (@/components/Header) goes where your project's tsconfig.json or jsconfig.json alias points. Every file but the page goes inside the one new folder the restore creates (the topmost folder on the page's path that does not exist yet), never into a folder that already exists: a page that imports other files comes back into a new folder (--out restored/app/page.tsx), and a file it imports through a path alias comes back only when that alias points inside the new folder. Nor does it write a file that a test, story or benchmark runner would run by its default patterns (Jest, Vitest, Bun, AVA, Playwright, Storybook, Cypress and node --test: names such as *.test.*, *.spec.*, *_test.*, *-test.*, *.bench.*, *.stories.*, *.cy.*, and anything under test/, tests/ or __tests__/), judged from the repository root (every folder above the file up to it), else the topmost folder with a package.json. Restored files are the sender's code: read them before you run or import them. Without --out, everything comes back into a new folder beside the HTML file, named after it (page.html comes back into page-restored/). A received file that carries anything else (a file the page never imports, a dotfile, package.json, a tool configuration, a path that leaves the folder) is refused with nothing written. A rendered file that lost its embedded source on the way (a sanitizer or an editor strips <script> blocks) and has no Astryx components to rebuild from is refused with ERR_RENDER_SOURCE_DAMAGED. An existing file is never replaced unless you pass --overwrite, and the refusal names every one; a generated, vendored or ignored file, and any path through a symbolic link, is not written even then. Add --json for the result as data: what was written, how big it is, and every warning.