dynamic/static embeds

This commit is contained in:
Robin Appelman 2023-04-12 21:08:09 +02:00
commit cb56c80555
22 changed files with 9068 additions and 665 deletions

View file

@ -1,11 +1,9 @@
use std::env::args;
mod script;
fn main() {
tracing_subscriber::fmt::init();
let path = args().skip(1).next().unwrap();
let output = script::bundle_script(&path);
let output = demostf_build::bundle_script(&path);
// println!("{output}")
}

View file

@ -1,30 +1,23 @@
mod script;
mod style;
pub use demostf_build_bundlers::bundle_raw;
pub use demostf_build_bundlers::bundle_script;
pub use demostf_build_bundlers::bundle_style;
pub use demostf_build_derive::Asset;
use rand::{distributions::Alphanumeric, Rng};
use std::borrow::Cow;
use const_fnv1a_hash::fnv1a_hash_str_32;
pub use script::bundle_script;
pub use style::bundle_style;
#[macro_export]
macro_rules! save_asset {
($name:expr, $val:expr) => {
let val = $val;
let out_dir = std::env::var("OUT_DIR").unwrap();
std::fs::write(format!("{out_dir}/{}", $name), &val).expect("failed to write asset");
let hash = demostf_build::hash(&val);
std::fs::write(format!("{out_dir}/{}.hash", $name), hash)
.expect("failed to write asset hash");
};
pub trait Asset {
fn mime() -> &'static str;
fn cache_buster() -> Cow<'static, str>;
fn etag() -> &'static str;
fn content() -> Cow<'static, [u8]>;
fn url() -> Cow<'static, str>;
fn route() -> &'static str;
}
pub fn hash(data: &str) -> String {
format!("{:x}", fnv1a_hash_str_32(data))
}
fn guess_mime(path: &str) -> (&'static str, bool) {
match path.split('.').last().unwrap() {
"svg" => ("image/svg+xml", false),
"png" => ("image/png", true),
ext => panic!("no mimetype known for {ext}"),
}
pub fn random_cache_buster() -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect()
}

View file

@ -1,193 +0,0 @@
use anyhow::Error;
use jsx_dom_expressions::TransformVisitor;
use std::collections::HashMap;
use std::io::Write;
use std::sync::Arc;
use swc_atoms::js_word;
use swc_bundler::{Bundler, Load, ModuleData, ModuleRecord};
use swc_common::comments::NoopComments;
use swc_common::sync::Lrc;
use swc_common::{
errors::{ColorConfig, Handler},
FileName, Mark, SourceMap, Span, GLOBALS,
};
use swc_ecma_ast::*;
use swc_ecma_codegen::text_writer::{omit_trailing_semi, JsWriter, WriteJs};
use swc_ecma_codegen::Emitter;
use swc_ecma_loader::resolvers::lru::CachingResolver;
use swc_ecma_loader::resolvers::node::NodeModulesResolver;
use swc_ecma_loader::TargetEnv;
use swc_ecma_parser::{parse_file_as_module, Syntax, TsConfig};
use swc_ecma_transforms_base::fixer::fixer;
use swc_ecma_transforms_base::hygiene::hygiene;
use swc_ecma_transforms_typescript::strip;
use swc_ecma_visit::{as_folder, FoldWith};
pub fn bundle_script(script: &str) -> String {
#[cfg(debug_assertions)]
let minify = false;
#[cfg(not(debug_assertions))]
let minify = true;
let output = GLOBALS.set(&Default::default(), || {
let cm = Arc::<SourceMap>::default();
let globals = &Box::default();
let mut bundler = Bundler::new(
globals,
cm.clone(),
Loader { cm: cm.clone() },
CachingResolver::new(
4096,
NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true),
),
swc_bundler::Config {
// disable_hygiene: !minify,
// disable_dce: !minify,
// disable_fixer: !minify,
// disable_inliner: !minify,
..Default::default()
},
Box::new(Hook),
);
let mut entries = HashMap::new();
entries.insert(
script.trim_end_matches(".js").to_string(),
FileName::Real(script.into()),
);
let modules = bundler.bundle(entries).expect("failed to bundle");
let mut buf = vec![];
for module in modules {
write(minify, cm.clone(), &module.module, &mut buf);
}
buf
});
String::from_utf8(output).expect("invalid utf8 bundle")
}
fn write<W: Write>(minify: bool, cm: Arc<SourceMap>, module: &Module, out: W) {
let wr = JsWriter::new(cm.clone(), "\n", out, None);
let mut emitter = Emitter {
cfg: swc_ecma_codegen::Config {
minify,
..Default::default()
},
cm: cm.clone(),
comments: None,
wr: if minify {
Box::new(omit_trailing_semi(wr)) as Box<dyn WriteJs>
} else {
Box::new(wr) as Box<dyn WriteJs>
},
};
emitter.emit_module(module).unwrap();
}
struct Hook;
impl swc_bundler::Hook for Hook {
fn get_import_meta_props(
&self,
span: Span,
module_record: &ModuleRecord,
) -> Result<Vec<KeyValueProp>, Error> {
let file_name = module_record.file_name.to_string();
Ok(vec![
KeyValueProp {
key: PropName::Ident(Ident::new(js_word!("url"), span)),
value: Box::new(Expr::Lit(Lit::Str(Str {
span,
raw: None,
value: file_name.into(),
}))),
},
KeyValueProp {
key: PropName::Ident(Ident::new(js_word!("main"), span)),
value: Box::new(if module_record.is_entry {
Expr::Member(MemberExpr {
span,
obj: Box::new(Expr::MetaProp(MetaPropExpr {
span,
kind: MetaPropKind::ImportMeta,
})),
prop: MemberProp::Ident(Ident::new(js_word!("main"), span)),
})
} else {
Expr::Lit(Lit::Bool(Bool { span, value: false }))
}),
},
])
}
}
pub struct Loader {
pub cm: Lrc<SourceMap>,
}
impl Load for Loader {
fn load(&self, f: &FileName) -> Result<ModuleData, Error> {
let fm = match f {
FileName::Real(path) => self.cm.load_file(path)?,
_ => unreachable!(),
};
let module = parse_file_as_module(
&fm,
Syntax::Typescript(TsConfig {
tsx: true,
..TsConfig::default()
}),
EsVersion::Es5,
None,
&mut vec![],
)
.unwrap_or_else(|err| {
let handler =
Handler::with_tty_emitter(ColorConfig::Always, false, false, Some(self.cm.clone()));
err.into_diagnostic(&handler).emit();
panic!("failed to parse")
});
let top_level_mark = Mark::new();
let module = module
.fold_with(&mut strip(top_level_mark))
.fold_with(&mut as_folder(TransformVisitor::new(
jsx_dom_expressions::config::Config {
module_name: "solid-js/web".to_string(),
builtins: vec![
"For".into(),
"Show".into(),
"Switch".into(),
"Match".into(),
"Suspense".into(),
"SuspenseList".into(),
"Portal".into(),
"Index".into(),
"Dynamic".into(),
"ErrorBoundary".into(),
],
..Default::default()
},
NoopComments,
)))
.fold_with(&mut hygiene())
.fold_with(&mut fixer(None));
// if let FileName::Real(path) = &f {
// let mut out = vec![];
// write(false, self.cm.clone(), &module, &mut out);
// let mut path = path.clone();
// path.set_extension("c.js");
// std::fs::write(path, out).unwrap();
// }
Ok(ModuleData {
fm,
module,
helpers: Default::default(),
})
}
}

