@zenithbuild/bundler 1.3.10 → 1.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/build-analyzer.ts +122 -0
- package/src/bundle-generator.ts +844 -0
- package/src/bundler.rs +81 -0
- package/src/bundler.ts +98 -0
- package/src/css.rs +266 -0
- package/src/css.ts +294 -0
- package/src/generateFinalBundle.ts +80 -0
- package/src/html.rs +147 -0
- package/src/index.ts +7 -0
- package/src/lib.rs +116 -0
- package/src/plugin.rs +354 -0
- package/src/runtime-generator.ts +24 -0
- package/src/store.rs +44 -0
- package/src/types.ts +6 -0
package/src/bundler.rs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//! Zenith Bundler Configuration
|
|
2
|
+
//!
|
|
3
|
+
//! Provides helper functions to create a pre-configured Rolldown bundler
|
|
4
|
+
//! with Zenith's required settings (capability splitting, plugin injection, etc).
|
|
5
|
+
|
|
6
|
+
use rolldown::{Bundler, BundlerBuilder, BundlerOptions, InputItem};
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
use crate::plugin::ZenithPlugin;
|
|
10
|
+
|
|
11
|
+
/// Create a configured Rolldown bundler for a Zenith project
|
|
12
|
+
pub fn create_zenith_bundler(entry_point: &str, components_dir: Option<&str>) -> Bundler {
|
|
13
|
+
// 1. Initialize the Zenith Plugin
|
|
14
|
+
let mut plugin = ZenithPlugin::new(entry_point);
|
|
15
|
+
if let Some(dir) = components_dir {
|
|
16
|
+
plugin = plugin.with_components_dir(dir);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Configure Bundler Options
|
|
20
|
+
// Note: manual_chunks is currently experimental in Rolldown Rust API,
|
|
21
|
+
// we'll need to verify the exact API surface.
|
|
22
|
+
// For now, we rely on the plugin's dynamic imports to drive chunking naturally,
|
|
23
|
+
// and we can enhance this with explicit manual_chunks if the API allows.
|
|
24
|
+
|
|
25
|
+
let options = BundlerOptions {
|
|
26
|
+
input: Some(vec![InputItem {
|
|
27
|
+
name: Some("index".into()),
|
|
28
|
+
import: "virtual:zenith-entry".into(), // Start with our Hydration Controller
|
|
29
|
+
}]),
|
|
30
|
+
// Enable code splitting
|
|
31
|
+
format: Some(rolldown_common::OutputFormat::Esm),
|
|
32
|
+
// Ensure we target browser environment
|
|
33
|
+
platform: Some(rolldown_common::Platform::Browser),
|
|
34
|
+
// Capability-Based Chunking:
|
|
35
|
+
// GSAP should be handled as a separate chunk (capability: "anim").
|
|
36
|
+
// We rely on dynamic imports (import('gsap')) in the user's code to automatically split it.
|
|
37
|
+
// Explicit manual_chunks can be added here if stricter control is needed.
|
|
38
|
+
// Capability-based chunking configuration will go here once verified
|
|
39
|
+
..Default::default()
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let builder = BundlerBuilder::default()
|
|
43
|
+
.with_options(options)
|
|
44
|
+
.with_plugins(vec![Arc::new(plugin)]);
|
|
45
|
+
|
|
46
|
+
builder.build().expect("Failed to build bundler")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Create a configured Rolldown bundler for Dev Mode (Watch + HMR + InMemory)
|
|
50
|
+
pub fn create_dev_bundler(
|
|
51
|
+
entry_point: &str,
|
|
52
|
+
components_dir: Option<&str>,
|
|
53
|
+
store: std::sync::Arc<crate::store::AssetStore>,
|
|
54
|
+
) -> Bundler {
|
|
55
|
+
// 1. Initialize the Zenith Plugin with Store and Dev Mode
|
|
56
|
+
let mut plugin = ZenithPlugin::new(entry_point)
|
|
57
|
+
.with_store(store)
|
|
58
|
+
.with_dev_mode(true);
|
|
59
|
+
|
|
60
|
+
if let Some(dir) = components_dir {
|
|
61
|
+
plugin = plugin.with_components_dir(dir);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. Configure Bundler Options (Dev Optimized)
|
|
65
|
+
let options = BundlerOptions {
|
|
66
|
+
input: Some(vec![InputItem {
|
|
67
|
+
name: Some("index".into()),
|
|
68
|
+
import: "virtual:zenith-entry".into(),
|
|
69
|
+
}]),
|
|
70
|
+
format: Some(rolldown_common::OutputFormat::Esm),
|
|
71
|
+
platform: Some(rolldown_common::Platform::Browser),
|
|
72
|
+
sourcemap: Some(rolldown_common::SourceMapType::File), // Enable sourcemaps for dev
|
|
73
|
+
..Default::default()
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
let builder = BundlerBuilder::default()
|
|
77
|
+
.with_options(options)
|
|
78
|
+
.with_plugins(vec![Arc::new(plugin)]);
|
|
79
|
+
|
|
80
|
+
builder.build().expect("Failed to build dev bundler")
|
|
81
|
+
}
|
package/src/bundler.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zenithbuild/bundler - Page Script Bundler
|
|
3
|
+
*
|
|
4
|
+
* COMPILER-FIRST ARCHITECTURE
|
|
5
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
6
|
+
*
|
|
7
|
+
* This bundler performs ZERO inference. It executes exactly what the compiler specifies.
|
|
8
|
+
*
|
|
9
|
+
* Rules:
|
|
10
|
+
* - If a BundlePlan is provided, bundling MUST occur
|
|
11
|
+
* - If bundling fails, throw a hard error (no fallback)
|
|
12
|
+
* - The bundler never inspects source code for intent
|
|
13
|
+
* - No temp files, no heuristics, no recovery
|
|
14
|
+
*
|
|
15
|
+
* Bundler failure = compiler bug.
|
|
16
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { rolldown } from 'rolldown'
|
|
20
|
+
import type { BundlePlan } from '@zenithbuild/compiler'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execute a compiler-emitted BundlePlan
|
|
24
|
+
*
|
|
25
|
+
* This is a PURE PLAN EXECUTOR. It does not:
|
|
26
|
+
* - Inspect source code for imports
|
|
27
|
+
* - Decide whether bundling is needed
|
|
28
|
+
* - Fall back on failure
|
|
29
|
+
* - Use temp files
|
|
30
|
+
*
|
|
31
|
+
* @param plan - Compiler-emitted BundlePlan (must exist; caller must not call if no plan)
|
|
32
|
+
* @throws Error if bundling fails (no fallback, no recovery)
|
|
33
|
+
*/
|
|
34
|
+
export async function bundlePageScript(plan: BundlePlan): Promise<string> {
|
|
35
|
+
// Virtual entry module ID
|
|
36
|
+
const VIRTUAL_ENTRY = '\0zenith:entry.tsx'
|
|
37
|
+
|
|
38
|
+
// Build virtual modules map from plan
|
|
39
|
+
const virtualModules = new Map<string, string>()
|
|
40
|
+
virtualModules.set(VIRTUAL_ENTRY, plan.entry)
|
|
41
|
+
for (const vm of plan.virtualModules) {
|
|
42
|
+
virtualModules.set(vm.id, vm.code)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Execute Rolldown with plan-specified configuration
|
|
46
|
+
// No inference, no heuristics, no semantic analysis
|
|
47
|
+
const bundle = await rolldown({
|
|
48
|
+
input: VIRTUAL_ENTRY,
|
|
49
|
+
platform: plan.platform,
|
|
50
|
+
resolve: {
|
|
51
|
+
modules: plan.resolveRoots
|
|
52
|
+
},
|
|
53
|
+
plugins: [{
|
|
54
|
+
name: 'zenith-virtual',
|
|
55
|
+
resolveId(source: string) {
|
|
56
|
+
// Virtual modules from plan
|
|
57
|
+
if (virtualModules.has(source)) {
|
|
58
|
+
return { id: source, moduleSideEffects: true }
|
|
59
|
+
}
|
|
60
|
+
// Special case: zenith:content namespace
|
|
61
|
+
if (source === 'zenith:content') {
|
|
62
|
+
return { id: '\0zenith:content', moduleSideEffects: true }
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
},
|
|
66
|
+
load(id: string) {
|
|
67
|
+
return virtualModules.get(id) ?? null
|
|
68
|
+
}
|
|
69
|
+
}],
|
|
70
|
+
// DETERMINISTIC OUTPUT: Disable all semantic optimizations
|
|
71
|
+
// Tree-shaking implies semantic analysis - bundler must not infer
|
|
72
|
+
treeshake: false,
|
|
73
|
+
// HARD FAILURE on unresolved imports - no silent external treatment
|
|
74
|
+
onLog(level: string, log: any) {
|
|
75
|
+
if (log.code === 'UNRESOLVED_IMPORT') {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`[Zenith Bundler] Unresolved import: ${log.message}. ` +
|
|
78
|
+
`This is a compiler error - the BundlePlan references a module that cannot be resolved.`
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Generate output with plan-specified format
|
|
85
|
+
const { output } = await bundle.generate({
|
|
86
|
+
format: plan.format
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Hard failure if no output - this is a compiler bug
|
|
90
|
+
if (!output[0]?.code) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'[Zenith Bundler] Rolldown produced no output. ' +
|
|
93
|
+
'This is a compiler error - the BundlePlan was invalid or Rolldown failed silently.'
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return output[0].code
|
|
98
|
+
}
|
package/src/css.rs
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
//! CSS Buffer and Pruning Module
|
|
2
|
+
//!
|
|
3
|
+
//! Handles buffering CSS from .zen files and pruning unused classes
|
|
4
|
+
//! using lightningcss with ZenManifest.css_classes as the allow-list.
|
|
5
|
+
//!
|
|
6
|
+
//! Uses AST-based pruning via lightningcss to ensure safety and correctness.
|
|
7
|
+
|
|
8
|
+
use dashmap::DashMap;
|
|
9
|
+
use lightningcss::rules::CssRule;
|
|
10
|
+
use lightningcss::selector::Component;
|
|
11
|
+
use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet};
|
|
12
|
+
use lightningcss::targets::Browsers;
|
|
13
|
+
use std::collections::HashSet;
|
|
14
|
+
|
|
15
|
+
/// Thread-safe CSS buffer for collecting styles from .zen files
|
|
16
|
+
#[derive(Debug)]
|
|
17
|
+
pub struct CssBuffer {
|
|
18
|
+
/// CSS content keyed by file path
|
|
19
|
+
styles: DashMap<String, String>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl CssBuffer {
|
|
23
|
+
pub fn new() -> Self {
|
|
24
|
+
Self {
|
|
25
|
+
styles: DashMap::new(),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Insert CSS content for a file
|
|
30
|
+
pub fn insert(&self, file_id: String, css: String) {
|
|
31
|
+
self.styles.insert(file_id, css);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Get all buffered CSS
|
|
35
|
+
pub fn get_all(&self) -> Vec<String> {
|
|
36
|
+
self.styles.iter().map(|r| r.value().clone()).collect()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Stitch all CSS and prune unused classes
|
|
40
|
+
///
|
|
41
|
+
/// Strategy:
|
|
42
|
+
/// 1. Parse the CSS into AST using lightningcss
|
|
43
|
+
/// 2. Walk the AST and remove rules/selectors that allow pruning
|
|
44
|
+
/// 3. Minify and print the result
|
|
45
|
+
pub fn stitch_and_prune(&self, used_classes: &[String]) -> Result<String, String> {
|
|
46
|
+
let all_css: String = self
|
|
47
|
+
.styles
|
|
48
|
+
.iter()
|
|
49
|
+
.map(|r| r.value().clone())
|
|
50
|
+
.collect::<Vec<_>>()
|
|
51
|
+
.join("\n");
|
|
52
|
+
|
|
53
|
+
if all_css.is_empty() {
|
|
54
|
+
return Ok(String::new());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build allow-list from used classes
|
|
58
|
+
let used_set: HashSet<&str> = used_classes.iter().map(|s| s.as_str()).collect();
|
|
59
|
+
|
|
60
|
+
// 1. Parse CSS
|
|
61
|
+
let mut stylesheet = StyleSheet::parse(&all_css, ParserOptions::default())
|
|
62
|
+
.map_err(|e| format!("CSS parse error: {:?}", e))?;
|
|
63
|
+
|
|
64
|
+
// 2. Prune AST (Recursive)
|
|
65
|
+
// Accessing rules directly requires ensuring we can iterate mutably
|
|
66
|
+
let rules_vec = &mut stylesheet.rules.0;
|
|
67
|
+
prune_rules(rules_vec, &used_set);
|
|
68
|
+
|
|
69
|
+
// 3. Minify and Print
|
|
70
|
+
|
|
71
|
+
stylesheet
|
|
72
|
+
.minify(MinifyOptions {
|
|
73
|
+
targets: Browsers::default().into(),
|
|
74
|
+
..Default::default()
|
|
75
|
+
})
|
|
76
|
+
.map_err(|e| format!("CSS minify error: {:?}", e))?;
|
|
77
|
+
|
|
78
|
+
let result = stylesheet
|
|
79
|
+
.to_css(PrinterOptions {
|
|
80
|
+
minify: true,
|
|
81
|
+
..Default::default()
|
|
82
|
+
})
|
|
83
|
+
.map_err(|e| format!("CSS print error: {:?}", e))?;
|
|
84
|
+
|
|
85
|
+
Ok(result.code)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Clear all buffered CSS
|
|
89
|
+
pub fn clear(&self) {
|
|
90
|
+
self.styles.clear();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
impl Default for CssBuffer {
|
|
95
|
+
fn default() -> Self {
|
|
96
|
+
Self::new()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Recursively prune CSS rules
|
|
101
|
+
///
|
|
102
|
+
/// Returns true if rule should be kept, false if it should be removed (if single rule context)
|
|
103
|
+
/// But here we operate on Vec<CssRule>, so we use retain_mut.
|
|
104
|
+
fn prune_rules(rules: &mut Vec<CssRule>, used_set: &HashSet<&str>) {
|
|
105
|
+
rules.retain_mut(|rule| {
|
|
106
|
+
match rule {
|
|
107
|
+
CssRule::Style(style_rule) => {
|
|
108
|
+
// Filter selectors in this rule
|
|
109
|
+
// style_rule.selectors is SelectorList.
|
|
110
|
+
|
|
111
|
+
// We iterate and keep selectors that are "used"
|
|
112
|
+
style_rule
|
|
113
|
+
.selectors
|
|
114
|
+
.0
|
|
115
|
+
.retain(|selector| is_selector_used(selector, used_set));
|
|
116
|
+
|
|
117
|
+
// Determine if we keep the rule:
|
|
118
|
+
// If NO selectors remain, the rule is empty and should be removed.
|
|
119
|
+
!style_rule.selectors.0.is_empty()
|
|
120
|
+
}
|
|
121
|
+
CssRule::Media(media_rule) => {
|
|
122
|
+
// Recursively prune rules inside @media
|
|
123
|
+
// media_rule.rules is CssRuleList (which wraps Vec<CssRule>).
|
|
124
|
+
// Access via .0
|
|
125
|
+
prune_rules(&mut media_rule.rules.0, used_set);
|
|
126
|
+
|
|
127
|
+
// Keep media rule only if it still has rules inside
|
|
128
|
+
!media_rule.rules.0.is_empty()
|
|
129
|
+
}
|
|
130
|
+
CssRule::Supports(supports_rule) => {
|
|
131
|
+
prune_rules(&mut supports_rule.rules.0, used_set);
|
|
132
|
+
!supports_rule.rules.0.is_empty()
|
|
133
|
+
}
|
|
134
|
+
// For other rules (Keyframes, FontFace, etc.), we keep them ALWAYS.
|
|
135
|
+
// We do not prune keyframes based on usage yet (harder analysis).
|
|
136
|
+
_ => true,
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Determine if a selector usage deems it valid to keep.
|
|
142
|
+
///
|
|
143
|
+
/// POLICY: CONSERVATIVE
|
|
144
|
+
/// - If selector has NO classes -> KEEP (Element, ID, *, etc.)
|
|
145
|
+
/// - If selector has ANY class that is in `used_set` -> KEEP.
|
|
146
|
+
/// - Only remove if ALL classes in the selector are KNOWN UNUSED.
|
|
147
|
+
fn is_selector_used(selector: &lightningcss::selector::Selector, used_set: &HashSet<&str>) -> bool {
|
|
148
|
+
let mut has_classes = false;
|
|
149
|
+
let mut any_used = false;
|
|
150
|
+
|
|
151
|
+
// Iterate over components in the selector
|
|
152
|
+
// Selector iteration yields &Component
|
|
153
|
+
for component in selector.iter() {
|
|
154
|
+
if let Component::Class(ident) = component {
|
|
155
|
+
has_classes = true;
|
|
156
|
+
// ident is Atom or similar string-like. as_ref() works for AsRef<str>.
|
|
157
|
+
if used_set.contains(ident.as_ref()) {
|
|
158
|
+
any_used = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if !has_classes {
|
|
164
|
+
// No classes involved (e.g. "div", "#app", "*"), so keep it.
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Has classes. Keep ONLY if at least one class is used.
|
|
169
|
+
any_used
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#[cfg(test)]
|
|
173
|
+
mod tests {
|
|
174
|
+
use super::*;
|
|
175
|
+
|
|
176
|
+
#[test]
|
|
177
|
+
fn test_css_buffer_insert_and_get() {
|
|
178
|
+
let buffer = CssBuffer::new();
|
|
179
|
+
buffer.insert("a.zen".into(), ".foo { color: red; }".into());
|
|
180
|
+
buffer.insert("b.zen".into(), ".bar { color: blue; }".into());
|
|
181
|
+
|
|
182
|
+
let all = buffer.get_all();
|
|
183
|
+
assert_eq!(all.len(), 2);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn test_css_stitch_and_minify() {
|
|
188
|
+
let buffer = CssBuffer::new();
|
|
189
|
+
buffer.insert("a.zen".into(), ".foo { color: red; }".into());
|
|
190
|
+
|
|
191
|
+
let result = buffer.stitch_and_prune(&["foo".into()]).unwrap();
|
|
192
|
+
assert!(result.contains("color:") || result.contains("color:red"));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[test]
|
|
196
|
+
fn test_css_pruning_removes_unused() {
|
|
197
|
+
let buffer = CssBuffer::new();
|
|
198
|
+
buffer.insert(
|
|
199
|
+
"a.zen".into(),
|
|
200
|
+
".foo { color: red; } .bar { color: blue; } .baz { color: green; }".into(),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Only "foo" is used, "bar" and "baz" should be pruned
|
|
204
|
+
let result = buffer.stitch_and_prune(&["foo".into()]).unwrap();
|
|
205
|
+
assert!(
|
|
206
|
+
result.contains("red"),
|
|
207
|
+
"Should keep .foo (used): {}",
|
|
208
|
+
result
|
|
209
|
+
);
|
|
210
|
+
assert!(
|
|
211
|
+
!result.contains("blue"),
|
|
212
|
+
"Should prune .bar (unused): {}",
|
|
213
|
+
result
|
|
214
|
+
);
|
|
215
|
+
assert!(
|
|
216
|
+
!result.contains("green"),
|
|
217
|
+
"Should prune .baz (unused): {}",
|
|
218
|
+
result
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn test_keeps_element_selectors() {
|
|
224
|
+
let buffer = CssBuffer::new();
|
|
225
|
+
buffer.insert(
|
|
226
|
+
"a.zen".into(),
|
|
227
|
+
"body { margin: 0; } h1 { font-size: 2rem; }".into(),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Element selectors should always be kept
|
|
231
|
+
let result = buffer.stitch_and_prune(&[]).unwrap();
|
|
232
|
+
assert!(
|
|
233
|
+
result.contains("margin") || result.contains("0"),
|
|
234
|
+
"Should keep body selector: {}",
|
|
235
|
+
result
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[test]
|
|
240
|
+
fn test_keeps_id_selectors() {
|
|
241
|
+
let buffer = CssBuffer::new();
|
|
242
|
+
buffer.insert("a.zen".into(), "#app { display: flex; }".into());
|
|
243
|
+
|
|
244
|
+
// ID selectors should always be kept
|
|
245
|
+
let result = buffer.stitch_and_prune(&[]).unwrap();
|
|
246
|
+
assert!(
|
|
247
|
+
result.contains("flex"),
|
|
248
|
+
"Should keep #app selector: {}",
|
|
249
|
+
result
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn test_keeps_used_class_in_compound() {
|
|
255
|
+
let buffer = CssBuffer::new();
|
|
256
|
+
buffer.insert("a.zen".into(), ".foo.bar { color: red; }".into());
|
|
257
|
+
|
|
258
|
+
// If either class is used, keep the rule
|
|
259
|
+
let result = buffer.stitch_and_prune(&["foo".into()]).unwrap();
|
|
260
|
+
assert!(
|
|
261
|
+
result.contains("red"),
|
|
262
|
+
"Should keep .foo.bar when foo is used: {}",
|
|
263
|
+
result
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|