Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@
"Bash(tree:*)",
"Bash(wc:*)",
"Bash(which:*)",

"Bash(npm ci:*)",
"Bash(npm run:*)",
"Bash(npm test:*)",

"Bash(cargo build:*)",
"Bash(cargo check:*)",
"Bash(cargo clippy:*)",
"Bash(cargo fmt:*)",
"Bash(cargo metadata:*)",
"Bash(cargo test:*)",

"Bash(git branch:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git status:*)"
"Bash(git status:*)",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__new_page",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__performance_stop_trace",
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__evaluate_script"
]
}
}
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,10 @@ IntegrationRegistration::builder(ID)
.build()
```

- Integration IDs match JS directory names: `prebid`, `lockr`, `permutive`, `datadome`, `didomi`, `testlight`.
- Integration IDs match JS directory names: `prebid` (deferred), `lockr`, `permutive`, `datadome`, `didomi`, `testlight`.
- `creative` is JS-only (no Rust registration); `nextjs`, `aps`, `adserver_mock` are Rust-only.
- `IntegrationRegistry::js_module_ids()` maps registered integrations to JS module names.
- Integrations opt into deferred loading via `.with_deferred_js()` on the registration builder. Deferred modules are served as separate `<script defer>` tags instead of being concatenated into the main bundle.
- `IntegrationRegistry::js_module_ids_immediate()` returns modules for the main bundle; `js_module_ids_deferred()` returns modules loaded with `defer`.

## JS Build Pipeline

Expand Down
2 changes: 1 addition & 1 deletion crates/common/src/creative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ pub fn rewrite_creative_html(settings: &Settings, markup: &str) -> String {
let injected = injected_ts_creative.clone();
move |el| {
if !injected.get() {
let script_tag = tsjs::tsjs_script_tag_all();
let script_tag = tsjs::tsjs_unified_script_tag();
el.prepend(&script_tag, ContentType::Html);
injected.set(true);
}
Expand Down
11 changes: 7 additions & 4 deletions crates/common/src/html_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,13 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
for insert in integrations.head_inserts(&ctx) {
snippet.push_str(&insert);
}
// Then inject the TSJS bundle — its top-level init code can now
// read the config that was set by the inline scripts above.
let module_ids = integrations.js_module_ids();
snippet.push_str(&tsjs::tsjs_script_tag(&module_ids));
// Main bundle: core + non-deferred integrations (synchronous).
let immediate_ids = integrations.js_module_ids_immediate();
snippet.push_str(&tsjs::tsjs_script_tag(&immediate_ids));
// Deferred bundles: large modules like prebid loaded after
// HTML parsing completes. Empty when none are enabled.
let deferred_ids = integrations.js_module_ids_deferred();
snippet.push_str(&tsjs::tsjs_deferred_script_tags(&deferred_ids));
el.prepend(&snippet, ContentType::Html);
injected_tsjs.set(true);
}
Expand Down
11 changes: 8 additions & 3 deletions crates/common/src/integrations/prebid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ pub fn register(settings: &Settings) -> Option<IntegrationRegistration> {
.with_proxy(integration.clone())
.with_attribute_rewriter(integration.clone())
.with_head_injector(integration)
.with_deferred_js()
.build(),
)
}
Expand Down Expand Up @@ -320,7 +321,7 @@ impl IntegrationHeadInjector for PrebidIntegration {
.replace("</", "<\\/");

vec![format!(
r#"<script>window.__tsjs_prebid={config_json};</script>"#
r#"<script>window.pbjs=window.pbjs||{{}};window.pbjs.que=window.pbjs.que||[];window.pbjs.cmd=window.pbjs.cmd||[];window.__tsjs_prebid={config_json};</script>"#
)]
}
}
Expand Down Expand Up @@ -1232,13 +1233,17 @@ template = "{{client_ip}}:{{user_agent}}"
"Unified bundle should be injected"
);
assert!(
!processed.contains("prebid.min.js"),
"Prebid script should be removed when auto-config is enabled"
!processed.contains("cdn.prebid.org/prebid.min.js"),
"Publisher prebid script should be removed when auto-config is enabled"
);
assert!(
!processed.contains("cdn.prebid.org/prebid.js"),
"Prebid preload should be removed when auto-config is enabled"
);
assert!(
processed.contains("tsjs-prebid.min.js"),
"Deferred prebid bundle should be injected"
);
}

#[test]
Expand Down
139 changes: 139 additions & 0 deletions crates/common/src/integrations/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ pub trait IntegrationHeadInjector: Send + Sync {
/// Registration payload returned by integration builders.
pub struct IntegrationRegistration {
pub integration_id: &'static str,
pub js_deferred: bool,
pub proxies: Vec<Arc<dyn IntegrationProxy>>,
pub attribute_rewriters: Vec<Arc<dyn IntegrationAttributeRewriter>>,
pub script_rewriters: Vec<Arc<dyn IntegrationScriptRewriter>>,
Expand All @@ -413,6 +414,7 @@ impl IntegrationRegistrationBuilder {
Self {
registration: IntegrationRegistration {
integration_id,
js_deferred: false,
proxies: Vec::new(),
attribute_rewriters: Vec::new(),
script_rewriters: Vec::new(),
Expand Down Expand Up @@ -458,6 +460,14 @@ impl IntegrationRegistrationBuilder {
self
}

/// Mark this integration's JS module for deferred loading via
/// `<script defer>` instead of the main synchronous bundle.
#[must_use]
pub fn with_deferred_js(mut self) -> Self {
self.registration.js_deferred = true;
self
}

#[must_use]
pub fn build(self) -> IntegrationRegistration {
self.registration
Expand All @@ -476,6 +486,7 @@ struct IntegrationRegistryInner {

// Metadata for introspection
routes: Vec<(IntegrationEndpoint, &'static str)>,
deferred_js_ids: Vec<&'static str>,
html_rewriters: Vec<Arc<dyn IntegrationAttributeRewriter>>,
script_rewriters: Vec<Arc<dyn IntegrationScriptRewriter>>,
html_post_processors: Vec<Arc<dyn IntegrationHtmlPostProcessor>>,
Expand All @@ -495,6 +506,7 @@ impl Default for IntegrationRegistryInner {
script_rewriters: Vec::new(),
html_post_processors: Vec::new(),
head_injectors: Vec::new(),
deferred_js_ids: Vec::new(),
}
}
}
Expand Down Expand Up @@ -600,6 +612,9 @@ impl IntegrationRegistry {
inner
.head_injectors
.extend(registration.head_injectors.into_iter());
if registration.js_deferred {
inner.deferred_js_ids.push(registration.integration_id);
}
}
}

Expand Down Expand Up @@ -789,6 +804,30 @@ impl IntegrationRegistry {
ids
}

/// Return JS module IDs for the main (synchronous) bundle, excluding
/// modules registered with [`with_deferred_js`](IntegrationRegistrationBuilder::with_deferred_js).
#[must_use]
pub fn js_module_ids_immediate(&self) -> Vec<&'static str> {
self.js_module_ids()
.into_iter()
.filter(|id| !self.inner.deferred_js_ids.contains(id))
.collect()
}

/// Return JS module IDs that should be loaded with `<script defer>`.
///
/// Only includes modules registered with
/// [`with_deferred_js`](IntegrationRegistrationBuilder::with_deferred_js)
/// that are actually enabled. Returns an empty vec when no deferred
/// integrations are configured.
#[must_use]
pub fn js_module_ids_deferred(&self) -> Vec<&'static str> {
self.js_module_ids()
.into_iter()
.filter(|id| self.inner.deferred_js_ids.contains(id))
.collect()
}

#[cfg(test)]
#[must_use]
pub fn from_rewriters(
Expand All @@ -807,6 +846,7 @@ impl IntegrationRegistry {
script_rewriters,
html_post_processors: Vec::new(),
head_injectors: Vec::new(),
deferred_js_ids: Vec::new(),
}),
}
}
Expand All @@ -830,6 +870,7 @@ impl IntegrationRegistry {
script_rewriters,
html_post_processors: Vec::new(),
head_injectors,
deferred_js_ids: Vec::new(),
}),
}
}
Expand Down Expand Up @@ -885,6 +926,7 @@ impl IntegrationRegistry {
script_rewriters: Vec::new(),
html_post_processors: Vec::new(),
head_injectors: Vec::new(),
deferred_js_ids: Vec::new(),
}),
}
}
Expand Down Expand Up @@ -1338,4 +1380,101 @@ mod tests {
"POST response should have x-synthetic-id header"
);
}

#[test]
fn js_module_ids_immediate_excludes_prebid() {
let settings = crate::test_support::tests::create_test_settings();
let mut settings_with_prebid = settings;
settings_with_prebid
.integrations
.insert_config(
"prebid",
&serde_json::json!({
"enabled": true,
"server_url": "https://test-prebid.com/openrtb2/auction",
"timeout_ms": 1000,
"bidders": ["mocktioneer"],
"debug": false
}),
)
.expect("should insert prebid config");

let registry =
IntegrationRegistry::new(&settings_with_prebid).expect("should create registry");

let all = registry.js_module_ids();
let immediate = registry.js_module_ids_immediate();
let deferred = registry.js_module_ids_deferred();

assert!(
all.contains(&"prebid"),
"should include prebid in full list"
);
assert!(
!immediate.contains(&"prebid"),
"should not include prebid in immediate IDs"
);
assert!(
deferred.contains(&"prebid"),
"should include prebid in deferred IDs"
);
}

#[test]
fn js_module_ids_deferred_empty_when_prebid_disabled() {
let mut settings = crate::test_support::tests::create_test_settings();
settings
.integrations
.insert_config(
"prebid",
&serde_json::json!({
"enabled": false,
"server_url": "https://test-prebid.com/openrtb2/auction"
}),
)
.expect("should update prebid config");

let registry = IntegrationRegistry::new(&settings).expect("should create registry");

let deferred = registry.js_module_ids_deferred();
assert!(
deferred.is_empty(),
"should have no deferred IDs when prebid is disabled"
);
}

#[test]
fn js_module_ids_split_is_exhaustive() {
let settings = crate::test_support::tests::create_test_settings();
let mut settings_with_prebid = settings;
settings_with_prebid
.integrations
.insert_config(
"prebid",
&serde_json::json!({
"enabled": true,
"server_url": "https://test-prebid.com/openrtb2/auction",
"timeout_ms": 1000,
"bidders": ["mocktioneer"],
"debug": false
}),
)
.expect("should insert prebid config");

let registry =
IntegrationRegistry::new(&settings_with_prebid).expect("should create registry");

let all = registry.js_module_ids();
let mut recombined = registry.js_module_ids_immediate();
recombined.extend(registry.js_module_ids_deferred());
recombined.sort();

let mut all_sorted = all;
all_sorted.sort();

assert_eq!(
recombined, all_sorted,
"should reconstruct full module list from immediate + deferred"
);
}
}
9 changes: 5 additions & 4 deletions crates/common/src/integrations/testlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,9 @@ fn default_timeout_ms() -> u32 {
}

fn default_shim_src() -> String {
// Testlight is included in the unified bundle, so we return the unified script source
tsjs::tsjs_script_src_all()
// Testlight is included in the unified bundle, so we return the unified script source.
// Uses conservative all-module hash since the registry is unavailable at config time.
tsjs::tsjs_unified_script_src()
}

fn default_enabled() -> bool {
Expand Down Expand Up @@ -260,7 +261,7 @@ mod tests {

#[test]
fn html_rewriter_replaces_integration_script() {
let shim_src = tsjs::tsjs_script_src_all();
let shim_src = tsjs::tsjs_unified_script_src();
let config = TestlightConfig {
enabled: true,
endpoint: "https://example.com/openrtb".to_string(),
Expand Down Expand Up @@ -290,7 +291,7 @@ mod tests {

#[test]
fn html_rewriter_is_noop_when_disabled() {
let shim_src = tsjs::tsjs_script_src_all();
let shim_src = tsjs::tsjs_unified_script_src();
let config = TestlightConfig {
enabled: true,
endpoint: "https://example.com/openrtb".to_string(),
Expand Down
Loading