Skip to content
Open
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
29 changes: 29 additions & 0 deletions crates/oxc_angular_compiler/src/component/defer_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,35 @@ pub fn template_has_defer_block<'a>(nodes: &[R3Node<'a>]) -> bool {
visitor.found
}

/// Source spans of every `@defer` block, in the order Angular's binder lists
/// them (`getDeferBlocks()`): a block comes before the blocks nested in it, and
/// its main content before its `@placeholder`, `@loading` and `@error`.
pub fn defer_block_spans(nodes: &[R3Node<'_>]) -> std::vec::Vec<oxc_span::Span> {
struct DeferCollector {
spans: std::vec::Vec<oxc_span::Span>,
}

impl<'a> R3Visitor<'a> for DeferCollector {
fn visit_deferred_block(&mut self, block: &R3DeferredBlock<'a>) {
self.spans.push(block.source_span);
visit_all(self, &block.children);
if let Some(placeholder) = &block.placeholder {
self.visit_deferred_block_placeholder(placeholder);
}
if let Some(loading) = &block.loading {
self.visit_deferred_block_loading(loading);
}
if let Some(error) = &block.error {
self.visit_deferred_block_error(error);
}
}
}

let mut visitor = DeferCollector { spans: std::vec::Vec::new() };
visit_all(&mut visitor, nodes);
visitor.spans
}

/// Build the list of `R3DeferPerComponentDependency` describing the
/// `@Component.deferredImports` entries, used to emit `setClassMetadataAsync`
/// and the per-component `ɵɵdefer` resolver.
Expand Down
24 changes: 23 additions & 1 deletion crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ use crate::ng_module::{
};
use crate::output::ast::{
ArrowFunctionBody, ArrowFunctionExpr, DeclareFunctionStmt, FunctionExpr, OutputExpression,
OutputStatement, ReadPropExpr, ReadVarExpr, StmtModifier,
OutputStatement, RawSourceExpr, ReadPropExpr, ReadVarExpr, StmtModifier,
};
use crate::output::emitter::JsEmitter;
use crate::parser::ParseTemplateOptions;
Expand Down Expand Up @@ -5666,6 +5666,7 @@ fn compile_component_full<'a>(
// PerComponent mode: pass the shared resolver expression so the
// ingest stage attaches it to every defer block's create op.
all_deferrable_deps_fn: defer_resolver_fn,
defer_blocks: FxHashMap::default(),
// Use the shared pool starting index to avoid duplicate constant names
// when compiling multiple components in the same file
pool_starting_index,
Expand Down Expand Up @@ -6221,6 +6222,7 @@ pub fn compile_template_to_js_with_options<'a>(
enable_debug_locations,
template_source: Some(template),
all_deferrable_deps_fn: None,
defer_blocks: FxHashMap::default(),
pool_starting_index: 0, // Standalone template compilation starts from 0
angular_version: options.angular_version,
legacy_optional_chaining: options.legacy_optional_chaining,
Expand Down Expand Up @@ -6407,6 +6409,7 @@ pub fn compile_template_for_hmr<'a>(
enable_debug_locations,
template_source: Some(template),
all_deferrable_deps_fn: None,
defer_blocks: FxHashMap::default(),
pool_starting_index: 0, // HMR template compilation starts from 0
angular_version: options.angular_version,
legacy_optional_chaining: options.legacy_optional_chaining,
Expand Down Expand Up @@ -7071,6 +7074,7 @@ pub fn compile_template_for_linker<'a>(
preserve_whitespaces: bool,
core_namespace: &str,
angular_version: Option<crate::AngularVersion>,
defer_block_deps: &[Option<&str>],
) -> Result<LinkerTemplateOutput, std::vec::Vec<OxcDiagnostic>> {
use crate::pipeline::ingest::{IngestOptions, ingest_component_with_options};
use oxc_allocator::FromIn;
Expand Down Expand Up @@ -7119,6 +7123,23 @@ pub fn compile_template_for_linker<'a>(
return Err(diagnostics);
}

// Like the TS linker's `createR3ComponentDeferMetadata`: the i-th entry of
// `deferBlockDependencies` belongs to the i-th `@defer` block, and a block
// past the end of the list (or a `null` entry) has no dependency function.
let defer_blocks = super::defer_resolver::defer_block_spans(&r3_result.nodes)
.into_iter()
.enumerate()
.map(|(i, span)| {
let deps_fn = defer_block_deps.get(i).copied().flatten().map(|source| {
OutputExpression::RawSource(oxc_allocator::Box::new_in(
RawSourceExpr { source: Ident::from_in(source, &allocator), source_span: None },
&allocator,
))
});
(span, deps_fn)
})
.collect();