View file

@ -1,80 +0,0 @@
use crate::guess_mime;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use lightningcss::bundler::{Bundler, FileProvider};
use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions};
use lightningcss::targets::Browsers;
use lightningcss::values::url::Url;
use lightningcss::visit_types;
use lightningcss::visitor::{Visit, VisitTypes, Visitor};
use std::convert::Infallible;
use std::fs::read;
use std::path::Path;
pub fn bundle_style(style: &str) -> String {
// todo build time?
let fs = FileProvider::new();
let mut bundler = Bundler::new(
&fs,
None,
ParserOptions {
nesting: true,
..ParserOptions::default()
},
);
let mut stylesheet = bundler
.bundle(Path::new(style))
.expect("failed to bundle css");
let browsers =
Browsers::from_browserslist(["last 2 versions"]).expect("failed to parse browserlist");
stylesheet
.minify(MinifyOptions {
targets: browsers.clone(),
..MinifyOptions::default()
})
.expect("failed to minify css");
#[cfg(debug_assertions)]
let minify = false;
#[cfg(not(debug_assertions))]
let minify = true;
stylesheet.visit(&mut InlineUrlVisitor).unwrap();
stylesheet
.to_css(PrinterOptions {
targets: browsers,
minify,
..PrinterOptions::default()
})
.expect("failed to output css")
.code
}
struct InlineUrlVisitor;
impl<'i> Visitor<'i> for InlineUrlVisitor {
type Error = Infallible;
const TYPES: VisitTypes = visit_types!(URLS);
fn visit_url(&mut self, url: &mut Url<'i>) -> Result<(), Self::Error> {
if let Some(path) = url.url.strip_prefix("inline://") {
let content = read(path).unwrap_or_else(|e| {
eprintln!("Failed to write inline file {path}: {e}");
panic!("Failed to inline");
});
let (mime, encode) = guess_mime(path);
if encode {
let encoded = STANDARD.encode(content);
url.url = format!("data:{mime};base64,{encoded}").into();
} else {
let content = String::from_utf8(content).expect("invalid utf8");
let encoded = urlencoding::encode(&content);
url.url = format!("data:{mime},{encoded}").into();
}
}
Ok(())
}
}