Skip to content

Conversation

@Dunqing
Copy link
Member

@Dunqing Dunqing commented Nov 13, 2025

Implement a transformation plugin for tagged template expressions containing </script> tags to prevent script tag issues in browser environments.

This plugin transforms tagged template literals containing </script (case-insensitive) to use a helper function call with template caching, matching esbuild's behavior.

Example

Transforms tagged templates to use cached template objects with a helper function:

Input:

foo`</script>`
bar`<script>${content}</script>`

Output:

var _templateObject, _templateObject2;
foo(_templateObject || (_templateObject = babelHelpers.taggedTemplateLiteral(["<\/script>"])));
bar(_templateObject2 || (_templateObject2 = babelHelpers.taggedTemplateLiteral(["<script>", "<\/script>"])), content);

Closes #15306

@github-actions github-actions bot added the A-transformer Area - Transformer / Transpiler label Nov 13, 2025
Copy link
Member Author

Dunqing commented Nov 13, 2025


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions github-actions bot added the C-enhancement Category - New feature or request label Nov 13, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Nov 13, 2025

CodSpeed Performance Report

Merging #15664 will not alter performance

Comparing 11-13-feat_transformer_support_tagged_template_expression_with__script_transformation (777ea0b) with main (c023ba6)

Summary

✅ 33 untouched
⏩ 4 skipped1

Footnotes

  1. 4 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Dunqing Dunqing force-pushed the 11-13-feat_transformer_support_tagged_template_expression_with__script_transformation branch 4 times, most recently from 97af8ab to 7745501 Compare November 13, 2025 13:47
@Dunqing Dunqing marked this pull request as ready for review November 13, 2025 13:50
Copilot AI review requested due to automatic review settings November 13, 2025 13:50
@Dunqing Dunqing requested a review from sapphi-red November 13, 2025 13:50
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements a transformation plugin for tagged template expressions containing </script> tags to prevent script tag parsing issues when JavaScript is embedded in HTML. The transformation converts tagged template literals into function calls using cached template objects via a Babel helper.

Key Changes

  • Adds a new TaggedTemplateTransform plugin that detects and transforms tagged templates containing </script> (case-insensitive)
  • Integrates the plugin into the transformer pipeline with proper option handling across napi/TypeScript/Rust boundaries
  • Includes comprehensive test fixtures covering basic cases, expressions, case variations, and edge cases

Reviewed Changes

