mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-16 16:16:32 +03:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2c7051853 | ||
|
|
c0a0f8a9ad |
Generated
-40
@@ -71,12 +71,6 @@ version = "1.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
@@ -1340,17 +1334,6 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
|
||||
|
||||
[[package]]
|
||||
name = "socks"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -1543,7 +1526,6 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"socks",
|
||||
"url",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
@@ -1674,28 +1656,6 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
|
||||
@@ -20,7 +20,7 @@ regex = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
sha2 = "0.10"
|
||||
flate2 = { version = "1", default-features = false, features = ["zlib-rs"] }
|
||||
ureq = { version = "2", default-features = false, features = ["tls", "json", "socks-proxy"] }
|
||||
ureq = { version = "2", default-features = false, features = ["tls", "json"] }
|
||||
rustls-native-certs = "0.8"
|
||||
webpki-roots = "1"
|
||||
tiny_http = "0.12"
|
||||
|
||||
@@ -485,7 +485,6 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
fn round_trip(edit: bool, override_model: Option<&str>, background: Option<&str>) {
|
||||
let _proxy_lock = crate::http::PROXY_ENV_LOCK.lock().unwrap();
|
||||
let server = tiny_http::Server::http("127.0.0.1:0").unwrap();
|
||||
let api_base = format!("http://{}", server.server_addr());
|
||||
let temp = std::env::temp_dir().join(format!("impeccable-image-{}-{}", std::process::id(), server.server_addr().to_ip().unwrap().port()));
|
||||
|
||||
+4
-233
@@ -15,30 +15,18 @@
|
||||
//! fails to load, verifies against the bundled roots exactly as before.
|
||||
//! `SSL_CERT_FILE` / `SSL_CERT_DIR` stand in for the OS store, as they do
|
||||
//! for OpenSSL and curl; the bundled roots stay either way.
|
||||
//!
|
||||
//! The shared agent builder also honors `ALL_PROXY`, `HTTPS_PROXY`, and
|
||||
//! `HTTP_PROXY` (and their lowercase forms) so `update` and `install` work
|
||||
//! behind a corporate proxy (#823). Live-mode localhost HTTP does not use
|
||||
//! this builder. The `socks-proxy` feature is enabled because ureq 2.x
|
||||
//! prefers `ALL_PROXY`, which is often `socks5://`. We opt in on this
|
||||
//! builder only, not globally via ureq's `proxy-from-env` feature.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) static PROXY_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use ureq::rustls::pki_types::CertificateDer;
|
||||
use ureq::rustls::{self, ClientConfig, RootCertStore};
|
||||
|
||||
/// `ureq::AgentBuilder::new()` with the engine's trust store installed and
|
||||
/// env proxy vars honored. Every HTTPS call site builds its agent from this;
|
||||
/// the plain-HTTP calls to the live server on localhost do not use it.
|
||||
/// `ureq::AgentBuilder::new()` with the engine's trust store installed.
|
||||
/// Every HTTPS call site builds its agent from this; the plain-HTTP calls
|
||||
/// to the live server on localhost do not need it.
|
||||
pub fn agent_builder() -> ureq::AgentBuilder {
|
||||
ureq::AgentBuilder::new()
|
||||
.tls_config(tls_config())
|
||||
.try_proxy_from_env(true)
|
||||
ureq::AgentBuilder::new().tls_config(tls_config())
|
||||
}
|
||||
|
||||
fn tls_config() -> Arc<ClientConfig> {
|
||||
@@ -111,223 +99,6 @@ tB0WGTOG3QIgdJa8gBPU9Y6WsrursItsnUeGTYHKDCZZ6MjlekLFuoc=
|
||||
fn agent_builds_from_this_hosts_store() {
|
||||
// Runs the real rustls-native-certs load: it must not panic, and the
|
||||
// shared config must be accepted by a ureq agent.
|
||||
let _lock = PROXY_ENV_LOCK.lock().unwrap();
|
||||
let _agent = agent_builder().build();
|
||||
}
|
||||
|
||||
struct ProxyEnvGuard {
|
||||
saved: Vec<(String, Option<String>)>,
|
||||
}
|
||||
|
||||
impl ProxyEnvGuard {
|
||||
fn set(vars: &[(&str, Option<&str>)]) -> Self {
|
||||
let saved = vars
|
||||
.iter()
|
||||
.map(|(key, _)| (key.to_string(), std::env::var(key).ok()))
|
||||
.collect();
|
||||
for (key, value) in vars {
|
||||
match value {
|
||||
// SAFETY: PROXY_ENV_LOCK serializes every test that
|
||||
// reads or writes these process-global proxy vars.
|
||||
Some(v) => unsafe { std::env::set_var(key, v) },
|
||||
None => unsafe { std::env::remove_var(key) },
|
||||
}
|
||||
}
|
||||
Self { saved }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProxyEnvGuard {
|
||||
fn drop(&mut self) {
|
||||
for (key, value) in &self.saved {
|
||||
match value {
|
||||
// SAFETY: same lock as set(); restore before unlock.
|
||||
Some(v) => unsafe { std::env::set_var(key, v) },
|
||||
None => unsafe { std::env::remove_var(key) },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_until(
|
||||
listener: std::net::TcpListener,
|
||||
mut handle: impl FnMut(&mut std::net::TcpStream) -> bool,
|
||||
) {
|
||||
use std::time::Duration;
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.expect("nonblocking proxy listener");
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(5);
|
||||
while std::time::Instant::now() < deadline {
|
||||
let Ok((mut stream, _)) = listener.accept() else {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
continue;
|
||||
};
|
||||
let _ = stream.set_nonblocking(false);
|
||||
let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(2)));
|
||||
let _ = stream.set_write_timeout(Some(std::time::Duration::from_secs(2)));
|
||||
if handle(&mut stream) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_honors_http_proxy_from_env() {
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::time::Duration;
|
||||
|
||||
let _lock = PROXY_ENV_LOCK.lock().unwrap();
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind proxy listener");
|
||||
let proxy_addr = listener.local_addr().expect("proxy listener addr");
|
||||
|
||||
let request = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
|
||||
let request_for_thread = request.clone();
|
||||
let handle = std::thread::spawn(move || {
|
||||
accept_until(listener, |stream| {
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = stream.read(&mut buf).unwrap_or(0);
|
||||
let chunk = &buf[..n];
|
||||
if chunk.is_empty()
|
||||
|| !String::from_utf8_lossy(chunk).contains("proxy-test.invalid")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
request_for_thread.lock().unwrap().extend_from_slice(chunk);
|
||||
let _ = stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok");
|
||||
true
|
||||
});
|
||||
});
|
||||
|
||||
let proxy_url = format!("http://127.0.0.1:{}", proxy_addr.port());
|
||||
let _env_guard = ProxyEnvGuard::set(&[
|
||||
("ALL_PROXY", None),
|
||||
("all_proxy", None),
|
||||
("HTTPS_PROXY", None),
|
||||
("https_proxy", None),
|
||||
("HTTP_PROXY", Some(&proxy_url)),
|
||||
("http_proxy", Some(&proxy_url)),
|
||||
]);
|
||||
|
||||
let agent = agent_builder()
|
||||
.timeout(Duration::from_secs(2))
|
||||
.build();
|
||||
let response = agent.get("http://proxy-test.invalid/").call();
|
||||
assert!(response.is_ok(), "expected proxy-routed GET to succeed");
|
||||
|
||||
handle.join().expect("proxy thread");
|
||||
|
||||
let request_bytes = request.lock().unwrap().clone();
|
||||
let request_text = String::from_utf8_lossy(&request_bytes);
|
||||
assert!(
|
||||
request_text.contains("proxy-test.invalid"),
|
||||
"proxy should receive request for target host, got: {request_text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_honors_socks5_all_proxy_from_env() {
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::time::Duration;
|
||||
|
||||
fn socks5_then_http(stream: &mut std::net::TcpStream) -> Option<Vec<u8>> {
|
||||
fn read_n(stream: &mut std::net::TcpStream, n: usize) -> Option<Vec<u8>> {
|
||||
let mut buf = vec![0u8; n];
|
||||
stream.read_exact(&mut buf).ok()?;
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
let greet = read_n(stream, 2)?;
|
||||
if greet[0] != 5 {
|
||||
return None;
|
||||
}
|
||||
let _ = read_n(stream, greet[1] as usize)?;
|
||||
stream.write_all(&[0x05, 0x00]).ok()?;
|
||||
|
||||
let req = read_n(stream, 4)?;
|
||||
if req[0] != 5 || req[1] != 1 {
|
||||
return None;
|
||||
}
|
||||
match req[3] {
|
||||
1 => {
|
||||
let _ = read_n(stream, 6)?;
|
||||
}
|
||||
3 => {
|
||||
let len = read_n(stream, 1)?;
|
||||
let _ = read_n(stream, len[0] as usize + 2)?;
|
||||
}
|
||||
4 => {
|
||||
let _ = read_n(stream, 18)?;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
stream
|
||||
.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
|
||||
.ok()?;
|
||||
|
||||
let mut chunk = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
let n = stream.read(&mut buf).ok()?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
chunk.extend_from_slice(&buf[..n]);
|
||||
if String::from_utf8_lossy(&chunk).contains("proxy-test.invalid") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !String::from_utf8_lossy(&chunk).contains("proxy-test.invalid") {
|
||||
return None;
|
||||
}
|
||||
let _ = stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok");
|
||||
Some(chunk)
|
||||
}
|
||||
|
||||
let _lock = PROXY_ENV_LOCK.lock().unwrap();
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind socks listener");
|
||||
let proxy_addr = listener.local_addr().expect("socks listener addr");
|
||||
|
||||
let request = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
|
||||
let request_for_thread = request.clone();
|
||||
let handle = std::thread::spawn(move || {
|
||||
accept_until(listener, |stream| {
|
||||
if let Some(chunk) = socks5_then_http(stream) {
|
||||
*request_for_thread.lock().unwrap() = chunk;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let proxy_url = format!("socks5://127.0.0.1:{}", proxy_addr.port());
|
||||
let _env_guard = ProxyEnvGuard::set(&[
|
||||
("ALL_PROXY", Some(&proxy_url)),
|
||||
("all_proxy", Some(&proxy_url)),
|
||||
("HTTPS_PROXY", None),
|
||||
("https_proxy", None),
|
||||
("HTTP_PROXY", None),
|
||||
("http_proxy", None),
|
||||
]);
|
||||
|
||||
let agent = agent_builder()
|
||||
.timeout(Duration::from_secs(2))
|
||||
.build();
|
||||
let response = agent.get("http://proxy-test.invalid/").call();
|
||||
assert!(response.is_ok(), "expected SOCKS5-routed GET to succeed");
|
||||
|
||||
handle.join().expect("socks thread");
|
||||
|
||||
let request_bytes = request.lock().unwrap().clone();
|
||||
let request_text = String::from_utf8_lossy(&request_bytes);
|
||||
assert!(
|
||||
request_text.contains("proxy-test.invalid"),
|
||||
"SOCKS proxy should receive request for target host, got: {request_text:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1093,12 +1093,26 @@ fn extract_inner_by_attr(text: &str, attr: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Drop leading and trailing blank lines while keeping a single empty line
|
||||
/// when the inner text is only whitespace.
|
||||
fn trim_surrounding_blank_lines(lines: Vec<String>) -> Vec<String> {
|
||||
let mut start = 0usize;
|
||||
let mut end = lines.len();
|
||||
while end - start > 1 && trim(&lines[start]).is_empty() {
|
||||
start += 1;
|
||||
}
|
||||
while end - start > 1 && trim(&lines[end - 1]).is_empty() {
|
||||
end -= 1;
|
||||
}
|
||||
lines[start..end].to_vec()
|
||||
}
|
||||
|
||||
/// JS: extractOriginal(lines, block)
|
||||
fn extract_original(lines: &[String], block: &MarkerBlock) -> Vec<String> {
|
||||
let text = strip_style_and_join(lines, block);
|
||||
match extract_inner_by_attr(&text, "data-impeccable-variant=\"original\"") {
|
||||
None => Vec::new(),
|
||||
Some(inner) => inner.split('\n').map(String::from).collect(),
|
||||
Some(inner) => trim_surrounding_blank_lines(inner.split('\n').map(String::from).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,13 +1127,7 @@ fn extract_variant(
|
||||
&text,
|
||||
&format!("data-impeccable-variant=\"{}\"", variant_num),
|
||||
)?;
|
||||
let mut result: Vec<String> = inner.split('\n').map(String::from).collect();
|
||||
while result.len() > 1 && trim(&result[0]).is_empty() {
|
||||
result.remove(0);
|
||||
}
|
||||
while result.len() > 1 && trim(result.last().unwrap()).is_empty() {
|
||||
result.pop();
|
||||
}
|
||||
let result = trim_surrounding_blank_lines(inner.split('\n').map(String::from).collect());
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -1249,6 +1257,55 @@ fn find_session_file(id: &str, cwd: &str) -> Option<(String, String, Vec<String>
|
||||
Some((file, content, lines))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod discard_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn discard_restores_the_original_without_blank_lines_around_it() {
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0);
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"impeccable-discard-{}-{nanos}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let file = dir.join("Login.tsx");
|
||||
let src = [
|
||||
" </div>",
|
||||
" <div data-impeccable-variants=\"ab12cd34\" data-impeccable-variant-count=\"3\" style={{ display: \"contents\" }}>",
|
||||
" {/* impeccable-variants-start ab12cd34 */}",
|
||||
" <style data-impeccable-css=\"ab12cd34\">{`",
|
||||
" @scope ([data-impeccable-variant=\"1\"]) { :scope > .x { color: red; } }",
|
||||
" `}</style>",
|
||||
" {/* Original */}",
|
||||
" <div data-impeccable-variant=\"original\">",
|
||||
" <Bar title=\"Log in\" />",
|
||||
" </div>",
|
||||
" {/* Variants: insert below this line */}",
|
||||
" <div data-impeccable-variant=\"1\">",
|
||||
" <Bar title=\"Log in\" />",
|
||||
" </div>",
|
||||
" {/* impeccable-variants-end ab12cd34 */}",
|
||||
" </div>",
|
||||
" </AuthPage>",
|
||||
];
|
||||
let lines: Vec<String> = src.iter().map(|s| s.to_string()).collect();
|
||||
let path = file.to_string_lossy().into_owned();
|
||||
std::fs::write(&file, lines.join("\n")).unwrap();
|
||||
handle_discard_unlocked("ab12cd34", &lines, &path).unwrap();
|
||||
let out = std::fs::read_to_string(&file).unwrap();
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
assert_eq!(
|
||||
out,
|
||||
" </div>\n <Bar title=\"Log in\" />\n </AuthPage>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod bake_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"files": {
|
||||
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"index.html\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
|
||||
".impeccable/live/config.json": "{\n \"files\": [\"index.html\", \"public/**/*.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
|
||||
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
|
||||
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"files": {
|
||||
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"index.html\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
|
||||
".impeccable/live/config.json": "{\n \"files\": [\"index.html\", \"public/**/*.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
|
||||
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
|
||||
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"files": {
|
||||
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"src/App.jsx\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
|
||||
".impeccable/live/config.json": "{\n \"files\": [\"index.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
|
||||
"src/App.jsx": "export default function App() {\n return (\n <main className=\"page\">\n\n <h1 className=\"hero-title\">Vite Fixture</h1>\n\n <p className=\"hero-hook\">Minimal React tree for oracle live-mode goldens.</p>\n <section id=\"features\" className=\"feature-grid\">\n <article className=\"feature-card\">One</article>\n <article className=\"feature-card\">Two</article>\n </section>\n <ul className=\"item-list\">\n {items.map((item) => (\n <li key={item.id} className=\"item-row\">{item.title}</li>\n ))}\n </ul>\n </main>\n );\n}\n\nconst items = [\n { id: 1, title: 'First' },\n { id: 2, title: 'Second' },\n];\n",
|
||||
"src/App.jsx": "export default function App() {\n return (\n <main className=\"page\">\n <h1 className=\"hero-title\">Vite Fixture</h1>\n <p className=\"hero-hook\">Minimal React tree for oracle live-mode goldens.</p>\n <section id=\"features\" className=\"feature-grid\">\n <article className=\"feature-card\">One</article>\n <article className=\"feature-card\">Two</article>\n </section>\n <ul className=\"item-list\">\n {items.map((item) => (\n <li key={item.id} className=\"item-row\">{item.title}</li>\n ))}\n </ul>\n </main>\n );\n}\n\nconst items = [\n { id: 1, title: 'First' },\n { id: 2, title: 'Second' },\n];\n",
|
||||
"src/main.jsx": "import { createRoot } from 'react-dom/client';\nimport App from './App.jsx';\n\ncreateRoot(document.getElementById('root')).render(<App />);\n",
|
||||
"src/Panel.tsx": "type PanelProps = { title: string; children?: React.ReactNode };\n\nexport function Panel({ title, children }: PanelProps) {\n return (\n <section className=\"panel\">\n <header className=\"panel-header\">\n <h2 className=\"panel-title\">{title}</h2>\n </header>\n <div className=\"panel-body\">{children}</div>\n </section>\n );\n}\n"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"src/routes/+page.svelte\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
|
||||
".impeccable/live/config.json": "{\n \"files\": [\"src/app.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
|
||||
"src/app.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>SvelteKit Fixture</title>\n %sveltekit.head%\n </head>\n <body data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">%sveltekit.body%</div>\n </body>\n</html>\n",
|
||||
"src/routes/+page.svelte": "<script>\n let title = 'SvelteKit Fixture';\n let expenses = [\n { id: 1, label: 'Coffee', amount: 3 },\n { id: 2, label: 'Lunch', amount: 12 },\n ];\n</script>\n\n<main class=\"page\">\n\n <h1 class=\"hero-title\">{title}</h1>\n\n <p class=\"hero-hook\">Minimal SvelteKit route for oracle live-mode goldens.</p>\n <ul class=\"expense-list\">\n {#each expenses as expense (expense.id)}\n <li class=\"expense-row\">{expense.label}: {expense.amount}</li>\n {/each}\n </ul>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n</main>\n\n<style>\n .hero-title { font-size: 2rem; }\n .expense-list { list-style: none; padding: 0; }\n .expense-row { padding: 4px 0; }\n</style>\n"
|
||||
"src/routes/+page.svelte": "<script>\n let title = 'SvelteKit Fixture';\n let expenses = [\n { id: 1, label: 'Coffee', amount: 3 },\n { id: 2, label: 'Lunch', amount: 12 },\n ];\n</script>\n\n<main class=\"page\">\n <h1 class=\"hero-title\">{title}</h1>\n <p class=\"hero-hook\">Minimal SvelteKit route for oracle live-mode goldens.</p>\n <ul class=\"expense-list\">\n {#each expenses as expense (expense.id)}\n <li class=\"expense-row\">{expense.label}: {expense.amount}</li>\n {/each}\n </ul>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n</main>\n\n<style>\n .hero-title { font-size: 2rem; }\n .expense-list { list-style: none; padding: 0; }\n .expense-row { padding: 4px 0; }\n</style>\n"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user