Skip to content

Commit 30299ab

Browse files
committed
feat: do unused values stripping correctly
1 parent 9005348 commit 30299ab

File tree

343 files changed

+519
-447
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

343 files changed

+519
-447
lines changed

.changeset/salty-oranges-float.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@astrojs/compiler-binding": patch
3+
"@astrojs/compiler-rs": patch
4+
---
5+
6+
Fixes an issue where certain compressHTML settings wouldn't work

crates/astro_codegen/src/printer/mod.rs

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ mod whitespace;
3434
mod sourcemap_builder;
3535
#[cfg(test)]
3636
mod sourcemap_tests;
37+
mod typescript;
3738

3839
// Re-export public result types at the `printer` level so that `lib.rs`
3940
// can `pub use printer::{...}` without reaching into `result`.
@@ -496,10 +497,20 @@ impl<'a> AstroCodegen<'a> {
496497
let phase1_sourcemap = self.sourcemap_builder.take();
497498
let source_path = self.options.filename.as_deref().unwrap_or("<stdin>");
498499

499-
// Strip TypeScript and compose sourcemaps.
500-
let (mut code, sourcemap) = sourcemap_builder::strip_and_compose_sourcemaps(
501-
self.allocator,
500+
// Strip TypeScript from the intermediate code. When a sourcemap is
501+
// requested we also ask the stripper to produce an intermediate→final
502+
// sourcemap (phase2) so we can compose it with the Phase 1 map below.
503+
let generate_sourcemap = phase1_sourcemap.is_some();
504+
let (mut code, phase2_map) =
505+
typescript::strip_typescript(self.allocator, &intermediate_code, generate_sourcemap);
506+
507+
// Compose Phase 1 (astro codegen) and Phase 2 (TS stripping) sourcemaps.
508+
// This is independent of stripping: you can strip without a sourcemap,
509+
// and the compose step is a no-op when no sourcemap was requested.
510+
let sourcemap = sourcemap_builder::compose_sourcemaps(
502511
&intermediate_code,
512+
&code,
513+
phase2_map,
503514
phase1_sourcemap,
504515
source_path,
505516
self.source_text,
@@ -1133,7 +1144,21 @@ impl<'a> AstroCodegen<'a> {
11331144
.unwrap_or(remaining.len());
11341145
let remaining = &remaining[..last_real_idx];
11351146

1136-
self.print_jsx_children_compact(remaining);
1147+
// Print all but the last child normally, then trim trailing whitespace
1148+
// from the last text node — matching Go compiler's TrimTrailingSpace
1149+
// behaviour (the source file may end with a newline after real content).
1150+
if let Some((last, rest)) = remaining.split_last() {
1151+
self.print_jsx_children_compact(rest);
1152+
if let JSXChild::Text(text) = last {
1153+
let trimmed = text.value.trim_end();
1154+
if !trimmed.is_empty() {
1155+
self.add_source_mapping_for_span(text.span);
1156+
self.print(&escape_template_literal(trimmed));
1157+
}
1158+
} else {
1159+
self.print_jsx_children_compact(std::slice::from_ref(last));
1160+
}
1161+
}
11371162
}
11381163

11391164
/// Check if we need to insert `$$maybeRenderHead` at the start of the template.
@@ -1746,10 +1771,9 @@ import Component from 'test';
17461771
"Missing $$renderHead in head"
17471772
);
17481773

1749-
let maybe_render_head_count = output.matches("$$maybeRenderHead").count();
1750-
assert_eq!(
1751-
maybe_render_head_count, 1,
1752-
"$$maybeRenderHead should only appear once (in import), found {maybe_render_head_count} times. Body should not have $$maybeRenderHead when explicit <head> exists"
1774+
assert!(
1775+
!output.contains("$$maybeRenderHead("),
1776+
"Body should not have $$maybeRenderHead when explicit <head> exists"
17531777
);
17541778
}
17551779

@@ -1767,10 +1791,9 @@ import Component from 'test';
17671791
"Missing meta element"
17681792
);
17691793

1770-
let maybe_render_head_count = output.matches("$$maybeRenderHead").count();
1771-
assert_eq!(
1772-
maybe_render_head_count, 1,
1773-
"$$maybeRenderHead should only appear once (in import), found {maybe_render_head_count} times. Head elements should not trigger $$maybeRenderHead"
1794+
assert!(
1795+
!output.contains("$$maybeRenderHead("),
1796+
"Head elements should not trigger $$maybeRenderHead"
17741797
);
17751798
}
17761799

@@ -1788,10 +1811,9 @@ import Component from 'test';
17881811
"Custom element should have tag name as both display name and quoted identifier"
17891812
);
17901813