// Stage 3-5: Ingest and compile
let ingest_options = IngestOptions {
mode: TemplateCompilationMode::Full,
Expand All @@ -7129,6 +7150,7 @@ pub fn compile_template_for_linker<'a>(
enable_debug_locations: false,
template_source: Some(template),
all_deferrable_deps_fn: None,
defer_blocks,
pool_starting_index: 0,
angular_version,
// Angular's linker uses native `?.` for declarations at version 22+ (and the
Expand Down
31 changes: 25 additions & 6 deletions crates/oxc_angular_compiler/src/linker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use oxc_span::{GetSpan, SourceType};

use crate::AngularVersion;
use crate::optimizer::Edit;
use crate::output::emitter::escape_string;
use crate::pipeline::phases::parse_extracted_styles::parse_style_string;
use crate::pipeline::selector::{R3SelectorElement, parse_selector_to_r3_selector};

Expand All @@ -55,7 +56,7 @@ fn needs_object_key_quoting(key: &str) -> bool {

/// Quote a property key if it contains unsafe characters (dots or hyphens).
fn quote_key(key: &str) -> String {
if needs_object_key_quoting(key) { format!("\"{key}\"") } else { key.to_string() }
if needs_object_key_quoting(key) { escape_string(key, false) } else { key.to_string() }
}

/// Partial declaration function names to link.
Expand Down Expand Up @@ -882,7 +883,7 @@ fn parse_selector(selector: &str) -> String {
let parts: Vec<String> = elements
.iter()
.map(|el| match el {
R3SelectorElement::String(s) => format!("\"{s}\""),
R3SelectorElement::String(s) => escape_string(s, false),
R3SelectorElement::Flag(f) => f.to_string(),
})
.collect();
Expand Down Expand Up @@ -940,7 +941,7 @@ fn build_host_attrs(host_obj: &ObjectExpression<'_>, source: &str) -> String {
// Split classes and add each separately
attrs.push("1".to_string()); // AttributeMarker.Classes
for class in class_attr.split_whitespace() {
attrs.push(format!("\"{class}\""));
attrs.push(escape_string(class, false));
}
}

Expand Down Expand Up @@ -1287,7 +1288,7 @@ fn link_pipe(
ns: &str,
type_name: &str,
) -> Option<String> {
let pipe_name = get_string_property(meta, "name")?;
let pipe_name = escape_string(get_string_property(meta, "name")?, false);
let pure = get_property_source(meta, "pure", source).unwrap_or("true");
let standalone = get_bool_property(meta, "isStandalone")
.unwrap_or_else(|| get_default_standalone_value(meta));
Expand All @@ -1296,7 +1297,7 @@ fn link_pipe(
if standalone { String::new() } else { ", standalone: false".to_string() };

Some(format!(
"/*@__PURE__*/ {ns}.\u{0275}\u{0275}definePipe({{ name: \"{pipe_name}\", type: {type_name}, pure: {pure}{standalone_part} }})"
"/*@__PURE__*/ {ns}.\u{0275}\u{0275}definePipe({{ name: {pipe_name}, type: {type_name}, pure: {pure}{standalone_part} }})"
))
}

Expand Down Expand Up @@ -1877,7 +1878,7 @@ fn build_features(meta: &ObjectExpression<'_>, source: &str, ns: &str) -> Option
// emits `ɵɵControlFeature("formField")` or `ɵɵControlFeature(null)`
if let Some(control_create) = get_object_property(meta, "controlCreate") {
let pass_through = match get_string_property(control_create, "passThroughInput") {
Some(s) => format!("\"{s}\""),
Some(s) => escape_string(s, false),
None => "null".to_string(),
};
features.push(format!("{ns}.\u{0275}\u{0275}ControlFeature({pass_through})"));
Expand Down Expand Up @@ -1918,6 +1919,23 @@ fn link_component(
let template_owned: String = template.to_string();
let template_ref: &str = template_allocator.alloc_str(&template_owned);

// `deferBlockDependencies: [() => [import("./a").then(m => m.A)], null]`,
// one entry per `@defer` block, emitted as written.
let defer_block_deps: Vec<Option<&str>> = get_array_property(meta, "deferBlockDependencies")
.map(|deps| {
deps.elements
.iter()
.map(|el| match el.as_expression() {
Some(Expression::NullLiteral(_)) | None => None,
Some(expr) => {
let span = expr.span();
Some(&source[span.start as usize..span.end as usize])
}
})
.collect()
})
.unwrap_or_default();

let template_output = crate::component::compile_template_for_linker(
&template_allocator,
template_ref,
Expand All @@ -1926,6 +1944,7 @@ fn link_component(
preserve_whitespaces,
ns,
options.angular_version,
&defer_block_deps,
)
.ok()?;

Expand Down
7 changes: 6 additions & 1 deletion crates/oxc_angular_compiler/src/pipeline/ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ pub struct IngestOptions<'a> {
/// Ported from Angular's ingestComponent parameter (line 65 in ingest.ts).
pub all_deferrable_deps_fn: Option<OutputExpression<'a>>,

/// Dependency function of each `@defer` block, keyed by the block's source
/// span. Only read in `PerBlock` mode.
pub defer_blocks: FxHashMap<oxc_span::Span, Option<OutputExpression<'a>>>,

/// Starting index for the constant pool's name counter.
///
/// This is used when compiling multiple components in the same file to ensure
Expand Down Expand Up @@ -145,6 +149,7 @@ impl Default for IngestOptions<'_> {
enable_debug_locations: false,
template_source: None,
all_deferrable_deps_fn: None,
defer_blocks: FxHashMap::default(),
pool_starting_index: 0,
angular_version: None,
legacy_optional_chaining: None,
Expand Down Expand Up @@ -891,7 +896,7 @@ pub fn ingest_component_with_options<'a>(
// The all_deferrable_deps_fn is stored on the job and referenced during emit
job.defer_meta = match options.defer_block_deps_emit_mode {
DeferBlockDepsEmitMode::PerBlock => {
DeferMetadata::PerBlock { blocks: FxHashMap::default() }
DeferMetadata::PerBlock { blocks: options.defer_blocks }
}
DeferBlockDepsEmitMode::PerComponent => {
// In PerComponent mode, dependencies_fn is set from all_deferrable_deps_fn during emit
Expand Down
49 changes: 49 additions & 0 deletions crates/oxc_angular_compiler/tests/linker_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,3 +551,52 @@ export { P };
result.code
);
}

#[test]
fn test_link_escapes_metadata_strings() {
let code = r#"import * as i0 from "@angular/core";
export class P {}
P.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.0", ngImport: i0, type: P, isStandalone: true, name: 'a"+x+"' });
export class D {}
D.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0", ngImport: i0, type: D, isStandalone: true, selector: "[t='a\"b']", inputs: { 'a.b"c': "c" }, host: { classAttribute: 'k"+x+"' }, controlCreate: { passThroughInput: 'f"+x+"' } });
"#;
let allocator = Allocator::default();
let result = link(&allocator, code, "test.mjs");
assert!(result.linked);
for expected in [
r#"name: "a\"+x+\"""#,
r#"selectors: [["", "t", "a\"b"]]"#,
r#"inputs: { "a.b\"c": "c" }"#,
r#"hostAttrs: [1, "k\"+x+\""]"#,
r#"ɵɵControlFeature("f\"+x+\"")"#,
] {
assert!(result.code.contains(expected), "missing `{expected}` in\n{}", result.code);
}
}

#[test]
fn test_link_defer_block_dependencies() {
// `deferBlockDependencies` follows the binder's block order: the outer
// block, the block nested in it, the block in its placeholder, then the
// next top-level block.
let code = r#"import * as i0 from "@angular/core";
export class C {}
C.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", ngImport: i0, type: C, isStandalone: true, selector: "c", template: "@defer { <lazy-cmp/> @defer { <b-b></b-b> } } @placeholder { @defer { <i-i></i-i> } } @defer (on idle) { <x-y/> }", deferBlockDependencies: [() => [import("./lazy").then(m => m.LazyCmp)], null, () => [import("./b").then(m => m.B)], () => [import("./x").then(m => m.X), import("./y").then(m => m.Y)]] });
"#;
let allocator = Allocator::default();
let result = link(&allocator, code, "test.mjs");
assert!(result.linked);
let linked: String = result.code.chars().filter(|c| !c.is_whitespace()).collect();
// Fragments of the TS linker's output.
for expected in [
r#"constC_Defer_2_DepsFn=()=>[import("./lazy").then(m=>m.LazyCmp)];"#,
r#"constC_Defer_5_DepsFn=()=>[import("./x").then(m=>m.X),import("./y").then(m=>m.Y)];"#,
r#"constC_DeferPlaceholder_1_Defer_1_DepsFn=()=>[import("./b").then(m=>m.B)];"#,
"i0.ɵɵdefer(2,1);",
"i0.ɵɵdefer(1,0,C_DeferPlaceholder_1_Defer_1_DepsFn);",
"i0.ɵɵdefer(2,0,C_Defer_2_DepsFn,null,1);",
"i0.ɵɵdefer(5,4,C_Defer_5_DepsFn);",
] {
assert!(linked.contains(expected), "missing `{expected}` in\n{}", result.code);
}
}