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
91 changes: 20 additions & 71 deletions src/js_parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1978,7 +1978,6 @@ impl<'a> Lexer<'a> {
let offset = self.scan_pragma(
self.start + i + (text.len() - rest.len()),
chunk,
CommentKind::MultiLine,
PureAnnotation::Allow,
);

Expand Down Expand Up @@ -2117,12 +2116,7 @@ impl<'a> Lexer<'a> {
};
first_marker = false;
let chunk = js_ast::StoreStr::new(self.remaining());
self.current += self.scan_pragma(
pragma_trigger_pos,
chunk.slice(),
CommentKind::SingleLine,
pure,
);
self.current += self.scan_pragma(pragma_trigger_pos, chunk.slice(), pure);
continue;
}
_ => {
Expand Down Expand Up @@ -2150,11 +2144,8 @@ impl<'a> Lexer<'a> {
&mut self,
offset_for_errors: usize,
chunk: &[u8],
comment: CommentKind,
pure: PureAnnotation,
) -> usize {
// A `//` comment ends at the line break, so a pragma argument stops there.
let allow_newline = comment == CommentKind::SingleLine;
if pure == PureAnnotation::Allow && !self.has_pure_comment_before {
if strings::has_prefix_with_word_boundary(chunk, b"__PURE__") {
self.has_pure_comment_before = true;
Expand All @@ -2163,61 +2154,29 @@ impl<'a> Lexer<'a> {
}

if strings::has_prefix_with_word_boundary(chunk, b"jsx") {
Comment thread
robobun marked this conversation as resolved.
Resolved
if let Some(span) =
PragmaArg::scan(self.start + offset_for_errors, b"jsx", chunk, allow_newline)
{
if let Some(span) = PragmaArg::scan(self.start + offset_for_errors, b"jsx", chunk) {
self.jsx_pragma._jsx = span;
return "jsx".len()
+ if span.range.len > 0 {
usize::try_from(span.range.len).expect("int cast")
} else {
0
};
return "jsx".len() + usize::try_from(span.range.len).expect("int cast");
}
} else if strings::has_prefix_with_word_boundary(chunk, b"jsxFrag") {
if let Some(span) = PragmaArg::scan(
self.start + offset_for_errors,
b"jsxFrag",
chunk,
allow_newline,
) {
if let Some(span) = PragmaArg::scan(self.start + offset_for_errors, b"jsxFrag", chunk) {
self.jsx_pragma._jsx_frag = span;
return "jsxFrag".len()
+ if span.range.len > 0 {
usize::try_from(span.range.len).expect("int cast")
} else {
0
};
return "jsxFrag".len() + usize::try_from(span.range.len).expect("int cast");
}
} else if strings::has_prefix_with_word_boundary(chunk, b"jsxRuntime") {
if let Some(span) = PragmaArg::scan(
self.start + offset_for_errors,
b"jsxRuntime",
chunk,
allow_newline,
) {
if let Some(span) =
PragmaArg::scan(self.start + offset_for_errors, b"jsxRuntime", chunk)
{
self.jsx_pragma._jsx_runtime = span;
return "jsxRuntime".len()
+ if span.range.len > 0 {
usize::try_from(span.range.len).expect("int cast")
} else {
0
};
return "jsxRuntime".len() + usize::try_from(span.range.len).expect("int cast");
}
} else if strings::has_prefix_with_word_boundary(chunk, b"jsxImportSource") {
if let Some(span) = PragmaArg::scan(
self.start + offset_for_errors,
b"jsxImportSource",
chunk,
allow_newline,
) {
if let Some(span) =
PragmaArg::scan(self.start + offset_for_errors, b"jsxImportSource", chunk)
{
self.jsx_pragma._jsx_import_source = span;
return "jsxImportSource".len()
+ if span.range.len > 0 {
usize::try_from(span.range.len).expect("int cast")
} else {
0
};
+ usize::try_from(span.range.len).expect("int cast");
}
} else if chunk.len() > " sourceMappingURL=".len()
&& chunk.starts_with(b" sourceMappingURL=")
Expand Down Expand Up @@ -3452,12 +3411,6 @@ pub(crate) fn latin1_identifier_continue_length_scalar(name: &[u8]) -> usize {
name.len()
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum CommentKind {
SingleLine,
MultiLine,
}

/// Whether a `__PURE__` match at this position marks the next call.
#[derive(Clone, Copy, PartialEq, Eq)]
enum PureAnnotation {
Expand All @@ -3468,10 +3421,6 @@ enum PureAnnotation {
pub struct PragmaArg;

impl PragmaArg {
pub(crate) fn is_newline(c: CodePoint) -> bool {
c == 0x0D || c == 0x0A || c == 0x2028 || c == 0x2029
}

// These can be extremely long, so we use SIMD.
/// "//# sourceMappingURL=data:/adspaoksdpkz"
/// ^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -3518,12 +3467,7 @@ impl PragmaArg {
PREFIX as usize + url_len // Correct total length
}

pub(crate) fn scan(
offset_: usize,
pragma: &[u8],
text_: &[u8],
allow_newline: bool,
) -> Option<js_ast::Span> {
pub(crate) fn scan(offset_: usize, pragma: &[u8], text_: &[u8]) -> Option<js_ast::Span> {
let mut text = &text_[pragma.len()..];
let mut iter = CodepointIterator::init(text);

Expand All @@ -3548,8 +3492,9 @@ impl PragmaArg {
iter = CodepointIterator::init(text);
let _ = iter.next(&mut cursor);

// A line break ends the value too. esbuild's `scanForPragmaArg` stops at white space only.
let mut i: usize = 0;
while !is_whitespace(cursor.c) && (!allow_newline || !Self::is_newline(cursor.c)) {
while !strings::is_js_whitespace(cursor.c as u32) {
i += cursor.width as usize;
if i >= text.len() {
break;
Expand All @@ -3560,6 +3505,10 @@ impl PragmaArg {
}
}

if i == 0 {
return None;
}

Some(js_ast::Span {
range: Range {
len: i32::try_from(i).expect("int cast"),
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ bun_core::declare_scope!(cache, visible);
/// the cache-HIT path reinstates #33904 for them.
/// Version 34: An import that a plugin `onResolve` answers is printed as it is written.
/// Version 35: Long hexadecimal, octal and binary literals are rounded to the nearest double.
const EXPECTED_VERSION: u32 = 35;
/// Version 36: A JSX pragma value in a block comment ends at a line break.
const EXPECTED_VERSION: u32 = 36;

/// Source files smaller than this are not written to / read from the on-disk
/// transpiler cache. Originally 50 KiB, which excluded almost every file in a
Expand Down
162 changes: 162 additions & 0 deletions test/bundler/bundler_jsx.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
import { join } from "path";
import { BundlerTestInput, itBundled } from "./expectBundled";

const helpers = {
Expand Down Expand Up @@ -527,6 +528,167 @@ describe("bundler", () => {
},
});

// The value of a pragma ends at the line break. So a pragma can have a line
// of its own in a block comment, and the next line can hold another pragma.
describe("pragma on its own line of a block comment", () => {
const lines = (...lines: string[]) => lines.join("\n") + "\n";
const h = { "h.ts": lines(`export const h = (tag: string) => "h: " + tag;`, `export const F = "F";`) };
const myRuntime = {
"my/jsx-dev-runtime.js": `export const jsxDEV = tag => "my runtime: " + tag;`,
"my/jsx-runtime.js": `export const jsx = tag => "my runtime: " + tag;`,
};
// The first file of each case is its entry point.
const cases: Record<string, { files: Record<string, string>; stdout: string }> = {
ImportSource: {
files: {
"index.jsx": lines("/**", " * @jsxImportSource ./my", " */", "console.log(<a />);"),
...myRuntime,
},
stdout: "my runtime: a",
},
Factory: {
files: {
"index.tsx": lines(
"/** @jsxRuntime classic */",
"/**",
" * @jsx h",
" */",
`import { h } from "./h";`,
"console.log(<a />);",
),
...h,
},
stdout: "h: a",
},
Fragment: {
files: {
"index.tsx": lines(
"/** @jsxRuntime classic */",
"/** @jsx h */",
"/**",
" * @jsxFrag F",
" */",
`import { F, h } from "./h";`,
"console.log(<></>);",
),
...h,
},
stdout: "h: F",
},
// With the first comment, a build that does not follow `@jsxRuntime classic` imports ./my and not react.
Runtime: {
files: {
"index.tsx": lines(
"/** @jsxImportSource ./my */",
"/**",
" * @jsxRuntime classic",
" * @jsx h",
" */",
`import { h } from "./h";`,
"console.log(<a />);",
),
...h,
...myRuntime,
},
stdout: "h: a",
},
FactoryWithRuntimeFromTsconfig: {
files: {
"index.tsx": lines("/**", " * @jsx h", " */", `import { h } from "./h";`, "console.log(<a />);"),
"tsconfig.json": JSON.stringify({ compilerOptions: { jsx: "react" } }),
...h,
},
stdout: "h: a",
},
};
const inRoot = (files: Record<string, string>) =>
Object.fromEntries(Object.entries(files).map(([file, source]) => ["/" + file, source]));

for (const [name, { files, stdout }] of Object.entries(cases)) {
itBundled(`jsx/PragmaOwnLine${name}`, {
files: inRoot(files),
target: "bun",
run: { stdout },
});

test.concurrent(`bun run: ${name}`, async () => {
using dir = tempDir(`jsx-pragma-own-line-${name}`, files);

await using proc = Bun.spawn({
cmd: [bunExe(), Object.keys(files)[0]],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
Comment thread
robobun marked this conversation as resolved.
Resolved
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: out, stderr: err, exitCode }).toEqual({ stdout: stdout + "\n", stderr: "", exitCode: 0 });
});
}

const localFactory = `const h = tag => "h: " + tag;`;
itBundled("jsx/PragmaOnEachLine", {
files: inRoot({
"index.js": lines(
`import "./all-four.jsx";`,
`import "./column-0-runtime.jsx";`,
`import "./column-0-fragment.jsx";`,
),
"all-four.jsx": lines(
"/**",
" * @jsxImportSource ./my",
" * @jsxRuntime classic",
" * @jsx h",
" * @jsxFrag F",
" */",
localFactory,
`const F = "F";`,
`console.log("all-four:", <a />, <></>);`,
),
"column-0-runtime.jsx": lines(
"/*",
"@jsxRuntime classic",
"@jsx h",
"*/",
localFactory,
`console.log("column-0-runtime:", <a />);`,
),
"column-0-fragment.jsx": lines(
"/** @jsxRuntime classic */",
"/*",
"@jsx h",
"@jsxFrag F",
"*/",
localFactory,
`const F = "F";`,
`console.log("column-0-fragment:", <></>);`,
),
}),
target: "bun",
run: { stdout: lines("all-four: h: a h: F", "column-0-runtime: h: a", "column-0-fragment: h: F") },
});

test("a warning about the value has the length of the value", async () => {
using dir = tempDir("jsx-pragma-own-line-warning", {
"index.jsx": lines("/**", " * @jsxRuntime bogus", " */", "export default <a />;"),
});

const { logs } = await Bun.build({ entrypoints: [join(String(dir), "index.jsx")], external: ["*"] });
expect(logs.map(log => ({ level: log.level, message: log.message, length: log.position?.length }))).toEqual([
{ level: "warn", message: 'Unsupported JSX runtime: "bogus"', length: 5 },
]);
});

test("the pragmas in a file without JSX give no warning", async () => {
using dir = tempDir("jsx-pragma-own-line-no-jsx", {
"index.js": lines("/**", " * @jsxRuntime classic", " * @jsx jsx", " */", "export default 1;"),
});

const { logs } = await Bun.build({ entrypoints: [join(String(dir), "index.js")] });
expect(logs.map(log => log.message)).toEqual([]);
});
});

// The classic transform reads the first member of the factory and fragment
// member lists. A factory or fragment option whose text has no identifier
// in it ("." or "..") must keep the list that was configured before it,
Expand Down
Loading
Loading