1791-
let maybe_render_head_count = output.matches("$$maybeRenderHead").count();
1792-
assert_eq!(
1793-
maybe_render_head_count, 1,
1794-
"$$maybeRenderHead should only appear once (in import), custom elements should not trigger it"
1814+
assert!(
1815+
!output.contains("$$maybeRenderHead("),
1816+
"Custom elements should not trigger $$maybeRenderHead"
17951817
);
17961818

17971819
assert!(

crates/astro_codegen/src/printer/sourcemap_builder.rs

Lines changed: 15 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
1010
use std::path::Path;
1111

12-
use oxc_allocator::Allocator;
13-
use oxc_codegen::CodegenOptions;
1412
use oxc_span::Span;
1513
use rustc_hash::{FxHashMap, FxHashSet};
1614

@@ -316,8 +314,8 @@ pub(super) struct SupplementContext<'a> {
316314
pub composed_positions: FxHashSet<(u32, u32)>,
317315
}
318316

319-
/// Strip TypeScript from `intermediate_code` and compose the resulting
320-
/// sourcemap with the Phase 1 sourcemap from Astro codegen.
317+
/// Compose the Phase 2 sourcemap (TypeScript stripping) with the Phase 1
318+
/// sourcemap from Astro codegen, supplementing template-literal tokens.
321319
///
322320
/// Phase 1 maps intermediate positions → original `.astro` positions.
323321
/// Phase 2 (TypeScript stripping / oxc_codegen re-emit) maps final positions
@@ -330,24 +328,27 @@ pub(super) struct SupplementContext<'a> {
330328
/// function carries forward Phase 1 tokens that were not covered by Phase 2 by
331329
/// computing line/column adjustments between the intermediate and final code.
332330
///
333-
/// Returns `(final_code, Option<SourceMap>)` where the sourcemap is `None` if
334-
/// no sourcemap was requested.
331+
/// `final_code` is the already-stripped JavaScript produced by the TypeScript
332+
/// stripping pass. `phase2_map` is the sourcemap from that pass (mapping
333+
/// `final_code` positions back to `intermediate_code` positions).
334+
///
335+
/// Returns `Some(SourceMap)` when both `phase2_map` and `phase1_sourcemap` are
336+
/// provided; returns `None` otherwise (no sourcemap requested).
335337
///
336338
/// # Panics
337339
///
338340
/// Panics if line or column values exceed `u32` or `i64` (impossible in
339341
/// practice for source files).
340-
pub fn strip_and_compose_sourcemaps(
341-
allocator: &Allocator,
342+
pub fn compose_sourcemaps(
342343
intermediate_code: &str,
344+
final_code: &str,
345+
phase2_map: Option<oxc_sourcemap::SourceMap>,
343346
phase1_sourcemap: Option<AstroSourcemapBuilder<'_>>,
344347
source_path: &str,
345348
source_text: &str,
346-
) -> (String, Option<oxc_sourcemap::SourceMap>) {
347-
let generate_sourcemap = phase1_sourcemap.is_some();
348-
let (code, phase2_map) = strip_typescript(allocator, intermediate_code, generate_sourcemap);
349-
350-
let map = if let (Some(phase2_map), Some(phase1_sm)) = (phase2_map, phase1_sourcemap) {
349+
) -> Option<oxc_sourcemap::SourceMap> {
350+
if let (Some(phase2_map), Some(phase1_sm)) = (phase2_map, phase1_sourcemap) {
351+
let code = final_code;
351352
let phase1_map = phase1_sm.into_sourcemap();
352353
let composed = remap_sourcemap(&phase2_map, &phase1_map, source_path, source_text);
353354

@@ -570,9 +571,7 @@ pub fn strip_and_compose_sourcemaps(
570571
Some(composed)
571572
} else {
572573
None
573-
};
574-
575-
(code, map)
574+
}
576575
}
577576

578577
/// Carry forward Phase 1 tokens inside the template literal region that
@@ -770,55 +769,6 @@ fn try_anchor_supplement(
770769
}
771770
}
772771

773-
/// Strip TypeScript syntax from generated code.
774-
///
775-
/// Parses the code as TypeScript, runs `oxc_transformer` (TS-only stripping,
776-
/// no JSX transform, no ES downleveling), and re-emits as JavaScript.
777-
fn strip_typescript(
778-
allocator: &Allocator,
779-
code: &str,
780-
generate_sourcemap: bool,
781-
) -> (String, Option<oxc_sourcemap::SourceMap>) {
782-
let source_type = oxc_span::SourceType::mjs().with_typescript(true);
783-
let ret = oxc_parser::Parser::new(allocator, code, source_type).parse();
784-
785-
if !ret.errors.is_empty() {
786-
// If parsing fails, return the code unchanged — the downstream
787-
// consumer will report a better error.
788-
return (code.to_string(), None);
789-
}
790-
791-
let mut program = ret.program;
792-
let scoping = oxc_semantic::SemanticBuilder::new()
793-
.with_excess_capacity(2.0)
794-
.build(&program)
795-
.semantic
796-
.into_scoping();
797-
798-
let mut options = oxc_transformer::TransformOptions::default();
799-
// Keep value imports that appear unused. In our generated code, imported
800-
// identifiers are referenced inside template literal strings (e.g.
801-
// `$$render\`${Component}\``) which semantic analysis cannot see, so
802-
// without this flag the transformer would incorrectly remove them.
803-
options.typescript.only_remove_type_imports = true;
804-
let _ = oxc_transformer::Transformer::new(allocator, std::path::Path::new(""), &options)
805-
.build_with_scoping(scoping, &mut program);
806-
807-
let codegen_options = CodegenOptions {
808-
single_quote: false,
809-
source_map_path: if generate_sourcemap {
810-
Some(std::path::PathBuf::from("intermediate.js"))
811-
} else {
812-
None
813-
},
814-
..CodegenOptions::default()
815-
};
816-
let result = oxc_codegen::Codegen::new()
817-
.with_options(codegen_options)
818-
.build(&program);
819-
(result.code, result.map)
820-
}
821-
822772
#[cfg(test)]
823773
mod tests {
824774
use super::*;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//! TypeScript stripping pass.
2+
//!
3+
//! Parses a TypeScript string, runs `oxc_transformer` (TS-only stripping,
4+
//! no JSX transform, no ES downleveling), and re-emits as plain JavaScript.
5+
//!
6+
//! This is intentionally independent of sourcemap concerns: you can strip
7+
//! TypeScript with or without a sourcemap. The sourcemap composition that
8+
//! chains this pass's output map back to the original `.astro` source lives
9+
//! in [`super::sourcemap_builder`].
10+
11+
use oxc_allocator::Allocator;
12+
use oxc_codegen::CodegenOptions;
13+
14+
/// Strip TypeScript syntax from `code`.
15+
///
16+
/// Returns `(js_code, Option<SourceMap>)`. The sourcemap is produced only when
17+
/// `generate_sourcemap` is `true`; it maps positions in the returned JavaScript
18+
/// back to positions in the input `code`.
19+
///
20+
/// If parsing fails the input is returned unchanged (with `None` for the map)
21+
/// so that the downstream consumer can report a better error.
22+
pub fn strip_typescript(
23+
allocator: &Allocator,
24+
code: &str,
25+
generate_sourcemap: bool,
26+
) -> (String, Option<oxc_sourcemap::SourceMap>) {
27+
let source_type = oxc_span::SourceType::mjs().with_typescript(true);
28+
let ret = oxc_parser::Parser::new(allocator, code, source_type).parse();
29+
30+
if !ret.errors.is_empty() {
31+
// If parsing fails, return the code unchanged — the downstream
32+
// consumer will report a better error.
33+
return (code.to_string(), None);
34+
}
35+
36+
let mut program = ret.program;
37+
let scoping = oxc_semantic::SemanticBuilder::new()
38+
.with_excess_capacity(2.0)
39+
.build(&program)
40+
.semantic
41+
.into_scoping();
42+
43+
let options = oxc_transformer::TransformOptions::default();
44+
let _ = oxc_transformer::Transformer::new(allocator, std::path::Path::new(""), &options)
45+
.build_with_scoping(scoping, &mut program);
46+
47+
let codegen_options = CodegenOptions {
48+
single_quote: false,
49+
source_map_path: if generate_sourcemap {
50+
Some(std::path::PathBuf::from("intermediate.js"))
51+
} else {
52+
None
53+
},
54+
..CodegenOptions::default()
55+
};
56+
let result = oxc_codegen::Codegen::new()
57+
.with_options(codegen_options)
58+
.build(&program);
59+
(result.code, result.map)
60+
}

crates/astro_codegen/tests/fixtures/_955_ternary_slot_with_elements.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
source: crates/astro_codegen/tests/snapshots.rs
33
input_file: crates/astro_codegen/tests/fixtures/_955_ternary_slot_with_elements.astro
44
---
5-
import { Fragment, render as $$render, createAstro as $$createAstro, createComponent as $$createComponent, renderComponent as $$renderComponent, renderHead as $$renderHead, maybeRenderHead as $$maybeRenderHead, unescapeHTML as $$unescapeHTML, renderSlot as $$renderSlot, mergeSlots as $$mergeSlots, addAttribute as $$addAttribute, spreadAttributes as $$spreadAttributes, defineStyleVars as $$defineStyleVars, defineScriptVars as $$defineScriptVars, renderTransition as $$renderTransition, createTransitionScope as $$createTransitionScope, renderScript as $$renderScript, createMetadata as $$createMetadata } from "http://localhost:3000/";
5+
import { render as $$render, createComponent as $$createComponent, renderComponent as $$renderComponent, maybeRenderHead as $$maybeRenderHead, createMetadata as $$createMetadata } from "http://localhost:3000/";
66
export const $$metadata = $$createMetadata(import.meta.url, {
77
modules: [],
88
hydratedComponents: [],

crates/astro_codegen/tests/fixtures/_955_ternary_slot_with_text.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
source: crates/astro_codegen/tests/snapshots.rs
33
input_file: crates/astro_codegen/tests/fixtures/_955_ternary_slot_with_text.astro
44
---
5-
import { Fragment, render as $$render, createAstro as $$createAstro, createComponent as $$createComponent, renderComponent as $$renderComponent, renderHead as $$renderHead, maybeRenderHead as $$maybeRenderHead, unescapeHTML as $$unescapeHTML, renderSlot as $$renderSlot, mergeSlots as $$mergeSlots, addAttribute as $$addAttribute, spreadAttributes as $$spreadAttributes, defineStyleVars as $$defineStyleVars, defineScriptVars as $$defineScriptVars, renderTransition as $$renderTransition, createTransitionScope as $$createTransitionScope, renderScript as $$renderScript, createMetadata as $$createMetadata } from "http://localhost:3000/";
5+
import { render as $$render, createComponent as $$createComponent, renderComponent as $$renderComponent, maybeRenderHead as $$maybeRenderHead, createMetadata as $$createMetadata } from "http://localhost:3000/";
66
export const $$metadata = $$createMetadata(import.meta.url, {
77
modules: [],
88
hydratedComponents: [],

crates/astro_codegen/tests/fixtures/advanced_svg_expression.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
source: crates/astro_codegen/tests/snapshots.rs
33
input_file: crates/astro_codegen/tests/fixtures/advanced_svg_expression.astro
44
---
5-
import { Fragment, render as $$render, createAstro as $$createAstro, createComponent as $$createComponent, renderComponent as $$renderComponent, renderHead as $$renderHead, maybeRenderHead as $$maybeRenderHead, unescapeHTML as $$unescapeHTML, renderSlot as $$renderSlot, mergeSlots as $$mergeSlots, addAttribute as $$addAttribute, spreadAttributes as $$spreadAttributes, defineStyleVars as $$defineStyleVars, defineScriptVars as $$defineScriptVars, renderTransition as $$renderTransition, createTransitionScope as $$createTransitionScope, renderScript as $$renderScript, createMetadata as $$createMetadata } from "http://localhost:3000/";
5+
import { render as $$render, createComponent as $$createComponent, maybeRenderHead as $$maybeRenderHead, createMetadata as $$createMetadata } from "http://localhost:3000/";
66
export const $$metadata = $$createMetadata(import.meta.url, {
77
modules: [],
88
hydratedComponents: [],

crates/astro_codegen/tests/fixtures/all_components.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
source: crates/astro_codegen/tests/snapshots.rs
33
input_file: crates/astro_codegen/tests/fixtures/all_components.astro
44
---
5-
import { Fragment, render as $$render, createAstro as $$createAstro, createComponent as $$createComponent, renderComponent as $$renderComponent, renderHead as $$renderHead, maybeRenderHead as $$maybeRenderHead, unescapeHTML as $$unescapeHTML, renderSlot as $$renderSlot, mergeSlots as $$mergeSlots, addAttribute as $$addAttribute, spreadAttributes as $$spreadAttributes, defineStyleVars as $$defineStyleVars, defineScriptVars as $$defineScriptVars, renderTransition as $$renderTransition, createTransitionScope as $$createTransitionScope, renderScript as $$renderScript, createMetadata as $$createMetadata } from "http://localhost:3000/";
5+
import { render as $$render, createComponent as $$createComponent, renderComponent as $$renderComponent, maybeRenderHead as $$maybeRenderHead, createMetadata as $$createMetadata } from "http://localhost:3000/";
66
import { Container, Col, Row } from "react-bootstrap";
77
import * as $$module1 from "react-bootstrap";
88
export const $$metadata = $$createMetadata(import.meta.url, {

crates/astro_codegen/tests/fixtures/anchor_content.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
source: crates/astro_codegen/tests/snapshots.rs
33
input_file: crates/astro_codegen/tests/fixtures/anchor_content.astro
44
---
5-
import { Fragment, render as $$render, createAstro as $$createAstro, createComponent as $$createComponent, renderComponent as $$renderComponent, renderHead as $$renderHead, maybeRenderHead as $$maybeRenderHead, unescapeHTML as $$unescapeHTML, renderSlot as $$renderSlot, mergeSlots as $$mergeSlots, addAttribute as $$addAttribute, spreadAttributes as $$spreadAttributes, defineStyleVars as $$defineStyleVars, defineScriptVars as $$defineScriptVars, renderTransition as $$renderTransition, createTransitionScope as $$createTransitionScope, renderScript as $$renderScript, createMetadata as $$createMetadata } from "http://localhost:3000/";
5+
import { render as $$render, createComponent as $$createComponent, maybeRenderHead as $$maybeRenderHead, createMetadata as $$createMetadata } from "http://localhost:3000/";
66
export const $$metadata = $$createMetadata(import.meta.url, {
77
modules: [],
88
hydratedComponents: [],

crates/astro_codegen/tests/fixtures/anchor_expressions.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
source: crates/astro_codegen/tests/snapshots.rs
33
input_file: crates/astro_codegen/tests/fixtures/anchor_expressions.astro
44
---
5-
import { Fragment, render as $$render, createAstro as $$createAstro, createComponent as $$createComponent, renderComponent as $$renderComponent, renderHead as $$renderHead, maybeRenderHead as $$maybeRenderHead, unescapeHTML as $$unescapeHTML, renderSlot as $$renderSlot, mergeSlots as $$mergeSlots, addAttribute as $$addAttribute, spreadAttributes as $$spreadAttributes, defineStyleVars as $$defineStyleVars, defineScriptVars as $$defineScriptVars, renderTransition as $$renderTransition, createTransitionScope as $$createTransitionScope, renderScript as $$renderScript, createMetadata as $$createMetadata } from "http://localhost:3000/";
5+
import { render as $$render, createComponent as $$createComponent, maybeRenderHead as $$maybeRenderHead, createMetadata as $$createMetadata } from "http://localhost:3000/";
66
export const $$metadata = $$createMetadata(import.meta.url, {
77
modules: [],
88
hydratedComponents: [],

0 commit comments

Comments
 (0)