diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 52465f077..cb723431b 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -166,6 +166,8 @@ impl Options { &url_builder, // Don't use strict mode in production yet false, + // Don't stabilise in production + false, ) .await?; shutdown.register_reloadable(&templates); diff --git a/crates/cli/src/commands/templates.rs b/crates/cli/src/commands/templates.rs index 75acce21d..39eb5e412 100644 --- a/crates/cli/src/commands/templates.rs +++ b/crates/cli/src/commands/templates.rs @@ -90,8 +90,10 @@ impl Options { let templates = templates_from_config( &template_config, &site_config, - &url_builder, // Use strict mode in template checks + &url_builder, + // Use strict mode in template checks true, + stabilise, ) .await?; let all_renders = templates.check_render(now, &rng)?; diff --git a/crates/cli/src/commands/worker.rs b/crates/cli/src/commands/worker.rs index a1eb0fcce..6dfcd27f1 100644 --- a/crates/cli/src/commands/worker.rs +++ b/crates/cli/src/commands/worker.rs @@ -58,6 +58,8 @@ impl Options { &url_builder, // Don't use strict mode on task workers for now false, + // Don't stabilise in production + false, ) .await?; diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 4925d9866..cd69a8acf 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -233,11 +233,12 @@ pub async fn templates_from_config( site_config: &SiteConfig, url_builder: &UrlBuilder, strict: bool, + stabilise: bool, ) -> Result { Templates::load( config.path.clone(), url_builder.clone(), - config.assets_manifest.clone(), + (!stabilise).then(|| config.assets_manifest.clone()), config.translations_path.clone(), site_config.templates_branding(), site_config.templates_features(), diff --git a/crates/handlers/src/test_utils.rs b/crates/handlers/src/test_utils.rs index f1859f352..1f54b7022 100644 --- a/crates/handlers/src/test_utils.rs +++ b/crates/handlers/src/test_utils.rs @@ -172,7 +172,7 @@ impl TestState { let templates = Templates::load( workspace_root.join("templates"), url_builder.clone(), - workspace_root.join("frontend/dist/manifest.json"), + Some(workspace_root.join("frontend/dist/manifest.json")), workspace_root.join("translations"), site_config.templates_branding(), site_config.templates_features(), diff --git a/crates/spa/src/vite.rs b/crates/spa/src/vite.rs index b488bea6b..e40233595 100644 --- a/crates/spa/src/vite.rs +++ b/crates/spa/src/vite.rs @@ -47,6 +47,48 @@ pub struct Manifest { inner: HashMap, } +impl Manifest { + /// Produce a sample manifest for use in reproducible sample renders. + #[must_use] + #[allow(clippy::missing_panics_doc)] + pub fn sample() -> Self { + let mut inner = HashMap::new(); + + for name in &[ + "src/shared.css", + "src/templates.css", + "src/main.tsx", + "src/swagger.ts", + ] { + inner.insert( + name.parse().unwrap(), + ManifestEntry { + name: None, + names: None, + src: None, + // Construct a fake but slightly plausible dummy asset name. + file: name + .replace('/', "__") + .replace('.', "-XXXXX.") + .replace(".tsx", ".js") + .replace(".ts", ".js") + .parse() + .unwrap(), + css: None, + assets: None, + is_entry: None, + is_dynamic_entry: None, + imports: None, + dynamic_imports: None, + integrity: None, + }, + ); + } + + Manifest { inner } + } +} + #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] enum FileType { Script, diff --git a/crates/templates/src/functions.rs b/crates/templates/src/functions.rs index c8943ebc0..82b79133a 100644 --- a/crates/templates/src/functions.rs +++ b/crates/templates/src/functions.rs @@ -434,10 +434,10 @@ impl Object for IncludeAsset { let path: &Utf8Path = path.into(); - let (main, imported) = self.vite_manifest.find_assets(path).map_err(|_e| { + let (main, imported) = self.vite_manifest.find_assets(path).map_err(|e| { Error::new( ErrorKind::InvalidOperation, - "Invalid assets manifest while calling function `include_asset`", + format!("Invalid assets manifest while calling function `include_asset` with path = {path:?}: {e}"), ) })?; diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 3c3ea7391..ec23bfdb4 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -72,7 +72,7 @@ pub struct Templates { url_builder: UrlBuilder, branding: SiteBranding, features: SiteFeatures, - vite_manifest_path: Utf8PathBuf, + vite_manifest_path: Option, translations_path: Utf8PathBuf, path: Utf8PathBuf, /// Whether template rendering is in strict mode (for testing, @@ -143,6 +143,11 @@ fn is_hidden(entry: &DirEntry) -> bool { impl Templates { /// Load the templates from the given config /// + /// # Parameters + /// + /// - `vite_manifest_path`: None if we are rendering resources for + /// reproducibility, in which case a dummy Vite manifest will be used. + /// /// # Errors /// /// Returns an error if the templates could not be loaded from disk. @@ -154,7 +159,7 @@ impl Templates { pub async fn load( path: Utf8PathBuf, url_builder: UrlBuilder, - vite_manifest_path: Utf8PathBuf, + vite_manifest_path: Option, translations_path: Utf8PathBuf, branding: SiteBranding, features: SiteFeatures, @@ -163,7 +168,7 @@ impl Templates { let (translator, environment) = Self::load_( &path, url_builder.clone(), - &vite_manifest_path, + vite_manifest_path.as_deref(), &translations_path, branding.clone(), features, @@ -186,7 +191,7 @@ impl Templates { async fn load_( path: &Utf8Path, url_builder: UrlBuilder, - vite_manifest_path: &Utf8Path, + vite_manifest_path: Option<&Utf8Path>, translations_path: &Utf8Path, branding: SiteBranding, features: SiteFeatures, @@ -196,13 +201,18 @@ impl Templates { let span = tracing::Span::current(); // Read the assets manifest from disk - let vite_manifest = tokio::fs::read(vite_manifest_path) - .await - .map_err(TemplateLoadingError::ViteManifestIO)?; + let vite_manifest = if let Some(vite_manifest_path) = vite_manifest_path { + let raw_vite_manifest = tokio::fs::read(vite_manifest_path) + .await + .map_err(TemplateLoadingError::ViteManifestIO)?; + + serde_json::from_slice::(&raw_vite_manifest) + .map_err(TemplateLoadingError::ViteManifest)? + } else { + ViteManifest::sample() + }; // Parse it - let vite_manifest: ViteManifest = - serde_json::from_slice(&vite_manifest).map_err(TemplateLoadingError::ViteManifest)?; let translations_path = translations_path.to_owned(); let translator = @@ -291,7 +301,7 @@ impl Templates { let (translator, environment) = Self::load_( &self.path, self.url_builder.clone(), - &self.vite_manifest_path, + self.vite_manifest_path.as_deref(), &self.translations_path, self.branding.clone(), self.features, @@ -506,23 +516,28 @@ mod tests { Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../frontend/dist/manifest.json"); let translations_path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../translations"); - let templates = Templates::load( - path, - url_builder, - vite_manifest_path, - translations_path, - branding, - features, - // Use strict mode in tests - true, - ) - .await - .unwrap(); - // Check the renders are deterministic, when given the same rng - let render1 = templates.check_render(now, &rng).unwrap(); - let render2 = templates.check_render(now, &rng).unwrap(); + for use_real_vite_manifest in [true, false] { + let templates = Templates::load( + path.clone(), + url_builder.clone(), + // Check both renders against the real vite manifest and the 'dummy' vite manifest + // used for reproducible renders. + use_real_vite_manifest.then_some(vite_manifest_path.clone()), + translations_path.clone(), + branding.clone(), + features, + // Use strict mode in tests + true, + ) + .await + .unwrap(); - assert_eq!(render1, render2); + // Check the renders are deterministic, when given the same rng + let render1 = templates.check_render(now, &rng).unwrap(); + let render2 = templates.check_render(now, &rng).unwrap(); + + assert_eq!(render1, render2); + } } }