Skip to content

Conversation

@mars
Copy link
Member

@mars mars commented Nov 21, 2025

A new approach to keeping config vars fresh at runtime 🍃

See rendered README doc:
Heroku Cloud Native Static Web Server Buildpack: Runtime App Configuration.

This will be a major version bump to v2.0.0, because all Release Phase integration is being dropped. This major version should not require any migration/upgrading docs, because this CNB project is still warned as experimental on the front-page README.

@mars mars requested a review from colincasey November 26, 2025 22:43
@dzuelke
Copy link
Contributor

dzuelke commented Nov 27, 2025

Would it not be much simpler (no re-rendering of the HTML document etc) to generate a .js file with the config var values that can then be included?

@mars
Copy link
Member Author

mars commented Dec 1, 2025

@dzuelke HTML data-* attributes are a standard interface within web browsers for access from JS and CSS. The interface is available whether working locally or deployed in the container.

This body data attribute injection is a technique that we've been working with in Heroku Front-end engineering, and have found our current proof-of-concept used for vibes.heroku.com is performing beautifully.

Problems with writing a JS file at runtime:

  • if the goal is no file injection, how does it get imported at runtime?
  • what does the developer experience look like, to have something that works locally and equally in deployment?
  • adding JS at runtime may create problems for sites' Content-Security-Policy or Subresource Integrity
  • the interface to access values and set defaults would be proprietary.

HTML Data attributes avoids these issues using a web-native interface which is agnostic to any JS module type or framework.

@mars mars marked this pull request as ready for review December 1, 2025 20:58
Comment on lines +42 to +43
.or(Some(PathBuf::from(DEFAULT_DOC_ROOT)))
.expect("should always default to some value");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.or(Some(PathBuf::from(DEFAULT_DOC_ROOT)))
.expect("should always default to some value");
.unwrap_or(PathBuf::from(DEFAULT_DOC_ROOT));

Comment on lines +48 to +49
.or(Some(DEFAULT_DOC_INDEX.to_owned()))
.expect("should always default to some value");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.or(Some(DEFAULT_DOC_INDEX.to_owned()))
.expect("should always default to some value");
.unwrap_or(DEFAULT_DOC_INDEX.to_string());

Comment on lines +82 to +83
.or(Some(true))
.expect("should always default to some boolean")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.or(Some(true))
.expect("should always default to some boolean")
.unwrap_or(true)

Ok(true) => {
eprintln!("Runtime configuration written into {file_path}");
}
_ => (),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silence for this branch?

pub fn env_as_html_data<S: BuildHasher>(
data: &HashMap<String, String, S>,
file_path: &PathBuf,
) -> Result<bool, Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'm having trouble following what this bool represents while reading through the code. Personally, I find enums or newtypes are a cheap way to clarify intent vs. boolean flags when it comes to both argument and return values. E.g.;

pub(crate) struct EnvInjected(bool);
// or
pub(crate) enum EnvInjected { Yes, No }

pub fn env_as_html_data<S: BuildHasher>(
    data: &HashMap<String, String, S>,
    file_path: &PathBuf,
) -> Result<EnvInjected, Error> { ... }

pub(crate) fn parse_html_and_inject_data<S: BuildHasher>(
data: &HashMap<String, String, S>,
html_bytes: &[u8],
) -> Result<(bool, String), Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment as above. Given how this value is propagating, maybe an enum makes more sense because it could simplify the false path which doesn't do anything with the html doc returned but is kind of forced to return it due to the use of the tuple (bool, String). E.g.;

pub(crate) enum EnvInjected { 
  Yes(String), 
  No 
}

fn match_html_body_and_inject_data<S: BuildHasher>(
data: &HashMap<String, String, S>,
node: &Handle,
) -> Result<(bool, bool), Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this boolean tuple is the most confusing 😅

Looking at the signature of the function, I have no clue what boolean 1 represents nor boolean 2 without reading the implementation and how the return values are used. I imagine this same will be true for anyone returning to this code in a week or two.

It also makes it hard to spot during review if someone changes this code in the future and accidentally confuses boolean 2 with boolean 1 in the return value.

Comment on lines +102 to +103
"ENV_AS_HTML_DATA_TARGET_FILES",
default_doc_path,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The environment variable (and usage in the env-as-html-data binary) implies that multiple files can be processed but only one is set here? How does this work when there are multiple HTML files in the static site? Is the user expected to manually configure each? Can they use a glob pattern?

std::process::exit(1);
}).map(|v| v.split(',').collect()).expect("should exit failure when none");

for file_path in file_paths {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may also be a good idea to only process files found within the app's workspace directory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants