Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4945,8 +4945,43 @@ The adapter handles all outputs in one `onEnd` pass.

### webui-press integration

`DocsConfig.show` is a typed `ShowMode` (`all` or `content`), defaulting to
`all`. Both native `build` and `serve` accept `--show`; an explicit CLI value
overrides configuration on the initial build and every serve config reload.
Page and 404 build errors retain the core error's complete source chain,
including parser diagnostic codes, locations, snippets, and help when present.

Content mode selects the bundled content document before region expansion,
component/script reachability, compilation, and SSR. It retains document
metadata, base URL, configured head tags, themes, authored page modules, state,
and semantic `main`/`article` wrappers. Markdown (including home Markdown),
custom-page HTML, examples, and API panels are content, regardless of their
element names. No template regions, navigation, sidebar/TOC, mobile context,
previous/next links, hero/features, footer, or shell scripts are generated.
Configured regions remain validated against the selected full template, but
their state and scripts are inactive. This also applies to the 404 document.

The bundled `docs.css` contains shared tokens and content typography;
`shell.css` contains full-site layout constraints. All mode concatenates both
into one served stylesheet, preserving the existing layout without an extra
request. Content mode uses only the bundled content styles, normal document
scrolling, and no shell width/height constraints. A custom full template and
its styles/entry script do not replace the content-mode scaffold; configured
head tags, CSS/theme, components, and custom pages continue to apply.

Full-site manual light/dark selection overrides the OS preference for both
native theme tokens and `color-scheme`. Manual overrides are inactive under
forced colors so site-authored forced-colors token rules retain precedence.
Content mode has no shell theme control, does not read or snapshot a persisted
theme into `data-theme`, and uses only the light default and system dark media
query. It follows live OS preference changes without JavaScript and leaves the
stored full-site preference untouched.

