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
12 changes: 6 additions & 6 deletions crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ pub fn extract_component_metadata<'a>(
}
"preserveWhitespaces" => {
metadata.preserve_whitespaces =
extract_boolean_value(&prop.value).unwrap_or(false);
Some(extract_boolean_value(&prop.value).unwrap_or(false));
}
"animations" => {
// Extract animations expression as full OutputExpression
Expand Down Expand Up @@ -1667,7 +1667,7 @@ mod tests {
class TestComponent {}
"#;
assert_metadata(code, |meta| {
assert!(meta.preserve_whitespaces);
assert_eq!(meta.preserve_whitespaces, Some(true));
});
}

Expand All @@ -1683,12 +1683,12 @@ mod tests {
class TestComponent {}
"#;
assert_metadata(code, |meta| {
assert!(!meta.preserve_whitespaces);
assert_eq!(meta.preserve_whitespaces, Some(false));
});
}

#[test]
fn test_preserve_whitespaces_defaults_to_false() {
fn test_preserve_whitespaces_unset() {
let code = r#"
import {Component} from '@angular/core';
@Component({
Expand All @@ -1698,7 +1698,7 @@ mod tests {
class TestComponent {}
"#;
assert_metadata(code, |meta| {
assert!(!meta.preserve_whitespaces);
assert_eq!(meta.preserve_whitespaces, None);
});
}

Expand Down Expand Up @@ -1831,7 +1831,7 @@ mod tests {
assert_eq!(meta.imports.len(), 1);
assert_eq!(meta.export_as.len(), 1);
assert_eq!(meta.export_as[0].as_str(), "complete");
assert!(meta.preserve_whitespaces);
assert_eq!(meta.preserve_whitespaces, Some(true));
assert_eq!(meta.schemas.len(), 1);
});
}
Expand Down
8 changes: 5 additions & 3 deletions crates/oxc_angular_compiler/src/component/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,10 @@ pub struct ComponentMetadata<'a> {
/// which is split into an array and emitted as `exportAs: ["foo", "bar"]`.
pub export_as: Vec<'a, Ident<'a>>,

/// Whether to preserve whitespace in templates.
pub preserve_whitespaces: bool,
/// The decorator's `preserveWhitespaces`, or `None` when it does not set
/// one and the project-wide `TransformOptions::preserve_whitespaces`
/// applies.
pub preserve_whitespaces: Option<bool>,

/// Constructor parameter dependencies for DI.
///
Expand Down Expand Up @@ -853,7 +855,7 @@ impl<'a> ComponentMetadata<'a> {
host: None,
imports: Vec::new_in(&allocator),
export_as: Vec::new_in(&allocator),
preserve_whitespaces: false,
preserve_whitespaces: None,
constructor_deps: None,
inputs: Vec::new_in(&allocator),
outputs: Vec::new_in(&allocator),
Expand Down
14 changes: 12 additions & 2 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ pub struct TransformOptions {
/// Override the change detection strategy.
pub change_detection: Option<super::metadata::ChangeDetectionStrategy>,

/// Override the preserve whitespaces setting.
/// Project-wide `preserveWhitespaces` (`angularCompilerOptions`): the
/// default for components whose decorator does not set it. The
/// template-only compiles (`compile_template_to_js_with_options`,
/// `compile_template_for_hmr`) use it as the template's value.
pub preserve_whitespaces: Option<bool>,

/// Host bindings metadata for the component.
Expand Down Expand Up @@ -5393,11 +5396,18 @@ fn compile_component_full<'a>(

let mut diagnostics = Vec::new();

// The decorator's `preserveWhitespaces` wins over the project-wide option
// (`parseTemplateDeclaration` in ngtsc's component resources.ts). Written
// back so the partial declaration carries the resolved value too.
let preserve_whitespaces =
metadata.preserve_whitespaces.or(options.preserve_whitespaces).unwrap_or(false);
metadata.preserve_whitespaces = Some(preserve_whitespaces);

// Stage 1: Parse HTML
// Build parse options from component metadata
// Angular always forces tokenizeExpansionForms: true in parseTemplate()
let parse_options = ParseTemplateOptions {
preserve_whitespaces: metadata.preserve_whitespaces,
preserve_whitespaces,
// Enable modern syntax features (block syntax, let declarations)
enable_block_syntax: true,
enable_let_syntax: true,
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_angular_compiler/src/partial/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ pub fn compile_declare_component_from_metadata<'a>(
));
}

if meta.preserve_whitespaces {
if meta.preserve_whitespaces == Some(true) {
entries.push(LiteralMapEntry::new(
Ident::from("preserveWhitespaces"),
bool_lit(allocator, true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ fn keeps_whitespace_the_decorator_preserves() {
assert!(update.template_js.contains(r#"ɵɵtext(1,"\n ")"#), "{}", update.template_js);
}

#[test]
fn keeps_whitespace_the_project_option_preserves() {
let source = component("", "<div>\n <span> {{ name }} </span>\n</div>");
let options = hmr(TransformOptions { preserve_whitespaces: Some(true), ..Default::default() });
let update = update(&source, &options, None);
assert_template_matches_transform(&update, &transform(&source, &options, None));
assert!(update.template_js.contains(r#"ɵɵtext(1,"\n ")"#), "{}", update.template_js);
}

#[test]
fn names_i18n_messages_after_the_file() {
let source = component("", "<p i18n>Hello {{ name }}</p>");
Expand Down
79 changes: 79 additions & 0 deletions crates/oxc_angular_compiler/tests/preserve_whitespaces_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! The project-wide `preserveWhitespaces` option (`angularCompilerOptions`) is
//! the default for components that do not set their own, as in ngtsc's
//! `parseTemplateDeclaration` (annotations/component/src/resources.ts).
//!
//! Ported from Angular's `ngtsc_spec.ts` `preserveWhitespaces` cases.

use oxc_allocator::Allocator;
use oxc_angular_compiler::{CompilationMode, TransformOptions, transform_angular_file};

fn compile(decorator_option: &str, options: &TransformOptions) -> String {
let source = format!(
"import {{Component}} from '@angular/core';
@Component({{
selector: 'test',{decorator_option}
template: `
<div>
Template with whitespaces
</div>
`
}})
export class FooCmp {{}}
"
);
let allocator = Allocator::default();
let result = transform_angular_file(&allocator, "test.ts", &source, Some(options), None);
assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
result.code
}

const COLLAPSED: &str = r#"ɵɵtext(1," Template with whitespaces ")"#;
const PRESERVED: &str = r#"ɵɵtext(2,"\n Template with whitespaces\n ")"#;

fn squash(code: &str) -> String {
code.replace(", ", ",")
}

#[test]
fn defaults_to_collapsing_whitespace() {
let code = compile("", &TransformOptions::default());
assert!(squash(&code).contains(COLLAPSED), "{code}");
}

#[test]
fn component_false_collapses_whitespace() {
let code = compile("\n preserveWhitespaces: false,", &TransformOptions::default());
assert!(squash(&code).contains(COLLAPSED), "{code}");
}

#[test]
fn takes_the_preserve_whitespaces_option_into_account() {
let options = TransformOptions { preserve_whitespaces: Some(true), ..Default::default() };
let code = compile("", &options);
assert!(squash(&code).contains(PRESERVED), "{code}");
}

#[test]
fn component_preserve_whitespaces_overrides_the_option() {
let options = TransformOptions { preserve_whitespaces: Some(true), ..Default::default() };
let code = compile("\n preserveWhitespaces: false,", &options);
assert!(squash(&code).contains(COLLAPSED), "{code}");

let options = TransformOptions { preserve_whitespaces: Some(false), ..Default::default() };
let code = compile("\n preserveWhitespaces: true,", &options);
assert!(squash(&code).contains(PRESERVED), "{code}");
}

#[test]
fn partial_declaration_carries_the_resolved_option() {
let options = TransformOptions {
preserve_whitespaces: Some(true),
compilation_mode: CompilationMode::Partial,
..Default::default()
};
let code = compile("", &options);
assert!(code.contains("preserveWhitespaces:true"), "{code}");

let code = compile("\n preserveWhitespaces: false,", &options);
assert!(!code.contains("preserveWhitespaces:true"), "{code}");
}
5 changes: 5 additions & 0 deletions napi/angular-compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ interface TransformOptions {
standalone?: boolean
encapsulation?: 'Emulated' | 'None' | 'ShadowDom'
changeDetection?: 'Default' | 'OnPush'
// Default for components that do not set their own (project-wide option)
preserveWhitespaces?: boolean

// Cross-file elision
Expand Down Expand Up @@ -220,6 +221,10 @@ interface PluginOptions {

// Template hook, applied during both the initial build and HMR
templateTransform?: (content: string, filePath: string) => string

// Project-wide default (`angularCompilerOptions.preserveWhitespaces`);
// a component's own `preserveWhitespaces` wins (default: false)
preserveWhitespaces?: boolean
}
```

Expand Down
3 changes: 2 additions & 1 deletion napi/angular-compiler/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1077,7 +1077,8 @@ export interface TransformOptions {
*/
changeDetection?: string
/**
* Whether to preserve whitespace in templates.
* Project-wide `preserveWhitespaces` (`angularCompilerOptions`): the
* default for components whose decorator does not set it.
* Defaults to false if not specified.
*/
preserveWhitespaces?: boolean
Expand Down
5 changes: 3 additions & 2 deletions napi/angular-compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ pub struct TransformOptions {
/// Defaults to "Default" if not specified.
pub change_detection: Option<String>,

/// Whether to preserve whitespace in templates.
/// Project-wide `preserveWhitespaces` (`angularCompilerOptions`): the
/// default for components whose decorator does not set it.
/// Defaults to false if not specified.
pub preserve_whitespaces: Option<bool>,

Expand Down Expand Up @@ -2178,7 +2179,7 @@ pub fn extract_component_metadata_sync(
.join(","),
)
},
preserve_whitespaces: metadata.preserve_whitespaces,
preserve_whitespaces: metadata.preserve_whitespaces.unwrap_or(false),
providers,
view_providers,
animations,
Expand Down
9 changes: 9 additions & 0 deletions napi/angular-compiler/vite-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ export interface PluginOptions {
* ```
*/
compilationMode?: 'full' | 'partial'

/**
* Project-wide `preserveWhitespaces`, like `angularCompilerOptions.preserveWhitespaces`
* in the tsconfig: the default for components whose decorator does not set
* `preserveWhitespaces`. Default: `false`.
*/
preserveWhitespaces?: boolean
}

// Match all TypeScript files - we'll filter by @Component/@Directive decorator in the handler
Expand Down Expand Up @@ -293,6 +300,7 @@ export function angular(options: PluginOptions = {}): Plugin[] {
angularVersion: options.angularVersion,
emitClassMetadata: options.emitClassMetadata ?? true,
compilationMode: options.compilationMode ?? 'full',
preserveWhitespaces: options.preserveWhitespaces,
tsconfig: options.tsconfig,
}

Expand Down Expand Up @@ -552,6 +560,7 @@ export function angular(options: PluginOptions = {}): Plugin[] {
minifyComponentStyles: getMinifyComponentStyles(context),
emitClassMetadata: pluginOptions.emitClassMetadata,
compilationMode: pluginOptions.compilationMode,
preserveWhitespaces: pluginOptions.preserveWhitespaces,
// Evaluate decorator metadata imported from other files the way
// ngtsc's program-wide checker does (`inputs: INPUTS`, `@Input(OPTS)`,
// `templateUrl: TEMPLATE_URL`). Read files land in
Expand Down