Copilot reviewed 33 out of 34 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/oxc_transformer/src/plugins/tagged_template_transform.rs Core plugin implementation with script tag detection and transformation logic
crates/oxc_transformer/src/plugins/mod.rs Plugin registration and integration into the traversal pipeline
crates/oxc_transformer/src/plugins/options.rs Plugin options structure definition
crates/oxc_transformer/src/options/mod.rs Option mapping from Babel configuration
crates/oxc_transformer/src/options/babel/plugins.rs Babel plugin entry parsing for "tagged-template-transform"
crates/oxc_transformer/src/common/helper_loader.rs Added TaggedTemplateLiteral helper enum variant
napi/transform/src/transformer.rs NAPI bindings for plugin options
napi/transform/index.d.ts TypeScript definitions for plugin configuration
npm/runtime/src/helpers/esm/taggedTemplateLiteralEscape.js Runtime helper function (currently unused)
tasks/transform_conformance/src/constants.rs Added plugin to conformance test suite
tasks/transform_conformance/snapshots/* Updated test snapshots showing improved pass rates
tasks/transform_conformance/tests/plugin-tagged-template-transform/* Comprehensive test fixtures for various scenarios

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Dunqing Dunqing changed the title feat(transformer): support tagged template expression with </script transformation feat(transformer): support tagged template expression with </script transformation Nov 14, 2025
@Boshen Boshen added the P-high Priority - High label Nov 14, 2025
@overlookmotel
Copy link
Member

overlookmotel commented Nov 15, 2025

@Dunqing in case you weren't aware, there's a panic in conformance.

@Dunqing
Copy link
Member Author

Dunqing commented Nov 16, 2025

@Dunqing in case you weren't aware, there's a panic in conformance.

Sorry, I forgot to push the local changes.

@Dunqing Dunqing force-pushed the 11-13-feat_transformer_support_tagged_template_expression_with__script_transformation branch 3 times, most recently from 5a07258 to 1d1dc5a Compare November 17, 2025 03:32
Copy link
Member

@sapphi-red sapphi-red left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM on my side

@overlookmotel overlookmotel force-pushed the 11-13-feat_transformer_support_tagged_template_expression_with__script_transformation branch from 1d1dc5a to 0f876fe Compare November 17, 2025 10:26
@overlookmotel
Copy link
Member

I'm afraid I don't think this is right. I'll make sure I'm correct about that, and write up what I think the problem is.

In meantime, have rebased on latest main + added one formatting nit commit.

@overlookmotel
Copy link
Member

overlookmotel commented Nov 17, 2025

What we're missing is a 2nd array for raw strings if cooked and raw are not the same:

Input:

foo`</script>`;
foo`</script>\n`;
foo`</script>\u`; // No cooked because invalid escape `\u`

Current output:

// Correct
foo(_temp || (_temp = _taggedTemplateLiteral(["</script>"])));
// `raw` property incorrect in `foo`
foo(_temp2 || (_temp2 = _taggedTemplateLiteral(["</script>\n"])));
// Syntax error!
foo(_temp3 || (_temp3 = _taggedTemplateLiteral(["</script>\u"])));

Output should be:

foo(_temp || (_temp = _taggedTemplateLiteral(["</script>"])));
foo(_temp2 || (_temp2 = _taggedTemplateLiteral(["</script>\n"], ["</script>\\n"])));
//                                                            ^^^^^^^^^^^^^^^^^^
foo(_temp3 || (_temp3 = _taggedTemplateLiteral([void 0], ["</script>\\u"])));
//                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^

Babel REPL

ESBuild REPL


Because this only transforms tagged template literals containing </script>, I think we're OK to merge this, and fix in a follow-up. It'll only be incorrect for tagged template literals which contain both </script> and escapes e.g. \n, and syntax error only occurs if there's an invalid escape sequence in the template.

Additionally, is this transform enabled in benchmarks? I think it may be a perf regression. We should check if expr is an Expression::TaggedTemplateExpression in enter_expression and NOT add #[inline] to transform_tagged_template - because most expressions are not TaggedTemplateExpressions.

@overlookmotel
Copy link
Member

overlookmotel commented Nov 17, 2025

Actually, this transform is enabled in benchmarks. Effect is only -1%.

That's acceptable for now. We can improve it in a follow-up.


let raw_bytes = raw.as_bytes();
// Get the bytes up to the last possible starting position of the script tag
let max_remain_len = raw_bytes.len().saturating_sub(SCRIPT_TAG.len());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for saturating_sub here. You already checked that len < SCRIPT_TAG.len() above, so just raw_bytes.len() - SCRIPT_TAG.len() would do (cheaper).

Comment on lines +88 to +97
for (idx, byte) in raw_bytes_iter {
if byte == b'<'
&& SCRIPT_TAG
.iter()
.zip(raw_bytes[idx..].iter())
.all(|(a, b)| *a == b.to_ascii_lowercase())
{
return true;
}
}
Copy link
Member

@overlookmotel overlookmotel Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a faster way to check for </script in oxc_codegen here:

/// Check if `slice` is `</script`, regardless of case.
///
/// `slice.len()` must be 8.
//
// `#[inline(always)]` so that compiler can see from caller that `slice.len() == 8`
// and so `slice.try_into().unwrap()` cannot fail. This function is only 4 instructions.
#[expect(clippy::inline_always)]
#[inline(always)]
pub fn is_script_close_tag(slice: &[u8]) -> bool {
// Compiler condenses these operations to an 8-byte read, u64 AND, and u64 compare.
// https://godbolt.org/z/K8q68WGn6
let mut bytes: [u8; 8] = slice.try_into().unwrap();
for byte in bytes.iter_mut().skip(2) {
// `| 32` converts ASCII upper case letters to lower case.
*byte |= 32;
}
bytes == *b"</script"
}

Copy link
Member

@overlookmotel overlookmotel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've decided to merge, and fix the bug + perf in follow-ups.

@overlookmotel overlookmotel added the 0-merge Merge with Graphite Merge Queue label Nov 17, 2025
Copy link
Member

overlookmotel commented Nov 17, 2025

Merge activity

… transformation (#15664)

Implement a transformation plugin for tagged template expressions containing </script> tags to prevent script tag issues in browser environments.

  This plugin transforms tagged template literals containing `</script` (case-insensitive) to use a helper function call with template caching, matching esbuild's behavior.

 ### Example

  Transforms tagged templates to use cached template objects with a helper function:

  Input:
```js
foo`</script>`
bar`<script>${content}</script>`
```
  Output:
```js
var _templateObject, _templateObject2;
foo(_templateObject || (_templateObject = babelHelpers.taggedTemplateLiteral(["<\/script>"])));
bar(_templateObject2 || (_templateObject2 = babelHelpers.taggedTemplateLiteral(["<script>", "<\/script>"])), content);
```

 Closes #15306
@graphite-app graphite-app bot force-pushed the 11-13-feat_transformer_support_tagged_template_expression_with__script_transformation branch from 777ea0b to 1b18457 Compare November 17, 2025 11:55
@graphite-app graphite-app bot merged commit 1b18457 into main Nov 17, 2025
20 checks passed
@graphite-app graphite-app bot deleted the 11-13-feat_transformer_support_tagged_template_expression_with__script_transformation branch November 17, 2025 12:01
@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Nov 17, 2025
camc314 added a commit to camc314/rolldown that referenced this pull request Nov 17, 2025
camc314 added a commit to camc314/rolldown that referenced this pull request Nov 17, 2025
camc314 added a commit to camc314/rolldown that referenced this pull request Nov 17, 2025
camc314 added a commit to camc314/rolldown that referenced this pull request Nov 17, 2025
Boshen pushed a commit to camc314/rolldown that referenced this pull request Nov 17, 2025
graphite-app bot pushed a commit that referenced this pull request Nov 19, 2025
…ces (#15830)

Fixes the issue identified in #15664 where the tagged template transform plugin incorrectly handled template literals containing escape sequences.

### Examples

```js
// Input
foo`</script>\n`
// Before (incorrect):
foo(_t || (_t = taggedTemplateLiteral(["</script>\\n"])));
// After (correct):
foo(_t || (_t = taggedTemplateLiteral(["</script>\n"], ["</script>\\n"])));

// Input with invalid escape
foo`</script>\u`
// Before (syntax error):
foo(_t || (_t = taggedTemplateLiteral(["</script>\\u"])));
// After (correct):
foo(_t || (_t = taggedTemplateLiteral([void 0], ["</script>\\u"])));
Copilot AI pushed a commit that referenced this pull request Nov 21, 2025
…ces (#15830)

Fixes the issue identified in #15664 where the tagged template transform plugin incorrectly handled template literals containing escape sequences.

### Examples

```js
// Input
foo`</script>\n`
// Before (incorrect):
foo(_t || (_t = taggedTemplateLiteral(["</script>\\n"])));
// After (correct):
foo(_t || (_t = taggedTemplateLiteral(["</script>\n"], ["</script>\\n"])));

// Input with invalid escape
foo`</script>\u`
// Before (syntax error):
foo(_t || (_t = taggedTemplateLiteral(["</script>\\u"])));
// After (correct):
foo(_t || (_t = taggedTemplateLiteral([void 0], ["</script>\\u"])));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-transformer Area - Transformer / Transpiler C-enhancement Category - New feature or request P-high Priority - High

Projects

None yet

Development

Successfully merging this pull request may close these issues.

codegen: add a way to keep </script> as-is for tagged template

5 participants