`webui-press` invokes esbuild's JavaScript API once through
`@microsoft/webui/projection.js`, then validates the generated manifest once.
Filesystem alias targets are resolved against an absolute config directory
before passing them to esbuild. Relative and absolute CLI config paths retain
the same config-relative alias semantics, independent of the invocation cwd.
The resulting `PreparedProjectionManifests` is reused by every page and the 404
build; page builds never re-open or re-hash bundle files. The prepared handle
is an `Arc`-backed immutable snapshot containing both component surfaces and
Expand Down
3 changes: 3 additions & 0 deletions crates/webui-press/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ microsoft-webui-dev-server = { path = "../webui-dev-server", version = "0.0.28"
actix-web = { workspace = true }
tokio = { workspace = true }
include_dir = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
27 changes: 27 additions & 0 deletions crates/webui-press/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,40 @@ Every `.md` file under `contentDir` becomes a page automatically. The sidebar/na

```
webui-press build [OPTIONS]
webui-press serve [OPTIONS]

Options:
-c, --config <PATH> Path to config.json [default: .webui-press/config.json]
-t, --template <PATH> Override the bundled template directory
--show <MODE> all or content [default: config show, otherwise all]
-h, --help Print help
```

Use `webui-press build --show=content` or `webui-press serve --show=content`
for a shell-free gallery or documentation view. Omit the flag for the complete
site, or set `"show": "content"` in config to make content mode the default.
An explicit flag overrides config, including after live config reloads.

Content mode keeps a complete HTML document, metadata/base URL, themes, SSR,
hydration, Markdown, examples, API panels, custom-page content, and page scripts.
It omits Press template regions, hero/features, header/navigation,
sidebars/TOC, mobile navigation, previous/next links, and footer before
compilation. Authored header/side-pane examples are not filtered. Home Markdown
is rendered in this mode; doc, page, full, custom, and 404 pages all use normal
document scrolling without reserved shell columns or viewport-fill behavior.

Content mode uses the bundled `main`/`article` scaffold even with `--template`.
Its colors follow the OS preference, including live changes, without reading
or changing the full site's saved theme selection. Full mode retains its
persisted theme control; manual light/dark selection applies to native theme
tokens and browser controls even when the OS preference differs. Forced-colors
styles retain precedence over manual theme overrides.

The full template's regions are still validated but are inactive, as are its
CSS and entry script. Configure `head`, `css`, `theme`, `components`, and page
scripts for assets that should apply in both modes. In the bundled full template,
shared `docs.css` and layout-only `shell.css` are combined into one output file.

The build pipeline:

```
Expand Down
48 changes: 40 additions & 8 deletions crates/webui-press/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use crate::error::{Error, Result};
use crate::markdown::Highlighter;
use crate::regions::RegionSet;
use crate::state::{load_render_states, merge_page_state};
use crate::types::{BuildStats, DocsConfig, PageDescriptor};
use crate::types::{BuildStats, DocsConfig, PageDescriptor, ShowMode};

webui_handler::define_string_response_writer!(StringWriter, buf);

Expand All @@ -41,6 +41,27 @@ fn region_layout(page: &PageDescriptor) -> &str {
}
}

fn template_css(template_dir: &Path, show: ShowMode) -> Result<String> {
if show == ShowMode::Content {
return Ok(include_str!("../template/docs.css").to_string());
}
let mut css = String::new();
for name in ["docs.css", "shell.css"] {
let path = template_dir.join(name);
match fs::read_to_string(&path) {
Ok(source) => css.push_str(&source),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(Error::Io(format!(
"Cannot read {}: {error}",
path.display()
)));
}
}
}
Ok(css)
}

/// Persistent state held by the dev server across rebuilds. The dev
/// server always performs a full rebuild on every watcher tick — the
/// previous incremental machinery proved too complex for the marginal
Expand Down Expand Up @@ -298,8 +319,8 @@ pub fn build_docs_with_cache(
// the link tags change. We don't write the CSS files yet — that
// happens after content processing succeeds, so a content failure
// can't corrupt the previous valid output.
let base_css_src = template_dir.join("docs.css");
let has_base_css = base_css_src.exists();
let base_css = template_css(template_dir, config.show)?;
let has_base_css = !base_css.is_empty();
let base_css_link = if has_base_css {
format!("<link rel=\"stylesheet\" href=\"{base_path}docs.css\">")
} else {
Expand Down Expand Up @@ -361,6 +382,17 @@ pub fn build_docs_with_cache(
let template_html = fs::read_to_string(template_dir.join("index.html"))
.map_err(|e| Error::Build(format!("Failed to read template: {e}")))?;
let regions = RegionSet::load(&config.regions, config_dir, template_html)?;
// Validate configured region names even when the selected presentation
// makes every shell region inactive. Authored page content is never filtered.
let regions = if config.show == ShowMode::Content {
RegionSet::load(
&Default::default(),
config_dir,
include_str!("../template/content.html").to_string(),
)?
} else {
regions
};
let component_script_index = discover_component_scripts(&component_sources)?;

// Step 3: Wipe the previous output and recreate the site root.
Expand All @@ -379,8 +411,8 @@ pub fn build_docs_with_cache(
// because we clean before processing — but that order means a
// failure leaves no output at all, never half-output).
if has_base_css {
fs::copy(&base_css_src, site_dir.join("docs.css"))
.map_err(|e| Error::Io(format!("Cannot copy docs.css: {e}")))?;
fs::write(site_dir.join("docs.css"), &base_css)
.map_err(|e| Error::Io(format!("Cannot write docs.css: {e}")))?;
}
if has_theme_css {
fs::write(site_dir.join("theme.css"), &custom_css)
Expand Down Expand Up @@ -453,7 +485,7 @@ pub fn build_docs_with_cache(
);

let template_script_path = template_dir.join("index.ts");
let template_script = if template_script_path.exists() {
let template_script = if config.show == ShowMode::All && template_script_path.exists() {
Some(
template_script_path
.canonicalize()
Expand Down Expand Up @@ -663,7 +695,7 @@ pub fn build_docs_with_cache(
projection_manifests: vec![projection_source.clone()],
..BuildOptions::default()
})
.map_err(|e| Error::Build(format!("{}: {e}", page.path)))?;
.map_err(|e| Error::Build(format!("{}: {}", page.path, e.chain_message())))?;
let preloads = generated_preloads.get().ok_or_else(|| {
Error::Build("Generated preload metadata was not published".to_string())
})?;
Expand Down Expand Up @@ -807,7 +839,7 @@ pub fn build_docs_with_cache(
projection_manifests: vec![projection_source],
..BuildOptions::default()
})
.map_err(|e| Error::Build(format!("404 build failed: {e}")))?;
.map_err(|e| Error::Build(format!("404 build failed: {}", e.chain_message())))?;
let preloads = generated_preloads
.get()
.ok_or_else(|| Error::Build("Generated preload metadata was not published".to_string()))?;
Expand Down
29 changes: 16 additions & 13 deletions crates/webui-press/src/bundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,8 @@ fn normalized_alias_target(config_dir: &Path, target: &str) -> String {
target.replace('\\', "/")
}

fn build_aliases(opts: &BundleOptions<'_>) -> BTreeMap<String, String> {
fn build_aliases(opts: &BundleOptions<'_>) -> Result<BTreeMap<String, String>> {
let config_dir = absolute_path(opts.config_dir)?;
let mut aliases: BTreeMap<String, String> = BTreeMap::new();
if let Some(node_modules) = opts.node_modules {
if let Some(path) = default_framework_alias(node_modules) {
Expand All @@ -1212,11 +1213,11 @@ fn build_aliases(opts: &BundleOptions<'_>) -> BTreeMap<String, String> {

if let Some(cfg) = opts.bundler_config {
for (from, to) in &cfg.alias {
aliases.insert(from.clone(), normalized_alias_target(opts.config_dir, to));
aliases.insert(from.clone(), normalized_alias_target(&config_dir, to));
}
}

aliases
Ok(aliases)
}

#[cfg(test)]
Expand All @@ -1238,8 +1239,8 @@ fn esbuild_args(
opts: &BundleOptions<'_>,
entry_files: &[(String, PathBuf)],
bundle_tmp: &Path,
) -> Vec<String> {
let aliases = build_aliases(opts);
) -> Result<Vec<String>> {
let aliases = build_aliases(opts)?;
let target = opts
.bundler_config
.and_then(|cfg| cfg.target.as_deref())
Expand Down Expand Up @@ -1275,7 +1276,7 @@ fn esbuild_args(
for (_, path) in entry_files {
args.push(path_for_js(path));
}
args
Ok(args)
}

fn esbuild_build_config(
Expand All @@ -1288,7 +1289,7 @@ fn esbuild_build_config(
let working_dir = absolute_path(opts.config_dir)?;
let site_dir = absolute_path(opts.site_dir)?;
let manifest_path = absolute_path(manifest_path)?;
let aliases = build_aliases(opts);
let aliases = build_aliases(opts)?;
let target = opts
.bundler_config
.and_then(|cfg| cfg.target.as_deref())
Expand Down Expand Up @@ -2051,7 +2052,7 @@ mod tests {
}

#[test]
fn esbuild_args_force_webui_decorator_semantics() {
fn esbuild_args_force_webui_decorator_semantics() -> TestResult {
let site_dir = Path::new("/site");
let config_dir = Path::new("/site/.webui-press");
let opts = BundleOptions {
Expand All @@ -2065,13 +2066,14 @@ mod tests {
config_dir,
content_dir: Path::new("/site"),
};
let args = esbuild_args(&opts, &[], Path::new("/tmp/webui-press-bundle"));
let args = esbuild_args(&opts, &[], Path::new("/tmp/webui-press-bundle"))?;

assert!(args.contains(&format!("--tsconfig-raw={WEBUI_TSCONFIG_RAW}")));
Ok(())
}

#[test]
fn esbuild_args_folds_webui_dev_flag_for_production_only() {
fn esbuild_args_folds_webui_dev_flag_for_production_only() -> TestResult {
fn opts<'a>(
site_dir: &'a Path,
config_dir: &'a Path,
Expand All @@ -2097,20 +2099,20 @@ mod tests {

// Production build: the flag is folded to `false` so the framework's
// dev-only diagnostics (and the module gating them) tree-shake out.
let prod = esbuild_args(&opts(site_dir, config_dir, false, None), &[], tmp);
let prod = esbuild_args(&opts(site_dir, config_dir, false, None), &[], tmp)?;
assert!(prod.contains(&define));

// Development build (`webui-press serve`): the flag is left undefined so
// the `typeof` guard defaults it to on and diagnostics run.
let dev = esbuild_args(&opts(site_dir, config_dir, true, None), &[], tmp);
let dev = esbuild_args(&opts(site_dir, config_dir, true, None), &[], tmp)?;
assert!(!dev.iter().any(|arg| arg.contains("__WEBUI_DEV__")));

// A user-supplied define wins: esbuild honors the last `--define` for a
// key, so our production default must be emitted before user defines.
let mut cfg = BundlerConfig::default();
cfg.define
.insert("__WEBUI_DEV__".to_string(), "true".to_string());
let overridden = esbuild_args(&opts(site_dir, config_dir, false, Some(&cfg)), &[], tmp);
let overridden = esbuild_args(&opts(site_dir, config_dir, false, Some(&cfg)), &[], tmp)?;
let ours = overridden.iter().position(|arg| arg == &define);
let theirs = overridden
.iter()
Expand All @@ -2121,6 +2123,7 @@ mod tests {
ours < theirs,
"framework default must precede the user override so esbuild's last-wins keeps the user's value",
);
Ok(())
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions crates/webui-press/src/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use serde_json::{Map, Value};
use crate::error::{Error, Result};
use crate::markdown::{render_markdown, Highlighter};
use crate::state::{load_render_states, merge_page_state, LoadedStates};
use crate::types::{DocsConfig, NavLink, PageDescriptor, SidebarItem, SidebarSection};
use crate::types::{DocsConfig, NavLink, PageDescriptor, ShowMode, SidebarItem, SidebarSection};

/// Normalize a config link (e.g. `/guide/intro/` or `/guide/intro`) to a
/// canonical URL path that includes the site's `base_path` prefix and
Expand Down Expand Up @@ -518,7 +518,7 @@ pub(crate) fn process_content_with_states(
fm.layout.unwrap_or_else(|| "doc".to_string())
};

if !is_home {
if !is_home || config.show == ShowMode::Content {
// Canonical page URL: every page is written as
// `<dir>/index.html`, so it is served with a trailing
// slash. In-page anchors need this exact path so they
Expand Down
Loading
Loading