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
4 changes: 3 additions & 1 deletion docs/bundler/minifier.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -757,17 +757,19 @@ foo&&bar||baz

**Mode:** `--minify-whitespace`

Removes comments except important license comments.
Removes comments except legal comments. A legal comment starts with `/*!` or `//!`, or contains the word `@license` or `@preserve`. A longer word such as `@licensee` does not count.

```ts Input
// This comment is removed
/* So is this */
/*! But this license comment is kept */
/** @license MIT, and so is this one */
function test() { /* inline comment */ }
```

```js Output
/*! But this license comment is kept */
/** @license MIT, and so is this one */
function test(){}
```

Expand Down
30 changes: 19 additions & 11 deletions src/js_parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ pub struct LexerSnapshot<'a> {
pub(crate) has_pure_comment_before: bool,
pub(crate) has_react_hooks_suppression_before: bool,
pub(crate) has_react_hooks_block_suppression: bool,
pub(crate) preserve_all_comments_before: bool,
pub(crate) is_legacy_octal_literal: bool,
pub(crate) is_log_disabled: bool,
pub(crate) code_point: CodePoint,
Expand Down Expand Up @@ -219,7 +218,6 @@ pub struct Lexer<'a> {
/// `eslint-disable` (no `-next-line` suffix). Never cleared by the
/// parser, so it applies to every subsequent function in the file.
pub(crate) has_react_hooks_block_suppression: bool,
pub(crate) preserve_all_comments_before: bool,
pub(crate) is_legacy_octal_literal: bool,
pub(crate) is_log_disabled: bool,
pub(crate) comments_to_preserve_before: Vec<js_ast::G::Comment>,
Expand Down Expand Up @@ -335,7 +333,6 @@ impl<'a> Lexer<'a> {
has_pure_comment_before: self.has_pure_comment_before,
has_react_hooks_suppression_before: self.has_react_hooks_suppression_before,
has_react_hooks_block_suppression: self.has_react_hooks_block_suppression,
preserve_all_comments_before: self.preserve_all_comments_before,
is_legacy_octal_literal: self.is_legacy_octal_literal,
is_log_disabled: self.is_log_disabled,
code_point: self.code_point,
Expand Down Expand Up @@ -373,7 +370,6 @@ impl<'a> Lexer<'a> {
self.has_pure_comment_before = original.has_pure_comment_before;
self.has_react_hooks_suppression_before = original.has_react_hooks_suppression_before;
self.has_react_hooks_block_suppression = original.has_react_hooks_block_suppression;
self.preserve_all_comments_before = original.preserve_all_comments_before;
self.is_legacy_octal_literal = original.is_legacy_octal_literal;
self.is_log_disabled = original.is_log_disabled;
self.code_point = original.code_point;
Expand Down Expand Up @@ -1892,7 +1888,7 @@ impl<'a> Lexer<'a> {

fn scan_comment_text(&mut self, for_pragma: bool) {
let text = &self.contents[self.start..self.end];
let has_legal_annotation = text.len() > 2 && text[2] == b'!';
let has_legal_annotation = is_legal_comment(text);
let is_multiline_comment = text.len() > 1 && text[1] == b'*';

if self.track_comments {
Expand Down Expand Up @@ -1937,11 +1933,7 @@ impl<'a> Lexer<'a> {
}
}

if has_legal_annotation || self.preserve_all_comments_before {
if is_multiline_comment {
// text = lexer.removeMultilineCommentIndent(lexer.source.contents[0..lexer.start], text);
}

if has_legal_annotation {
self.comments_to_preserve_before.push(js_ast::G::Comment {
text: text.into(),
loc: self.loc(),
Expand Down Expand Up @@ -2227,7 +2219,6 @@ impl<'a> Lexer<'a> {
has_pure_comment_before: false,
has_react_hooks_suppression_before: false,
has_react_hooks_block_suppression: false,
preserve_all_comments_before: false,
is_legacy_octal_literal: false,
is_log_disabled: false,
comments_to_preserve_before: Vec::new(),
Expand Down Expand Up @@ -3331,6 +3322,23 @@ pub(crate) fn is_whitespace(codepoint: CodePoint) -> bool {
|| strings::is_unicode_space_separator(codepoint as u32)
}

/// esbuild's rule: `/*! ... */`, `//! ...`, or the word `@license` / `@preserve` anywhere in the comment.
pub(crate) fn is_legal_comment(text: &[u8]) -> bool {
if text.len() > 2 && text[2] == b'!' {
return true;
}
let mut rest = text;
while let Some(i) = strings::index_of_char_usize(rest, b'@') {
rest = &rest[i + 1..];
if strings::has_prefix_with_word_boundary(rest, b"license")
|| strings::has_prefix_with_word_boundary(rest, b"preserve")
{
return true;
}
}
false
}

pub use bun_core::identifier::{is_identifier, is_identifier_utf16};

pub fn range_of_identifier(source: &Source, loc: Loc) -> Range {
Expand Down
3 changes: 1 addition & 2 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7379,8 +7379,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
/// `lexer.is_log_disabled`. Lists are captured as lengths and truncated
/// on restore; the attempt's arena allocations just become unreachable.
/// The legal comments waiting for the next statement are moved out
/// instead: a block body or `import()` drains them, so a length cannot
/// restore them.
/// instead: a block body drains them, so a length cannot restore them.
pub(crate) fn parser_snapshot(&mut self) -> ParserSnapshot<'a> {
let comments_to_preserve_before =
core::mem::take(&mut self.lexer.comments_to_preserve_before);
Expand Down
9 changes: 0 additions & 9 deletions src/js_parser/parse/parse_import_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let old_allow_in = p.allow_in;
p.allow_in = true;

p.lexer.preserve_all_comments_before = true;
p.lexer.expect(T::TOpenParen)?;

// const comments = try p.lexer.comments_to_preserve_before.toOwnedSlice();
p.lexer.comments_to_preserve_before.clear();

p.lexer.preserve_all_comments_before = false;

let mut value = p.parse_expr(Level::Comma)?;

let mut import_options = Expr::EMPTY;
Expand Down Expand Up @@ -98,12 +92,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

// _ = comments; // TODO: leading_interior comments

Ok(p.new_expr(
E::Import {
expr: value,
// .leading_interior_comments = comments,
import_record_index: u32::MAX,
options: import_options,
},
Expand Down
32 changes: 16 additions & 16 deletions test/bundler/bundler_bytecode_portable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,10 +474,10 @@ describe("bytecode cache portability", () => {
"sha256": "2a5d62fb4ca9d107e3a5bb2abe6a73f3c859a6f1a91c6c3d77653cb8c2053361",
},
"bun build --bytecode --minify all.js": {
"js": "721d67b0c2135aad554c9962b4e4505a4d6856b50da537fee7566d43f6cfdbda",
"js": "02f69e5448f525a3f098f06e9da1565b887125621607fcf5ef3d6be4f2642d6f",
"jsc": {
"bytes": 1998992,
"sha256": "f2631b3ba2b532e52183d5e6a44a7dbf5c0515b368d95fc692bf0e5ad15eb8f6",
"bytes": 1999024,
"sha256": "68d66351c6904f530efe9bb3d7d4bec29216cfca84ad0e592cf0f9cc2a6eab7a",
},
},
"bun build --bytecode --minify features.js": {
Expand All @@ -502,10 +502,10 @@ describe("bytecode cache portability", () => {
},
},
"bun build --bytecode all.js": {
"js": "a2f566b81700f852fef68247aef22af8b916858de30e4dc787846fd874c9e4d2",
"js": "3fed52509bcec75d9e29ae544e594c2bd0b656435391c4b25d93b77781e6875d",
"jsc": {
"bytes": 2178656,
"sha256": "f743f4e9cff325bfd8d344f2dead6a8ba6b9933393a09c75334429ead924c480",
"bytes": 2178664,
"sha256": "7e1b891eeadd25c17d7cb0d0b2bbbd6f834c9460d1a52fc1ce80e362d806b919",
},
},
"bun build --bytecode big.js": {
Expand All @@ -530,31 +530,31 @@ describe("bytecode cache portability", () => {
},
},
"bun build --bytecode immutable/dist/immutable.es.js": {
"js": "c9a2ba9f6b6a662e6bdfd44128bc66284276f5eac2a8875adb4578472328dc9f",
"js": "05f0cde57bc6b435ac582d1dd835a4ca534e4463aece6cf8ab0ba804bada4b95",
"jsc": {
"bytes": 280016,
"sha256": "2a99c33a2516e2d41187bc322ddd4523318530b51c60c3d9a8bd566d4f2929a1",
"bytes": 280032,
"sha256": "76e7397e6837de9932399628bf313864cb204fbf0cca6b9c01c8d919778084a6",
},
},
"bun build --bytecode libraries.js": {
"js": "f9d0c270d3f41997b904689435104703b1f8b6b1c67ead48dff30e7354890cfd",
"js": "d07e6f6b8072d025b5dabc555ae1956f35eb93ae9949f4e28d3241d24454f860",
"jsc": {
"bytes": 23806376,
"sha256": "1e2e31a113d8b5ca3e9cd73820d465e678756c0942023480569f785b15987dcf",
"bytes": 23806816,
"sha256": "b19fb12773cc11afa6e1fa4f2a2a67db4ae8dc9f9faa68f3fba178d9626e522e",
},
},
"bun build --bytecode lodash/lodash.js": {
"js": "e372f1aec6b2bba20581abfce3b7273aae0ec4f593ed2aa4aa5c60c66836856d",
"js": "d848e69990c2f1f1de4cc4f085b82a68d4f9fe41dec935bc324c3446756638d1",
"jsc": {
"bytes": 347488,
"sha256": "531d6bb544c53641289a44e72ac096560b19d360c2ee19b77483987c367b9b5e",
"sha256": "c0a867e3d08d091010a748af6864b5080040ff6a1fd26450ece8b4e887ea6ba9",
},
},
"bun build --bytecode react-dom/cjs/react-dom.development.js": {
"js": "e2cf34c8eb6f5952a33ca91e3949b28786349cf97ea3fbf0b3b8500dbccd4860",
"js": "0c1c056e3cf8c745148f5e60449707d9c997a9390ba8740e5d2d3371f9dbb8ae",
"jsc": {
"bytes": 980080,
"sha256": "32fa8492ad9fdf3ea31395756898d556df9dc6bdfbac9bba07bcafa748ab38ca",
"sha256": "1cd131e19ca8feff1cceb2dcbcada6a08656ef0653ad22c40e18f003fca7acf3",
},
},
"bun build --bytecode records.js": {
Expand Down
92 changes: 92 additions & 0 deletions test/bundler/bundler_comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,95 @@ describe("multi-line comments", () => {
},
});
});

describe("legal comments", () => {
// Like esbuild, keep a comment that contains the word "@license" or
// "@preserve", not only "/*!" and "//!". "@copyright" alone is not legal,
// and neither is a longer word such as "@licensee".
const legalForms = [
"/*! bang block */",
"//! bang line",
"/* @license MIT Copyright (c) Foo */",
"/** @license Apache-2.0 */",
"/* @preserve keep this */",
"// @license line form",
"// @preserve line form",
"/**\n * @license BSD-3-Clause\n * Copyright (c) Bar\n */",
"/* @license*/",
"/* see foo@license for the word boundary */",
];
const droppedForms = [
"/* @copyright not legal */",
"/* plain comment */",
"/* the @licensee agrees */",
"/* @preserved for posterity */",
"// see the @licenses folder",
"/* @license_x */",
"/* @license9 */",
];
const header = legalForms.join("\n") + "\n" + droppedForms.join("\n") + "\n";

function expectEachFormOnce(output: string) {
for (const form of legalForms) {
expect(output.split(form)).toHaveLength(2);
}
for (const form of droppedForms) {
expect(output).not.toContain(form);
}
}

for (const minify of [false, true]) {
itBundled(`@license and @preserve comments are kept ${minify ? "with" : "without"} minification`, {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
files: {
"/entry.js": header + `console.log("hello");\n`,
},
minifyWhitespace: minify,
minifySyntax: minify,
minifyIdentifiers: minify,
run: {
stdout: "hello",
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expectEachFormOnce(output);
// The comments stay ahead of the statement they annotate.
expect(output.indexOf(legalForms.at(-1)!)).toBeLessThan(output.indexOf("console.log"));
},
});
}

itBundled("@license comments are kept when the file is transpiled without bundling", {
files: {
"/entry.ts": header + `export const x: number = 1;\n`,
},
bundling: false,
onAfterBundle(api) {
const output = api.readFile("/out.js");
expectEachFormOnce(output);
expect(output.indexOf(legalForms.at(-1)!)).toBeLessThan(output.indexOf("export const x"));
},
});

// Parsing "import(" used to clear every legal comment that was still waiting
// for the next statement boundary.
itBundled("a legal comment before import() is kept", {
files: {
"/entry.js": `
export const load = /*! before the arrow */ () => import("external-pkg");
export const load2 = () => import(/* @license inside the parens */ "external-pkg");
export const load3 = () => /* @preserve before import */ import("external-pkg");
console.log(typeof load, typeof load2, typeof load3);
`,
},
external: ["external-pkg"],
run: {
stdout: "function function function",
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output.split("/*! before the arrow */")).toHaveLength(2);
expect(output.split("/* @license inside the parens */")).toHaveLength(2);
expect(output.split("/* @preserve before import */")).toHaveLength(2);
},
});
});
14 changes: 7 additions & 7 deletions test/bundler/bundler_npm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,17 @@ describe("bundler", () => {
"../entry.tsx",
],
mappings: [
["react.development.js:524:'getContextName'", "1:5623:at"],
["react.development.js:2495:'actScopeDepth'", "23:4082:or++"],
["react.development.js:696:''Component'", '1:7685:\'Component "%s"'],
["entry.tsx:6:'\"Content-Type\"'", '100:18817:"Content-Type"'],
["entry.tsx:11:'<html>'", "100:19071:void"],
["entry.tsx:23:'await'", "100:19170:await"],
["react.development.js:524:'getContextName'", "9:4156:at"],
["react.development.js:2495:'actScopeDepth'", "31:4082:or++"],
["react.development.js:696:''Component'", '9:6218:\'Component "%s"'],
["entry.tsx:6:'\"Content-Type\"'", '132:18817:"Content-Type"'],
["entry.tsx:11:'<html>'", "132:19071:void"],
["entry.tsx:23:'await'", "132:19170:await"],
],
},
},
expectExactFilesize: {
"out/entry.js": 221995,
"out/entry.js": 222988,
},
run: {
stdout: "<!DOCTYPE html><html><body><h1>Hello World</h1><p>This is an example.</p></body></html>",
Expand Down
Loading
Loading