Files
bun.sh/src/ast/P.zig

6864 lines
323 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const ParserFeatures = struct {
typescript: bool = false,
jsx: JSXTransformType = .none,
scan_only: bool = false,
};
// workaround for https://github.com/ziglang/zig/issues/10903
pub fn NewParser(
comptime parser_features: ParserFeatures,
) type {
return NewParser_(
parser_features.typescript,
parser_features.jsx,
parser_features.scan_only,
);
}
pub fn NewParser_(
comptime parser_feature__typescript: bool,
comptime parser_feature__jsx: JSXTransformType,
comptime parser_feature__scan_only: bool,
) type {
const js_parser_features: ParserFeatures = .{
.typescript = parser_feature__typescript,
.jsx = parser_feature__jsx,
.scan_only = parser_feature__scan_only,
};
// P is for Parser!
return struct {
const js_parser_jsx = js_parser_features.jsx;
pub const is_typescript_enabled = js_parser_features.typescript;
pub const is_jsx_enabled = js_parser_jsx != .none;
pub const only_scan_imports_and_do_not_visit = js_parser_features.scan_only;
const ImportRecordList = if (only_scan_imports_and_do_not_visit) *std.array_list.Managed(ImportRecord) else std.array_list.Managed(ImportRecord);
const NamedImportsType = if (only_scan_imports_and_do_not_visit) *js_ast.Ast.NamedImports else js_ast.Ast.NamedImports;
const NeedsJSXType = if (only_scan_imports_and_do_not_visit) bool else void;
pub const track_symbol_usage_during_parse_pass = only_scan_imports_and_do_not_visit and is_typescript_enabled;
const ParsePassSymbolUsageType = if (track_symbol_usage_during_parse_pass) *ScanPassResult.ParsePassSymbolUsageMap else void;
pub const parser_features: ParserFeatures = js_parser_features;
const P = @This();
pub const jsx_transform_type: JSXTransformType = js_parser_jsx;
pub const allow_macros = FeatureFlags.is_macro_enabled;
const MacroCallCountType = if (allow_macros) u32 else u0;
const skipTypescript_zig = @import("./skipTypescript.zig").SkipTypescript(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only);
pub const skipTypescriptReturnType = skipTypescript_zig.skipTypescriptReturnType;
pub const skipTypescriptReturnTypeWithMetadata = skipTypescript_zig.skipTypescriptReturnTypeWithMetadata;
pub const skipTypeScriptType = skipTypescript_zig.skipTypeScriptType;
pub const skipTypeScriptTypeWithMetadata = skipTypescript_zig.skipTypeScriptTypeWithMetadata;
pub const skipTypeScriptBinding = skipTypescript_zig.skipTypeScriptBinding;
pub const skipTypescriptFnArgs = skipTypescript_zig.skipTypescriptFnArgs;
pub const skipTypeScriptParenOrFnType = skipTypescript_zig.skipTypeScriptParenOrFnType;
pub const skipTypeScriptTypeWithOpts = skipTypescript_zig.skipTypeScriptTypeWithOpts;
pub const skipTypeScriptObjectType = skipTypescript_zig.skipTypeScriptObjectType;
pub const skipTypeScriptTypeParameters = skipTypescript_zig.skipTypeScriptTypeParameters;
pub const skipTypeScriptTypeStmt = skipTypescript_zig.skipTypeScriptTypeStmt;
pub const skipTypeScriptInterfaceStmt = skipTypescript_zig.skipTypeScriptInterfaceStmt;
pub const skipTypeScriptTypeArguments = skipTypescript_zig.skipTypeScriptTypeArguments;
pub const trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking = skipTypescript_zig.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking;
pub const trySkipTypeScriptTypeArgumentsWithBacktracking = skipTypescript_zig.trySkipTypeScriptTypeArgumentsWithBacktracking;
pub const trySkipTypeScriptArrowReturnTypeWithBacktracking = skipTypescript_zig.trySkipTypeScriptArrowReturnTypeWithBacktracking;
pub const trySkipTypeScriptArrowArgsWithBacktracking = skipTypescript_zig.trySkipTypeScriptArrowArgsWithBacktracking;
pub const trySkipTypeScriptConstraintOfInferTypeWithBacktracking = skipTypescript_zig.trySkipTypeScriptConstraintOfInferTypeWithBacktracking;
const parse_zig = @import("./parse.zig").Parse(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only);
pub const parsePrefix = parse_zig.parsePrefix;
pub const parseSuffix = parse_zig.parseSuffix;
pub const parseStmt = parse_zig.parseStmt;
pub const parseProperty = parse_zig.parseProperty;
pub const parseFn = parse_zig.parseFn;
pub const parseFnStmt = parse_zig.parseFnStmt;
pub const parseFnExpr = parse_zig.parseFnExpr;
pub const parseFnBody = parse_zig.parseFnBody;
pub const parseArrowBody = parse_zig.parseArrowBody;
pub const parseJSXElement = parse_zig.parseJSXElement;
pub const parseImportExpr = parse_zig.parseImportExpr;
pub const parseImportClause = parse_zig.parseImportClause;
pub const parseExportClause = parse_zig.parseExportClause;
pub const parseExprOrBindings = parse_zig.parseExprOrBindings;
pub const parseExpr = parse_zig.parseExpr;
pub const parseExprWithFlags = parse_zig.parseExprWithFlags;
pub const parseExprCommon = parse_zig.parseExprCommon;
pub const parseYieldExpr = parse_zig.parseYieldExpr;
pub const parseClass = parse_zig.parseClass;
pub const parseTemplateParts = parse_zig.parseTemplateParts;
pub const parseStringLiteral = parse_zig.parseStringLiteral;
pub const parseCallArgs = parse_zig.parseCallArgs;
pub const parseJSXPropValueIdentifier = parse_zig.parseJSXPropValueIdentifier;
pub const parseParenExpr = parse_zig.parseParenExpr;
pub const parseLabelName = parse_zig.parseLabelName;
pub const parseClassStmt = parse_zig.parseClassStmt;
pub const parseClauseAlias = parse_zig.parseClauseAlias;
pub const parseExprOrLetStmt = parse_zig.parseExprOrLetStmt;
pub const parseBinding = parse_zig.parseBinding;
pub const parsePropertyBinding = parse_zig.parsePropertyBinding;
pub const parseAndDeclareDecls = parse_zig.parseAndDeclareDecls;
pub const parsePath = parse_zig.parsePath;
pub const parseStmtsUpTo = parse_zig.parseStmtsUpTo;
pub const parseAsyncPrefixExpr = parse_zig.parseAsyncPrefixExpr;
pub const parseTypeScriptDecorators = parse_zig.parseTypeScriptDecorators;
pub const parseTypeScriptNamespaceStmt = parse_zig.parseTypeScriptNamespaceStmt;
pub const parseTypeScriptImportEqualsStmt = parse_zig.parseTypeScriptImportEqualsStmt;
pub const parseTypescriptEnumStmt = parse_zig.parseTypescriptEnumStmt;
const astVisit = @import("./visit.zig").Visit(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only);
pub const visitStmtsAndPrependTempRefs = astVisit.visitStmtsAndPrependTempRefs;
pub const recordDeclaredSymbol = astVisit.recordDeclaredSymbol;
pub const visitExpr = astVisit.visitExpr;
pub const visitExprInOut = astVisit.visitExprInOut;
pub const visitFunc = astVisit.visitFunc;
pub const visitArgs = astVisit.visitArgs;
pub const visitTSDecorators = astVisit.visitTSDecorators;
pub const visitDecls = astVisit.visitDecls;
pub const visitBindingAndExprForMacro = astVisit.visitBindingAndExprForMacro;
pub const visitDecl = astVisit.visitDecl;
pub const visitForLoopInit = astVisit.visitForLoopInit;
pub const visitBinding = astVisit.visitBinding;
pub const visitLoopBody = astVisit.visitLoopBody;
pub const visitSingleStmtBlock = astVisit.visitSingleStmtBlock;
pub const visitSingleStmt = astVisit.visitSingleStmt;
pub const visitClass = astVisit.visitClass;
pub const visitStmts = astVisit.visitStmts;
pub const visitAndAppendStmt = astVisit.visitAndAppendStmt;
pub const BinaryExpressionVisitor = @import("./visitBinaryExpression.zig").CreateBinaryExpressionVisitor(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).BinaryExpressionVisitor;
const maybe = @import("./maybe.zig").AstMaybe(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only);
pub const maybeRelocateVarsToTopLevel = maybe.maybeRelocateVarsToTopLevel;
pub const maybeRewritePropertyAccess = maybe.maybeRewritePropertyAccess;
pub const maybeCommaSpreadError = maybe.maybeCommaSpreadError;
pub const maybeDefinedHelper = maybe.maybeDefinedHelper;
pub const checkIfDefinedHelper = maybe.checkIfDefinedHelper;
const symbols_zig = @import("./symbols.zig").Symbols(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only);
pub const findSymbol = symbols_zig.findSymbol;
pub const findSymbolWithRecordUsage = symbols_zig.findSymbolWithRecordUsage;
macro: MacroState = undefined,
allocator: Allocator,
options: Parser.Options,
log: *logger.Log,
define: *Define,
source: *const logger.Source,
lexer: js_lexer.Lexer,
allow_in: bool = false,
allow_private_identifiers: bool = false,
has_top_level_return: bool = false,
latest_return_had_semicolon: bool = false,
has_import_meta: bool = false,
has_es_module_syntax: bool = false,
top_level_await_keyword: logger.Range = logger.Range.None,
fn_or_arrow_data_parse: FnOrArrowDataParse = FnOrArrowDataParse{},
fn_or_arrow_data_visit: FnOrArrowDataVisit = FnOrArrowDataVisit{},
fn_only_data_visit: FnOnlyDataVisit = FnOnlyDataVisit{},
allocated_names: List(string) = .{},
// allocated_names: ListManaged(string) = ListManaged(string).init(bun.default_allocator),
// allocated_names_pool: ?*AllocatedNamesPool.Node = null,
latest_arrow_arg_loc: logger.Loc = logger.Loc.Empty,
forbid_suffix_after_as_loc: logger.Loc = logger.Loc.Empty,
current_scope: *js_ast.Scope = undefined,
scopes_for_current_part: List(*js_ast.Scope) = .{},
symbols: ListManaged(js_ast.Symbol) = undefined,
ts_use_counts: List(u32) = .{},
exports_ref: Ref = Ref.None,
require_ref: Ref = Ref.None,
module_ref: Ref = Ref.None,
filename_ref: Ref = Ref.None,
dirname_ref: Ref = Ref.None,
import_meta_ref: Ref = Ref.None,
hmr_api_ref: Ref = Ref.None,
/// If bake is enabled and this is a server-side file, we want to use
/// special `Response` class inside the `bun:app` built-in module to
/// support syntax like `return Response(<jsx />, {...})` or `return Response.render("/my-page")`
/// or `return Response.redirect("/other")`.
///
/// So we'll need to add a `import { Response } from 'bun:app'` to the
/// top of the file
///
/// We need to declare this `response_ref` upfront
response_ref: Ref = Ref.None,
/// We also need to declare the namespace ref for `bun:app` and attach
/// it to the symbol so the code generated `e_import_identifier`'s
bun_app_namespace_ref: Ref = Ref.None,
/// Used to track the `feature` function from `import { feature } from "bun:bundle"`.
/// When visiting e_call, if the target ref matches this, we replace the call with
/// a boolean based on whether the feature flag is enabled.
bundler_feature_flag_ref: Ref = Ref.None,
/// Set to true when visiting an if/ternary condition. feature() calls are only valid in this context.
in_branch_condition: bool = false,
scopes_in_order_visitor_index: usize = 0,
has_classic_runtime_warned: bool = false,
macro_call_count: MacroCallCountType = 0,
hoisted_ref_for_sloppy_mode_block_fn: RefRefMap = .{},
/// Used for transforming export default -> module.exports
has_export_default: bool = false,
has_export_keyword: bool = false,
// Used for forcing CommonJS
has_with_scope: bool = false,
is_file_considered_to_have_esm_exports: bool = false,
has_called_runtime: bool = false,
legacy_cjs_import_stmts: std.array_list.Managed(Stmt),
injected_define_symbols: List(Ref) = .{},
symbol_uses: SymbolUseMap = .{},
declared_symbols: DeclaredSymbol.List = .{},
declared_symbols_for_reuse: DeclaredSymbol.List = .{},
runtime_imports: RuntimeImports = RuntimeImports{},
/// Used with unwrap_commonjs_packages
imports_to_convert_from_require: List(DeferredImportNamespace) = .{},
unwrap_all_requires: bool = false,
commonjs_named_exports: js_ast.Ast.CommonJSNamedExports = .{},
commonjs_named_exports_deoptimized: bool = false,
commonjs_module_exports_assigned_deoptimized: bool = false,
commonjs_named_exports_needs_conversion: u32 = std.math.maxInt(u32),
had_commonjs_named_exports_this_visit: bool = false,
commonjs_replacement_stmts: StmtNodeList = &.{},
parse_pass_symbol_uses: ParsePassSymbolUsageType = undefined,
/// Used by commonjs_at_runtime
has_commonjs_export_names: bool = false,
stack_check: bun.StackCheck,
/// When this flag is enabled, we attempt to fold all expressions that
/// TypeScript would consider to be "constant expressions". This flag is
/// enabled inside each enum body block since TypeScript requires numeric
/// constant folding in enum definitions.
///
/// We also enable this flag in certain cases in JavaScript files such as when
/// parsing "const" declarations at the top of a non-ESM file, but we still
/// reuse TypeScript's notion of "constant expressions" for our own convenience.
///
/// As of TypeScript 5.0, a "constant expression" is defined as follows:
///
/// An expression is considered a constant expression if it is
///
/// * a number or string literal,
/// * a unary +, -, or ~ applied to a numeric constant expression,
/// * a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions,
/// * a binary + applied to two constant expressions whereof at least one is a string,
/// * a template expression where each substitution expression is a constant expression,
/// * a parenthesized constant expression,
/// * a dotted name (e.g. x.y.z) that references a const variable with a constant expression initializer and no type annotation,
/// * a dotted name that references an enum member with an enum literal type, or
/// * a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type.
///
/// More detail: https://github.com/microsoft/TypeScript/pull/50528. Note that
/// we don't implement certain items in this list. For example, we don't do all
/// number-to-string conversions since ours might differ from how JavaScript
/// would do it, which would be a correctness issue.
///
/// This flag is also set globally when minify_syntax is enabled, in which this means
/// we always fold constant expressions.
should_fold_typescript_constant_expressions: bool = false,
emitted_namespace_vars: RefMap = RefMap{},
is_exported_inside_namespace: RefRefMap = .{},
local_type_names: StringBoolMap = StringBoolMap{},
// This is the reference to the generated function argument for the namespace,
// which is different than the reference to the namespace itself:
//
// namespace ns {
// }
//
// The code above is transformed into something like this:
//
// var ns1;
// (function(ns2) {
// })(ns1 or (ns1 = {}));
//
// This variable is "ns2" not "ns1". It is only used during the second
// "visit" pass.
enclosing_namespace_arg_ref: ?Ref = null,
jsx_imports: JSXImport.Symbols = .{},
/// only applicable when `.options.features.react_fast_refresh` is set.
/// populated before visit pass starts.
react_refresh: ReactRefresh = .{},
/// only applicable when `.options.features.server_components` is
/// configured to wrap exports. populated before visit pass starts.
server_components_wrap_ref: Ref = Ref.None,
jest: Jest = .{},
// Imports (both ES6 and CommonJS) are tracked at the top level
import_records: ImportRecordList,
import_records_for_current_part: List(u32) = .{},
export_star_import_records: List(u32) = .{},
import_symbol_property_uses: SymbolPropertyUseMap = .{},
// These are for handling ES6 imports and exports
esm_import_keyword: logger.Range = logger.Range.None,
esm_export_keyword: logger.Range = logger.Range.None,
enclosing_class_keyword: logger.Range = logger.Range.None,
import_items_for_namespace: std.AutoHashMapUnmanaged(Ref, ImportItemForNamespaceMap) = .{},
is_import_item: RefMap = .{},
named_imports: NamedImportsType,
named_exports: js_ast.Ast.NamedExports,
import_namespace_cc_map: Map(ImportNamespaceCallOrConstruct, bool) = .{},
// When we're only scanning the imports
// If they're using the automatic JSX runtime
// We won't know that we need to import JSX robustly because we don't track
// symbol counts. Instead, we ask:
// "Did we parse anything that looked like JSX"?
// If yes, then automatically add the JSX import.
needs_jsx_import: NeedsJSXType,
// The parser does two passes and we need to pass the scope tree information
// from the first pass to the second pass. That's done by tracking the calls
// to pushScopeForParsePass() and popScope() during the first pass in
// scopesInOrder.
//
// Then, when the second pass calls pushScopeForVisitPass() and popScope(),
// we consume entries from scopesInOrder and make sure they are in the same
// order. This way the second pass can efficiently use the same scope tree
// as the first pass without having to attach the scope tree to the AST.
//
// We need to split this into two passes because the pass that declares the
// symbols must be separate from the pass that binds identifiers to declared
// symbols to handle declaring a hoisted "var" symbol in a nested scope and
// binding a name to it in a parent or sibling scope.
scopes_in_order: ScopeOrderList = .{},
scope_order_to_visit: []ScopeOrder = &.{},
// These properties are for the visit pass, which runs after the parse pass.
// The visit pass binds identifiers to declared symbols, does constant
// folding, substitutes compile-time variable definitions, and lowers certain
// syntactic constructs as appropriate.
stmt_expr_value: Expr.Data,
call_target: Expr.Data,
delete_target: Expr.Data,
loop_body: Stmt.Data,
module_scope: *js_ast.Scope = undefined,
module_scope_directive_loc: logger.Loc = .{},
is_control_flow_dead: bool = false,
/// We must be careful to avoid revisiting nodes that have scopes.
is_revisit_for_substitution: bool = false,
method_call_must_be_replaced_with_undefined: bool = false,
// Inside a TypeScript namespace, an "export declare" statement can be used
// to cause a namespace to be emitted even though it has no other observable
// effect. This flag is used to implement this feature.
//
// Specifically, namespaces should be generated for all of the following
// namespaces below except for "f", which should not be generated:
//
// namespace a { export declare const a }
// namespace b { export declare let [[b]] }
// namespace c { export declare function c() }
// namespace d { export declare class d {} }
// namespace e { export declare enum e {} }
// namespace f { export declare namespace f {} }
//
// The TypeScript compiler compiles this into the following code (notice "f"
// is missing):
//
// var a; (function (a_1) {})(a or (a = {}));
// var b; (function (b_1) {})(b or (b = {}));
// var c; (function (c_1) {})(c or (c = {}));
// var d; (function (d_1) {})(d or (d = {}));
// var e; (function (e_1) {})(e or (e = {}));
//
// Note that this should not be implemented by declaring symbols for "export
// declare" statements because the TypeScript compiler doesn't generate any
// code for these statements, so these statements are actually references to
// global variables. There is one exception, which is that local variables
// *should* be declared as symbols because they are replaced with. This seems
// like very arbitrary behavior but it's what the TypeScript compiler does,
// so we try to match it.
//
// Specifically, in the following code below "a" and "b" should be declared
// and should be substituted with "ns.a" and "ns.b" but the other symbols
// shouldn't. References to the other symbols actually refer to global
// variables instead of to symbols that are exported from the namespace.
// This is the case as of TypeScript 4.3. I assume this is a TypeScript bug:
//
// namespace ns {
// export declare const a
// export declare let [[b]]
// export declare function c()
// export declare class d { }
// export declare enum e { }
// console.log(a, b, c, d, e)
// }
//
// The TypeScript compiler compiles this into the following code:
//
// var ns;
// (function (ns) {
// console.log(ns.a, ns.b, c, d, e);
// })(ns or (ns = {}));
//
// Relevant issue: https://github.com/evanw/esbuild/issues/1158
has_non_local_export_declare_inside_namespace: bool = false,
// This helps recognize the "await import()" pattern. When this is present,
// warnings about non-string import paths will be omitted inside try blocks.
await_target: ?js_ast.Expr.Data = null,
to_expr_wrapper_namespace: Binding2ExprWrapper.Namespace,
to_expr_wrapper_hoisted: Binding2ExprWrapper.Hoisted,
// This helps recognize the "import().catch()" pattern. We also try to avoid
// warning about this just like the "try { await import() }" pattern.
then_catch_chain: ThenCatchChain,
// Temporary variables used for lowering
temp_refs_to_declare: List(TempRef) = .{},
temp_ref_count: i32 = 0,
// When bundling, hoisted top-level local variables declared with "var" in
// nested scopes are moved up to be declared in the top-level scope instead.
// The old "var" statements are turned into regular assignments instead. This
// makes it easier to quickly scan the top-level statements for "var" locals
// with the guarantee that all will be found.
relocated_top_level_vars: List(js_ast.LocRef) = .{},
// ArrowFunction is a special case in the grammar. Although it appears to be
// a PrimaryExpression, it's actually an AssignmentExpression. This means if
// a AssignmentExpression ends up producing an ArrowFunction then nothing can
// come after it other than the comma operator, since the comma operator is
// the only thing above AssignmentExpression under the Expression rule:
//
// AssignmentExpression:
// ArrowFunction
// ConditionalExpression
// LeftHandSideExpression = AssignmentExpression
// LeftHandSideExpression AssignmentOperator AssignmentExpression
//
// Expression:
// AssignmentExpression
// Expression , AssignmentExpression
//
after_arrow_body_loc: logger.Loc = logger.Loc.Empty,
import_transposer: ImportTransposer,
require_transposer: RequireTransposer,
require_resolve_transposer: RequireResolveTransposer,
const_values: js_ast.Ast.ConstValuesMap = .{},
// These are backed by stack fallback allocators in _parse, and are uninitialized until then.
binary_expression_stack: ListManaged(BinaryExpressionVisitor) = undefined,
binary_expression_simplify_stack: ListManaged(SideEffects.BinaryExpressionSimplifyVisitor) = undefined,
/// We build up enough information about the TypeScript namespace hierarchy to
/// be able to resolve scope lookups and property accesses for TypeScript enum
/// and namespace features. Each JavaScript scope object inside a namespace
/// has a reference to a map of exported namespace members from sibling scopes.
///
/// In addition, there is a map from each relevant symbol reference to the data
/// associated with that namespace or namespace member: "ref_to_ts_namespace_member".
/// This gives enough info to be able to resolve queries into the namespace.
ref_to_ts_namespace_member: std.AutoHashMapUnmanaged(Ref, js_ast.TSNamespaceMember.Data) = .{},
/// When visiting expressions, namespace metadata is associated with the most
/// recently visited node. If namespace metadata is present, "tsNamespaceTarget"
/// will be set to the most recently visited node (as a way to mark that this
/// node has metadata) and "tsNamespaceMemberData" will be set to the metadata.
ts_namespace: RecentlyVisitedTSNamespace = .{},
top_level_enums: std.ArrayListUnmanaged(Ref) = .{},
scopes_in_order_for_enum: std.AutoArrayHashMapUnmanaged(logger.Loc, []ScopeOrder) = .{},
// If this is true, then all top-level statements are wrapped in a try/catch
will_wrap_module_in_try_catch_for_using: bool = false,
/// Used for react refresh, it must be able to insert `const _s = $RefreshSig$();`
nearest_stmt_list: ?*ListManaged(Stmt) = null,
const RecentlyVisitedTSNamespace = struct {
expr: Expr.Data = Expr.empty.data,
map: ?*js_ast.TSNamespaceMemberMap = null,
const ExpressionData = union(enum) {
ref: Ref,
ptr: *E.Dot,
};
};
/// use this instead of checking p.source.index
/// because when not bundling, p.source.index is `0`
pub inline fn isSourceRuntime(p: *const P) bool {
return p.options.bundle and p.source.index.isRuntime();
}
pub fn transposeImport(noalias p: *P, arg: Expr, state: *const TransposeState) Expr {
// The argument must be a string
if (arg.data.as(.e_string)) |str| {
// Ignore calls to import() if the control flow is provably dead here.
// We don't want to spend time scanning the required files if they will
// never be used.
if (p.is_control_flow_dead) {
return p.newExpr(E.Null{}, arg.loc);
}
const import_record_index = p.addImportRecord(.dynamic, arg.loc, str.slice(p.allocator));
if (state.import_record_tag) |tag| {
p.import_records.items[import_record_index].tag = tag;
}
p.import_records.items[import_record_index].flags.handles_import_errors = (state.is_await_target and p.fn_or_arrow_data_visit.try_body_count != 0) or state.is_then_catch_target;
p.import_records_for_current_part.append(p.allocator, import_record_index) catch unreachable;
return p.newExpr(E.Import{
.expr = arg,
.import_record_index = @intCast(import_record_index),
.options = state.import_options,
}, state.loc);
}
if (p.options.warn_about_unbundled_modules) {
// Use a debug log so people can see this if they want to
const r = js_lexer.rangeOfIdentifier(p.source, state.loc);
p.log.addRangeDebug(p.source, r, "This \"import\" expression cannot be bundled because the argument is not a string literal") catch unreachable;
}
return p.newExpr(E.Import{
.expr = arg,
.options = state.import_options,
.import_record_index = std.math.maxInt(u32),
}, state.loc);
}
pub fn transposeRequireResolve(noalias p: *P, arg: Expr, require_resolve_ref: Expr) Expr {
// The argument must be a string
if (arg.data == .e_string) {
return p.transposeRequireResolveKnownString(arg);
}
if (p.options.warn_about_unbundled_modules) {
// Use a debug log so people can see this if they want to
const r = js_lexer.rangeOfIdentifier(p.source, arg.loc);
p.log.addRangeDebug(p.source, r, "This \"require.resolve\" expression cannot be bundled because the argument is not a string literal") catch unreachable;
}
const args = p.allocator.alloc(Expr, 1) catch unreachable;
args[0] = arg;
return p.newExpr(E.Call{
.target = require_resolve_ref,
.args = ExprNodeList.fromOwnedSlice(args),
}, arg.loc);
}
pub inline fn transposeRequireResolveKnownString(noalias p: *P, arg: Expr) Expr {
bun.assert(arg.data == .e_string);
// Ignore calls to import() if the control flow is provably dead here.
// We don't want to spend time scanning the required files if they will
// never be used.
if (p.is_control_flow_dead) {
return p.newExpr(E.Null{}, arg.loc);
}
const import_record_index = p.addImportRecord(.require_resolve, arg.loc, arg.data.e_string.string(p.allocator) catch unreachable);
p.import_records.items[import_record_index].flags.handles_import_errors = p.fn_or_arrow_data_visit.try_body_count != 0;
p.import_records_for_current_part.append(p.allocator, import_record_index) catch unreachable;
return p.newExpr(
E.RequireResolveString{
.import_record_index = import_record_index,
// .leading_interior_comments = arg.getString().
},
arg.loc,
);
}
pub fn transposeRequire(noalias p: *P, arg: Expr, state: *const TransposeState) Expr {
if (!p.options.features.allow_runtime) {
const args = bun.handleOom(p.allocator.alloc(Expr, 1));
args[0] = arg;
return p.newExpr(
E.Call{
.target = p.valueForRequire(arg.loc),
.args = ExprNodeList.fromOwnedSlice(args),
},
arg.loc,
);
}
switch (arg.data) {
.e_string => |str| {
// Ignore calls to require() if the control flow is provably dead here.
// We don't want to spend time scanning the required files if they will
// never be used.
if (p.is_control_flow_dead) {
return Expr{ .data = nullExprData, .loc = arg.loc };
}
str.resolveRopeIfNeeded(p.allocator);
const pathname = str.string(p.allocator) catch unreachable;
const path = fs.Path.init(pathname);
const handles_import_errors = p.fn_or_arrow_data_visit.try_body_count != 0;
// For unwrapping CommonJS into ESM to fully work
// we must also unwrap requires into imports.
const should_unwrap_require = p.options.features.unwrap_commonjs_to_esm and
(p.unwrap_all_requires or
if (path.packageName()) |pkg| p.options.features.shouldUnwrapRequire(pkg) else false) and
// We cannot unwrap a require wrapped in a try/catch because
// import statements cannot be wrapped in a try/catch and
// require cannot return a promise.
!handles_import_errors;
if (should_unwrap_require) {
const import_record_index = p.addImportRecordByRangeAndPath(.stmt, p.source.rangeOfString(arg.loc), path);
p.import_records.items[import_record_index].flags.handles_import_errors = handles_import_errors;
// Note that this symbol may be completely removed later.
var path_name = fs.PathName.init(path.text);
const name = bun.handleOom(path_name.nonUniqueNameString(p.allocator));
const namespace_ref = bun.handleOom(p.newSymbol(.other, name));
p.imports_to_convert_from_require.append(p.allocator, .{
.namespace = .{
.ref = namespace_ref,
.loc = arg.loc,
},
.import_record_id = import_record_index,
}) catch |err| bun.handleOom(err);
bun.handleOom(p.import_items_for_namespace.put(p.allocator, namespace_ref, ImportItemForNamespaceMap.init(p.allocator)));
p.recordUsage(namespace_ref);
if (!state.is_require_immediately_assigned_to_decl) {
return p.newExpr(E.Identifier{
.ref = namespace_ref,
}, arg.loc);
}
return p.newExpr(
E.RequireString{
.import_record_index = import_record_index,
.unwrapped_id = @as(u32, @intCast(p.imports_to_convert_from_require.items.len - 1)),
},
arg.loc,
);
}
const import_record_index = p.addImportRecordByRangeAndPath(.require, p.source.rangeOfString(arg.loc), path);
p.import_records.items[import_record_index].flags.handles_import_errors = handles_import_errors;
p.import_records_for_current_part.append(p.allocator, import_record_index) catch unreachable;
return p.newExpr(E.RequireString{ .import_record_index = import_record_index }, arg.loc);
},
else => {
p.recordUsageOfRuntimeRequire();
const args = p.allocator.alloc(Expr, 1) catch unreachable;
args[0] = arg;
return p.newExpr(
E.Call{
.target = p.valueForRequire(arg.loc),
.args = ExprNodeList.fromOwnedSlice(args),
},
arg.loc,
);
},
}
}
pub inline fn shouldUnwrapCommonJSToESM(p: *const P) bool {
return p.options.features.unwrap_commonjs_to_esm;
}
fn isBindingUsed(noalias p: *P, binding: Binding, default_export_ref: Ref) bool {
switch (binding.data) {
.b_identifier => |ident| {
if (default_export_ref.eql(ident.ref)) return true;
if (p.named_imports.contains(ident.ref))
return true;
for (p.named_exports.values()) |named_export| {
if (named_export.ref.eql(ident.ref))
return true;
}
const symbol: *const Symbol = &p.symbols.items[ident.ref.innerIndex()];
return symbol.use_count_estimate > 0;
},
.b_array => |array| {
for (array.items) |item| {
if (isBindingUsed(p, item.binding, default_export_ref)) {
return true;
}
}
return false;
},
.b_object => |obj| {
for (obj.properties) |prop| {
if (isBindingUsed(p, prop.value, default_export_ref)) {
return true;
}
}
return false;
},
.b_missing => return false,
}
}
pub fn treeShake(noalias p: *P, parts: *[]js_ast.Part, merge: bool) void {
var parts_: []js_ast.Part = parts.*;
defer {
if (merge and parts_.len > 1) {
var first_none_part: usize = parts_.len;
var stmts_count: usize = 0;
for (parts_, 0..) |part, i| {
if (part.tag == .none) {
stmts_count += part.stmts.len;
first_none_part = @min(i, first_none_part);
}
}
if (first_none_part < parts_.len) {
const stmts_list = p.allocator.alloc(Stmt, stmts_count) catch unreachable;
var stmts_remain = stmts_list;
for (parts_) |part| {
if (part.tag == .none) {
bun.copy(Stmt, stmts_remain, part.stmts);
stmts_remain = stmts_remain[part.stmts.len..];
}
}
parts_[first_none_part].stmts = stmts_list;
parts_ = parts_[0 .. first_none_part + 1];
}
}
parts.* = parts_;
}
const default_export_ref =
if (p.named_exports.get("default")) |default_| default_.ref else Ref.None;
while (parts_.len > 1) {
var parts_end: usize = 0;
const last_end = parts_.len;
for (parts_) |part| {
const is_dead = part.can_be_removed_if_unused and can_remove_part: {
for (part.stmts) |stmt| {
switch (stmt.data) {
.s_local => |local| {
if (local.is_export) break :can_remove_part false;
for (local.decls.slice()) |decl| {
if (isBindingUsed(p, decl.binding, default_export_ref))
break :can_remove_part false;
}
},
.s_if => |if_statement| {
const result = SideEffects.toBoolean(p, if_statement.test_.data);
if (!(result.ok and result.side_effects == .no_side_effects and !result.value)) {
break :can_remove_part false;
}
},
.s_while => |while_statement| {
const result = SideEffects.toBoolean(p, while_statement.test_.data);
if (!(result.ok and result.side_effects == .no_side_effects and !result.value)) {
break :can_remove_part false;
}
},
.s_for => |for_statement| {
if (for_statement.test_) |expr| {
const result = SideEffects.toBoolean(p, expr.data);
if (!(result.ok and result.side_effects == .no_side_effects and !result.value)) {
break :can_remove_part false;
}
}
},
.s_function => |func| {
if (func.func.flags.contains(.is_export)) break :can_remove_part false;
if (func.func.name) |name| {
const symbol: *const Symbol = &p.symbols.items[name.ref.?.innerIndex()];
if (name.ref.?.eql(default_export_ref) or
symbol.use_count_estimate > 0 or
p.named_exports.contains(symbol.original_name) or
p.named_imports.contains(name.ref.?) or
p.is_import_item.get(name.ref.?) != null)
{
break :can_remove_part false;
}
}
},
.s_import,
.s_export_clause,
.s_export_from,
.s_export_default,
=> break :can_remove_part false,
.s_class => |class| {
if (class.is_export) break :can_remove_part false;
if (class.class.class_name) |name| {
const symbol: *const Symbol = &p.symbols.items[name.ref.?.innerIndex()];
if (name.ref.?.eql(default_export_ref) or
symbol.use_count_estimate > 0 or
p.named_exports.contains(symbol.original_name) or
p.named_imports.contains(name.ref.?) or
p.is_import_item.get(name.ref.?) != null)
{
break :can_remove_part false;
}
}
},
else => break :can_remove_part false,
}
}
break :can_remove_part true;
};
if (is_dead) {
p.clearSymbolUsagesFromDeadPart(&part);
continue;
}
parts_[parts_end] = part;
parts_end += 1;
}
parts_.len = parts_end;
if (last_end == parts_.len) {
break;
}
}
}
const ImportTransposer = ExpressionTransposer(P, *const TransposeState, P.transposeImport);
const RequireTransposer = ExpressionTransposer(P, *const TransposeState, P.transposeRequire);
const RequireResolveTransposer = ExpressionTransposer(P, Expr, P.transposeRequireResolve);
const Binding2ExprWrapper = struct {
pub const Namespace = Binding.ToExpr(P, P.wrapIdentifierNamespace);
pub const Hoisted = Binding.ToExpr(P, P.wrapIdentifierHoisting);
};
fn clearSymbolUsagesFromDeadPart(noalias p: *P, part: *const js_ast.Part) void {
const symbol_use_refs = part.symbol_uses.keys();
const symbol_use_values = part.symbol_uses.values();
var symbols = p.symbols.items;
for (symbol_use_refs, symbol_use_values) |ref, prev| {
symbols[ref.innerIndex()].use_count_estimate -|= prev.count_estimate;
}
const declared_refs = part.declared_symbols.refs();
for (declared_refs) |declared| {
symbols[declared.innerIndex()].use_count_estimate = 0;
}
}
pub fn s(noalias _: *const P, t: anytype, loc: logger.Loc) Stmt {
const Type = @TypeOf(t);
if (!is_typescript_enabled and (Type == S.TypeScript or Type == *S.TypeScript)) {
@compileError("Attempted to use TypeScript syntax in a non-TypeScript environment");
}
// Output.print("\nStmt: {s} - {d}\n", .{ @typeName(@TypeOf(t)), loc.start });
if (@typeInfo(Type) == .pointer) {
// ExportFrom normally becomes import records during the visiting pass
// However, we skip the visiting pass in this mode
// So we must generate a minimum version of it here.
if (comptime only_scan_imports_and_do_not_visit) {
// if (@TypeOf(t) == *S.ExportFrom) {
// switch (call.target.data) {
// .e_identifier => |ident| {
// // is this a require("something")
// if (strings.eqlComptime(p.loadNameFromRef(ident.ref), "require") and call.args.len == 1 and std.meta.activeTag(call.args[0].data) == .e_string) {
// _ = p.addImportRecord(.require, loc, call.args[0].data.e_string.string(p.allocator) catch unreachable);
// }
// },
// else => {},
// }
// }
}
return Stmt.init(std.meta.Child(Type), t, loc);
} else {
return Stmt.alloc(Type, t, loc);
}
}
fn computeCharacterFrequency(p: *P) ?js_ast.CharFreq {
if (!p.options.features.minify_identifiers or p.isSourceRuntime()) {
return null;
}
// Add everything in the file to the histogram
var freq: js_ast.CharFreq = .{
.freqs = [_]i32{0} ** 64,
};
freq.scan(p.source.contents, 1);
// Subtract out all comments
for (p.lexer.all_comments.items) |comment_range| {
freq.scan(p.source.textForRange(comment_range), -1);
}
// Subtract out all import paths
for (p.import_records.items) |record| {
freq.scan(record.path.text, -1);
}
const ScopeVisitor = struct {
pub fn visit(symbols: []const js_ast.Symbol, char_freq: *js_ast.CharFreq, scope: *js_ast.Scope) void {
var iter = scope.members.iterator();
while (iter.next()) |entry| {
const symbol: *const Symbol = &symbols[entry.value_ptr.ref.innerIndex()];
if (symbol.slotNamespace() != .must_not_be_renamed) {
char_freq.scan(symbol.original_name, -@as(i32, @intCast(symbol.use_count_estimate)));
}
}
if (scope.label_ref) |ref| {
const symbol = &symbols[ref.innerIndex()];
if (symbol.slotNamespace() != .must_not_be_renamed) {
char_freq.scan(symbol.original_name, -@as(i32, @intCast(symbol.use_count_estimate)) - 1);
}
}
for (scope.children.slice()) |child| {
visit(symbols, char_freq, child);
}
}
};
ScopeVisitor.visit(p.symbols.items, &freq, p.module_scope);
// TODO: mangledProps
return freq;
}
pub fn newExpr(noalias p: *P, t: anytype, loc: logger.Loc) Expr {
const Type = @TypeOf(t);
comptime {
if (jsx_transform_type == .none) {
if (Type == E.JSXElement or Type == *E.JSXElement) {
@compileError("JSXElement is not supported in this environment");
}
}
}
// Output.print("\nExpr: {s} - {d}\n", .{ @typeName(@TypeOf(t)), loc.start });
if (@typeInfo(Type) == .pointer) {
if (comptime only_scan_imports_and_do_not_visit) {
if (Type == *E.Call) {
const call: *E.Call = t;
switch (call.target.data) {
.e_identifier => |ident| {
// is this a require("something")
if (strings.eqlComptime(p.loadNameFromRef(ident.ref), "require") and call.args.len == 1 and call.args.ptr[0].data == .e_string) {
_ = p.addImportRecord(.require, loc, call.args.at(0).data.e_string.string(p.allocator) catch unreachable);
}
},
else => {},
}
}
}
return Expr.init(std.meta.Child(Type), t.*, loc);
} else {
if (comptime only_scan_imports_and_do_not_visit) {
if (Type == E.Call) {
const call: E.Call = t;
switch (call.target.data) {
.e_identifier => |ident| {
// is this a require("something")
if (strings.eqlComptime(p.loadNameFromRef(ident.ref), "require") and call.args.len == 1 and call.args.ptr[0].data == .e_string) {
_ = p.addImportRecord(.require, loc, call.args.at(0).data.e_string.string(p.allocator) catch unreachable);
}
},
else => {},
}
}
}
return Expr.init(Type, t, loc);
}
}
pub fn b(p: *P, t: anytype, loc: logger.Loc) Binding {
if (@typeInfo(@TypeOf(t)) == .pointer) {
return Binding.init(t, loc);
} else {
return Binding.alloc(p.allocator, t, loc);
}
}
pub fn recordExportedBinding(noalias p: *P, binding: Binding) void {
switch (binding.data) {
.b_missing => {},
.b_identifier => |ident| {
p.recordExport(binding.loc, p.symbols.items[ident.ref.innerIndex()].original_name, ident.ref) catch unreachable;
},
.b_array => |array| {
for (array.items) |prop| {
p.recordExportedBinding(prop.binding);
}
},
.b_object => |obj| {
for (obj.properties) |prop| {
p.recordExportedBinding(prop.value);
}
},
}
}
pub fn recordExport(noalias p: *P, loc: logger.Loc, alias: string, ref: Ref) !void {
if (p.named_exports.get(alias)) |name| {
// Duplicate exports are an error
var notes = try p.allocator.alloc(logger.Data, 1);
notes[0] = logger.Data{
.text = try std.fmt.allocPrint(p.allocator, "\"{s}\" was originally exported here", .{alias}),
.location = logger.Location.initOrNull(p.source, js_lexer.rangeOfIdentifier(p.source, name.alias_loc)),
};
try p.log.addRangeErrorFmtWithNotes(
p.source,
js_lexer.rangeOfIdentifier(p.source, loc),
p.allocator,
notes,
"Multiple exports with the same name \"{s}\"",
.{std.mem.trim(u8, alias, "\"'")},
);
} else if (!p.isDeoptimizedCommonJS()) {
try p.named_exports.put(p.allocator, alias, js_ast.NamedExport{ .alias_loc = loc, .ref = ref });
}
}
pub fn isDeoptimizedCommonJS(noalias p: *P) bool {
return p.commonjs_named_exports_deoptimized and p.commonjs_named_exports.count() > 0;
}
pub fn recordUsage(noalias p: *P, ref: Ref) void {
if (p.is_revisit_for_substitution) return;
// The use count stored in the symbol is used for generating symbol names
// during minification. These counts shouldn't include references inside dead
// code regions since those will be culled.
if (!p.is_control_flow_dead) {
if (comptime Environment.allow_assert) assert(p.symbols.items.len > ref.innerIndex());
p.symbols.items[ref.innerIndex()].use_count_estimate += 1;
var result = p.symbol_uses.getOrPut(p.allocator, ref) catch unreachable;
if (!result.found_existing) {
result.value_ptr.* = Symbol.Use{ .count_estimate = 1 };
} else {
result.value_ptr.count_estimate += 1;
}
}
// The correctness of TypeScript-to-JavaScript conversion relies on accurate
// symbol use counts for the whole file, including dead code regions. This is
// tracked separately in a parser-only data structure.
if (is_typescript_enabled) {
p.ts_use_counts.items[ref.innerIndex()] += 1;
}
}
pub fn logArrowArgErrors(noalias p: *P, errors: *DeferredArrowArgErrors) void {
if (errors.invalid_expr_await.len > 0) {
const r = errors.invalid_expr_await;
p.log.addRangeError(p.source, r, "Cannot use an \"await\" expression here") catch unreachable;
}
if (errors.invalid_expr_yield.len > 0) {
const r = errors.invalid_expr_yield;
p.log.addRangeError(p.source, r, "Cannot use a \"yield\" expression here") catch unreachable;
}
}
pub fn keyNameForError(noalias p: *P, key: *const js_ast.Expr) string {
switch (key.data) {
.e_string => {
return key.data.e_string.string(p.allocator) catch unreachable;
},
.e_private_identifier => |private| {
return p.loadNameFromRef(private.ref);
},
else => {
return "property";
},
}
}
/// This function is very very hot.
pub fn handleIdentifier(noalias p: *P, loc: logger.Loc, ident: E.Identifier, original_name: ?string, opts: IdentifierOpts) Expr {
const ref = ident.ref;
if (p.options.features.inlining) {
if (p.const_values.get(ref)) |replacement| {
p.ignoreUsage(ref);
return replacement;
}
}
// Create an error for assigning to an import namespace
if ((opts.assign_target != .none or opts.is_delete_target) and p.symbols.items[ref.innerIndex()].kind == .import) {
const r = js_lexer.rangeOfIdentifier(p.source, loc);
p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot assign to import \"{s}\"", .{
p.symbols.items[ref.innerIndex()].original_name,
}) catch unreachable;
}
// Substitute an EImportIdentifier now if this has a namespace alias
if (opts.assign_target == .none and !opts.is_delete_target) {
const symbol = &p.symbols.items[ref.inner_index];
if (symbol.namespace_alias) |ns_alias| {
if (p.ref_to_ts_namespace_member.get(ns_alias.namespace_ref)) |ts_member_data| {
if (ts_member_data == .namespace) {
if (ts_member_data.namespace.get(ns_alias.alias)) |member| {
switch (member.data) {
.enum_number => |num| return p.wrapInlinedEnum(
.{ .loc = loc, .data = .{ .e_number = .{ .value = num } } },
p.symbols.items[ref.inner_index].original_name,
),
.enum_string => |str| return p.wrapInlinedEnum(
.{ .loc = loc, .data = .{ .e_string = str } },
p.symbols.items[ref.inner_index].original_name,
),
.namespace => |map| {
const expr = p.newExpr(E.Dot{
.target = p.newExpr(E.Identifier.init(ns_alias.namespace_ref), loc),
.name = ns_alias.alias,
.name_loc = loc,
}, loc);
p.ts_namespace = .{
.expr = expr.data,
.map = map,
};
return expr;
},
else => {},
}
}
}
}
return p.newExpr(E.ImportIdentifier{
.ref = ident.ref,
.was_originally_identifier = true,
}, loc);
}
}
// Substitute an EImportIdentifier now if this is an import item
if (p.is_import_item.contains(ref)) {
return p.newExpr(
E.ImportIdentifier{ .ref = ref, .was_originally_identifier = opts.was_originally_identifier },
loc,
);
}
if (is_typescript_enabled) {
if (p.ref_to_ts_namespace_member.get(ref)) |member_data| {
switch (member_data) {
.enum_number => |num| return p.wrapInlinedEnum(
.{ .loc = loc, .data = .{ .e_number = .{ .value = num } } },
p.symbols.items[ref.inner_index].original_name,
),
.enum_string => |str| return p.wrapInlinedEnum(
.{ .loc = loc, .data = .{ .e_string = str } },
p.symbols.items[ref.inner_index].original_name,
),
.namespace => |map| {
const expr: Expr = .{
.data = .{ .e_identifier = ident },
.loc = loc,
};
p.ts_namespace = .{
.expr = expr.data,
.map = map,
};
return expr;
},
else => {},
}
}
// Substitute a namespace export reference now if appropriate
if (p.is_exported_inside_namespace.get(ref)) |ns_ref| {
const name = p.symbols.items[ref.innerIndex()].original_name;
p.recordUsage(ns_ref);
const prop = p.newExpr(E.Dot{
.target = p.newExpr(E.Identifier.init(ns_ref), loc),
.name = name,
.name_loc = loc,
}, loc);
if (p.ts_namespace.expr == .e_identifier and
p.ts_namespace.expr.e_identifier.ref.eql(ident.ref))
{
p.ts_namespace.expr = prop.data;
}
return prop;
}
}
if (original_name) |name| {
const result = p.findSymbol(loc, name) catch unreachable;
var id_clone = ident;
id_clone.ref = result.ref;
return p.newExpr(id_clone, loc);
}
return .{
.data = .{ .e_identifier = ident },
.loc = loc,
};
}
pub fn generateImportStmtForBakeResponse(
noalias p: *P,
parts: *ListManaged(js_ast.Part),
) !void {
bun.assert(!p.response_ref.isNull());
bun.assert(!p.bun_app_namespace_ref.isNull());
const allocator = p.allocator;
const import_path = "bun:app";
const import_record_i = p.addImportRecordByRange(.stmt, logger.Range.None, import_path);
var declared_symbols = DeclaredSymbol.List{};
try declared_symbols.ensureTotalCapacity(allocator, 2);
var stmts = try allocator.alloc(Stmt, 1);
declared_symbols.appendAssumeCapacity(
DeclaredSymbol{ .ref = p.bun_app_namespace_ref, .is_top_level = true },
);
try p.module_scope.generated.append(allocator, p.bun_app_namespace_ref);
const clause_items = try allocator.dupe(js_ast.ClauseItem, &.{
js_ast.ClauseItem{
.alias = "Response",
.original_name = "Response",
.alias_loc = logger.Loc{},
.name = LocRef{ .ref = p.response_ref, .loc = logger.Loc{} },
},
});
declared_symbols.appendAssumeCapacity(DeclaredSymbol{
.ref = p.response_ref,
.is_top_level = true,
});
// ensure every e_import_identifier holds the namespace
if (p.options.features.hot_module_reloading) {
const symbol = &p.symbols.items[p.response_ref.inner_index];
bun.assert(symbol.namespace_alias != null);
symbol.namespace_alias.?.import_record_index = import_record_i;
}
try p.is_import_item.put(allocator, p.response_ref, {});
try p.named_imports.put(allocator, p.response_ref, js_ast.NamedImport{
.alias = "Response",
.alias_loc = logger.Loc{},
.namespace_ref = p.bun_app_namespace_ref,
.import_record_index = import_record_i,
});
stmts[0] = p.s(
S.Import{
.namespace_ref = p.bun_app_namespace_ref,
.items = clause_items,
.import_record_index = import_record_i,
.is_single_line = true,
},
logger.Loc{},
);
var import_records = try allocator.alloc(u32, 1);
import_records[0] = import_record_i;
// This import is placed in a part before the main code, however
// the bundler ends up re-ordering this to be after... The order
// does not matter as ESM imports are always hoisted.
parts.append(js_ast.Part{
.stmts = stmts,
.declared_symbols = declared_symbols,
.import_record_indices = bun.BabyList(u32).fromOwnedSlice(import_records),
.tag = .runtime,
}) catch unreachable;
}
pub fn generateImportStmt(
noalias p: *P,
import_path: string,
imports: anytype,
parts: *ListManaged(js_ast.Part),
symbols: anytype,
additional_stmt: ?Stmt,
comptime prefix: string,
comptime is_internal: bool,
) anyerror!void {
const allocator = p.allocator;
const import_record_i = p.addImportRecordByRange(.stmt, logger.Range.None, import_path);
var import_record: *ImportRecord = &p.import_records.items[import_record_i];
if (comptime is_internal)
import_record.path.namespace = "runtime";
import_record.flags.is_internal = is_internal;
const import_path_identifier = try import_record.path.name.nonUniqueNameString(allocator);
var namespace_identifier = try allocator.alloc(u8, import_path_identifier.len + prefix.len);
const clause_items = try allocator.alloc(js_ast.ClauseItem, imports.len);
var stmts = try allocator.alloc(Stmt, 1 + if (additional_stmt != null) @as(usize, 1) else @as(usize, 0));
var declared_symbols = DeclaredSymbol.List{};
try declared_symbols.ensureTotalCapacity(allocator, imports.len + 1);
bun.copy(u8, namespace_identifier, prefix);
bun.copy(u8, namespace_identifier[prefix.len..], import_path_identifier);
const namespace_ref = try p.newSymbol(.other, namespace_identifier);
declared_symbols.appendAssumeCapacity(.{
.ref = namespace_ref,
.is_top_level = true,
});
try p.module_scope.generated.append(allocator, namespace_ref);
for (imports, clause_items) |alias, *clause_item| {
const ref = symbols.get(alias) orelse unreachable;
const alias_name = if (@TypeOf(symbols) == RuntimeImports) RuntimeImports.all[alias] else alias;
clause_item.* = js_ast.ClauseItem{
.alias = alias_name,
.original_name = alias_name,
.alias_loc = logger.Loc{},
.name = LocRef{ .ref = ref, .loc = logger.Loc{} },
};
declared_symbols.appendAssumeCapacity(.{ .ref = ref, .is_top_level = true });
// ensure every e_import_identifier holds the namespace
if (p.options.features.hot_module_reloading) {
const symbol = &p.symbols.items[ref.inner_index];
if (symbol.namespace_alias == null) {
symbol.namespace_alias = .{
.namespace_ref = namespace_ref,
.alias = alias_name,
.import_record_index = import_record_i,
};
}
}
try p.is_import_item.put(allocator, ref, {});
try p.named_imports.put(allocator, ref, js_ast.NamedImport{
.alias = alias_name,
.alias_loc = logger.Loc{},
.namespace_ref = namespace_ref,
.import_record_index = import_record_i,
});
}
stmts[0] = p.s(
S.Import{
.namespace_ref = namespace_ref,
.items = clause_items,
.import_record_index = import_record_i,
.is_single_line = true,
},
logger.Loc{},
);
if (additional_stmt) |add| {
stmts[1] = add;
}
var import_records = try allocator.alloc(@TypeOf(import_record_i), 1);
import_records[0] = import_record_i;
// This import is placed in a part before the main code, however
// the bundler ends up re-ordering this to be after... The order
// does not matter as ESM imports are always hoisted.
parts.append(js_ast.Part{
.stmts = stmts,
.declared_symbols = declared_symbols,
.import_record_indices = bun.BabyList(u32).fromOwnedSlice(import_records),
.tag = .runtime,
}) catch unreachable;
}
pub fn generateReactRefreshImport(
noalias p: *P,
parts: *ListManaged(js_ast.Part),
import_path: []const u8,
clauses: []const ReactRefreshImportClause,
) !void {
switch (p.options.features.hot_module_reloading) {
inline else => |hmr| try p.generateReactRefreshImportHmr(parts, import_path, clauses, hmr),
}
}
const ReactRefreshImportClause = struct {
name: []const u8,
enabled: bool,
ref: Ref,
};
fn generateReactRefreshImportHmr(
noalias p: *P,
parts: *ListManaged(js_ast.Part),
import_path: []const u8,
clauses: []const ReactRefreshImportClause,
comptime hot_module_reloading: bool,
) !void {
// If `hot_module_reloading`, we are going to generate a require call:
//
// const { $RefreshSig$, $RefreshReg$ } = require("react-refresh/runtime")`
//
// Otherwise we are going to settle on an import statement. Using
// require is fine in HMR bundling because `react-refresh` itself is
// already a CommonJS module, and it will actually be more efficient
// at runtime this way.
const allocator = p.allocator;
const import_record_index = p.addImportRecordByRange(.stmt, logger.Range.None, import_path);
const Item = if (hot_module_reloading) B.Object.Property else js_ast.ClauseItem;
const len = 1 + @as(usize, @intFromBool(p.react_refresh.register_used)) +
@as(usize, @intFromBool(p.react_refresh.signature_used));
var items = try List(Item).initCapacity(allocator, len);
const stmts = try allocator.alloc(Stmt, 1);
var declared_symbols = DeclaredSymbol.List{};
try declared_symbols.ensureTotalCapacity(allocator, len);
const namespace_ref = try p.newSymbol(.other, "RefreshRuntime");
declared_symbols.appendAssumeCapacity(.{
.ref = namespace_ref,
.is_top_level = true,
});
try p.module_scope.generated.append(allocator, namespace_ref);
for (clauses) |entry| {
if (entry.enabled) {
items.appendAssumeCapacity(if (hot_module_reloading) .{
.key = p.newExpr(E.String{ .data = entry.name }, logger.Loc.Empty),
.value = p.b(B.Identifier{ .ref = entry.ref }, logger.Loc.Empty),
} else .{
.alias = entry.name,
.original_name = entry.name,
.alias_loc = logger.Loc{},
.name = LocRef{ .ref = entry.ref, .loc = logger.Loc{} },
});
declared_symbols.appendAssumeCapacity(.{ .ref = entry.ref, .is_top_level = true });
try p.module_scope.generated.append(allocator, entry.ref);
try p.is_import_item.put(allocator, entry.ref, {});
try p.named_imports.put(allocator, entry.ref, .{
.alias = entry.name,
.alias_loc = logger.Loc.Empty,
.namespace_ref = namespace_ref,
.import_record_index = import_record_index,
});
}
}
stmts[0] = p.s(if (hot_module_reloading)
S.Local{
.kind = .k_const,
.decls = try Decl.List.fromSlice(p.allocator, &.{.{
.binding = p.b(B.Object{
.properties = items.items,
}, logger.Loc.Empty),
.value = p.newExpr(E.RequireString{
.import_record_index = import_record_index,
}, logger.Loc.Empty),
}}),
}
else
S.Import{
.namespace_ref = namespace_ref,
.items = items.items,
.import_record_index = import_record_index,
.is_single_line = false,
}, logger.Loc.Empty);
try parts.append(.{
.stmts = stmts,
.declared_symbols = declared_symbols,
.import_record_indices = try bun.BabyList(u32).fromSlice(allocator, &.{import_record_index}),
.tag = .runtime,
});
}
pub fn substituteSingleUseSymbolInStmt(noalias p: *P, stmt: Stmt, ref: Ref, replacement: Expr) bool {
const expr: *Expr = brk: {
switch (stmt.data) {
.s_expr => |exp| {
break :brk &exp.value;
},
.s_throw => |throw| {
break :brk &throw.value;
},
.s_return => |ret| {
if (ret.value) |*value| {
break :brk value;
}
},
.s_if => |if_stmt| {
break :brk &if_stmt.test_;
},
.s_switch => |switch_stmt| {
break :brk &switch_stmt.test_;
},
.s_local => |local| {
if (local.decls.len > 0) {
var first: *Decl = &local.decls.ptr[0];
if (first.value) |*value| {
if (first.binding.data == .b_identifier) {
break :brk value;
}
}
}
},
else => {},
}
return false;
};
// Only continue trying to insert this replacement into sub-expressions
// after the first one if the replacement has no side effects:
//
// // Substitution is ok
// let replacement = 123;
// return x + replacement;
//
// // Substitution is not ok because "fn()" may change "x"
// let replacement = fn();
// return x + replacement;
//
// // Substitution is not ok because "x == x" may change "x" due to "valueOf()" evaluation
// let replacement = [x];
// return (x == x) + replacement;
//
const replacement_can_be_removed = p.exprCanBeRemovedIfUnused(&replacement);
switch (p.substituteSingleUseSymbolInExpr(expr.*, ref, replacement, replacement_can_be_removed)) {
.success => |result| {
if (result.data == .e_binary or result.data == .e_unary or result.data == .e_if) {
const prev_substituting = p.is_revisit_for_substitution;
p.is_revisit_for_substitution = true;
defer p.is_revisit_for_substitution = prev_substituting;
// O(n^2) and we will need to think more carefully about
// this once we implement syntax compression
expr.* = p.visitExpr(result);
} else {
expr.* = result;
}
return true;
},
else => {},
}
return false;
}
fn substituteSingleUseSymbolInExpr(
noalias p: *P,
expr: Expr,
ref: Ref,
replacement: Expr,
replacement_can_be_removed: bool,
) Substitution {
outer: {
switch (expr.data) {
.e_identifier => |ident| {
if (ident.ref.eql(ref) or p.symbols.items[ident.ref.innerIndex()].link.eql(ref)) {
p.ignoreUsage(ref);
return .{ .success = replacement };
}
},
.e_new => |new| {
switch (p.substituteSingleUseSymbolInExpr(new.target, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
new.target = result;
return .{ .success = expr };
},
.failure => |result| {
new.target = result;
return .{ .failure = expr };
},
}
if (replacement_can_be_removed) {
for (new.args.slice()) |*arg| {
switch (p.substituteSingleUseSymbolInExpr(arg.*, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
arg.* = result;
return .{ .success = expr };
},
.failure => |result| {
arg.* = result;
return .{ .failure = expr };
},
}
}
}
},
.e_spread => |spread| {
switch (p.substituteSingleUseSymbolInExpr(spread.value, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
spread.value = result;
return .{ .success = expr };
},
.failure => |result| {
spread.value = result;
return .{ .failure = expr };
},
}
},
.e_await => |await_expr| {
switch (p.substituteSingleUseSymbolInExpr(await_expr.value, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
await_expr.value = result;
return .{ .success = expr };
},
.failure => |result| {
await_expr.value = result;
return .{ .failure = expr };
},
}
},
.e_yield => |yield| {
switch (p.substituteSingleUseSymbolInExpr(yield.value orelse Expr{ .data = .{ .e_missing = .{} }, .loc = expr.loc }, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
yield.value = result;
return .{ .success = expr };
},
.failure => |result| {
yield.value = result;
return .{ .failure = expr };
},
}
},
.e_import => |import| {
switch (p.substituteSingleUseSymbolInExpr(import.expr, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
import.expr = result;
return .{ .success = expr };
},
.failure => |result| {
import.expr = result;
return .{ .failure = expr };
},
}
// The "import()" expression has side effects but the side effects are
// always asynchronous so there is no way for the side effects to modify
// the replacement value. So it's ok to reorder the replacement value
// past the "import()" expression assuming everything else checks out.
if (replacement_can_be_removed and p.exprCanBeRemovedIfUnused(&import.expr)) {
return .{ .continue_ = expr };
}
},
.e_unary => |e| {
switch (e.op) {
.un_pre_inc, .un_post_inc, .un_pre_dec, .un_post_dec, .un_delete => {
// Do not substitute into an assignment position
},
else => {
switch (p.substituteSingleUseSymbolInExpr(e.value, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
e.value = result;
return .{ .success = expr };
},
.failure => |result| {
e.value = result;
return .{ .failure = expr };
},
}
},
}
},
.e_dot => |e| {
switch (p.substituteSingleUseSymbolInExpr(e.target, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
e.target = result;
return .{ .success = expr };
},
.failure => |result| {
e.target = result;
return .{ .failure = expr };
},
}
},
.e_binary => |e| {
// Do not substitute into an assignment position
if (e.op.binaryAssignTarget() == .none) {
switch (p.substituteSingleUseSymbolInExpr(e.left, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
e.left = result;
return .{ .success = expr };
},
.failure => |result| {
e.left = result;
return .{ .failure = expr };
},
}
} else if (!p.exprCanBeRemovedIfUnused(&e.left)) {
// Do not reorder past a side effect in an assignment target, as that may
// change the replacement value. For example, "fn()" may change "a" here:
//
// let a = 1;
// foo[fn()] = a;
//
return .{ .failure = expr };
} else if (e.op.binaryAssignTarget() == .update and !replacement_can_be_removed) {
// If this is a read-modify-write assignment and the replacement has side
// effects, don't reorder it past the assignment target. The assignment
// target is being read so it may be changed by the side effect. For
// example, "fn()" may change "foo" here:
//
// let a = fn();
// foo += a;
//
return .{ .failure = expr };
}
// If we get here then it should be safe to attempt to substitute the
// replacement past the left operand into the right operand.
switch (p.substituteSingleUseSymbolInExpr(e.right, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
e.right = result;
return .{ .success = expr };
},
.failure => |result| {
e.right = result;
return .{ .failure = expr };
},
}
},
.e_if => |e| {
switch (p.substituteSingleUseSymbolInExpr(expr.data.e_if.test_, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
e.test_ = result;
return .{ .success = expr };
},
.failure => |result| {
e.test_ = result;
return .{ .failure = expr };
},
}
// Do not substitute our unconditionally-executed value into a branch
// unless the value itself has no side effects
if (replacement_can_be_removed) {
// Unlike other branches in this function such as "a && b" or "a?.[b]",
// the "a ? b : c" form has potential code evaluation along both control
// flow paths. Handle this by allowing substitution into either branch.
// Side effects in one branch should not prevent the substitution into
// the other branch.
const yes = p.substituteSingleUseSymbolInExpr(e.yes, ref, replacement, replacement_can_be_removed);
if (yes == .success) {
e.yes = yes.success;
return .{ .success = expr };
}
const no = p.substituteSingleUseSymbolInExpr(e.no, ref, replacement, replacement_can_be_removed);
if (no == .success) {
e.no = no.success;
return .{ .success = expr };
}
// Side effects in either branch should stop us from continuing to try to
// substitute the replacement after the control flow branches merge again.
if (yes != .continue_ or no != .continue_) {
return .{ .failure = expr };
}
}
},
.e_index => |index| {
switch (p.substituteSingleUseSymbolInExpr(index.target, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
index.target = result;
return .{ .success = expr };
},
.failure => |result| {
index.target = result;
return .{ .failure = expr };
},
}
// Do not substitute our unconditionally-executed value into a branch
// unless the value itself has no side effects
if (replacement_can_be_removed or index.optional_chain == null) {
switch (p.substituteSingleUseSymbolInExpr(index.index, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
index.index = result;
return .{ .success = expr };
},
.failure => |result| {
index.index = result;
return .{ .failure = expr };
},
}
}
},
.e_call => |e| {
// Don't substitute something into a call target that could change "this"
switch (replacement.data) {
.e_dot, .e_index => {
if (e.target.data == .e_identifier and e.target.data.e_identifier.ref.eql(ref)) {
break :outer;
}
},
else => {},
}
switch (p.substituteSingleUseSymbolInExpr(e.target, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
e.target = result;
return .{ .success = expr };
},
.failure => |result| {
e.target = result;
return .{ .failure = expr };
},
}
// Do not substitute our unconditionally-executed value into a branch
// unless the value itself has no side effects
if (replacement_can_be_removed or e.optional_chain == null) {
for (e.args.slice()) |*arg| {
switch (p.substituteSingleUseSymbolInExpr(arg.*, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
arg.* = result;
return .{ .success = expr };
},
.failure => |result| {
arg.* = result;
return .{ .failure = expr };
},
}
}
}
},
.e_array => |e| {
for (e.items.slice()) |*item| {
switch (p.substituteSingleUseSymbolInExpr(item.*, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
item.* = result;
return .{ .success = expr };
},
.failure => |result| {
item.* = result;
return .{ .failure = expr };
},
}
}
},
.e_object => |e| {
for (e.properties.slice()) |*property| {
// Check the key
if (property.flags.contains(.is_computed)) {
switch (p.substituteSingleUseSymbolInExpr(property.key.?, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
property.key = result;
return .{ .success = expr };
},
.failure => |result| {
property.key = result;
return .{ .failure = expr };
},
}
// Stop now because both computed keys and property spread have side effects
return .{ .failure = expr };
}
// Check the value
if (property.value) |value| {
switch (p.substituteSingleUseSymbolInExpr(value, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
if (result.data == .e_missing) {
property.value = null;
} else {
property.value = result;
}
return .{ .success = expr };
},
.failure => |result| {
if (result.data == .e_missing) {
property.value = null;
} else {
property.value = result;
}
return .{ .failure = expr };
},
}
}
}
},
.e_template => |e| {
if (e.tag) |*tag| {
switch (p.substituteSingleUseSymbolInExpr(tag.*, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
tag.* = result;
return .{ .success = expr };
},
.failure => |result| {
tag.* = result;
return .{ .failure = expr };
},
}
}
for (e.parts) |*part| {
switch (p.substituteSingleUseSymbolInExpr(part.value, ref, replacement, replacement_can_be_removed)) {
.continue_ => {},
.success => |result| {
part.value = result;
// todo: mangle template parts
return .{ .success = expr };
},
.failure => |result| {
part.value = result;
return .{ .failure = expr };
},
}
}
},
else => {},
}
}
// If both the replacement and this expression have no observable side
// effects, then we can reorder the replacement past this expression
if (replacement_can_be_removed and p.exprCanBeRemovedIfUnused(&expr)) {
return .{ .continue_ = expr };
}
const tag: Expr.Tag = @as(Expr.Tag, expr.data);
// We can always reorder past primitive values
if (tag.isPrimitiveLiteral()) {
return .{ .continue_ = expr };
}
// Otherwise we should stop trying to substitute past this point
return .{ .failure = expr };
}
pub fn prepareForVisitPass(noalias p: *P) anyerror!void {
{
var i: usize = 0;
p.scope_order_to_visit = try p.allocator.alloc(ScopeOrder, p.scopes_in_order.items.len);
for (p.scopes_in_order.items) |item| {
if (item) |_item| {
p.scope_order_to_visit[i] = _item;
i += 1;
}
}
p.scope_order_to_visit.len = i;
}
p.is_file_considered_to_have_esm_exports =
!p.top_level_await_keyword.isEmpty() or !p.esm_export_keyword.isEmpty() or
p.options.module_type == .esm;
try p.pushScopeForVisitPass(js_ast.Scope.Kind.entry, locModuleScope);
p.fn_or_arrow_data_visit.is_outside_fn_or_arrow = true;
p.module_scope = p.current_scope;
p.has_es_module_syntax = p.has_es_module_syntax or p.esm_import_keyword.len > 0 or p.esm_export_keyword.len > 0 or p.top_level_await_keyword.len > 0;
if (p.lexer.jsx_pragma.jsx()) |factory| {
p.options.jsx.factory = options.JSX.Pragma.memberListToComponentsIfDifferent(p.allocator, p.options.jsx.factory, factory.text) catch unreachable;
}
if (p.lexer.jsx_pragma.jsxFrag()) |fragment| {
p.options.jsx.fragment = options.JSX.Pragma.memberListToComponentsIfDifferent(p.allocator, p.options.jsx.fragment, fragment.text) catch unreachable;
}
if (p.lexer.jsx_pragma.jsxImportSource()) |import_source| {
p.options.jsx.classic_import_source = import_source.text;
p.options.jsx.package_name = p.options.jsx.classic_import_source;
p.options.jsx.setImportSource(p.allocator);
}
if (p.lexer.jsx_pragma.jsxRuntime()) |runtime| {
if (options.JSX.RuntimeMap.get(runtime.text)) |jsx_runtime| {
p.options.jsx.runtime = jsx_runtime.runtime;
if (jsx_runtime.development) |dev| {
p.options.jsx.development = dev;
}
} else {
// make this a warning instead of an error because we don't support "preserve" right now
try p.log.addRangeWarningFmt(p.source, runtime.range, p.allocator, "Unsupported JSX runtime: \"{s}\"", .{runtime.text});
}
}
// ECMAScript modules are always interpreted as strict mode. This has to be
// done before "hoistSymbols" because strict mode can alter hoisting (!).
if (p.esm_import_keyword.len > 0) {
p.module_scope.recursiveSetStrictMode(js_ast.StrictModeKind.implicit_strict_mode_import);
} else if (p.esm_export_keyword.len > 0) {
p.module_scope.recursiveSetStrictMode(js_ast.StrictModeKind.implicit_strict_mode_export);
} else if (p.top_level_await_keyword.len > 0) {
p.module_scope.recursiveSetStrictMode(js_ast.StrictModeKind.implicit_strict_mode_top_level_await);
}
p.hoistSymbols(p.module_scope);
var generated_symbols_count: u32 = 3;
if (p.options.features.react_fast_refresh) {
generated_symbols_count += 3;
}
if (is_jsx_enabled) {
generated_symbols_count += 7;
if (p.options.jsx.development) generated_symbols_count += 1;
}
try p.module_scope.generated.ensureUnusedCapacity(p.allocator, generated_symbols_count * 3);
try p.module_scope.members.ensureUnusedCapacity(p.allocator, generated_symbols_count * 3 + p.module_scope.members.count());
p.exports_ref = try p.declareCommonJSSymbol(.hoisted, "exports");
p.module_ref = try p.declareCommonJSSymbol(.hoisted, "module");
p.require_ref = try p.declareCommonJSSymbol(.unbound, "require");
p.dirname_ref = try p.declareCommonJSSymbol(.unbound, "__dirname");
p.filename_ref = try p.declareCommonJSSymbol(.unbound, "__filename");
if (p.options.features.inject_jest_globals) {
p.jest.@"test" = try p.declareCommonJSSymbol(.unbound, "test");
p.jest.it = try p.declareCommonJSSymbol(.unbound, "it");
p.jest.describe = try p.declareCommonJSSymbol(.unbound, "describe");
p.jest.expect = try p.declareCommonJSSymbol(.unbound, "expect");
p.jest.expectTypeOf = try p.declareCommonJSSymbol(.unbound, "expectTypeOf");
p.jest.beforeAll = try p.declareCommonJSSymbol(.unbound, "beforeAll");
p.jest.beforeEach = try p.declareCommonJSSymbol(.unbound, "beforeEach");
p.jest.afterEach = try p.declareCommonJSSymbol(.unbound, "afterEach");
p.jest.afterAll = try p.declareCommonJSSymbol(.unbound, "afterAll");
p.jest.jest = try p.declareCommonJSSymbol(.unbound, "jest");
p.jest.vi = try p.declareCommonJSSymbol(.unbound, "vi");
p.jest.xit = try p.declareCommonJSSymbol(.unbound, "xit");
p.jest.xtest = try p.declareCommonJSSymbol(.unbound, "xtest");
p.jest.xdescribe = try p.declareCommonJSSymbol(.unbound, "xdescribe");
}
if (p.options.features.react_fast_refresh) {
p.react_refresh.create_signature_ref = try p.declareGeneratedSymbol(.other, "$RefreshSig$");
p.react_refresh.register_ref = try p.declareGeneratedSymbol(.other, "$RefreshReg$");
}
switch (p.options.features.server_components) {
.none, .client_side => {},
.wrap_exports_for_client_reference => {
p.server_components_wrap_ref = try p.declareGeneratedSymbol(.other, "registerClientReference");
},
// TODO: these wrapping modes.
.wrap_anon_server_functions => {},
.wrap_exports_for_server_reference => {},
}
// Server-side components:
// Declare upfront the symbols for "Response" and "bun:app"
switch (p.options.features.server_components) {
.none, .client_side => {},
else => {
p.response_ref = try p.declareGeneratedSymbol(.import, "Response");
p.bun_app_namespace_ref = try p.newSymbol(
.other,
"import_bun_app",
);
const symbol = &p.symbols.items[p.response_ref.inner_index];
symbol.namespace_alias = .{
.namespace_ref = p.bun_app_namespace_ref,
.alias = "Response",
.import_record_index = std.math.maxInt(u32),
};
},
}
if (p.options.features.hot_module_reloading) {
p.hmr_api_ref = try p.declareCommonJSSymbol(.unbound, "hmr");
}
}
fn ensureRequireSymbol(p: *P) void {
if (p.runtime_imports.__require != null) return;
const static_symbol = generatedSymbolName("__require");
p.runtime_imports.__require = bun.handleOom(declareSymbolMaybeGenerated(p, .other, logger.Loc.Empty, static_symbol, true));
p.runtime_imports.put("__require", p.runtime_imports.__require.?);
}
pub fn resolveCommonJSSymbols(p: *P) void {
if (!p.options.features.allow_runtime)
return;
p.ensureRequireSymbol();
}
fn willUseRenamer(p: *P) bool {
return p.options.bundle or p.options.features.minify_identifiers;
}
fn hoistSymbols(noalias p: *P, scope: *js_ast.Scope) void {
if (!scope.kindStopsHoisting()) {
var iter = scope.members.iterator();
const allocator = p.allocator;
var symbols = p.symbols.items;
defer {
if (comptime Environment.allow_assert) {
// we call `.newSymbol` in this function
// we need to avoid using a potentially re-sized array
// so we assert that the array is in sync
assert(symbols.ptr == p.symbols.items.ptr);
assert(symbols.len == p.symbols.items.len);
}
}
// Check for collisions that would prevent to hoisting "var" symbols up to the enclosing function scope
if (scope.parent) |parent_scope| {
nextMember: while (iter.next()) |res| {
var value = res.value_ptr.*;
var symbol: *Symbol = &symbols[value.ref.innerIndex()];
const name = symbol.original_name;
var hash: ?u64 = null;
if (parent_scope.kind == .catch_binding and symbol.kind != .hoisted) {
hash = Scope.getMemberHash(name);
if (parent_scope.getMemberWithHash(name, hash.?)) |existing_member| {
p.log.addSymbolAlreadyDeclaredError(
p.allocator,
p.source,
symbol.original_name,
value.loc,
existing_member.loc,
) catch unreachable;
continue;
}
}
if (!symbol.isHoisted()) {
continue;
}
var __scope = scope.parent;
if (comptime Environment.allow_assert)
assert(__scope != null);
var is_sloppy_mode_block_level_fn_stmt = false;
const original_member_ref = value.ref;
if (p.willUseRenamer() and symbol.kind == .hoisted_function) {
// Block-level function declarations behave like "let" in strict mode
if (scope.strict_mode != .sloppy_mode) {
continue;
}
// In sloppy mode, block level functions behave like "let" except with
// an assignment to "var", sort of. This code:
//
// if (x) {
// f();
// function f() {}
// }
// f();
//
// behaves like this code:
//
// if (x) {
// let f2 = function() {}
// var f = f2;
// f2();
// }
// f();
//
const hoisted_ref = p.newSymbol(.hoisted, symbol.original_name) catch unreachable;
symbols = p.symbols.items;
bun.handleOom(scope.generated.append(p.allocator, hoisted_ref));
p.hoisted_ref_for_sloppy_mode_block_fn.put(p.allocator, value.ref, hoisted_ref) catch unreachable;
value.ref = hoisted_ref;
symbol = &symbols[hoisted_ref.innerIndex()];
is_sloppy_mode_block_level_fn_stmt = true;
}
if (hash == null) hash = Scope.getMemberHash(name);
while (__scope) |_scope| {
const scope_kind = _scope.kind;
// Variable declarations hoisted past a "with" statement may actually end
// up overwriting a property on the target of the "with" statement instead
// of initializing the variable. We must not rename them or we risk
// causing a behavior change.
//
// var obj = { foo: 1 }
// with (obj) { var foo = 2 }
// assert(foo === undefined)
// assert(obj.foo === 2)
//
if (scope_kind == .with) {
symbol.must_not_be_renamed = true;
}
if (_scope.getMemberWithHash(name, hash.?)) |member_in_scope| {
var existing_symbol: *Symbol = &symbols[member_in_scope.ref.innerIndex()];
const existing_kind = existing_symbol.kind;
// We can hoist the symbol from the child scope into the symbol in
// this scope if:
//
// - The symbol is unbound (i.e. a global variable access)
// - The symbol is also another hoisted variable
// - The symbol is a function of any kind and we're in a function or module scope
//
// Is this unbound (i.e. a global access) or also hoisted?
if (existing_kind == .unbound or existing_kind == .hoisted or
(Symbol.isKindFunction(existing_kind) and (scope_kind == .entry or scope_kind == .function_body)))
{
// Silently merge this symbol into the existing symbol
symbol.link = member_in_scope.ref;
const entry = _scope.getOrPutMemberWithHash(p.allocator, name, hash.?) catch unreachable;
entry.value_ptr.* = member_in_scope;
entry.key_ptr.* = name;
continue :nextMember;
}
// Otherwise if this isn't a catch identifier, it's a collision
if (existing_kind != .catch_identifier and existing_kind != .arguments) {
// An identifier binding from a catch statement and a function
// declaration can both silently shadow another hoisted symbol
if (symbol.kind != .catch_identifier and symbol.kind != .hoisted_function) {
if (!is_sloppy_mode_block_level_fn_stmt) {
const r = js_lexer.rangeOfIdentifier(p.source, value.loc);
var notes = allocator.alloc(logger.Data, 1) catch unreachable;
notes[0] =
logger.rangeData(
p.source,
r,
std.fmt.allocPrint(
allocator,
"{s} was originally declared here",
.{name},
) catch unreachable,
);
p.log.addRangeErrorFmtWithNotes(p.source, js_lexer.rangeOfIdentifier(p.source, member_in_scope.loc), allocator, notes, "{s} has already been declared", .{name}) catch unreachable;
} else if (_scope == scope.parent) {
// Never mind about this, turns out it's not needed after all
_ = p.hoisted_ref_for_sloppy_mode_block_fn.remove(original_member_ref);
}
}
continue :nextMember;
}
// If this is a catch identifier, silently merge the existing symbol
// into this symbol but continue hoisting past this catch scope
existing_symbol.link = value.ref;
const entry = _scope.getOrPutMemberWithHash(p.allocator, name, hash.?) catch unreachable;
entry.value_ptr.* = value;
entry.key_ptr.* = name;
}
if (_scope.kindStopsHoisting()) {
const entry = _scope.getOrPutMemberWithHash(allocator, name, hash.?) catch unreachable;
entry.value_ptr.* = value;
entry.key_ptr.* = name;
break;
}
__scope = _scope.parent;
}
}
}
}
{
const children = scope.children.slice();
for (children) |child| {
p.hoistSymbols(child);
}
}
}
inline fn nextScopeInOrderForVisitPass(p: *P) ScopeOrder {
const head = p.scope_order_to_visit[0];
p.scope_order_to_visit = p.scope_order_to_visit[1..p.scope_order_to_visit.len];
return head;
}
pub fn pushScopeForVisitPass(noalias p: *P, kind: js_ast.Scope.Kind, loc: logger.Loc) anyerror!void {
const order = p.nextScopeInOrderForVisitPass();
// Sanity-check that the scopes generated by the first and second passes match
if (bun.Environment.allow_assert and
order.loc.start != loc.start or order.scope.kind != kind)
{
p.log.level = .verbose;
bun.handleOom(p.log.addDebugFmt(p.source, loc, p.allocator, "Expected this scope (.{s})", .{@tagName(kind)}));
bun.handleOom(p.log.addDebugFmt(p.source, order.loc, p.allocator, "Found this scope (.{s})", .{@tagName(order.scope.kind)}));
p.panic("Scope mismatch while visiting", .{});
}
p.current_scope = order.scope;
try p.scopes_for_current_part.append(p.allocator, order.scope);
}
pub fn pushScopeForParsePass(noalias p: *P, comptime kind: js_ast.Scope.Kind, loc: logger.Loc) !usize {
var parent: *Scope = p.current_scope;
const allocator = p.allocator;
var scope = try allocator.create(Scope);
scope.* = Scope{
.kind = kind,
.label_ref = null,
.parent = parent,
.generated = .{},
};
try parent.children.append(allocator, scope);
scope.strict_mode = parent.strict_mode;
p.current_scope = scope;
if (comptime kind == .with) {
// "with" statements change the default from ESModule to CommonJS at runtime.
// "with" statements are not allowed in strict mode.
if (p.options.features.commonjs_at_runtime) {
p.has_with_scope = true;
}
}
if (comptime Environment.isDebug) {
// Enforce that scope locations are strictly increasing to help catch bugs
// where the pushed scopes are mismatched between the first and second passes
if (p.scopes_in_order.items.len > 0) {
var last_i = p.scopes_in_order.items.len - 1;
while (p.scopes_in_order.items[last_i] == null and last_i > 0) {
last_i -= 1;
}
if (p.scopes_in_order.items[last_i]) |prev_scope| {
if (prev_scope.loc.start >= loc.start) {
p.log.level = .verbose;
bun.handleOom(p.log.addDebugFmt(p.source, prev_scope.loc, p.allocator, "Previous Scope", .{}));
bun.handleOom(p.log.addDebugFmt(p.source, loc, p.allocator, "Next Scope", .{}));
p.panic("Scope location {d} must be greater than {d}", .{ loc.start, prev_scope.loc.start });
}
}
}
}
// Copy down function arguments into the function body scope. That way we get
// errors if a statement in the function body tries to re-declare any of the
// arguments.
if (comptime kind == js_ast.Scope.Kind.function_body) {
if (comptime Environment.allow_assert)
assert(parent.kind == js_ast.Scope.Kind.function_args);
var iter = scope.parent.?.members.iterator();
while (iter.next()) |entry| {
// Don't copy down the optional function expression name. Re-declaring
// the name of a function expression is allowed.
const value = entry.value_ptr.*;
const adjacent_kind = p.symbols.items[value.ref.innerIndex()].kind;
if (adjacent_kind != .hoisted_function) {
try scope.members.put(allocator, entry.key_ptr.*, value);
}
}
}
// Remember the length in case we call popAndDiscardScope() later
const scope_index = p.scopes_in_order.items.len;
try p.scopes_in_order.append(allocator, ScopeOrder{ .loc = loc, .scope = scope });
// Output.print("\nLoc: {d}\n", .{loc.start});
return scope_index;
}
// Note: do not write to "p.log" in this function. Any errors due to conversion
// from expression to binding should be written to "invalidLog" instead. That
// way we can potentially keep this as an expression if it turns out it's not
// needed as a binding after all.
fn convertExprToBinding(noalias p: *P, expr: ExprNodeIndex, invalid_loc: *LocList) ?Binding {
switch (expr.data) {
.e_missing => {
return null;
},
.e_identifier => |ex| {
return p.b(B.Identifier{ .ref = ex.ref }, expr.loc);
},
.e_array => |ex| {
if (ex.comma_after_spread) |spread| {
invalid_loc.append(.{
.loc = spread,
.kind = .spread,
}) catch unreachable;
}
if (ex.is_parenthesized) {
invalid_loc.append(.{
.loc = p.source.rangeOfOperatorBefore(expr.loc, "(").loc,
.kind = .parentheses,
}) catch unreachable;
}
// p.markSyntaxFeature(Destructing)
var items = List(js_ast.ArrayBinding).initCapacity(p.allocator, ex.items.len) catch unreachable;
var is_spread = false;
for (ex.items.slice(), 0..) |_, i| {
var item = ex.items.ptr[i];
if (item.data == .e_spread) {
is_spread = true;
item = item.data.e_spread.value;
}
const res = p.convertExprToBindingAndInitializer(&item, invalid_loc, is_spread);
items.appendAssumeCapacity(js_ast.ArrayBinding{
// It's valid for it to be missing
// An example:
// Promise.all(promises).then(([, len]) => true);
// ^ Binding is missing there
.binding = res.binding orelse p.b(B.Missing{}, item.loc),
.default_value = res.expr,
});
}
return p.b(B.Array{
.items = items.items,
.has_spread = is_spread,
.is_single_line = ex.is_single_line,
}, expr.loc);
},
.e_object => |ex| {
if (ex.comma_after_spread) |sp| {
invalid_loc.append(.{ .loc = sp, .kind = .spread }) catch unreachable;
}
if (ex.is_parenthesized) {
invalid_loc.append(.{ .loc = p.source.rangeOfOperatorBefore(expr.loc, "(").loc, .kind = .parentheses }) catch unreachable;
}
// p.markSyntaxFeature(compat.Destructuring, p.source.RangeOfOperatorAfter(expr.Loc, "{"))
var properties = List(B.Property).initCapacity(p.allocator, ex.properties.len) catch unreachable;
for (ex.properties.slice()) |*item| {
if (item.flags.contains(.is_method) or item.kind == .get or item.kind == .set) {
invalid_loc.append(.{
.loc = item.key.?.loc,
.kind = if (item.flags.contains(.is_method))
InvalidLoc.Tag.method
else if (item.kind == .get)
InvalidLoc.Tag.getter
else
InvalidLoc.Tag.setter,
}) catch unreachable;
continue;
}
const value = &item.value.?;
const tup = p.convertExprToBindingAndInitializer(value, invalid_loc, false);
const initializer = tup.expr orelse item.initializer;
const is_spread = item.kind == .spread or item.flags.contains(.is_spread);
properties.appendAssumeCapacity(B.Property{
.flags = Flags.Property.init(.{
.is_spread = is_spread,
.is_computed = item.flags.contains(.is_computed),
}),
.key = item.key orelse p.newExpr(E.Missing{}, expr.loc),
.value = tup.binding orelse p.b(B.Missing{}, expr.loc),
.default_value = initializer,
});
}
return p.b(B.Object{
.properties = properties.items,
.is_single_line = ex.is_single_line,
}, expr.loc);
},
else => {
invalid_loc.append(.{ .loc = expr.loc, .kind = .unknown }) catch unreachable;
return null;
},
}
return null;
}
pub fn convertExprToBindingAndInitializer(noalias p: *P, _expr: *ExprNodeIndex, invalid_log: *LocList, is_spread: bool) ExprBindingTuple {
var initializer: ?ExprNodeIndex = null;
var expr = _expr;
// zig syntax is sometimes painful
switch (expr.*.data) {
.e_binary => |bin| {
if (bin.op == .bin_assign) {
initializer = bin.right;
expr = &bin.left;
}
},
else => {},
}
const bind = p.convertExprToBinding(expr.*, invalid_log);
if (initializer) |initial| {
const equalsRange = p.source.rangeOfOperatorBefore(initial.loc, "=");
if (is_spread) {
p.log.addRangeError(p.source, equalsRange, "A rest argument cannot have a default initializer") catch unreachable;
} else {
// p.markSyntaxFeature();
}
}
return ExprBindingTuple{ .binding = bind, .expr = initializer };
}
pub fn forbidLexicalDecl(noalias p: *const P, loc: logger.Loc) anyerror!void {
try p.log.addError(p.source, loc, "Cannot use a declaration in a single-statement context");
}
/// If we attempt to parse TypeScript syntax outside of a TypeScript file
/// make it a compile error
pub inline fn markTypeScriptOnly(noalias _: *const P) void {
if (comptime !is_typescript_enabled) {
@compileError("This function can only be used in TypeScript");
}
// explicitly mark it as unreachable in the hopes that the function doesn't exist at all
if (!is_typescript_enabled) {
unreachable;
}
}
pub fn logExprErrors(noalias p: *P, noalias errors: *DeferredErrors) void {
if (errors.invalid_expr_default_value) |r| {
p.log.addRangeError(
p.source,
r,
"Unexpected \"=\"",
) catch unreachable;
}
if (errors.invalid_expr_after_question) |r| {
p.log.addRangeErrorFmt(p.source, r, p.allocator, "Unexpected {s}", .{p.source.contents[r.loc.i()..r.endI()]}) catch unreachable;
}
// if (errors.array_spread_feature) |err| {
// p.markSyntaxFeature(compat.ArraySpread, errors.arraySpreadFeature)
// }
}
pub fn popAndDiscardScope(noalias p: *P, scope_index: usize) void {
// Move up to the parent scope
const to_discard = p.current_scope;
const parent = to_discard.parent orelse unreachable;
p.current_scope = parent;
// Truncate the scope order where we started to pretend we never saw this scope
p.scopes_in_order.shrinkRetainingCapacity(scope_index);
var children = parent.children;
// Remove the last child from the parent scope
const last = children.len - 1;
if (children.slice()[last] != to_discard) {
p.panic("Internal error", .{});
}
_ = children.pop();
}
pub fn processImportStatement(p: *P, stmt_: S.Import, path: ParsedPath, loc: logger.Loc, was_originally_bare_import: bool) anyerror!Stmt {
const is_macro = FeatureFlags.is_macro_enabled and (path.is_macro or js_ast.Macro.isMacroPath(path.text));
var stmt = stmt_;
if (is_macro) {
const id = p.addImportRecord(.stmt, path.loc, path.text);
p.import_records.items[id].path.namespace = js_ast.Macro.namespace;
p.import_records.items[id].flags.is_unused = true;
if (stmt.default_name) |name_loc| {
const name = p.loadNameFromRef(name_loc.ref.?);
const ref = try p.declareSymbol(.other, name_loc.loc, name);
try p.is_import_item.put(p.allocator, ref, {});
try p.macro.refs.put(ref, .{
.import_record_id = id,
.name = "default",
});
}
if (stmt.star_name_loc) |star| {
const name = p.loadNameFromRef(stmt.namespace_ref);
const ref = try p.declareSymbol(.other, star, name);
stmt.namespace_ref = ref;
try p.macro.refs.put(ref, .{ .import_record_id = id });
}
for (stmt.items) |item| {
const name = p.loadNameFromRef(item.name.ref.?);
const ref = try p.declareSymbol(.other, item.name.loc, name);
try p.is_import_item.put(p.allocator, ref, {});
try p.macro.refs.put(ref, .{
.import_record_id = id,
.name = item.alias,
});
}
return p.s(S.Empty{}, loc);
}
// Handle `import { feature } from "bun:bundle"` - this is a special import
// that provides static feature flag checking at bundle time.
// We handle it here at parse time (similar to macros) rather than at visit time.
if (strings.eqlComptime(path.text, "bun:bundle")) {
// Look for the "feature" import and validate specifiers
for (stmt.items) |*item| {
// In ClauseItem from parseImportClause:
// - alias is the name from the source module ("feature")
// - original_name is the local binding name
// - name.ref is the ref for the local binding
if (strings.eqlComptime(item.alias, "feature")) {
// Check for duplicate imports of feature
if (p.bundler_feature_flag_ref.isValid()) {
try p.log.addError(p.source, item.alias_loc, "`feature` from \"bun:bundle\" may only be imported once");
continue;
}
// Declare the symbol and store the ref
const name = p.loadNameFromRef(item.name.ref.?);
const ref = try p.declareSymbol(.other, item.name.loc, name);
p.bundler_feature_flag_ref = ref;
} else {
try p.log.addErrorFmt(p.source, item.alias_loc, p.allocator, "\"bun:bundle\" has no export named \"{s}\"", .{item.alias});
}
}
// Return empty statement - the import is completely removed
return p.s(S.Empty{}, loc);
}
const macro_remap = if (comptime allow_macros)
p.options.macro_context.getRemap(path.text)
else
null;
stmt.import_record_index = p.addImportRecord(.stmt, path.loc, path.text);
p.import_records.items[stmt.import_record_index].flags.was_originally_bare_import = was_originally_bare_import;
if (stmt.star_name_loc) |star| {
const name = p.loadNameFromRef(stmt.namespace_ref);
stmt.namespace_ref = try p.declareSymbol(.import, star, name);
if (comptime track_symbol_usage_during_parse_pass) {
p.parse_pass_symbol_uses.put(name, .{
.ref = stmt.namespace_ref,
.import_record_index = stmt.import_record_index,
}) catch unreachable;
}
// TODO: not sure how to handle macro remappings for namespace imports
} else {
var path_name = fs.PathName.init(path.text);
const name = try strings.append(p.allocator, "import_", try path_name.nonUniqueNameString(p.allocator));
stmt.namespace_ref = try p.newSymbol(.other, name);
var scope: *Scope = p.current_scope;
try scope.generated.append(p.allocator, stmt.namespace_ref);
}
var item_refs = ImportItemForNamespaceMap.init(p.allocator);
const count_excluding_namespace = @as(u16, @intCast(stmt.items.len)) +
@as(u16, @intCast(@intFromBool(stmt.default_name != null)));
try item_refs.ensureUnusedCapacity(count_excluding_namespace);
// Even though we allocate ahead of time here
// we cannot use putAssumeCapacity because a symbol can have existing links
// those may write to this hash table, so this estimate may be innaccurate
try p.is_import_item.ensureUnusedCapacity(p.allocator, count_excluding_namespace);
var remap_count: u32 = 0;
// Link the default item to the namespace
if (stmt.default_name) |*name_loc| outer: {
const name = p.loadNameFromRef(name_loc.ref.?);
const ref = try p.declareSymbol(.import, name_loc.loc, name);
name_loc.ref = ref;
try p.is_import_item.put(p.allocator, ref, {});
// ensure every e_import_identifier holds the namespace
if (p.options.features.hot_module_reloading) {
const symbol = &p.symbols.items[ref.inner_index];
if (symbol.namespace_alias == null) {
symbol.namespace_alias = .{
.namespace_ref = stmt.namespace_ref,
.alias = "default",
.import_record_index = stmt.import_record_index,
};
}
}
if (macro_remap) |*remap| {
if (remap.get("default")) |remapped_path| {
const new_import_id = p.addImportRecord(.stmt, path.loc, remapped_path);
try p.macro.refs.put(ref, .{
.import_record_id = new_import_id,
.name = "default",
});
p.import_records.items[new_import_id].path.namespace = js_ast.Macro.namespace;
p.import_records.items[new_import_id].flags.is_unused = true;
if (comptime only_scan_imports_and_do_not_visit) {
p.import_records.items[new_import_id].flags.is_internal = true;
p.import_records.items[new_import_id].path.is_disabled = true;
}
stmt.default_name = null;
remap_count += 1;
break :outer;
}
}
if (comptime track_symbol_usage_during_parse_pass) {
p.parse_pass_symbol_uses.put(name, .{
.ref = ref,
.import_record_index = stmt.import_record_index,
}) catch unreachable;
}
if (comptime ParsePassSymbolUsageType != void) {
p.parse_pass_symbol_uses.put(name, .{
.ref = ref,
.import_record_index = stmt.import_record_index,
}) catch unreachable;
}
// No need to add the `default_name` to `item_refs` because
// `.scanImportsAndExports(...)` special cases and handles
// `default_name` separately
}
var end: usize = 0;
for (stmt.items) |item_| {
var item = item_;
const name = p.loadNameFromRef(item.name.ref orelse unreachable);
const ref = try p.declareSymbol(.import, item.name.loc, name);
item.name.ref = ref;
try p.is_import_item.put(p.allocator, ref, {});
p.checkForNonBMPCodePoint(item.alias_loc, item.alias);
// ensure every e_import_identifier holds the namespace
if (p.options.features.hot_module_reloading) {
const symbol = &p.symbols.items[ref.inner_index];
if (symbol.namespace_alias == null) {
symbol.namespace_alias = .{
.namespace_ref = stmt.namespace_ref,
.alias = item.alias,
.import_record_index = stmt.import_record_index,
};
}
}
if (macro_remap) |*remap| {
if (remap.get(item.alias)) |remapped_path| {
const new_import_id = p.addImportRecord(.stmt, path.loc, remapped_path);
try p.macro.refs.put(ref, .{
.import_record_id = new_import_id,
.name = item.alias,
});
p.import_records.items[new_import_id].path.namespace = js_ast.Macro.namespace;
p.import_records.items[new_import_id].flags.is_unused = true;
if (comptime only_scan_imports_and_do_not_visit) {
p.import_records.items[new_import_id].flags.is_internal = true;
p.import_records.items[new_import_id].path.is_disabled = true;
}
remap_count += 1;
continue;
}
}
if (comptime track_symbol_usage_during_parse_pass) {
p.parse_pass_symbol_uses.put(name, .{
.ref = ref,
.import_record_index = stmt.import_record_index,
}) catch unreachable;
}
item_refs.putAssumeCapacity(item.alias, item.name);
stmt.items[end] = item;
end += 1;
}
stmt.items = stmt.items[0..end];
// If we remapped the entire import away
// i.e. import {graphql} "react-relay"
if (remap_count > 0 and stmt.items.len == 0 and stmt.default_name == null) {
p.import_records.items[stmt.import_record_index].path.namespace = js_ast.Macro.namespace;
p.import_records.items[stmt.import_record_index].flags.is_unused = true;
if (comptime only_scan_imports_and_do_not_visit) {
p.import_records.items[stmt.import_record_index].path.is_disabled = true;
p.import_records.items[stmt.import_record_index].flags.is_internal = true;
}
return p.s(S.Empty{}, loc);
} else if (remap_count > 0) {
item_refs.shrinkAndFree(stmt.items.len + @as(usize, @intFromBool(stmt.default_name != null)));
}
if (path.import_tag != .none or path.loader != null) {
try p.validateAndSetImportType(&path, &stmt);
}
// Track the items for this namespace
try p.import_items_for_namespace.put(p.allocator, stmt.namespace_ref, item_refs);
return p.s(stmt, loc);
}
fn validateAndSetImportType(p: *P, path: *const ParsedPath, stmt: *S.Import) !void {
@branchHint(.cold);
if (path.loader) |loader| {
p.import_records.items[stmt.import_record_index].loader = loader;
if (loader == .sqlite or loader == .sqlite_embedded) {
for (stmt.items) |*item| {
if (!(strings.eqlComptime(item.alias, "default") or strings.eqlComptime(item.alias, "db"))) {
try p.log.addError(
p.source,
item.name.loc,
"sqlite imports only support the \"default\" or \"db\" imports",
);
break;
}
}
} else if (loader == .file or loader == .text) {
for (stmt.items) |*item| {
if (!(strings.eqlComptime(item.alias, "default"))) {
try p.log.addError(
p.source,
item.name.loc,
"This loader type only supports the \"default\" import",
);
break;
}
}
}
} else if (path.import_tag == .bake_resolve_to_ssr_graph) {
p.import_records.items[stmt.import_record_index].tag = path.import_tag;
}
}
pub fn createDefaultName(p: *P, loc: logger.Loc) !js_ast.LocRef {
const identifier = try std.fmt.allocPrint(p.allocator, "{s}_default", .{try p.source.path.name.nonUniqueNameString(p.allocator)});
const name = js_ast.LocRef{ .loc = loc, .ref = try p.newSymbol(Symbol.Kind.other, identifier) };
var scope = p.current_scope;
try scope.generated.append(p.allocator, name.ref.?);
return name;
}
pub fn newSymbol(p: *P, kind: Symbol.Kind, identifier: string) !Ref {
const inner_index: Ref.Int = @truncate(p.symbols.items.len);
try p.symbols.append(Symbol{
.kind = kind,
.original_name = identifier,
});
if (is_typescript_enabled) {
try p.ts_use_counts.append(p.allocator, 0);
}
return Ref{
.inner_index = inner_index,
.source_index = @intCast(p.source.index.get()),
.tag = .symbol,
};
}
pub fn defaultNameForExpr(p: *P, expr: Expr, loc: logger.Loc) LocRef {
switch (expr.data) {
.e_function => |func_container| {
if (func_container.func.name) |_name| {
if (_name.ref) |ref| {
return LocRef{ .loc = loc, .ref = ref };
}
}
},
.e_identifier => |ident| {
return LocRef{ .loc = loc, .ref = ident.ref };
},
.e_import_identifier => |ident| {
if (!allow_macros or (allow_macros and !p.macro.refs.contains(ident.ref))) {
return LocRef{ .loc = loc, .ref = ident.ref };
}
},
.e_class => |class| {
if (class.class_name) |_name| {
if (_name.ref) |ref| {
return LocRef{ .loc = loc, .ref = ref };
}
}
},
else => {},
}
return createDefaultName(p, loc) catch unreachable;
}
pub fn discardScopesUpTo(p: *P, scope_index: usize) void {
// Remove any direct children from their parent
const scope = p.current_scope;
var children = scope.children;
defer scope.children = children;
for (p.scopes_in_order.items[scope_index..]) |_child| {
const child = _child orelse continue;
if (child.scope.parent == p.current_scope) {
var i: usize = children.len - 1;
while (i >= 0) {
if (children.mut(i).* == child.scope) {
_ = children.orderedRemove(i);
break;
}
i -= 1;
}
}
}
// Truncate the scope order where we started to pretend we never saw this scope
p.scopes_in_order.shrinkRetainingCapacity(scope_index);
}
pub fn defineExportedNamespaceBinding(
p: *P,
exported_members: *js_ast.TSNamespaceMemberMap,
binding: Binding,
) !void {
switch (binding.data) {
.b_missing => {},
.b_identifier => |id| {
const name = p.symbols.items[id.ref.inner_index].original_name;
try exported_members.put(p.allocator, name, .{
.loc = binding.loc,
.data = .property,
});
try p.ref_to_ts_namespace_member.put(
p.allocator,
id.ref,
.property,
);
},
.b_object => |obj| {
for (obj.properties) |prop| {
try p.defineExportedNamespaceBinding(exported_members, prop.value);
}
},
.b_array => |obj| {
for (obj.items) |prop| {
try p.defineExportedNamespaceBinding(exported_members, prop.binding);
}
},
}
}
pub fn forbidInitializers(p: *P, decls: []G.Decl, comptime loop_type: string, is_var: bool) anyerror!void {
switch (decls.len) {
0 => {},
1 => {
if (decls[0].value) |value| {
if (is_var) {
// This is a weird special case. Initializers are allowed in "var"
// statements with identifier bindings.
return;
}
try p.log.addError(p.source, value.loc, comptime std.fmt.comptimePrint("for-{s} loop variables cannot have an initializer", .{loop_type}));
}
},
else => {
try p.log.addError(p.source, decls[0].binding.loc, comptime std.fmt.comptimePrint("for-{s} loops must have a single declaration", .{loop_type}));
},
}
}
pub fn requireInitializers(noalias p: *P, comptime kind: S.Local.Kind, decls: []G.Decl) anyerror!void {
const what = switch (kind) {
.k_await_using, .k_using => "declaration",
.k_const => "constant",
else => @compileError("unreachable"),
};
for (decls) |*decl| {
if (decl.value == null) {
switch (decl.binding.data) {
.b_identifier => |ident| {
const r = js_lexer.rangeOfIdentifier(p.source, decl.binding.loc);
try p.log.addRangeErrorFmt(p.source, r, p.allocator, "The " ++ what ++ " \"{s}\" must be initialized", .{
p.symbols.items[ident.ref.innerIndex()].original_name,
});
// return;/
},
else => {
try p.log.addError(p.source, decl.binding.loc, "This " ++ what ++ " must be initialized");
},
}
}
}
}
// Generate a TypeScript namespace object for this namespace's scope. If this
// namespace is another block that is to be merged with an existing namespace,
// use that earlier namespace's object instead.
pub fn getOrCreateExportedNamespaceMembers(p: *P, name: []const u8, is_export: bool, is_enum_scope: bool) *js_ast.TSNamespaceScope {
const map = brk: {
// Merge with a sibling namespace from the same scope
if (p.current_scope.members.get(name)) |existing_member| {
if (p.ref_to_ts_namespace_member.get(existing_member.ref)) |member_data| {
if (member_data == .namespace)
break :brk member_data.namespace;
}
}
// Merge with a sibling namespace from a different scope
if (is_export) {
if (p.current_scope.ts_namespace) |ns| {
if (ns.exported_members.get(name)) |member| {
if (member.data == .namespace)
break :brk member.data.namespace;
}
}
}
break :brk null;
};
if (map) |existing| {
return bun.create(p.allocator, js_ast.TSNamespaceScope, .{
.exported_members = existing,
.is_enum_scope = is_enum_scope,
.arg_ref = Ref.None,
});
}
// Otherwise, generate a new namespace object
// Batch the allocation of the namespace object and the map into a single allocation.
const Pair = struct {
map: js_ast.TSNamespaceMemberMap,
scope: js_ast.TSNamespaceScope,
};
var pair = bun.handleOom(p.allocator.create(Pair));
pair.map = .{};
pair.scope = .{
.exported_members = &pair.map,
.is_enum_scope = is_enum_scope,
.arg_ref = Ref.None,
};
return &pair.scope;
}
// TODO:
pub fn checkForNonBMPCodePoint(_: *P, _: logger.Loc, _: string) void {}
pub fn markStrictModeFeature(p: *P, feature: StrictModeFeature, r: logger.Range, detail: string) anyerror!void {
const can_be_transformed = feature == StrictModeFeature.for_in_var_init;
const text = switch (feature) {
.with_statement => "With statements",
.delete_bare_name => "\"delete\" of a bare identifier",
.for_in_var_init => "Variable initializers within for-in loops",
.eval_or_arguments => try std.fmt.allocPrint(p.allocator, "Declarations with the name \"{s}\"", .{detail}),
.reserved_word => try std.fmt.allocPrint(p.allocator, "\"{s}\" is a reserved word and", .{detail}),
.legacy_octal_literal => "Legacy octal literals",
.legacy_octal_escape => "Legacy octal escape sequences",
.if_else_function_stmt => "Function declarations inside if statements",
// else => {
// text = "This feature";
// },
};
const scope = p.current_scope;
if (p.isStrictMode()) {
var why: string = "";
var where: logger.Range = logger.Range.None;
switch (scope.strict_mode) {
.implicit_strict_mode_import => {
where = p.esm_import_keyword;
},
.implicit_strict_mode_export => {
where = p.esm_export_keyword;
},
.implicit_strict_mode_top_level_await => {
where = p.top_level_await_keyword;
},
.implicit_strict_mode_class => {
why = "All code inside a class is implicitly in strict mode";
where = p.enclosing_class_keyword;
},
else => {},
}
if (why.len == 0) {
why = try std.fmt.allocPrint(p.allocator, "This file is implicitly in strict mode because of the \"{s}\" keyword here", .{p.source.textForRange(where)});
}
var notes = try p.allocator.alloc(logger.Data, 1);
notes[0] = logger.rangeData(p.source, where, why);
try p.log.addRangeErrorWithNotes(p.source, r, try std.fmt.allocPrint(p.allocator, "{s} cannot be used in strict mode", .{text}), notes);
} else if (!can_be_transformed and p.isStrictModeOutputFormat()) {
try p.log.addRangeError(p.source, r, try std.fmt.allocPrint(p.allocator, "{s} cannot be used with the ESM output format due to strict mode", .{text}));
}
}
pub inline fn isStrictMode(p: *P) bool {
return p.current_scope.strict_mode != .sloppy_mode;
}
pub inline fn isStrictModeOutputFormat(p: *P) bool {
return p.options.bundle and p.options.output_format.isESM();
}
pub fn declareCommonJSSymbol(p: *P, comptime kind: Symbol.Kind, comptime name: string) !Ref {
const name_hash = comptime Scope.getMemberHash(name);
const member = p.module_scope.getMemberWithHash(name, name_hash);
// If the code declared this symbol using "var name", then this is actually
// not a collision. For example, node will let you do this:
//
// var exports;
// module.exports.foo = 123;
// console.log(exports.foo);
//
// This works because node's implementation of CommonJS wraps the entire
// source file like this:
//
// (function(require, exports, module, __filename, __dirname) {
// var exports;
// module.exports.foo = 123;
// console.log(exports.foo);
// })
//
// Both the "exports" argument and "var exports" are hoisted variables, so
// they don't collide.
if (member) |_member| {
if (p.symbols.items[_member.ref.innerIndex()].kind == .hoisted and kind == .hoisted and !p.has_es_module_syntax) {
return _member.ref;
}
}
// Create a new symbol if we didn't merge with an existing one above
const ref = try p.newSymbol(kind, name);
if (member == null) {
try p.module_scope.members.put(p.allocator, name, Scope.Member{ .ref = ref, .loc = logger.Loc.Empty });
return ref;
}
// If the variable was declared, then it shadows this symbol. The code in
// this module will be unable to reference this symbol. However, we must
// still add the symbol to the scope so it gets minified (automatically-
// generated code may still reference the symbol).
try p.module_scope.generated.append(p.allocator, ref);
return ref;
}
pub fn declareGeneratedSymbol(p: *P, kind: Symbol.Kind, comptime name: string) !Ref {
// The bundler runs the renamer, so it is ok to not append a hash
if (p.options.bundle) {
return try declareSymbolMaybeGenerated(p, kind, logger.Loc.Empty, name, true);
}
return try declareSymbolMaybeGenerated(p, kind, logger.Loc.Empty, generatedSymbolName(name), true);
}
pub fn declareSymbol(p: *P, kind: Symbol.Kind, loc: logger.Loc, name: string) !Ref {
return try @call(bun.callmod_inline, declareSymbolMaybeGenerated, .{ p, kind, loc, name, false });
}
pub fn declareSymbolMaybeGenerated(p: *P, kind: Symbol.Kind, loc: logger.Loc, name: string, comptime is_generated: bool) !Ref {
// p.checkForNonBMPCodePoint(loc, name)
if (comptime !is_generated) {
// Forbid declaring a symbol with a reserved word in strict mode
if (p.isStrictMode() and name.ptr != arguments_str.ptr and js_lexer.StrictModeReservedWords.has(name)) {
try p.markStrictModeFeature(.reserved_word, js_lexer.rangeOfIdentifier(p.source, loc), name);
}
}
// Allocate a new symbol
var ref = try p.newSymbol(kind, name);
const scope = p.current_scope;
const entry = try scope.members.getOrPut(p.allocator, name);
if (entry.found_existing) {
const existing = entry.value_ptr.*;
var symbol: *Symbol = &p.symbols.items[existing.ref.innerIndex()];
if (comptime !is_generated) {
switch (scope.canMergeSymbols(symbol.kind, kind, is_typescript_enabled)) {
.forbidden => {
try p.log.addSymbolAlreadyDeclaredError(p.allocator, p.source, symbol.original_name, loc, existing.loc);
return existing.ref;
},
.keep_existing => {
ref = existing.ref;
},
.replace_with_new => {
symbol.link = ref;
// If these are both functions, remove the overwritten declaration
if (kind.isFunction() and symbol.kind.isFunction()) {
symbol.remove_overwritten_function_declaration = true;
}
},
.become_private_get_set_pair => {
ref = existing.ref;
symbol.kind = .private_get_set_pair;
},
.become_private_static_get_set_pair => {
ref = existing.ref;
symbol.kind = .private_static_get_set_pair;
},
.overwrite_with_new => {},
}
} else {
p.symbols.items[ref.innerIndex()].link = existing.ref;
}
}
entry.key_ptr.* = name;
entry.value_ptr.* = js_ast.Scope.Member{ .ref = ref, .loc = loc };
if (comptime is_generated) {
try p.module_scope.generated.append(p.allocator, ref);
}
return ref;
}
pub fn validateFunctionName(p: *P, func: G.Fn, kind: FunctionKind) void {
if (func.name) |name| {
const original_name = p.symbols.items[name.ref.?.innerIndex()].original_name;
if (func.flags.contains(.is_async) and strings.eqlComptime(original_name, "await")) {
p.log.addRangeError(
p.source,
js_lexer.rangeOfIdentifier(p.source, name.loc),
"An async function cannot be named \"await\"",
) catch unreachable;
} else if (kind == .expr and func.flags.contains(.is_generator) and strings.eqlComptime(original_name, "yield")) {
p.log.addRangeError(
p.source,
js_lexer.rangeOfIdentifier(p.source, name.loc),
"An generator function expression cannot be named \"yield\"",
) catch unreachable;
}
}
}
pub fn declareBinding(p: *P, kind: Symbol.Kind, binding: *BindingNodeIndex, opts: *ParseStatementOptions) anyerror!void {
switch (binding.data) {
.b_missing => {},
.b_identifier => |bind| {
if (!opts.is_typescript_declare or (opts.is_namespace_scope and opts.is_export)) {
bind.ref = try p.declareSymbol(kind, binding.loc, p.loadNameFromRef(bind.ref));
}
},
.b_array => |bind| {
for (bind.items) |*item| {
p.declareBinding(kind, &item.binding, opts) catch unreachable;
}
},
.b_object => |bind| {
for (bind.properties) |*prop| {
p.declareBinding(kind, &prop.value, opts) catch unreachable;
}
},
}
}
pub fn storeNameInRef(p: *P, name: string) !Ref {
if (comptime track_symbol_usage_during_parse_pass) {
if (p.parse_pass_symbol_uses.getPtr(name)) |res| {
res.used = true;
}
}
if (@intFromPtr(p.source.contents.ptr) <= @intFromPtr(name.ptr) and (@intFromPtr(name.ptr) + name.len) <= (@intFromPtr(p.source.contents.ptr) + p.source.contents.len)) {
return Ref.initSourceEnd(.{
.source_index = @intCast(@intFromPtr(name.ptr) - @intFromPtr(p.source.contents.ptr)),
.inner_index = @intCast(name.len),
.tag = .source_contents_slice,
});
} else {
const inner_index: u31 = @intCast(p.allocated_names.items.len);
try p.allocated_names.append(p.allocator, name);
return Ref.init(
inner_index,
p.source.index.get(),
false,
);
}
}
pub fn loadNameFromRef(p: *P, ref: Ref) string {
return switch (ref.tag) {
.symbol => p.symbols.items[ref.innerIndex()].original_name,
.source_contents_slice => p.source.contents[ref.sourceIndex() .. ref.sourceIndex() + ref.innerIndex()],
.allocated_name => p.allocated_names.items[ref.innerIndex()],
else => @panic("Internal error: JS parser tried to load an invalid name from a Ref"),
};
}
pub inline fn addImportRecord(p: *P, kind: ImportKind, loc: logger.Loc, name: string) u32 {
return p.addImportRecordByRange(kind, p.source.rangeOfString(loc), name);
}
pub fn addImportRecordByRange(p: *P, kind: ImportKind, range: logger.Range, name: string) u32 {
return p.addImportRecordByRangeAndPath(kind, range, fs.Path.init(name));
}
pub fn addImportRecordByRangeAndPath(p: *P, kind: ImportKind, range: logger.Range, path: fs.Path) u32 {
const index = p.import_records.items.len;
const record = ImportRecord{
.kind = kind,
.range = range,
.path = path,
};
p.import_records.append(record) catch unreachable;
return @as(u32, @intCast(index));
}
pub fn popScope(noalias p: *P) void {
const current_scope = p.current_scope;
// We cannot rename anything inside a scope containing a direct eval() call
if (current_scope.contains_direct_eval) {
var iter = current_scope.members.iterator();
while (iter.next()) |member| {
// Using direct eval when bundling is not a good idea in general because
// esbuild must assume that it can potentially reach anything in any of
// the containing scopes. We try to make it work but this isn't possible
// in some cases.
//
// For example, symbols imported using an ESM import are a live binding
// to the underlying symbol in another file. This is emulated during
// scope hoisting by erasing the ESM import and just referencing the
// underlying symbol in the flattened bundle directly. However, that
// symbol may have a different name which could break uses of direct
// eval:
//
// // Before bundling
// import { foo as bar } from './foo.js'
// console.log(eval('bar'))
//
// // After bundling
// let foo = 123 // The contents of "foo.js"
// console.log(eval('bar'))
//
// There really isn't any way to fix this. You can't just rename "foo" to
// "bar" in the example above because there may be a third bundled file
// that also contains direct eval and imports the same symbol with a
// different conflicting import alias. And there is no way to store a
// live binding to the underlying symbol in a variable with the import's
// name so that direct eval can access it:
//
// // After bundling
// let foo = 123 // The contents of "foo.js"
// const bar = /* cannot express a live binding to "foo" here */
// console.log(eval('bar'))
//
// Technically a "with" statement could potentially make this work (with
// a big hit to performance), but they are deprecated and are unavailable
// in strict mode. This is a non-starter since all ESM code is strict mode.
//
// So while we still try to obey the requirement that all symbol names are
// pinned when direct eval is present, we make an exception for top-level
// symbols in an ESM file when bundling is enabled. We make no guarantee
// that "eval" will be able to reach these symbols and we allow them to be
// renamed or removed by tree shaking.
// if (p.currentScope.parent == null and p.has_es_module_syntax) {
// continue;
// }
p.symbols.items[member.value_ptr.ref.innerIndex()].must_not_be_renamed = true;
}
}
p.current_scope = current_scope.parent orelse p.panic("Internal error: attempted to call popScope() on the topmost scope", .{});
}
pub fn markExprAsParenthesized(noalias _: *P, expr: *Expr) void {
switch (expr.data) {
.e_array => |ex| {
ex.is_parenthesized = true;
},
.e_object => |ex| {
ex.is_parenthesized = true;
},
else => {
return;
},
}
}
pub fn panic(p: *P, comptime fmt: string, args: anytype) noreturn {
@branchHint(.cold);
p.panicLoc(fmt, args, null);
}
pub fn panicLoc(p: *P, comptime fmt: string, args: anytype, loc: ?logger.Loc) noreturn {
const panic_buffer = p.allocator.alloc(u8, 32 * 1024) catch unreachable;
var panic_stream = std.Io.Writer.fixed(panic_buffer);
// panic during visit pass leaves the lexer at the end, which
// would make this location absolutely useless.
const location = loc orelse p.lexer.loc();
if (location.start < p.lexer.source.contents.len and !location.isEmpty()) {
p.log.addRangeErrorFmt(
p.source,
.{ .loc = location },
p.allocator,
"panic here",
.{},
) catch |err| bun.handleOom(err);
}
p.log.level = .verbose;
p.log.print(&panic_stream) catch unreachable;
Output.panic(fmt ++ "\n{s}", args ++ .{panic_stream.buffered()});
}
pub fn jsxStringsToMemberExpression(p: *P, loc: logger.Loc, parts: []const []const u8) !Expr {
const result = try p.findSymbol(loc, parts[0]);
const value = p.handleIdentifier(
loc,
E.Identifier{
.ref = result.ref,
.must_keep_due_to_with_stmt = result.is_inside_with_scope,
.can_be_removed_if_unused = true,
},
parts[0],
.{
.was_originally_identifier = true,
},
);
if (parts.len > 1) {
return p.memberExpression(loc, value, parts[1..]);
}
return value;
}
fn memberExpression(p: *P, loc: logger.Loc, initial_value: Expr, parts: []const []const u8) Expr {
var value = initial_value;
for (parts) |part| {
if (p.maybeRewritePropertyAccess(
loc,
value,
part,
loc,
.{
.is_call_target = false,
.assign_target = .none,
// .is_template_tag = false,
.is_delete_target = false,
},
)) |rewrote| {
value = rewrote;
} else {
value = p.newExpr(
E.Dot{
.target = value,
.name = part,
.name_loc = loc,
.can_be_removed_if_unused = p.options.features.dead_code_elimination,
},
loc,
);
}
}
return value;
}
pub fn willNeedBindingPattern(noalias p: *const P) bool {
return switch (p.lexer.token) {
// "[a] = b;"
.t_equals => true,
// "for ([a] in b) {}"
.t_in => !p.allow_in,
// "for ([a] of b) {}"
.t_identifier => !p.allow_in and p.lexer.isContextualKeyword("of"),
else => false,
};
}
pub fn appendPart(noalias p: *P, parts: *ListManaged(js_ast.Part), stmts: []Stmt) anyerror!void {
// Reuse the memory if possible
// This is reusable if the last part turned out to be dead
p.symbol_uses.clearRetainingCapacity();
p.declared_symbols.clearRetainingCapacity();
p.scopes_for_current_part.clearRetainingCapacity();
p.import_records_for_current_part.clearRetainingCapacity();
p.import_symbol_property_uses.clearRetainingCapacity();
p.had_commonjs_named_exports_this_visit = false;
const allocator = p.allocator;
var opts = PrependTempRefsOpts{};
var partStmts = ListManaged(Stmt).fromOwnedSlice(allocator, stmts);
try p.visitStmtsAndPrependTempRefs(&partStmts, &opts);
// Insert any relocated variable statements now
if (p.relocated_top_level_vars.items.len > 0) {
var already_declared = RefMap{};
var already_declared_allocator_stack = std.heap.stackFallback(1024, allocator);
const already_declared_allocator = already_declared_allocator_stack.get();
defer if (already_declared_allocator_stack.fixed_buffer_allocator.end_index >= 1023) already_declared.deinit(already_declared_allocator);
for (p.relocated_top_level_vars.items) |*local| {
// Follow links because "var" declarations may be merged due to hoisting
while (local.ref != null) {
var symbol = &p.symbols.items[local.ref.?.innerIndex()];
if (!symbol.hasLink()) {
break;
}
local.ref = symbol.link;
}
const ref = local.ref orelse continue;
const declaration_entry = try already_declared.getOrPut(already_declared_allocator, ref);
if (!declaration_entry.found_existing) {
const decls = try allocator.alloc(G.Decl, 1);
decls[0] = Decl{
.binding = p.b(B.Identifier{ .ref = ref }, local.loc),
};
try partStmts.append(p.s(
S.Local{ .decls = G.Decl.List.fromOwnedSlice(decls) },
local.loc,
));
try p.declared_symbols.append(p.allocator, .{ .ref = ref, .is_top_level = true });
}
}
p.relocated_top_level_vars.clearRetainingCapacity();
}
if (partStmts.items.len > 0) {
const final_stmts = partStmts.items;
try parts.append(js_ast.Part{
.stmts = final_stmts,
.symbol_uses = p.symbol_uses,
.import_symbol_property_uses = p.import_symbol_property_uses,
.declared_symbols = p.declared_symbols.toOwnedSlice(),
.import_record_indices = bun.BabyList(u32).fromOwnedSlice(
p.import_records_for_current_part.toOwnedSlice(
p.allocator,
) catch unreachable,
),
.scopes = try p.scopes_for_current_part.toOwnedSlice(p.allocator),
.can_be_removed_if_unused = p.stmtsCanBeRemovedIfUnused(final_stmts),
.tag = if (p.had_commonjs_named_exports_this_visit) js_ast.Part.Tag.commonjs_named_export else .none,
});
p.symbol_uses = .{};
p.import_symbol_property_uses = .{};
p.had_commonjs_named_exports_this_visit = false;
} else if (p.declared_symbols.len() > 0 or p.symbol_uses.count() > 0) {
// if the part is dead, invalidate all the usage counts
p.clearSymbolUsagesFromDeadPart(&.{ .stmts = undefined, .declared_symbols = p.declared_symbols, .symbol_uses = p.symbol_uses });
p.declared_symbols.clearRetainingCapacity();
p.import_records_for_current_part.clearRetainingCapacity();
}
}
fn bindingCanBeRemovedIfUnused(p: *P, binding: Binding) bool {
if (!p.options.features.dead_code_elimination) return false;
return bindingCanBeRemovedIfUnusedWithoutDCECheck(p, binding);
}
fn bindingCanBeRemovedIfUnusedWithoutDCECheck(p: *P, binding: Binding) bool {
switch (binding.data) {
.b_array => |bi| {
for (bi.items) |*item| {
if (!p.bindingCanBeRemovedIfUnusedWithoutDCECheck(item.binding)) {
return false;
}
if (item.default_value) |*default| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(default)) {
return false;
}
}
}
},
.b_object => |bi| {
for (bi.properties) |*property| {
if (!property.flags.contains(.is_spread) and !p.exprCanBeRemovedIfUnusedWithoutDCECheck(&property.key)) {
return false;
}
if (!p.bindingCanBeRemovedIfUnusedWithoutDCECheck(property.value)) {
return false;
}
if (property.default_value) |*default| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(default)) {
return false;
}
}
}
},
else => {},
}
return true;
}
fn stmtsCanBeRemovedIfUnused(p: *P, stmts: []Stmt) bool {
if (!p.options.features.dead_code_elimination) return false;
return stmtsCanBeRemovedifUnusedWithoutDCECheck(p, stmts);
}
fn stmtsCanBeRemovedifUnusedWithoutDCECheck(p: *P, stmts: []Stmt) bool {
for (stmts) |stmt| {
switch (stmt.data) {
// These never have side effects
.s_function, .s_empty => {},
// Let these be removed if they are unused. Note that we also need to
// check if the imported file is marked as "sideEffects: false" before we
// can remove a SImport statement. Otherwise the import must be kept for
// its side effects.
.s_import => {},
.s_class => |st| {
if (!p.classCanBeRemovedIfUnused(&st.class)) {
return false;
}
},
.s_expr => |st| {
if (st.does_not_affect_tree_shaking) {
// Expressions marked with this are automatically generated and have
// no side effects by construction.
continue;
}
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(&st.value)) {
return false;
}
},
.s_local => |st| {
// "await" is a side effect because it affects code timing
if (st.kind == .k_await_using) return false;
for (st.decls.slice()) |*decl| {
if (!p.bindingCanBeRemovedIfUnusedWithoutDCECheck(decl.binding)) {
return false;
}
if (decl.value) |*decl_value| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(decl_value)) {
return false;
} else if (st.kind == .k_using) {
// "using" declarations are only side-effect free if they are initialized to null or undefined
if (decl_value.data != .e_null and decl_value.data != .e_undefined) {
return false;
}
}
}
}
},
.s_try => |try_| {
if (!p.stmtsCanBeRemovedifUnusedWithoutDCECheck(try_.body) or (try_.finally != null and !p.stmtsCanBeRemovedifUnusedWithoutDCECheck(try_.finally.?.stmts))) {
return false;
}
},
// Exports are tracked separately, so this isn't necessary
.s_export_clause, .s_export_from => {},
.s_export_default => |st| {
switch (st.value) {
.stmt => |s2| {
switch (s2.data) {
.s_expr => |s_expr| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(&s_expr.value)) {
return false;
}
},
// These never have side effects
.s_function => {},
.s_class => {
if (!p.classCanBeRemovedIfUnused(&s2.data.s_class.class)) {
return false;
}
},
else => {
Output.panic("Unexpected type in export default", .{});
},
}
},
.expr => |*exp| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(exp)) {
return false;
}
},
}
},
else => {
// Assume that all statements not explicitly special-cased here have side
// effects, and cannot be removed even if unused
return false;
},
}
}
return true;
}
pub fn deoptimizeCommonJSNamedExports(p: *P) void {
// exists for debugging
p.commonjs_named_exports_deoptimized = true;
}
pub fn maybeKeepExprSymbolName(p: *P, expr: Expr, original_name: string, was_anonymous_named_expr: bool) Expr {
return if (was_anonymous_named_expr) p.keepExprSymbolName(expr, original_name) else expr;
}
pub fn valueForThis(p: *P, loc: logger.Loc) ?Expr {
// Substitute "this" if we're inside a static class property initializer
if (p.fn_only_data_visit.should_replace_this_with_class_name_ref) {
if (p.fn_only_data_visit.class_name_ref) |ref| {
p.recordUsage(ref.*);
return p.newExpr(E.Identifier{ .ref = ref.* }, loc);
}
}
// oroigianlly was !=- modepassthrough
if (!p.fn_only_data_visit.is_this_nested) {
if (p.has_es_module_syntax and p.commonjs_named_exports.count() == 0) {
// In an ES6 module, "this" is supposed to be undefined. Instead of
// doing this at runtime using "fn.call(undefined)", we do it at
// compile time using expression substitution here.
return Expr{ .loc = loc, .data = nullValueExpr };
} else {
// In a CommonJS module, "this" is supposed to be the same as "exports".
// Instead of doing this at runtime using "fn.call(module.exports)", we
// do it at compile time using expression substitution here.
p.recordUsage(p.exports_ref);
p.deoptimizeCommonJSNamedExports();
return p.newExpr(E.Identifier{ .ref = p.exports_ref }, loc);
}
}
return null;
}
pub fn isValidAssignmentTarget(p: *P, expr: Expr) bool {
return switch (expr.data) {
.e_identifier => |ident| !isEvalOrArguments(p.loadNameFromRef(ident.ref)),
.e_dot => |e| e.optional_chain == null,
.e_index => |e| e.optional_chain == null,
.e_array => |e| !e.is_parenthesized,
.e_object => |e| !e.is_parenthesized,
else => false,
};
}
/// This is only allowed to be called if allow_runtime is true
/// If --target=bun, this does nothing.
pub fn recordUsageOfRuntimeRequire(p: *P) void {
// target bun does not have __require
if (p.options.features.auto_polyfill_require) {
bun.assert(p.options.features.allow_runtime);
p.ensureRequireSymbol();
p.recordUsage(p.runtimeIdentifierRef(logger.Loc.Empty, "__require"));
}
}
pub fn ignoreUsageOfRuntimeRequire(p: *P) void {
if (p.options.features.auto_polyfill_require) {
bun.assert(p.runtime_imports.__require != null);
p.ignoreUsage(p.runtimeIdentifierRef(logger.Loc.Empty, "__require"));
p.symbols.items[p.require_ref.innerIndex()].use_count_estimate -|= 1;
}
}
pub inline fn valueForRequire(p: *P, loc: logger.Loc) Expr {
bun.assert(!p.isSourceRuntime());
return Expr{
.data = .{
.e_require_call_target = {},
},
.loc = loc,
};
}
pub inline fn valueForImportMetaMain(p: *P, inverted: bool, loc: logger.Loc) Expr {
if (p.options.import_meta_main_value) |known| {
return .{ .loc = loc, .data = .{ .e_boolean = .{ .value = if (inverted) !known else known } } };
} else {
// Node.js does not have import.meta.main, so we end up lowering
// this to `require.main === module`, but with the ESM format,
// both `require` and `module` are not present, so the code
// generation we need is:
//
// import { createRequire } from "node:module";
// var __require = createRequire(import.meta.url);
// var import_meta_main = __require.main === __require.module;
//
// The printer can handle this for us, but we need to reference
// a handle to the `__require` function.
if (p.options.lower_import_meta_main_for_node_js) {
p.recordUsageOfRuntimeRequire();
}
return .{
.loc = loc,
.data = .{ .e_import_meta_main = .{ .inverted = inverted } },
};
}
}
pub fn keepExprSymbolName(_: *P, _value: Expr, _: string) Expr {
return _value;
// var start = p.expr_list.items.len;
// p.expr_list.ensureUnusedCapacity(2) catch unreachable;
// p.expr_list.appendAssumeCapacity(_value);
// p.expr_list.appendAssumeCapacity(p.newExpr(E.String{
// .utf8 = name,
// }, _value.loc));
// var value = p.callRuntime(_value.loc, "", p.expr_list.items[start..p.expr_list.items.len]);
// // Make sure tree shaking removes this if the function is never used
// value.getCall().can_be_unwrapped_if_unused = true;
// return value;
}
pub fn isSimpleParameterList(args: []G.Arg, has_rest_arg: bool) bool {
if (has_rest_arg) {
return false;
}
for (args) |arg| {
if (@as(Binding.Tag, arg.binding.data) != .b_identifier or arg.default != null) {
return false;
}
}
return true;
}
// This one is never called in places that haven't already checked if DCE is enabled.
pub fn classCanBeRemovedIfUnused(p: *P, class: *G.Class) bool {
if (class.extends) |*extends| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(extends)) {
return false;
}
}
for (class.properties) |*property| {
if (property.kind == .class_static_block) {
if (!p.stmtsCanBeRemovedifUnusedWithoutDCECheck(property.class_static_block.?.stmts.slice())) {
return false;
}
continue;
}
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(&(property.key orelse unreachable))) {
return false;
}
if (property.value) |*val| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(val)) {
return false;
}
}
if (property.initializer) |*val| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(val)) {
return false;
}
}
}
return true;
}
// TODO:
// When React Fast Refresh is enabled, anything that's a JSX component should not be removable
// This is to improve the reliability of fast refresh between page loads.
pub fn exprCanBeRemovedIfUnused(p: *P, expr: *const Expr) bool {
if (!p.options.features.dead_code_elimination) return false;
return exprCanBeRemovedIfUnusedWithoutDCECheck(p, expr);
}
fn exprCanBeRemovedIfUnusedWithoutDCECheck(p: *P, expr: *const Expr) bool {
switch (expr.data) {
.e_null,
.e_undefined,
.e_missing,
.e_boolean,
.e_branch_boolean,
.e_number,
.e_big_int,
.e_string,
.e_this,
.e_reg_exp,
.e_function,
.e_arrow,
.e_import_meta,
=> {
return true;
},
.e_inlined_enum => |e| return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&e.value),
.e_dot => |ex| {
return ex.can_be_removed_if_unused;
},
.e_class => |ex| {
return p.classCanBeRemovedIfUnused(ex);
},
.e_identifier => |ex| {
bun.assert(!ex.ref.isSourceContentsSlice()); // was not visited
if (ex.must_keep_due_to_with_stmt) {
return false;
}
// Unbound identifiers cannot be removed because they can have side effects.
// One possible side effect is throwing a ReferenceError if they don't exist.
// Another one is a getter with side effects on the global object:
//
// Object.defineProperty(globalThis, 'x', {
// get() {
// sideEffect();
// },
// });
//
// Be very careful about this possibility. It's tempting to treat all
// identifier expressions as not having side effects but that's wrong. We
// must make sure they have been declared by the code we are currently
// compiling before we can tell that they have no side effects.
//
// Note that we currently ignore ReferenceErrors due to TDZ access. This is
// incorrect but proper TDZ analysis is very complicated and would have to
// be very conservative, which would inhibit a lot of optimizations of code
// inside closures. This may need to be revisited if it proves problematic.
if (ex.can_be_removed_if_unused or p.symbols.items[ex.ref.innerIndex()].kind != .unbound) {
return true;
}
},
.e_commonjs_export_identifier, .e_import_identifier => {
// References to an ES6 import item are always side-effect free in an
// ECMAScript environment.
//
// They could technically have side effects if the imported module is a
// CommonJS module and the import item was translated to a property access
// (which esbuild's bundler does) and the property has a getter with side
// effects.
//
// But this is very unlikely and respecting this edge case would mean
// disabling tree shaking of all code that references an export from a
// CommonJS module. It would also likely violate the expectations of some
// developers because the code *looks* like it should be able to be tree
// shaken.
//
// So we deliberately ignore this edge case and always treat import item
// references as being side-effect free.
return true;
},
.e_if => |ex| {
return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.test_) and
(p.isSideEffectFreeUnboundIdentifierRef(
ex.yes,
ex.test_,
true,
) or
p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.yes)) and
(p.isSideEffectFreeUnboundIdentifierRef(
ex.no,
ex.test_,
false,
) or p.exprCanBeRemovedIfUnusedWithoutDCECheck(
&ex.no,
));
},
.e_array => |ex| {
for (ex.items.slice()) |*item| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(item)) {
return false;
}
}
return true;
},
.e_object => |ex| {
for (ex.properties.slice()) |*property| {
// The key must still be evaluated if it's computed or a spread
if (property.kind == .spread or (property.flags.contains(.is_computed) and !property.key.?.isPrimitiveLiteral()) or property.flags.contains(.is_spread)) {
return false;
}
if (property.value) |*val| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(val)) {
return false;
}
}
}
return true;
},
.e_call => |ex| {
// A call that has been marked "__PURE__" can be removed if all arguments
// can be removed. The annotation causes us to ignore the target.
if (ex.can_be_unwrapped_if_unused != .never) {
for (ex.args.slice()) |*arg| {
if (!(p.exprCanBeRemovedIfUnusedWithoutDCECheck(arg) or (ex.can_be_unwrapped_if_unused == .if_unused_and_toString_safe and arg.data.isSafeToString()))) {
return false;
}
}
return true;
}
},
.e_new => |ex| {
// A call that has been marked "__PURE__" can be removed if all arguments
// can be removed. The annotation causes us to ignore the target.
if (ex.can_be_unwrapped_if_unused != .never) {
for (ex.args.slice()) |*arg| {
if (!(p.exprCanBeRemovedIfUnusedWithoutDCECheck(arg) or (ex.can_be_unwrapped_if_unused == .if_unused_and_toString_safe and arg.data.isSafeToString()))) {
return false;
}
}
return true;
}
},
.e_unary => |ex| {
switch (ex.op) {
// These operators must not have any type conversions that can execute code
// such as "toString" or "valueOf". They must also never throw any exceptions.
.un_void, .un_not => {
return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.value);
},
// The "typeof" operator doesn't do any type conversions so it can be removed
// if the result is unused and the operand has no side effects. However, it
// has a special case where if the operand is an identifier expression such
// as "typeof x" and "x" doesn't exist, no reference error is thrown so the
// operation has no side effects.
//
// Note that there *is* actually a case where "typeof x" can throw an error:
// when "x" is being referenced inside of its TDZ (temporal dead zone). TDZ
// checks are not yet handled correctly by bun or esbuild, so this possibility is
// currently ignored.
.un_typeof => {
if (ex.value.data == .e_identifier and ex.flags.was_originally_typeof_identifier) {
return true;
}
return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.value);
},
else => {},
}
},
.e_binary => |ex| {
switch (ex.op) {
// These operators must not have any type conversions that can execute code
// such as "toString" or "valueOf". They must also never throw any exceptions.
.bin_strict_eq,
.bin_strict_ne,
.bin_comma,
.bin_nullish_coalescing,
=> return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.left) and p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.right),
// Special-case "||" to make sure "typeof x === 'undefined' || x" can be removed
.bin_logical_or => return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.left) and
(p.isSideEffectFreeUnboundIdentifierRef(ex.right, ex.left, false) or p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.right)),
// Special-case "&&" to make sure "typeof x !== 'undefined' && x" can be removed
.bin_logical_and => return p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.left) and
(p.isSideEffectFreeUnboundIdentifierRef(ex.right, ex.left, true) or p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.right)),
// For "==" and "!=", pretend the operator was actually "===" or "!==". If
// we know that we can convert it to "==" or "!=", then we can consider the
// operator itself to have no side effects. This matters because our mangle
// logic will convert "typeof x === 'object'" into "typeof x == 'object'"
// and since "typeof x === 'object'" is considered to be side-effect free,
// we must also consider "typeof x == 'object'" to be side-effect free.
.bin_loose_eq, .bin_loose_ne => return SideEffects.canChangeStrictToLoose(
ex.left.data,
ex.right.data,
) and
p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.left) and p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.right),
// Special-case "<" and ">" with string, number, or bigint arguments
.bin_lt, .bin_gt, .bin_le, .bin_ge => {
const left = ex.left.knownPrimitive();
const right = ex.right.knownPrimitive();
switch (left) {
.string, .number, .bigint => {
return right == left and p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.left) and p.exprCanBeRemovedIfUnusedWithoutDCECheck(&ex.right);
},
else => {},
}
},
else => {},
}
},
.e_template => |templ| {
if (templ.tag == null) {
for (templ.parts) |part| {
if (!p.exprCanBeRemovedIfUnusedWithoutDCECheck(&part.value) or part.value.knownPrimitive() == .unknown) {
return false;
}
}
return true;
}
},
else => {},
}
return false;
}
// // This is based on exprCanBeRemoved
// // The main difference: identifiers, functions, arrow functions cause it to return false
// pub fn exprCanBeHoistedForJSX(p: *P, expr: *const Expr) bool {
// if (comptime jsx_transform_type != .react) {
// unreachable;
// }
// switch (expr.data) {
// .e_null,
// .e_undefined,
// .e_missing,
// .e_boolean,
// .e_number,
// .e_big_int,
// .e_string,
// .e_reg_exp,
// => {
// return true;
// },
// .e_dot => |ex| {
// return ex.can_be_removed_if_unused;
// },
// .e_import_identifier => {
// // References to an ES6 import item are always side-effect free in an
// // ECMAScript environment.
// //
// // They could technically have side effects if the imported module is a
// // CommonJS module and the import item was translated to a property access
// // (which esbuild's bundler does) and the property has a getter with side
// // effects.
// //
// // But this is very unlikely and respecting this edge case would mean
// // disabling tree shaking of all code that references an export from a
// // CommonJS module. It would also likely violate the expectations of some
// // developers because the code *looks* like it should be able to be tree
// // shaken.
// //
// // So we deliberately ignore this edge case and always treat import item
// // references as being side-effect free.
// return true;
// },
// .e_if => |ex| {
// return p.exprCanBeHoistedForJSX(&ex.test_) and
// (p.isSideEffectFreeUnboundIdentifierRef(
// ex.yes,
// ex.test_,
// true,
// ) or
// p.exprCanBeHoistedForJSX(&ex.yes)) and
// (p.isSideEffectFreeUnboundIdentifierRef(
// ex.no,
// ex.test_,
// false,
// ) or p.exprCanBeHoistedForJSX(
// &ex.no,
// ));
// },
// .e_array => |ex| {
// for (ex.items.slice()) |*item| {
// if (!p.exprCanBeHoistedForJSX(item)) {
// return false;
// }
// }
// return true;
// },
// .e_object => |ex| {
// // macros disable this because macros get inlined
// // so it's sort of the opposite of the purpose of this function
// if (ex.was_originally_macro)
// return false;
// for (ex.properties.slice()) |*property| {
// // The key must still be evaluated if it's computed or a spread
// if (property.kind == .spread or property.flags.contains(.is_computed) or property.flags.contains(.is_spread)) {
// return false;
// }
// if (property.value) |*val| {
// if (!p.exprCanBeHoistedForJSX(val)) {
// return false;
// }
// }
// }
// return true;
// },
// .e_call => |ex| {
// // A call that has been marked "__PURE__" can be removed if all arguments
// // can be removed. The annotation causes us to ignore the target.
// if (ex.can_be_unwrapped_if_unused) {
// for (ex.args.slice()) |*arg| {
// if (!p.exprCanBeHoistedForJSX(arg)) {
// return false;
// }
// }
// return true;
// }
// },
// .e_new => |ex| {
// // A call that has been marked "__PURE__" can be removed if all arguments
// // can be removed. The annotation causes us to ignore the target.
// if (ex.can_be_unwrapped_if_unused) {
// for (ex.args.slice()) |*arg| {
// if (!p.exprCanBeHoistedForJSX(arg)) {
// return false;
// }
// }
// return true;
// }
// },
// .e_unary => |ex| {
// switch (ex.op) {
// // These operators must not have any type conversions that can execute code
// // such as "toString" or "valueOf". They must also never throw any exceptions.
// .un_void, .un_not => {
// return p.exprCanBeHoistedForJSX(&ex.value);
// },
// // The "typeof" operator doesn't do any type conversions so it can be removed
// // if the result is unused and the operand has no side effects. However, it
// // has a special case where if the operand is an identifier expression such
// // as "typeof x" and "x" doesn't exist, no reference error is thrown so the
// // operation has no side effects.
// //
// // Note that there *is* actually a case where "typeof x" can throw an error:
// // when "x" is being referenced inside of its TDZ (temporal dead zone). TDZ
// // checks are not yet handled correctly by bun or esbuild, so this possibility is
// // currently ignored.
// .un_typeof => {
// if (ex.value.data == .e_identifier) {
// return true;
// }
// return p.exprCanBeHoistedForJSX(&ex.value);
// },
// else => {},
// }
// },
// .e_binary => |ex| {
// switch (ex.op) {
// // These operators must not have any type conversions that can execute code
// // such as "toString" or "valueOf". They must also never throw any exceptions.
// .bin_strict_eq,
// .bin_strict_ne,
// .bin_comma,
// .bin_nullish_coalescing,
// => return p.exprCanBeHoistedForJSX(&ex.left) and p.exprCanBeHoistedForJSX(&ex.right),
// // Special-case "||" to make sure "typeof x === 'undefined' || x" can be removed
// .bin_logical_or => return p.exprCanBeHoistedForJSX(&ex.left) and
// (p.isSideEffectFreeUnboundIdentifierRef(ex.right, ex.left, false) or p.exprCanBeHoistedForJSX(&ex.right)),
// // Special-case "&&" to make sure "typeof x !== 'undefined' && x" can be removed
// .bin_logical_and => return p.exprCanBeHoistedForJSX(&ex.left) and
// (p.isSideEffectFreeUnboundIdentifierRef(ex.right, ex.left, true) or p.exprCanBeHoistedForJSX(&ex.right)),
// // For "==" and "!=", pretend the operator was actually "===" or "!==". If
// // we know that we can convert it to "==" or "!=", then we can consider the
// // operator itself to have no side effects. This matters because our mangle
// // logic will convert "typeof x === 'object'" into "typeof x == 'object'"
// // and since "typeof x === 'object'" is considered to be side-effect free,
// // we must also consider "typeof x == 'object'" to be side-effect free.
// .bin_loose_eq, .bin_loose_ne => return SideEffects.canChangeStrictToLoose(
// ex.left.data,
// ex.right.data,
// ) and
// p.exprCanBeHoistedForJSX(&ex.left) and p.exprCanBeHoistedForJSX(&ex.right),
// else => {},
// }
// },
// .e_template => |templ| {
// if (templ.tag == null) {
// for (templ.parts) |part| {
// if (!p.exprCanBeHoistedForJSX(&part.value) or part.value.knownPrimitive() == .unknown) {
// return false;
// }
// }
// }
// return true;
// },
// else => {},
// // These may reference variables from an upper scope
// // it's possible to detect that, but we are cutting scope for now
// // .e_function,
// // .e_arrow,
// // .e_this,
// }
// return false;
// }
fn isSideEffectFreeUnboundIdentifierRef(p: *P, value: Expr, guard_condition: Expr, is_yes_branch_: bool) bool {
if (value.data != .e_identifier or
p.symbols.items[value.data.e_identifier.ref.innerIndex()].kind != .unbound or
guard_condition.data != .e_binary)
return false;
const binary = guard_condition.data.e_binary.*;
var is_yes_branch = is_yes_branch_;
switch (binary.op) {
.bin_strict_eq, .bin_strict_ne, .bin_loose_eq, .bin_loose_ne => {
// typeof x !== 'undefined'
var typeof: Expr.Data = binary.left.data;
var compare: Expr.Data = binary.right.data;
// typeof 'undefined' !== x
if (typeof == .e_string) {
typeof = binary.right.data;
compare = binary.left.data;
}
// this order because Expr.Data Tag is not a pointer
// so it should be slightly faster to compare
if (compare != .e_string or
typeof != .e_unary)
return false;
const unary = typeof.e_unary.*;
if (unary.op != .un_typeof or unary.value.data != .e_identifier)
return false;
const id = value.data.e_identifier.ref;
const id2 = unary.value.data.e_identifier.ref;
return ((compare.e_string.eqlComptime("undefined") == is_yes_branch) ==
(binary.op == .bin_strict_ne or binary.op == .bin_loose_ne)) and
id.eql(id2);
},
.bin_lt, .bin_gt, .bin_le, .bin_ge => {
// Pattern match for "typeof x < <string>"
var typeof: Expr.Data = binary.left.data;
var str: Expr.Data = binary.right.data;
// Check if order is flipped: 'u' >= typeof x
if (typeof == .e_string) {
typeof = binary.right.data;
str = binary.left.data;
is_yes_branch = !is_yes_branch;
}
if (typeof == .e_unary and str == .e_string) {
const unary = typeof.e_unary.*;
if (unary.op == .un_typeof and
unary.value.data == .e_identifier and
unary.flags.was_originally_typeof_identifier and
str.e_string.eqlComptime("u"))
{
// In "typeof x < 'u' ? x : null", the reference to "x" is side-effect free
// In "typeof x > 'u' ? x : null", the reference to "x" is side-effect free
if (is_yes_branch == (binary.op == .bin_lt or binary.op == .bin_le)) {
const id = value.data.e_identifier.ref;
const id2 = unary.value.data.e_identifier.ref;
if (id.eql(id2)) {
return true;
}
}
}
}
return false;
},
else => return false,
}
}
pub fn jsxImportAutomatic(p: *P, loc: logger.Loc, is_static: bool) Expr {
return p.jsxImport(
if (is_static and !p.options.jsx.development and FeatureFlags.support_jsxs_in_jsx_transform)
.jsxs
else if (p.options.jsx.development)
.jsxDEV
else
.jsx,
loc,
);
}
pub fn jsxImport(p: *P, kind: JSXImport, loc: logger.Loc) Expr {
switch (kind) {
inline else => |field| {
const ref: Ref = brk: {
if (p.jsx_imports.getWithTag(kind) == null) {
const symbol_name = @tagName(field);
const loc_ref = LocRef{
.loc = loc,
.ref = (p.declareGeneratedSymbol(.other, symbol_name) catch unreachable),
};
bun.handleOom(p.module_scope.generated.append(p.allocator, loc_ref.ref.?));
p.is_import_item.put(p.allocator, loc_ref.ref.?, {}) catch unreachable;
@field(p.jsx_imports, @tagName(field)) = loc_ref;
break :brk loc_ref.ref.?;
}
break :brk p.jsx_imports.getWithTag(kind).?;
};
p.recordUsage(ref);
return p.handleIdentifier(
loc,
E.Identifier{
.ref = ref,
.can_be_removed_if_unused = true,
.call_can_be_unwrapped_if_unused = true,
},
null,
.{
.was_originally_identifier = true,
},
);
},
}
}
pub fn selectLocalKind(p: *P, kind: S.Local.Kind) S.Local.Kind {
// Use "var" instead of "let" and "const" if the variable declaration may
// need to be separated from the initializer. This allows us to safely move
// this declaration into a nested scope.
if ((p.options.bundle or p.will_wrap_module_in_try_catch_for_using) and
p.current_scope.parent == null and !kind.isUsing())
{
return .k_var;
}
// Optimization: use "let" instead of "const" because it's shorter. This is
// only done when bundling because assigning to "const" is only an error when bundling.
if (p.options.bundle and kind == .k_const and p.options.features.minify_syntax) {
return .k_let;
}
return kind;
}
pub fn ignoreUsage(p: *P, ref: Ref) void {
if (!p.is_control_flow_dead and !p.is_revisit_for_substitution) {
if (comptime Environment.allow_assert) assert(@as(usize, ref.innerIndex()) < p.symbols.items.len);
p.symbols.items[ref.innerIndex()].use_count_estimate -|= 1;
var use = p.symbol_uses.get(ref) orelse return;
use.count_estimate -|= 1;
if (use.count_estimate == 0) {
_ = p.symbol_uses.swapRemove(ref);
} else {
p.symbol_uses.putAssumeCapacity(ref, use);
}
}
// Don't roll back the "tsUseCounts" increment. This must be counted even if
// the value is ignored because that's what the TypeScript compiler does.
}
pub fn ignoreUsageOfIdentifierInDotChain(p: *P, expr: Expr) void {
var current = expr;
while (true) {
switch (current.data) {
.e_identifier => |id| {
p.ignoreUsage(id.ref);
},
.e_dot => |dot| {
current = dot.target;
continue;
},
.e_index => |index| {
if (index.index.isString()) {
current = index.target;
continue;
}
},
else => return,
}
return;
}
}
pub fn isExportToEliminate(p: *P, ref: Ref) bool {
const symbol_name = p.loadNameFromRef(ref);
return p.options.features.replace_exports.contains(symbol_name);
}
pub fn injectReplacementExport(p: *P, stmts: *StmtList, name_ref: Ref, loc: logger.Loc, replacement: *const RuntimeFeatures.ReplaceableExport) bool {
switch (replacement.*) {
.delete => return false,
.replace => |value| {
const count = stmts.items.len;
var decls = p.allocator.alloc(G.Decl, 1) catch unreachable;
decls[0] = .{ .binding = p.b(B.Identifier{ .ref = name_ref }, loc), .value = value };
var local = p.s(
S.Local{
.is_export = true,
.decls = Decl.List.fromOwnedSlice(decls),
},
loc,
);
p.visitAndAppendStmt(stmts, &local) catch unreachable;
return count != stmts.items.len;
},
.inject => |with| {
const count = stmts.items.len;
var decls = p.allocator.alloc(G.Decl, 1) catch unreachable;
decls[0] = .{
.binding = p.b(
B.Identifier{ .ref = p.declareSymbol(.other, loc, with.name) catch unreachable },
loc,
),
.value = with.value,
};
var local = p.s(
S.Local{
.is_export = true,
.decls = Decl.List.fromOwnedSlice(decls),
},
loc,
);
p.visitAndAppendStmt(stmts, &local) catch unreachable;
return count != stmts.items.len;
},
}
}
pub fn replaceDeclAndPossiblyRemove(p: *P, decl: *G.Decl, replacement: *const RuntimeFeatures.ReplaceableExport) bool {
switch (replacement.*) {
.delete => return false,
.replace => |value| {
decl.*.value = p.visitExpr(value);
return true;
},
.inject => |with| {
decl.* = .{
.binding = p.b(
B.Identifier{ .ref = p.declareSymbol(.other, decl.binding.loc, with.name) catch unreachable },
decl.binding.loc,
),
.value = p.visitExpr(Expr{ .data = with.value.data, .loc = if (decl.value != null) decl.value.?.loc else decl.binding.loc }),
};
return true;
},
}
}
pub fn markExportedDeclsInsideNamespace(p: *P, ns_ref: Ref, decls: []G.Decl) void {
for (decls) |decl| {
p.markExportedBindingInsideNamespace(ns_ref, decl.binding);
}
}
pub fn appendIfBodyPreservingScope(noalias p: *P, stmts: *ListManaged(Stmt), body: Stmt) anyerror!void {
switch (body.data) {
.s_block => |block| {
var keep_block = false;
for (block.stmts) |stmt| {
if (statementCaresAboutScope(stmt)) {
keep_block = true;
break;
}
}
if (!keep_block and block.stmts.len > 0) {
try stmts.appendSlice(block.stmts);
return;
}
},
else => {},
}
if (statementCaresAboutScope(body)) {
var block_stmts = try p.allocator.alloc(Stmt, 1);
block_stmts[0] = body;
try stmts.append(p.s(S.Block{ .stmts = block_stmts }, body.loc));
return;
}
try stmts.append(body);
return;
}
fn markExportedBindingInsideNamespace(p: *P, ref: Ref, binding: BindingNodeIndex) void {
switch (binding.data) {
.b_missing => {},
.b_identifier => |ident| {
p.is_exported_inside_namespace.put(p.allocator, ident.ref, ref) catch unreachable;
},
.b_array => |array| {
for (array.items) |item| {
p.markExportedBindingInsideNamespace(ref, item.binding);
}
},
.b_object => |obj| {
for (obj.properties) |item| {
p.markExportedBindingInsideNamespace(ref, item.value);
}
},
}
}
pub fn generateClosureForTypeScriptNamespaceOrEnum(
noalias p: *P,
noalias stmts: *ListManaged(Stmt),
stmt_loc: logger.Loc,
is_export: bool,
name_loc: logger.Loc,
original_name_ref: Ref,
arg_ref: Ref,
stmts_inside_closure: []Stmt,
all_values_are_pure: bool,
) anyerror!void {
var name_ref = original_name_ref;
// Follow the link chain in case symbols were merged
var symbol: Symbol = p.symbols.items[name_ref.innerIndex()];
while (symbol.hasLink()) {
const link = symbol.link;
name_ref = link;
symbol = p.symbols.items[name_ref.innerIndex()];
}
const allocator = p.allocator;
// Make sure to only emit a variable once for a given namespace, since there
// can be multiple namespace blocks for the same namespace
if ((symbol.kind == .ts_namespace or symbol.kind == .ts_enum) and
!p.emitted_namespace_vars.contains(name_ref))
{
bun.handleOom(p.emitted_namespace_vars.putNoClobber(allocator, name_ref, {}));
var decls = bun.handleOom(allocator.alloc(G.Decl, 1));
decls[0] = G.Decl{ .binding = p.b(B.Identifier{ .ref = name_ref }, name_loc) };
if (p.enclosing_namespace_arg_ref == null) {
// Top-level namespace: "var"
stmts.append(
p.s(S.Local{
.kind = .k_var,
.decls = G.Decl.List.fromOwnedSlice(decls),
.is_export = is_export,
}, stmt_loc),
) catch |err| bun.handleOom(err);
} else {
// Nested namespace: "let"
stmts.append(
p.s(S.Local{
.kind = .k_let,
.decls = G.Decl.List.fromOwnedSlice(decls),
}, stmt_loc),
) catch |err| bun.handleOom(err);
}
}
const arg_expr: Expr = arg_expr: {
// TODO: unsupportedJSFeatures.has(.logical_assignment)
// If the "||=" operator is supported, our minified output can be slightly smaller
if (is_export) if (p.enclosing_namespace_arg_ref) |namespace| {
const name = p.symbols.items[name_ref.innerIndex()].original_name;
// "name = (enclosing.name ||= {})"
p.recordUsage(namespace);
p.recordUsage(name_ref);
break :arg_expr Expr.assign(
Expr.initIdentifier(name_ref, name_loc),
p.newExpr(E.Binary{
.op = .bin_logical_or_assign,
.left = p.newExpr(
E.Dot{
.target = Expr.initIdentifier(namespace, name_loc),
.name = name,
.name_loc = name_loc,
},
name_loc,
),
.right = p.newExpr(E.Object{}, name_loc),
}, name_loc),
);
};
// "name ||= {}"
p.recordUsage(name_ref);
break :arg_expr p.newExpr(E.Binary{
.op = .bin_logical_or_assign,
.left = Expr.initIdentifier(name_ref, name_loc),
.right = p.newExpr(E.Object{}, name_loc),
}, name_loc);
};
var func_args = bun.handleOom(allocator.alloc(G.Arg, 1));
func_args[0] = .{ .binding = p.b(B.Identifier{ .ref = arg_ref }, name_loc) };
var args_list = bun.handleOom(allocator.alloc(ExprNodeIndex, 1));
args_list[0] = arg_expr;
// TODO: if unsupported features includes arrow functions
// const target = p.newExpr(
// E.Function{ .func = .{
// .args = func_args,
// .name = null,
// .open_parens_loc = stmt_loc,
// .body = G.FnBody{
// .loc = stmt_loc,
// .stmts = try allocator.dupe(StmtNodeIndex, stmts_inside_closure),
// },
// } },
// stmt_loc,
// );
const target = target: {
// "(() => { foo() })()" => "(() => foo())()"
if (p.options.features.minify_syntax and stmts_inside_closure.len == 1) {
if (stmts_inside_closure[0].data == .s_expr) {
stmts_inside_closure[0] = p.s(S.Return{
.value = stmts_inside_closure[0].data.s_expr.value,
}, stmts_inside_closure[0].loc);
}
}
break :target p.newExpr(E.Arrow{
.args = func_args,
.body = .{
.loc = stmt_loc,
.stmts = try allocator.dupe(StmtNodeIndex, stmts_inside_closure),
},
.prefer_expr = true,
}, stmt_loc);
};
// Call the closure with the name object
const call = p.newExpr(
E.Call{
.target = target,
.args = ExprNodeList.fromOwnedSlice(args_list),
// TODO: make these fully tree-shakable. this annotation
// as-is is incorrect. This would be done by changing all
// enum wrappers into `var Enum = ...` instead of two
// separate statements. This way, the @__PURE__ annotation
// is attached to the variable binding.
//
// .can_be_unwrapped_if_unused = all_values_are_pure,
},
stmt_loc,
);
const closure = p.s(S.SExpr{
.value = call,
.does_not_affect_tree_shaking = all_values_are_pure,
}, stmt_loc);
stmts.append(closure) catch unreachable;
}
pub fn lowerClass(
noalias p: *P,
stmtorexpr: js_ast.StmtOrExpr,
) []Stmt {
switch (stmtorexpr) {
.stmt => |stmt| {
if (comptime !is_typescript_enabled) {
if (!stmt.data.s_class.class.has_decorators) {
var stmts = p.allocator.alloc(Stmt, 1) catch unreachable;
stmts[0] = stmt;
return stmts;
}
}
var class = &stmt.data.s_class.class;
var constructor_function: ?*E.Function = null;
var static_decorators = ListManaged(Stmt).init(p.allocator);
var instance_decorators = ListManaged(Stmt).init(p.allocator);
var instance_members = ListManaged(Stmt).init(p.allocator);
var static_members = ListManaged(Stmt).init(p.allocator);
var class_properties = ListManaged(Property).init(p.allocator);
for (class.properties) |*prop| {
// merge parameter decorators with method decorators
if (prop.flags.contains(.is_method)) {
if (prop.value) |prop_value| {
switch (prop_value.data) {
.e_function => |func| {
const is_constructor = (prop.key.?.data == .e_string and prop.key.?.data.e_string.eqlComptime("constructor"));
if (is_constructor) constructor_function = func;
for (func.func.args, 0..) |arg, i| {
for (arg.ts_decorators.ptr[0..arg.ts_decorators.len]) |arg_decorator| {
var decorators = if (is_constructor)
&class.ts_decorators
else
&prop.ts_decorators;
const args = p.allocator.alloc(Expr, 2) catch unreachable;
args[0] = p.newExpr(E.Number{ .value = @as(f64, @floatFromInt(i)) }, arg_decorator.loc);
args[1] = arg_decorator;
decorators.append(
p.allocator,
p.callRuntime(arg_decorator.loc, "__legacyDecorateParamTS", args),
) catch |err| bun.handleOom(err);
}
}
},
else => unreachable,
}
}
}
// TODO: prop.kind == .declare and prop.value == null
if (prop.ts_decorators.len > 0) {
const descriptor_key = prop.key.?;
const loc = descriptor_key.loc;
// TODO: when we have the `accessor` modifier, add `and !prop.flags.contains(.has_accessor_modifier)` to
// the if statement.
const descriptor_kind: Expr = if (!prop.flags.contains(.is_method))
p.newExpr(E.Undefined{}, loc)
else
p.newExpr(E.Null{}, loc);
var target: Expr = undefined;
if (prop.flags.contains(.is_static)) {
p.recordUsage(class.class_name.?.ref.?);
target = p.newExpr(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc);
} else {
target = p.newExpr(E.Dot{ .target = p.newExpr(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc), .name = "prototype", .name_loc = loc }, loc);
}
var array: std.array_list.Managed(Expr) = .init(p.allocator);
if (p.options.features.emit_decorator_metadata) {
switch (prop.kind) {
.normal, .abstract => {
{
// design:type
var args = p.allocator.alloc(Expr, 2) catch unreachable;
args[0] = p.newExpr(E.String{ .data = "design:type" }, logger.Loc.Empty);
args[1] = p.serializeMetadata(prop.ts_metadata) catch unreachable;
array.append(p.callRuntime(loc, "__legacyMetadataTS", args)) catch unreachable;
}
// design:paramtypes and design:returntype if method
if (prop.flags.contains(.is_method)) {
if (prop.value) |prop_value| {
{
var args = p.allocator.alloc(Expr, 2) catch unreachable;
args[0] = p.newExpr(E.String{ .data = "design:paramtypes" }, logger.Loc.Empty);
const method_args = prop_value.data.e_function.func.args;
const args_array = p.allocator.alloc(Expr, method_args.len) catch unreachable;
for (args_array, method_args) |*entry, method_arg| {
entry.* = p.serializeMetadata(method_arg.ts_metadata) catch unreachable;
}
args[1] = p.newExpr(E.Array{ .items = ExprNodeList.fromOwnedSlice(args_array) }, logger.Loc.Empty);
array.append(p.callRuntime(loc, "__legacyMetadataTS", args)) catch unreachable;
}
{
var args = p.allocator.alloc(Expr, 2) catch unreachable;
args[0] = p.newExpr(E.String{ .data = "design:returntype" }, logger.Loc.Empty);
args[1] = p.serializeMetadata(prop_value.data.e_function.func.return_ts_metadata) catch unreachable;
array.append(p.callRuntime(loc, "__legacyMetadataTS", args)) catch unreachable;
}
}
}
},
.get => if (prop.flags.contains(.is_method)) {
// typescript sets design:type to the return value & design:paramtypes to [].
if (prop.value) |prop_value| {
{
var args = p.allocator.alloc(Expr, 2) catch unreachable;
args[0] = p.newExpr(E.String{ .data = "design:type" }, logger.Loc.Empty);
args[1] = p.serializeMetadata(prop_value.data.e_function.func.return_ts_metadata) catch unreachable;
array.append(p.callRuntime(loc, "__legacyMetadataTS", args)) catch unreachable;
}
{
var args = p.allocator.alloc(Expr, 2) catch unreachable;
args[0] = p.newExpr(E.String{ .data = "design:paramtypes" }, logger.Loc.Empty);
args[1] = p.newExpr(E.Array{ .items = ExprNodeList.empty }, logger.Loc.Empty);
array.append(p.callRuntime(loc, "__legacyMetadataTS", args)) catch unreachable;
}
}
},
.set => if (prop.flags.contains(.is_method)) {
// typescript sets design:type to the return value & design:paramtypes to [arg].
// note that typescript does not allow you to put a decorator on both the getter and the setter.
// if you do anyway, bun will set design:type and design:paramtypes twice, so it's fine.
if (prop.value) |prop_value| {
const method_args = prop_value.data.e_function.func.args;
{
var args = p.allocator.alloc(Expr, 2) catch unreachable;
args[0] = p.newExpr(E.String{ .data = "design:paramtypes" }, logger.Loc.Empty);
const args_array = p.allocator.alloc(Expr, method_args.len) catch unreachable;
for (args_array, method_args) |*entry, method_arg| {
entry.* = p.serializeMetadata(method_arg.ts_metadata) catch unreachable;
}
args[1] = p.newExpr(E.Array{ .items = ExprNodeList.fromOwnedSlice(args_array) }, logger.Loc.Empty);
array.append(p.callRuntime(loc, "__legacyMetadataTS", args)) catch unreachable;
}
if (method_args.len >= 1) {
var args = p.allocator.alloc(Expr, 2) catch unreachable;
args[0] = p.newExpr(E.String{ .data = "design:type" }, logger.Loc.Empty);
args[1] = p.serializeMetadata(method_args[0].ts_metadata) catch unreachable;
array.append(p.callRuntime(loc, "__legacyMetadataTS", args)) catch unreachable;
}
}
},
.spread, .declare => {}, // not allowed in a class
.class_static_block => {}, // not allowed to decorate this
}
}
bun.handleOom(array.insertSlice(0, prop.ts_decorators.slice()));
const args = p.allocator.alloc(Expr, 4) catch unreachable;
args[0] = p.newExpr(E.Array{ .items = ExprNodeList.moveFromList(&array) }, loc);
args[1] = target;
args[2] = descriptor_key;
args[3] = descriptor_kind;
const decorator = p.callRuntime(prop.key.?.loc, "__legacyDecorateClassTS", args);
const decorator_stmt = p.s(S.SExpr{ .value = decorator }, decorator.loc);
if (prop.flags.contains(.is_static)) {
static_decorators.append(decorator_stmt) catch unreachable;
} else {
instance_decorators.append(decorator_stmt) catch unreachable;
}
}
if (prop.kind != .class_static_block and !prop.flags.contains(.is_method) and prop.key.?.data != .e_private_identifier and prop.ts_decorators.len > 0) {
// remove decorated fields without initializers to avoid assigning undefined.
const initializer = if (prop.initializer) |initializer_value| initializer_value else continue;
var target: Expr = undefined;
if (prop.flags.contains(.is_static)) {
p.recordUsage(class.class_name.?.ref.?);
target = p.newExpr(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc);
} else {
target = p.newExpr(E.This{}, prop.key.?.loc);
}
if (prop.flags.contains(.is_computed) or prop.key.?.data == .e_number) {
target = p.newExpr(E.Index{
.target = target,
.index = prop.key.?,
}, prop.key.?.loc);
} else {
target = p.newExpr(E.Dot{
.target = target,
.name = prop.key.?.data.e_string.data,
.name_loc = prop.key.?.loc,
}, prop.key.?.loc);
}
// remove fields with decorators from class body. Move static members outside of class.
if (prop.flags.contains(.is_static)) {
static_members.append(Stmt.assign(target, initializer)) catch unreachable;
} else {
instance_members.append(Stmt.assign(target, initializer)) catch unreachable;
}
continue;
}
class_properties.append(prop.*) catch unreachable;
}
class.properties = class_properties.items;
if (instance_members.items.len > 0) {
if (constructor_function == null) {
var properties = ListManaged(Property).fromOwnedSlice(p.allocator, class.properties);
var constructor_stmts = ListManaged(Stmt).init(p.allocator);
if (class.extends != null) {
const target = p.newExpr(E.Super{}, stmt.loc);
const arguments_ref = p.newSymbol(.unbound, arguments_str) catch unreachable;
bun.handleOom(p.current_scope.generated.append(p.allocator, arguments_ref));
const super = p.newExpr(E.Spread{ .value = p.newExpr(E.Identifier{ .ref = arguments_ref }, stmt.loc) }, stmt.loc);
const args = bun.handleOom(ExprNodeList.initOne(p.allocator, super));
constructor_stmts.append(p.s(S.SExpr{ .value = p.newExpr(E.Call{ .target = target, .args = args }, stmt.loc) }, stmt.loc)) catch unreachable;
}
constructor_stmts.appendSlice(instance_members.items) catch unreachable;
properties.insert(0, G.Property{
.flags = Flags.Property.init(.{ .is_method = true }),
.key = p.newExpr(E.String{ .data = "constructor" }, stmt.loc),
.value = p.newExpr(E.Function{ .func = G.Fn{
.name = null,
.open_parens_loc = logger.Loc.Empty,
.args = &[_]Arg{},
.body = .{ .loc = stmt.loc, .stmts = constructor_stmts.items },
.flags = Flags.Function.init(.{}),
} }, stmt.loc),
}) catch unreachable;
class.properties = properties.items;
} else {
var constructor_stmts = ListManaged(Stmt).fromOwnedSlice(p.allocator, constructor_function.?.func.body.stmts);
// statements coming from class body inserted after super call or beginning of constructor.
var super_index: ?usize = null;
for (constructor_stmts.items, 0..) |item, index| {
if (item.data != .s_expr or item.data.s_expr.value.data != .e_call or item.data.s_expr.value.data.e_call.target.data != .e_super) continue;
super_index = index;
break;
}
const i = if (super_index) |j| j + 1 else 0;
constructor_stmts.insertSlice(i, instance_members.items) catch unreachable;
constructor_function.?.func.body.stmts = constructor_stmts.items;
}
// TODO: make sure "super()" comes before instance field initializers
// https://github.com/evanw/esbuild/blob/e9413cc4f7ab87263ea244a999c6fa1f1e34dc65/internal/js_parser/js_parser_lower.go#L2742
}
var stmts_count: usize = 1 + static_members.items.len + instance_decorators.items.len + static_decorators.items.len;
if (class.ts_decorators.len > 0) stmts_count += 1;
var stmts = ListManaged(Stmt).initCapacity(p.allocator, stmts_count) catch unreachable;
stmts.appendAssumeCapacity(stmt);
stmts.appendSliceAssumeCapacity(static_members.items);
stmts.appendSliceAssumeCapacity(instance_decorators.items);
stmts.appendSliceAssumeCapacity(static_decorators.items);
if (class.ts_decorators.len > 0) {
var array = class.ts_decorators.moveToListManaged(p.allocator);
if (p.options.features.emit_decorator_metadata) {
if (constructor_function != null) {
// design:paramtypes
var args = p.allocator.alloc(Expr, 2) catch unreachable;
args[0] = p.newExpr(E.String{ .data = "design:paramtypes" }, logger.Loc.Empty);
const constructor_args = constructor_function.?.func.args;
if (constructor_args.len > 0) {
var param_array = p.allocator.alloc(Expr, constructor_args.len) catch unreachable;
for (constructor_args, 0..) |constructor_arg, i| {
param_array[i] = p.serializeMetadata(constructor_arg.ts_metadata) catch unreachable;
}
args[1] = p.newExpr(E.Array{ .items = ExprNodeList.fromOwnedSlice(param_array) }, logger.Loc.Empty);
} else {
args[1] = p.newExpr(E.Array{ .items = ExprNodeList.empty }, logger.Loc.Empty);
}
array.append(p.callRuntime(stmt.loc, "__legacyMetadataTS", args)) catch unreachable;
}
}
const args = p.allocator.alloc(Expr, 2) catch unreachable;
args[0] = p.newExpr(E.Array{ .items = ExprNodeList.fromOwnedSlice(array.items) }, stmt.loc);
args[1] = p.newExpr(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc);
stmts.appendAssumeCapacity(Stmt.assign(
p.newExpr(E.Identifier{ .ref = class.class_name.?.ref.? }, class.class_name.?.loc),
p.callRuntime(stmt.loc, "__legacyDecorateClassTS", args),
));
p.recordUsage(class.class_name.?.ref.?);
p.recordUsage(class.class_name.?.ref.?);
}
return stmts.items;
},
.expr => |expr| {
var stmts = p.allocator.alloc(Stmt, 1) catch unreachable;
stmts[0] = p.s(S.SExpr{ .value = expr }, expr.loc);
return stmts;
},
}
}
fn serializeMetadata(noalias p: *P, ts_metadata: TypeScript.Metadata) !Expr {
return switch (ts_metadata) {
.m_none,
.m_any,
.m_unknown,
.m_object,
=> p.newExpr(
E.Identifier{
.ref = (p.findSymbol(logger.Loc.Empty, "Object") catch unreachable).ref,
},
logger.Loc.Empty,
),
.m_never,
.m_undefined,
.m_null,
.m_void,
=> p.newExpr(
E.Undefined{},
logger.Loc.Empty,
),
.m_string => p.newExpr(
E.Identifier{
.ref = (p.findSymbol(logger.Loc.Empty, "String") catch unreachable).ref,
},
logger.Loc.Empty,
),
.m_number => p.newExpr(
E.Identifier{
.ref = (p.findSymbol(logger.Loc.Empty, "Number") catch unreachable).ref,
},
logger.Loc.Empty,
),
.m_function => p.newExpr(
E.Identifier{
.ref = (p.findSymbol(logger.Loc.Empty, "Function") catch unreachable).ref,
},
logger.Loc.Empty,
),
.m_boolean => p.newExpr(
E.Identifier{
.ref = (p.findSymbol(logger.Loc.Empty, "Boolean") catch unreachable).ref,
},
logger.Loc.Empty,
),
.m_array => p.newExpr(
E.Identifier{
.ref = (p.findSymbol(logger.Loc.Empty, "Array") catch unreachable).ref,
},
logger.Loc.Empty,
),
.m_bigint => p.maybeDefinedHelper(
p.newExpr(
E.Identifier{
.ref = (p.findSymbol(logger.Loc.Empty, "BigInt") catch unreachable).ref,
},
logger.Loc.Empty,
),
),
.m_symbol => p.maybeDefinedHelper(
p.newExpr(
E.Identifier{
.ref = (p.findSymbol(logger.Loc.Empty, "Symbol") catch unreachable).ref,
},
logger.Loc.Empty,
),
),
.m_promise => p.newExpr(
E.Identifier{
.ref = (p.findSymbol(logger.Loc.Empty, "Promise") catch unreachable).ref,
},
logger.Loc.Empty,
),
.m_identifier => |ref| {
p.recordUsage(ref);
if (p.is_import_item.contains(ref)) {
return p.maybeDefinedHelper(p.newExpr(
E.ImportIdentifier{
.ref = ref,
},
logger.Loc.Empty,
));
}
return p.maybeDefinedHelper(p.newExpr(
E.Identifier{ .ref = ref },
logger.Loc.Empty,
));
},
.m_dot => |_refs| {
var refs = _refs;
bun.assert(refs.items.len >= 2);
defer refs.deinit(p.allocator);
var dots = p.newExpr(
E.Dot{
.name = p.loadNameFromRef(refs.items[refs.items.len - 1]),
.name_loc = logger.Loc.Empty,
.target = undefined,
},
logger.Loc.Empty,
);
var current_expr = &dots.data.e_dot.target;
var i: usize = refs.items.len - 2;
while (i > 0) {
current_expr.* = p.newExpr(E.Dot{
.name = p.loadNameFromRef(refs.items[i]),
.name_loc = logger.Loc.Empty,
.target = undefined,
}, logger.Loc.Empty);
current_expr = &current_expr.data.e_dot.target;
i -= 1;
}
if (p.is_import_item.contains(refs.items[0])) {
current_expr.* = p.newExpr(
E.ImportIdentifier{
.ref = refs.items[0],
},
logger.Loc.Empty,
);
} else {
current_expr.* = p.newExpr(
E.Identifier{
.ref = refs.items[0],
},
logger.Loc.Empty,
);
}
const dot_identifier = current_expr.*;
var current_dot = dots;
var maybe_defined_dots = p.newExpr(
E.Binary{
.op = .bin_logical_or,
.right = try p.checkIfDefinedHelper(current_dot),
.left = undefined,
},
logger.Loc.Empty,
);
if (i < refs.items.len - 2) {
current_dot = current_dot.data.e_dot.target;
}
current_expr = &maybe_defined_dots.data.e_binary.left;
while (i < refs.items.len - 2) {
current_expr.* = p.newExpr(
E.Binary{
.op = .bin_logical_or,
.right = try p.checkIfDefinedHelper(current_dot),
.left = undefined,
},
logger.Loc.Empty,
);
current_expr = &current_expr.data.e_binary.left;
i += 1;
if (i < refs.items.len - 2) {
current_dot = current_dot.data.e_dot.target;
}
}
current_expr.* = try p.checkIfDefinedHelper(dot_identifier);
const root = p.newExpr(
E.If{
.yes = p.newExpr(
E.Identifier{
.ref = (p.findSymbol(logger.Loc.Empty, "Object") catch unreachable).ref,
},
logger.Loc.Empty,
),
.no = dots,
.test_ = maybe_defined_dots,
},
logger.Loc.Empty,
);
return root;
},
};
}
fn wrapIdentifierNamespace(
p: *P,
loc: logger.Loc,
ref: Ref,
) Expr {
const enclosing_ref = p.enclosing_namespace_arg_ref.?;
p.recordUsage(enclosing_ref);
return p.newExpr(E.Dot{
.target = Expr.initIdentifier(enclosing_ref, loc),
.name = p.symbols.items[ref.innerIndex()].original_name,
.name_loc = loc,
}, loc);
}
fn wrapIdentifierHoisting(
p: *P,
loc: logger.Loc,
ref: Ref,
) Expr {
// There was a Zig stage1 bug here we had to copy `ref` into a local
// const variable or else the result would be wrong
// I remember that bug in particular took hours, possibly days to uncover.
p.relocated_top_level_vars.append(p.allocator, LocRef{ .loc = loc, .ref = ref }) catch unreachable;
p.recordUsage(ref);
return Expr.initIdentifier(ref, loc);
}
pub fn wrapInlinedEnum(noalias p: *P, value: Expr, comment: string) Expr {
if (bun.strings.containsComptime(comment, "*/")) {
// Don't wrap with a comment
return value;
}
// Wrap with a comment
return p.newExpr(E.InlinedEnum{
.value = value,
.comment = comment,
}, value.loc);
}
pub fn valueForDefine(noalias p: *P, loc: logger.Loc, assign_target: js_ast.AssignTarget, is_delete_target: bool, define_data: *const DefineData) Expr {
switch (define_data.value) {
.e_identifier => {
return p.handleIdentifier(
loc,
define_data.value.e_identifier,
define_data.original_name().?,
IdentifierOpts{
.assign_target = assign_target,
.is_delete_target = is_delete_target,
.was_originally_identifier = true,
},
);
},
.e_string => |str| {
return p.newExpr(str, loc);
},
else => {},
}
return Expr{
.data = define_data.value,
.loc = loc,
};
}
pub fn isDotDefineMatch(noalias p: *P, expr: Expr, parts: []const string) bool {
switch (expr.data) {
.e_dot => |ex| {
if (parts.len > 1) {
if (ex.optional_chain != null) {
return false;
}
// Intermediates must be dot expressions
const last = parts.len - 1;
const is_tail_match = strings.eql(parts[last], ex.name);
return is_tail_match and p.isDotDefineMatch(ex.target, parts[0..last]);
}
},
.e_import_meta => {
return (parts.len == 2 and strings.eqlComptime(parts[0], "import") and strings.eqlComptime(parts[1], "meta"));
},
// Note: this behavior differs from esbuild
// esbuild does not try to match index accessors
// we do, but only if it's a UTF8 string
// the intent is to handle people using this form instead of E.Dot. So we really only want to do this if the accessor can also be an identifier
.e_index => |index| {
if (parts.len > 1 and index.index.data == .e_string and index.index.data.e_string.isUTF8()) {
if (index.optional_chain != null) {
return false;
}
const last = parts.len - 1;
const is_tail_match = strings.eql(parts[last], index.index.data.e_string.slice(p.allocator));
return is_tail_match and p.isDotDefineMatch(index.target, parts[0..last]);
}
},
.e_identifier => |ex| {
// The last expression must be an identifier
if (parts.len == 1) {
const name = p.loadNameFromRef(ex.ref);
if (!strings.eql(name, parts[0])) {
return false;
}
const result = p.findSymbolWithRecordUsage(expr.loc, name, false) catch return false;
// We must not be in a "with" statement scope
if (result.is_inside_with_scope) {
return false;
}
// when there's actually no symbol by that name, we return Ref.None
// If a symbol had already existed by that name, we return .unbound
return (result.ref.isNull() or p.symbols.items[result.ref.innerIndex()].kind == .unbound);
}
},
else => {},
}
return false;
}
// One statement could potentially expand to several statements
pub fn stmtsToSingleStmt(noalias p: *P, loc: logger.Loc, stmts: []Stmt) Stmt {
if (stmts.len == 0) {
return Stmt{ .data = Prefill.Data.SEmpty, .loc = loc };
}
if (stmts.len == 1 and !statementCaresAboutScope(stmts[0])) {
// "let" and "const" must be put in a block when in a single-statement context
return stmts[0];
}
return p.s(S.Block{ .stmts = stmts }, loc);
}
pub fn findLabelSymbol(noalias p: *P, loc: logger.Loc, name: string) FindLabelSymbolResult {
var res = FindLabelSymbolResult{ .ref = Ref.None, .is_loop = false };
var _scope: ?*Scope = p.current_scope;
while (_scope != null and !_scope.?.kindStopsHoisting()) : (_scope = _scope.?.parent.?) {
const scope = _scope orelse unreachable;
const label_ref = scope.label_ref orelse continue;
if (scope.kind == .label and strings.eql(name, p.symbols.items[label_ref.innerIndex()].original_name)) {
// Track how many times we've referenced this symbol
p.recordUsage(label_ref);
res.ref = label_ref;
res.is_loop = scope.label_stmt_is_loop;
res.found = true;
return res;
}
}
const r = js_lexer.rangeOfIdentifier(p.source, loc);
p.log.addRangeErrorFmt(p.source, r, p.allocator, "There is no containing label named \"{s}\"", .{name}) catch unreachable;
// Allocate an "unbound" symbol
const ref = p.newSymbol(.unbound, name) catch unreachable;
// Track how many times we've referenced this symbol
p.recordUsage(ref);
return res;
}
fn keepStmtSymbolName(p: *P, loc: logger.Loc, ref: Ref, name: string) Stmt {
_ = p;
_ = loc;
_ = ref;
_ = name;
// TODO:
@compileError("not implemented");
}
fn runtimeIdentifierRef(p: *P, loc: logger.Loc, comptime name: string) Ref {
p.has_called_runtime = true;
if (!p.runtime_imports.contains(name)) {
if (!p.options.bundle) {
const generated_symbol = p.declareGeneratedSymbol(.other, name) catch unreachable;
p.runtime_imports.put(name, generated_symbol);
return generated_symbol;
} else {
const loc_ref = js_ast.LocRef{
.loc = loc,
.ref = p.newSymbol(.other, name) catch unreachable,
};
p.runtime_imports.put(
name,
loc_ref.ref.?,
);
bun.handleOom(p.module_scope.generated.append(p.allocator, loc_ref.ref.?));
return loc_ref.ref.?;
}
} else {
return p.runtime_imports.at(name).?;
}
}
fn runtimeIdentifier(p: *P, loc: logger.Loc, comptime name: string) Expr {
const ref = p.runtimeIdentifierRef(loc, name);
p.recordUsage(ref);
return p.newExpr(
E.ImportIdentifier{
.ref = ref,
.was_originally_identifier = false,
},
loc,
);
}
pub fn callRuntime(p: *P, loc: logger.Loc, comptime name: string, args: []Expr) Expr {
return p.newExpr(
E.Call{
.target = p.runtimeIdentifier(loc, name),
.args = ExprNodeList.fromOwnedSlice(args),
},
loc,
);
}
pub fn extractDeclsForBinding(binding: Binding, decls: *ListManaged(G.Decl)) anyerror!void {
switch (binding.data) {
.b_missing => {},
.b_identifier => {
try decls.append(G.Decl{ .binding = binding });
},
.b_array => |arr| {
for (arr.items) |item| {
extractDeclsForBinding(item.binding, decls) catch unreachable;
}
},
.b_object => |obj| {
for (obj.properties) |prop| {
extractDeclsForBinding(prop.value, decls) catch unreachable;
}
},
}
}
pub inline fn @"module.exports"(p: *P, loc: logger.Loc) Expr {
return p.newExpr(E.Dot{ .name = exports_string_name, .name_loc = loc, .target = p.newExpr(E.Identifier{ .ref = p.module_ref }, loc) }, loc);
}
// This code is tricky.
// - Doing it incorrectly will cause segfaults.
// - Doing it correctly drastically affects runtime performance while parsing larger files
// The key is in how we remove scopes from the list
// If we do an orderedRemove, it gets very slow.
// swapRemove is fast. But a little more dangerous.
// Instead, we just tombstone it.
pub fn popAndFlattenScope(p: *P, scope_index: usize) void {
// Move up to the parent scope
var to_flatten = p.current_scope;
var parent = to_flatten.parent.?;
p.current_scope = parent;
// Erase this scope from the order. This will shift over the indices of all
// the scopes that were created after us. However, we shouldn't have to
// worry about other code with outstanding scope indices for these scopes.
// These scopes were all created in between this scope's push and pop
// operations, so they should all be child scopes and should all be popped
// by the time we get here.
p.scopes_in_order.items[scope_index] = null;
// Decrement the length so that in code with lots of scopes, we use
// less memory and do less work
p.scopes_in_order.items.len -= @as(usize, @intFromBool(p.scopes_in_order.items.len == scope_index + 1));
// Remove the last child from the parent scope
const last = parent.children.len - 1;
if (comptime Environment.allow_assert) assert(parent.children.ptr[last] == to_flatten);
parent.children.len -|= 1;
for (to_flatten.children.slice()) |item| {
item.parent = parent;
bun.handleOom(parent.children.append(p.allocator, item));
}
}
/// When not transpiling we dont use the renamer, so our solution is to generate really
/// hard to collide with variables, instead of actually making things collision free
pub fn generateTempRef(p: *P, default_name: ?string) Ref {
return p.generateTempRefWithScope(default_name, p.current_scope);
}
pub fn generateTempRefWithScope(p: *P, default_name: ?string, scope: *Scope) Ref {
const name = (if (p.willUseRenamer()) default_name else null) orelse brk: {
p.temp_ref_count += 1;
break :brk bun.handleOom(std.fmt.allocPrint(p.allocator, "__bun_temp_ref_{x}$", .{p.temp_ref_count}));
};
const ref = bun.handleOom(p.newSymbol(.other, name));
p.temp_refs_to_declare.append(p.allocator, .{
.ref = ref,
}) catch |err| bun.handleOom(err);
bun.handleOom(scope.generated.append(p.allocator, ref));
return ref;
}
pub fn computeTsEnumsMap(p: *const P, allocator: Allocator) !js_ast.Ast.TsEnumsMap {
// When hot module reloading is enabled, we disable enum inlining
// to avoid making the HMR graph more complicated.
if (p.options.features.hot_module_reloading)
return .{};
const InlinedEnumValue = js_ast.InlinedEnumValue;
var map: js_ast.Ast.TsEnumsMap = .{};
try map.ensureTotalCapacity(allocator, @intCast(p.top_level_enums.items.len));
for (p.top_level_enums.items) |ref| {
const entry = p.ref_to_ts_namespace_member.getEntry(ref).?;
const namespace = entry.value_ptr.namespace;
var inner_map: bun.StringHashMapUnmanaged(InlinedEnumValue) = .{};
try inner_map.ensureTotalCapacity(allocator, @intCast(namespace.count()));
for (namespace.keys(), namespace.values()) |key, val| {
switch (val.data) {
.enum_number => |num| inner_map.putAssumeCapacityNoClobber(
key,
InlinedEnumValue.encode(.{ .number = num }),
),
.enum_string => |str| inner_map.putAssumeCapacityNoClobber(
key,
InlinedEnumValue.encode(.{ .string = str }),
),
else => continue,
}
}
map.putAssumeCapacity(entry.key_ptr.*, inner_map);
}
return map;
}
pub fn shouldLowerUsingDeclarations(p: *const P, stmts: []Stmt) bool {
// TODO: We do not support lowering await, but when we do this needs to point to that var
const lower_await = false;
// Check feature flags first, then iterate statements.
if (!p.options.features.lower_using and !lower_await) return false;
for (stmts) |stmt| {
if (stmt.data == .s_local and
// Need to re-check lower_using for the k_using case in case lower_await is true
((stmt.data.s_local.kind == .k_using and p.options.features.lower_using) or
(stmt.data.s_local.kind == .k_await_using)))
{
return true;
}
}
return false;
}
pub const LowerUsingDeclarationsContext = struct {
first_using_loc: logger.Loc,
stack_ref: Ref,
has_await_using: bool,
pub fn init(p: *P) !LowerUsingDeclarationsContext {
return LowerUsingDeclarationsContext{
.first_using_loc = logger.Loc.Empty,
.stack_ref = p.generateTempRef("__stack"),
.has_await_using = false,
};
}
pub fn scanStmts(ctx: *LowerUsingDeclarationsContext, p: *P, stmts: []Stmt) void {
for (stmts) |stmt| {
switch (stmt.data) {
.s_local => |local| {
if (!local.kind.isUsing()) continue;
if (ctx.first_using_loc.isEmpty()) {
ctx.first_using_loc = stmt.loc;
}
if (local.kind == .k_await_using) {
ctx.has_await_using = true;
}
for (local.decls.slice()) |*decl| {
if (decl.value) |*decl_value| {
const value_loc = decl_value.loc;
p.recordUsage(ctx.stack_ref);
const args = bun.handleOom(p.allocator.alloc(Expr, 3));
args[0] = Expr{
.data = .{ .e_identifier = .{ .ref = ctx.stack_ref } },
.loc = stmt.loc,
};
args[1] = decl_value.*;
// 1. always pass this param for hopefully better jit performance
// 2. pass 1 or 0 to be shorter than `true` or `false`
args[2] = Expr{
.data = .{ .e_number = .{ .value = if (local.kind == .k_await_using) 1 else 0 } },
.loc = stmt.loc,
};
decl.value = p.callRuntime(value_loc, "__using", args);
}
}
if (p.will_wrap_module_in_try_catch_for_using and p.current_scope.kind == .entry) {
local.kind = .k_var;
} else {
local.kind = .k_const;
}
},
else => {},
}
}
}
pub fn finalize(ctx: *LowerUsingDeclarationsContext, p: *P, stmts: []Stmt, should_hoist_fns: bool) ListManaged(Stmt) {
var result = ListManaged(Stmt).init(p.allocator);
var exports = ListManaged(js_ast.ClauseItem).init(p.allocator);
var end: u32 = 0;
for (stmts) |stmt| {
switch (stmt.data) {
.s_directive, .s_import, .s_export_from, .s_export_star => {
// These can't go in a try/catch block
bun.handleOom(result.append(stmt));
continue;
},
.s_class => {
if (stmt.data.s_class.is_export) {
// can't go in try/catch; hoist out
bun.handleOom(result.append(stmt));
continue;
}
},
.s_export_default => {
continue; // this prevents re-exporting default since we already have it as an .s_export_clause
},
.s_export_clause => |data| {
// Merge export clauses together
bun.handleOom(exports.appendSlice(data.items));
continue;
},
.s_function => {
if (should_hoist_fns) {
// Hoist function declarations for cross-file ESM references
bun.handleOom(result.append(stmt));
continue;
}
},
.s_local => |local| {
// If any of these are exported, turn it into a "var" and add export clauses
if (local.is_export) {
local.is_export = false;
for (local.decls.slice()) |decl| {
if (decl.binding.data == .b_identifier) {
const identifier = decl.binding.data.b_identifier;
exports.append(js_ast.ClauseItem{
.name = .{
.loc = decl.binding.loc,
.ref = identifier.ref,
},
.alias = p.symbols.items[identifier.ref.inner_index].original_name,
.alias_loc = decl.binding.loc,
}) catch |err| bun.handleOom(err);
local.kind = .k_var;
}
}
}
},
else => {},
}
stmts[end] = stmt;
end += 1;
}
const non_exported_statements = stmts[0..end];
const caught_ref = p.generateTempRef("_catch");
const err_ref = p.generateTempRef("_err");
const has_err_ref = p.generateTempRef("_hasErr");
var scope = p.current_scope;
while (!scope.kindStopsHoisting()) {
scope = scope.parent.?;
}
const is_top_level = scope == p.module_scope;
scope.generated.appendSlice(p.allocator, &.{
ctx.stack_ref,
caught_ref,
err_ref,
has_err_ref,
}) catch |err| bun.handleOom(err);
p.declared_symbols.ensureUnusedCapacity(
p.allocator,
// 5 to include the _promise decl later on:
if (ctx.has_await_using) 5 else 4,
) catch |err| bun.handleOom(err);
p.declared_symbols.appendAssumeCapacity(.{ .is_top_level = is_top_level, .ref = ctx.stack_ref });
p.declared_symbols.appendAssumeCapacity(.{ .is_top_level = is_top_level, .ref = caught_ref });
p.declared_symbols.appendAssumeCapacity(.{ .is_top_level = is_top_level, .ref = err_ref });
p.declared_symbols.appendAssumeCapacity(.{ .is_top_level = is_top_level, .ref = has_err_ref });
const loc = ctx.first_using_loc;
const call_dispose = call_dispose: {
p.recordUsage(ctx.stack_ref);
p.recordUsage(err_ref);
p.recordUsage(has_err_ref);
const args = bun.handleOom(p.allocator.alloc(Expr, 3));
args[0] = Expr{
.data = .{ .e_identifier = .{ .ref = ctx.stack_ref } },
.loc = loc,
};
args[1] = Expr{
.data = .{ .e_identifier = .{ .ref = err_ref } },
.loc = loc,
};
args[2] = Expr{
.data = .{ .e_identifier = .{ .ref = has_err_ref } },
.loc = loc,
};
break :call_dispose p.callRuntime(loc, "__callDispose", args);
};
const finally_stmts = finally: {
if (ctx.has_await_using) {
const promise_ref = p.generateTempRef("_promise");
bun.handleOom(scope.generated.append(p.allocator, promise_ref));
p.declared_symbols.appendAssumeCapacity(.{ .is_top_level = is_top_level, .ref = promise_ref });
const promise_ref_expr = p.newExpr(E.Identifier{ .ref = promise_ref }, loc);
const await_expr = p.newExpr(E.Await{
.value = promise_ref_expr,
}, loc);
p.recordUsage(promise_ref);
const statements = bun.handleOom(p.allocator.alloc(Stmt, 2));
statements[0] = p.s(S.Local{
.decls = decls: {
const decls = bun.handleOom(p.allocator.alloc(Decl, 1));
decls[0] = .{
.binding = p.b(B.Identifier{ .ref = promise_ref }, loc),
.value = call_dispose,
};
break :decls G.Decl.List.fromOwnedSlice(decls);
},
}, loc);
// The "await" must not happen if an error was thrown before the
// "await using", so we conditionally await here:
//
// var promise = __callDispose(stack, error, hasError);
// promise && await promise;
//
statements[1] = p.s(S.SExpr{
.value = p.newExpr(E.Binary{
.op = .bin_logical_and,
.left = promise_ref_expr,
.right = await_expr,
}, loc),
}, loc);
break :finally statements;
} else {
const single = bun.handleOom(p.allocator.alloc(Stmt, 1));
single[0] = p.s(S.SExpr{ .value = call_dispose }, call_dispose.loc);
break :finally single;
}
};
// Wrap everything in a try/catch/finally block
p.recordUsage(caught_ref);
bun.handleOom(result.ensureUnusedCapacity(2 + @as(usize, @intFromBool(exports.items.len > 0))));
result.appendAssumeCapacity(p.s(S.Local{
.decls = decls: {
const decls = bun.handleOom(p.allocator.alloc(Decl, 1));
decls[0] = .{
.binding = p.b(B.Identifier{ .ref = ctx.stack_ref }, loc),
.value = p.newExpr(E.Array{}, loc),
};
break :decls G.Decl.List.fromOwnedSlice(decls);
},
.kind = .k_let,
}, loc));
result.appendAssumeCapacity(p.s(S.Try{
.body = non_exported_statements,
.body_loc = loc,
.catch_ = .{
.binding = p.b(B.Identifier{ .ref = caught_ref }, loc),
.body = catch_body: {
const statements = bun.handleOom(p.allocator.alloc(Stmt, 1));
statements[0] = p.s(S.Local{
.decls = decls: {
const decls = bun.handleOom(p.allocator.alloc(Decl, 2));
decls[0] = .{
.binding = p.b(B.Identifier{ .ref = err_ref }, loc),
.value = p.newExpr(E.Identifier{ .ref = caught_ref }, loc),
};
decls[1] = .{
.binding = p.b(B.Identifier{ .ref = has_err_ref }, loc),
.value = p.newExpr(E.Number{ .value = 1 }, loc),
};
break :decls G.Decl.List.fromOwnedSlice(decls);
},
}, loc);
break :catch_body statements;
},
.body_loc = loc,
.loc = loc,
},
.finally = .{
.loc = loc,
.stmts = finally_stmts,
},
}, loc));
if (exports.items.len > 0) {
result.appendAssumeCapacity(p.s(S.ExportClause{
.items = exports.items,
.is_single_line = false,
}, loc));
}
return result;
}
};
const import_meta_hot_accept_err = "Dependencies to `import.meta.hot.accept` must be statically analyzable module specifiers matching direct imports.";
/// The signatures for `import.meta.hot.accept` are:
/// `accept()` - self accept
/// `accept(Function)` - self accept
/// `accept(string, Function)` - accepting another module
/// `accept(string[], Function)` - accepting multiple modules
///
/// The strings that can be passed in the first argument must be module
/// specifiers that were imported. We enforce that they line up exactly
/// with ones that were imported, so that it can share an import record.
///
/// This function replaces all specifier strings with `e_require_resolve_string`
pub fn handleImportMetaHotAcceptCall(p: *@This(), call: *E.Call) void {
if (call.args.len == 0) return;
switch (call.args.at(0).data) {
.e_string => |str| {
call.args.mut(0).data = p.rewriteImportMetaHotAcceptString(str, call.args.at(0).loc) orelse
return;
},
.e_array => |arr| for (arr.items.slice()) |*item| {
if (item.data != .e_string) {
bun.handleOom(p.log.addError(p.source, item.loc, import_meta_hot_accept_err));
continue;
}
item.data = p.rewriteImportMetaHotAcceptString(item.data.e_string, item.loc) orelse
return;
},
else => return,
}
call.target.data.e_special = .hot_accept_visited;
}
fn rewriteImportMetaHotAcceptString(p: *P, str: *E.String, loc: logger.Loc) ?Expr.Data {
bun.handleOom(str.toUTF8(p.allocator));
const specifier = str.data;
const import_record_index = for (p.import_records.items, 0..) |import_record, i| {
if (bun.strings.eql(specifier, import_record.path.text)) {
break i;
}
} else {
bun.handleOom(p.log.addError(p.source, loc, import_meta_hot_accept_err));
return null;
};
return .{ .e_special = .{
.resolved_specifier_string = .init(@intCast(import_record_index)),
} };
}
const ReactRefreshExportKind = enum { named, default };
pub fn handleReactRefreshRegister(p: *P, stmts: *ListManaged(Stmt), original_name: []const u8, ref: Ref, export_kind: ReactRefreshExportKind) !void {
bun.assert(p.options.features.react_fast_refresh);
bun.assert(p.current_scope == p.module_scope);
if (ReactRefresh.isComponentishName(original_name)) {
try p.emitReactRefreshRegister(stmts, original_name, ref, export_kind);
}
}
pub fn emitReactRefreshRegister(p: *P, stmts: *ListManaged(Stmt), original_name: []const u8, ref: Ref, export_kind: ReactRefreshExportKind) !void {
bun.assert(p.options.features.react_fast_refresh);
bun.assert(p.current_scope == p.module_scope);
// $RefreshReg$(component, "file.ts:Original Name")
const loc = logger.Loc.Empty;
try stmts.append(p.s(S.SExpr{ .value = p.newExpr(E.Call{
.target = Expr.initIdentifier(p.react_refresh.register_ref, loc),
.args = try ExprNodeList.fromSlice(p.allocator, &.{
Expr.initIdentifier(ref, loc),
p.newExpr(E.String{
.data = try bun.strings.concat(p.allocator, &.{
p.source.path.pretty,
":",
switch (export_kind) {
.named => original_name,
.default => "default",
},
}),
}, loc),
}),
}, loc) }, loc));
p.recordUsage(ref);
p.react_refresh.register_used = true;
}
pub fn wrapValueForServerComponentReference(p: *P, val: Expr, original_name: []const u8) Expr {
bun.assert(p.options.features.server_components.wrapsExports());
bun.assert(p.current_scope == p.module_scope);
if (p.options.features.server_components == .wrap_exports_for_server_reference)
bun.todoPanic(@src(), "registerServerReference", .{});
const module_path = p.newExpr(E.String{
.data = if (p.options.jsx.development)
p.source.path.pretty
else
bun.todoPanic(@src(), "TODO: unique_key here", .{}),
}, logger.Loc.Empty);
// registerClientReference(
// Comp,
// "src/filepath.tsx",
// "Comp"
// );
return p.newExpr(E.Call{
.target = Expr.initIdentifier(p.server_components_wrap_ref, logger.Loc.Empty),
.args = js_ast.ExprNodeList.fromSlice(p.allocator, &.{
val,
module_path,
p.newExpr(E.String{ .data = original_name }, logger.Loc.Empty),
}) catch |err| bun.handleOom(err),
}, logger.Loc.Empty);
}
pub fn handleReactRefreshHookCall(p: *P, hook_call: *E.Call, original_name: []const u8) void {
bun.assert(p.options.features.react_fast_refresh);
bun.assert(ReactRefresh.isHookName(original_name));
const ctx_storage = p.react_refresh.hook_ctx_storage orelse
return; // not in a function, ignore this hook call.
// if this function has no hooks recorded, initialize a hook context
// every function visit provides stack storage, which it will inspect at visit finish.
const ctx: *ReactRefresh.HookContext = if (ctx_storage.*) |*ctx| ctx else init: {
p.react_refresh.signature_used = true;
var scope = p.current_scope;
while (scope.kind != .function_body and scope.kind != .block and scope.kind != .entry) {
scope = scope.parent orelse break;
}
ctx_storage.* = .{
.hasher = std.hash.Wyhash.init(0),
.signature_cb = p.generateTempRefWithScope("_s", scope),
.user_hooks = .{},
};
// TODO(paperclover): fix the renamer bug. this bug
// theoretically affects all usages of temp refs, but i cannot
// find another example of it breaking (like with `using`)
p.declared_symbols.append(p.allocator, .{
.is_top_level = true,
.ref = ctx_storage.*.?.signature_cb,
}) catch |err| bun.handleOom(err);
break :init &(ctx_storage.*.?);
};
ctx.hasher.update(original_name);
if (ReactRefresh.built_in_hooks.get(original_name)) |built_in_hook| hash_arg: {
const arg_index: usize = switch (built_in_hook) {
// useState first argument is initial state.
.useState => 0,
// useReducer second argument is initial state.
.useReducer => 1,
else => break :hash_arg,
};
if (hook_call.args.len <= arg_index) break :hash_arg;
const arg = hook_call.args.slice()[arg_index];
arg.data.writeToHasher(&ctx.hasher, p.symbols.items);
} else switch (hook_call.target.data) {
inline .e_identifier,
.e_import_identifier,
.e_commonjs_export_identifier,
=> |id, tag| {
const gop = bun.handleOom(ctx.user_hooks.getOrPut(p.allocator, id.ref));
if (!gop.found_existing) {
gop.value_ptr.* = .{
.data = @unionInit(Expr.Data, @tagName(tag), id),
.loc = .Empty,
};
}
},
else => {},
}
ctx.hasher.update("\x00");
}
pub fn handleReactRefreshPostVisitFunctionBody(p: *P, stmts: *ListManaged(Stmt), hook: *ReactRefresh.HookContext) void {
bun.assert(p.options.features.react_fast_refresh);
// We need to prepend `_s();` as a statement.
if (stmts.items.len == stmts.capacity) {
// If the ArrayList does not have enough capacity, it is
// re-allocated entirely to fit. Only one slot of new capacity
// is used since we know this statement list is not going to be
// appended to afterwards; This function is a post-visit handler.
const new_stmts = bun.handleOom(p.allocator.alloc(Stmt, stmts.items.len + 1));
@memcpy(new_stmts[1..], stmts.items);
stmts.deinit();
stmts.* = ListManaged(Stmt).fromOwnedSlice(p.allocator, new_stmts);
} else {
// The array has enough capacity, so there is no possibility of
// allocation failure. We just move all of the statements over
// by one, and increase the length using `addOneAssumeCapacity`
_ = stmts.addOneAssumeCapacity();
bun.copy(Stmt, stmts.items[1..], stmts.items[0 .. stmts.items.len - 1]);
}
const loc = logger.Loc.Empty;
const prepended_stmt = p.s(S.SExpr{ .value = p.newExpr(E.Call{
.target = Expr.initIdentifier(hook.signature_cb, loc),
}, loc) }, loc);
stmts.items[0] = prepended_stmt;
}
pub fn getReactRefreshHookSignalDecl(p: *P, signal_cb_ref: Ref) Stmt {
const loc = logger.Loc.Empty;
p.react_refresh.latest_signature_ref = signal_cb_ref;
// var s_ = $RefreshSig$();
return p.s(S.Local{ .decls = G.Decl.List.fromSlice(p.allocator, &.{.{
.binding = p.b(B.Identifier{ .ref = signal_cb_ref }, loc),
.value = p.newExpr(E.Call{
.target = Expr.initIdentifier(p.react_refresh.create_signature_ref, loc),
}, loc),
}}) catch |err| bun.handleOom(err) }, loc);
}
pub fn getReactRefreshHookSignalInit(p: *P, ctx: *ReactRefresh.HookContext, function_with_hook_calls: Expr) Expr {
const loc = logger.Loc.Empty;
const final = ctx.hasher.final();
const hash_data = bun.handleOom(p.allocator.alloc(u8, comptime bun.base64.encodeLenFromSize(@sizeOf(@TypeOf(final)))));
bun.assert(bun.base64.encode(hash_data, std.mem.asBytes(&final)) == hash_data.len);
const have_custom_hooks = ctx.user_hooks.count() > 0;
const have_force_arg = have_custom_hooks or p.react_refresh.force_reset;
const args = p.allocator.alloc(
Expr,
2 +
@as(usize, @intFromBool(have_force_arg)) +
@as(usize, @intFromBool(have_custom_hooks)),
) catch |err| bun.handleOom(err);
args[0] = function_with_hook_calls;
args[1] = p.newExpr(E.String{ .data = hash_data }, loc);
if (have_force_arg) args[2] = p.newExpr(E.Boolean{ .value = p.react_refresh.force_reset }, loc);
if (have_custom_hooks) {
// () => [useCustom1, useCustom2]
args[3] = p.newExpr(E.Arrow{
.body = .{
.stmts = p.allocator.dupe(Stmt, &.{
p.s(S.Return{ .value = p.newExpr(E.Array{
.items = ExprNodeList.fromBorrowedSliceDangerous(ctx.user_hooks.values()),
}, loc) }, loc),
}) catch |err| bun.handleOom(err),
.loc = loc,
},
.prefer_expr = true,
}, loc);
}
// _s(func, "<hash>", force, () => [useCustom])
return p.newExpr(E.Call{
.target = Expr.initIdentifier(ctx.signature_cb, loc),
.args = ExprNodeList.fromOwnedSlice(args),
}, loc);
}
pub fn toAST(
p: *P,
parts: *ListManaged(js_ast.Part),
exports_kind: js_ast.ExportsKind,
wrap_mode: WrapMode,
hashbang: []const u8,
) !js_ast.Ast {
const allocator = p.allocator;
// if (p.options.tree_shaking and p.options.features.trim_unused_imports) {
// p.treeShake(&parts, false);
// }
const bundling = p.options.bundle;
var parts_end: usize = @as(usize, @intFromBool(bundling));
// When bundling with HMR, we need every module to be just a
// single part, as we later wrap each module into a function,
// which requires a single part. Otherwise, you'll end up with
// multiple instances of a module, each with different parts of
// the file. That is also why tree-shaking is disabled.
if (p.options.features.hot_module_reloading) {
bun.assert(!p.options.tree_shaking);
bun.assert(p.options.features.hot_module_reloading);
var hmr_transform_ctx = ConvertESMExportsForHmr{
.last_part = &parts.items[parts.items.len - 1],
.is_in_node_modules = p.source.path.isNodeModule(),
};
try hmr_transform_ctx.stmts.ensureTotalCapacity(p.allocator, prealloc_count: {
// get a estimate on how many statements there are going to be
var count: usize = 0;
for (parts.items) |part| count += part.stmts.len;
break :prealloc_count count + 2;
});
for (parts.items) |part| {
// Bake does not care about 'import =', as it handles it on it's own
_ = try ImportScanner.scan(P, p, part.stmts, wrap_mode != .none, true, &hmr_transform_ctx);
}
try hmr_transform_ctx.finalize(p, parts.items);
} else {
// Handle import paths after the whole file has been visited because we need
// symbol usage counts to be able to remove unused type-only imports in
// TypeScript code.
while (true) {
var kept_import_equals = false;
var removed_import_equals = false;
const begin = parts_end;
// Potentially remove some statements, then filter out parts to remove any
// with no statements
for (parts.items[begin..]) |part_| {
var part = part_;
p.import_records_for_current_part.clearRetainingCapacity();
p.declared_symbols.clearRetainingCapacity();
const result = try ImportScanner.scan(P, p, part.stmts, wrap_mode != .none, false, {});
kept_import_equals = kept_import_equals or result.kept_import_equals;
removed_import_equals = removed_import_equals or result.removed_import_equals;
part.stmts = result.stmts;
if (part.stmts.len > 0) {
if (p.module_scope.contains_direct_eval and part.declared_symbols.len() > 0) {
// If this file contains a direct call to "eval()", all parts that
// declare top-level symbols must be kept since the eval'd code may
// reference those symbols.
part.can_be_removed_if_unused = false;
}
if (part.declared_symbols.len() == 0) {
part.declared_symbols = p.declared_symbols.clone(p.allocator) catch unreachable;
} else {
part.declared_symbols.appendList(p.allocator, p.declared_symbols) catch unreachable;
}
if (part.import_record_indices.len == 0) {
part.import_record_indices = .fromOwnedSlice(bun.handleOom(
p.allocator.dupe(u32, p.import_records_for_current_part.items),
));
} else {
part.import_record_indices.appendSlice(
p.allocator,
p.import_records_for_current_part.items,
) catch |err| bun.handleOom(err);
}
parts.items[parts_end] = part;
parts_end += 1;
}
}
// We need to iterate multiple times if an import-equals statement was
// removed and there are more import-equals statements that may be removed
if (!kept_import_equals or !removed_import_equals) {
break;
}
}
// leave the first part in there for namespace export when bundling
parts.items.len = parts_end;
// Do a second pass for exported items now that imported items are filled out.
// This isn't done for HMR because it already deletes all `.s_export_clause`s
for (parts.items) |part| {
for (part.stmts) |stmt| {
switch (stmt.data) {
.s_export_clause => |clause| {
for (clause.items) |item| {
if (p.named_imports.getEntry(item.name.ref.?)) |_import| {
_import.value_ptr.is_exported = true;
}
}
},
else => {},
}
}
}
}
if (wrap_mode == .bun_commonjs and !p.options.features.remove_cjs_module_wrapper) {
// This transforms the user's code into.
//
// (function (exports, require, module, __filename, __dirname) {
// ...
// })
//
// which is then called in `evaluateCommonJSModuleOnce`
var args = bun.handleOom(allocator.alloc(Arg, 5 + @as(usize, @intFromBool(p.has_import_meta))));
args[0..5].* = .{
Arg{ .binding = p.b(B.Identifier{ .ref = p.exports_ref }, logger.Loc.Empty) },
Arg{ .binding = p.b(B.Identifier{ .ref = p.require_ref }, logger.Loc.Empty) },
Arg{ .binding = p.b(B.Identifier{ .ref = p.module_ref }, logger.Loc.Empty) },
Arg{ .binding = p.b(B.Identifier{ .ref = p.filename_ref }, logger.Loc.Empty) },
Arg{ .binding = p.b(B.Identifier{ .ref = p.dirname_ref }, logger.Loc.Empty) },
};
if (p.has_import_meta) {
p.import_meta_ref = bun.handleOom(p.newSymbol(.other, "$Bun_import_meta"));
args[5] = Arg{ .binding = p.b(B.Identifier{ .ref = p.import_meta_ref }, logger.Loc.Empty) };
}
var total_stmts_count: usize = 0;
for (parts.items) |part| {
total_stmts_count += part.stmts.len;
}
const preserve_strict_mode = p.module_scope.strict_mode == .explicit_strict_mode and
!(parts.items.len > 0 and
parts.items[0].stmts.len > 0 and
parts.items[0].stmts[0].data == .s_directive);
total_stmts_count += @as(usize, @intCast(@intFromBool(preserve_strict_mode)));
const stmts_to_copy = bun.handleOom(allocator.alloc(Stmt, total_stmts_count));
{
var remaining_stmts = stmts_to_copy;
if (preserve_strict_mode) {
remaining_stmts[0] = p.s(
S.Directive{
.value = "use strict",
},
p.module_scope_directive_loc,
);
remaining_stmts = remaining_stmts[1..];
}
for (parts.items) |part| {
for (part.stmts, remaining_stmts[0..part.stmts.len]) |src, *dest| {
dest.* = src;
}
remaining_stmts = remaining_stmts[part.stmts.len..];
}
}
const wrapper = p.newExpr(
E.Function{
.func = G.Fn{
.name = null,
.open_parens_loc = logger.Loc.Empty,
.args = args,
.body = .{ .loc = logger.Loc.Empty, .stmts = stmts_to_copy },
.flags = Flags.Function.init(.{ .is_export = false }),
},
},
logger.Loc.Empty,
);
var top_level_stmts = bun.handleOom(p.allocator.alloc(Stmt, 1));
top_level_stmts[0] = p.s(
S.SExpr{
.value = wrapper,
},
logger.Loc.Empty,
);
try parts.ensureUnusedCapacity(1);
parts.items.len = 1;
parts.items[0].stmts = top_level_stmts;
}
var top_level_symbols_to_parts = js_ast.Ast.TopLevelSymbolToParts{};
var top_level = &top_level_symbols_to_parts;
if (p.options.bundle) {
const Ctx = struct {
allocator: std.mem.Allocator,
top_level_symbols_to_parts: *js_ast.Ast.TopLevelSymbolToParts,
symbols: []const js_ast.Symbol,
part_index: u32,
pub fn next(ctx: @This(), input: Ref) void {
// If this symbol was merged, use the symbol at the end of the
// linked list in the map. This is the case for multiple "var"
// declarations with the same name, for example.
var ref = input;
var symbol_ref = &ctx.symbols[ref.innerIndex()];
while (symbol_ref.hasLink()) : (symbol_ref = &ctx.symbols[ref.innerIndex()]) {
ref = symbol_ref.link;
}
var entry = ctx.top_level_symbols_to_parts.getOrPut(ctx.allocator, ref) catch unreachable;
if (!entry.found_existing) {
entry.value_ptr.* = .{};
}
bun.handleOom(entry.value_ptr.append(ctx.allocator, @as(u32, @truncate(ctx.part_index))));
}
};
// Each part tracks the other parts it depends on within this file
for (parts.items, 0..) |*part, part_index| {
const decls = &part.declared_symbols;
const ctx = Ctx{
.allocator = p.allocator,
.top_level_symbols_to_parts = top_level,
.symbols = p.symbols.items,
.part_index = @as(u32, @truncate(part_index)),
};
DeclaredSymbol.forEachTopLevelSymbol(decls, ctx, Ctx.next);
}
// Pulling in the exports of this module always pulls in the export part
{
var entry = top_level.getOrPut(p.allocator, p.exports_ref) catch unreachable;
if (!entry.found_existing) {
entry.value_ptr.* = .{};
}
bun.handleOom(entry.value_ptr.append(p.allocator, js_ast.namespace_export_part_index));
}
}
const wrapper_ref: Ref = brk: {
if (p.options.features.hot_module_reloading) {
break :brk p.hmr_api_ref;
}
if (p.options.bundle and p.needsWrapperRef(parts.items)) {
break :brk p.newSymbol(
.other,
std.fmt.allocPrint(
p.allocator,
"require_{f}",
.{p.source.fmtIdentifier()},
) catch |err| bun.handleOom(err),
) catch |err| bun.handleOom(err);
}
break :brk Ref.None;
};
return .{
.runtime_imports = p.runtime_imports,
.module_scope = p.module_scope.*,
.exports_ref = p.exports_ref,
.wrapper_ref = wrapper_ref,
.module_ref = p.module_ref,
.export_star_import_records = p.export_star_import_records.items,
.approximate_newline_count = p.lexer.approximate_newline_count,
.exports_kind = exports_kind,
.named_imports = p.named_imports,
.named_exports = p.named_exports,
.import_keyword = p.esm_import_keyword,
.export_keyword = p.esm_export_keyword,
.top_level_symbols_to_parts = top_level_symbols_to_parts,
.char_freq = p.computeCharacterFrequency(),
.directive = if (p.module_scope.strict_mode == .explicit_strict_mode) "use strict" else null,
// Assign slots to symbols in nested scopes. This is some precomputation for
// the symbol renaming pass that will happen later in the linker. It's done
// now in the parser because we want it to be done in parallel per file and
// we're already executing code in parallel here
.nested_scope_slot_counts = if (p.options.features.minify_identifiers)
renamer.assignNestedScopeSlots(p.allocator, p.module_scope, p.symbols.items)
else
js_ast.SlotCounts{},
.require_ref = if (p.runtime_imports.__require != null)
p.runtime_imports.__require.?
else
p.require_ref,
.force_cjs_to_esm = p.unwrap_all_requires or exports_kind == .esm_with_dynamic_fallback_from_cjs,
.uses_module_ref = p.symbols.items[p.module_ref.inner_index].use_count_estimate > 0,
.uses_exports_ref = p.symbols.items[p.exports_ref.inner_index].use_count_estimate > 0,
.uses_require_ref = if (p.options.bundle)
p.runtime_imports.__require != null and
p.symbols.items[p.runtime_imports.__require.?.inner_index].use_count_estimate > 0
else
p.symbols.items[p.require_ref.inner_index].use_count_estimate > 0,
.commonjs_module_exports_assigned_deoptimized = p.commonjs_module_exports_assigned_deoptimized,
.top_level_await_keyword = p.top_level_await_keyword,
.commonjs_named_exports = p.commonjs_named_exports,
.has_commonjs_export_names = p.has_commonjs_export_names,
.hashbang = hashbang,
// TODO: cross-module constant inlining
// .const_values = p.const_values,
.ts_enums = try p.computeTsEnumsMap(allocator),
.import_meta_ref = p.import_meta_ref,
.symbols = js_ast.Symbol.List.moveFromList(&p.symbols),
.parts = bun.BabyList(js_ast.Part).moveFromList(parts),
.import_records = ImportRecord.List.moveFromList(&p.import_records),
};
}
/// The bundler will generate wrappers to contain top-level side effects using
/// the '__esm' helper. Example:
///
/// var init_foo = __esm(() => {
/// someExport = Math.random();
/// });
///
/// This wrapper can be removed if all of the constructs get moved
/// outside of the file. Due to paralleization, we can't retroactively
/// delete the `init_foo` symbol, but instead it must be known far in
/// advance if the symbol is needed or not.
///
/// The logic in this function must be in sync with the hoisting
/// logic in `LinkerContext.generateCodeForFileInChunkJS`
fn needsWrapperRef(p: *const P, parts: []const js_ast.Part) bool {
bun.assert(p.options.bundle);
for (parts) |part| {
for (part.stmts) |stmt| {
switch (stmt.data) {
.s_function => {},
.s_class => |class| if (!class.class.canBeMoved()) return true,
.s_local => |local| {
if (local.was_commonjs_export or p.commonjs_named_exports.count() == 0) {
for (local.decls.slice()) |decl| {
if (decl.value) |value|
if (value.data != .e_missing and !value.canBeMoved())
return true;
}
continue;
}
return true;
},
.s_export_default => |ed| {
if (!ed.canBeMoved())
return true;
},
.s_export_equals => |e| {
if (!e.value.canBeMoved())
return true;
},
else => return true,
}
}
}
return false;
}
pub fn init(
allocator: Allocator,
log: *logger.Log,
source: *const logger.Source,
define: *Define,
lexer: js_lexer.Lexer,
opts: Parser.Options,
this: *P,
) anyerror!void {
var scope_order = try ScopeOrderList.initCapacity(allocator, 1);
const scope = try allocator.create(Scope);
scope.* = Scope{
.members = .{},
.children = .{},
.generated = .{},
.kind = .entry,
.label_ref = null,
.parent = null,
};
scope_order.appendAssumeCapacity(ScopeOrder{ .loc = locModuleScope, .scope = scope });
this.* = P{
.legacy_cjs_import_stmts = @TypeOf(this.legacy_cjs_import_stmts).init(allocator),
// This must default to true or else parsing "in" won't work right.
// It will fail for the case in the "in-keyword.js" file
.allow_in = true,
.call_target = nullExprData,
.delete_target = nullExprData,
.stmt_expr_value = nullExprData,
.loop_body = nullStmtData,
.define = define,
.import_records = undefined,
.named_imports = undefined,
.named_exports = .{},
.log = log,
.stack_check = bun.StackCheck.init(),
.allocator = allocator,
.options = opts,
.then_catch_chain = ThenCatchChain{ .next_target = nullExprData },
.to_expr_wrapper_namespace = undefined,
.to_expr_wrapper_hoisted = undefined,
.import_transposer = undefined,
.require_transposer = undefined,
.require_resolve_transposer = undefined,
.source = source,
.macro = MacroState.init(allocator),
.current_scope = scope,
.module_scope = scope,
.scopes_in_order = scope_order,
.needs_jsx_import = if (comptime only_scan_imports_and_do_not_visit) false else NeedsJSXType{},
.lexer = lexer,
// Only enable during bundling, when not bundling CJS
.commonjs_named_exports_deoptimized = if (opts.bundle) opts.output_format == .cjs else true,
};
this.lexer.track_comments = opts.features.minify_identifiers;
this.unwrap_all_requires = brk: {
if (opts.bundle and opts.output_format != .cjs) {
if (source.path.packageName()) |pkg| {
if (opts.features.shouldUnwrapRequire(pkg)) {
if (strings.eqlComptime(pkg, "react") or strings.eqlComptime(pkg, "react-dom")) {
const version = opts.package_version;
if (version.len > 2 and (version[0] == '0' or (version[0] == '1' and version[1] < '8'))) {
break :brk false;
}
}
break :brk true;
}
}
}
break :brk false;
};
this.symbols = std.array_list.Managed(Symbol).init(allocator);
if (comptime !only_scan_imports_and_do_not_visit) {
this.import_records = @TypeOf(this.import_records).init(allocator);
this.named_imports = .{};
}
this.to_expr_wrapper_namespace = Binding2ExprWrapper.Namespace.init(this);
this.to_expr_wrapper_hoisted = Binding2ExprWrapper.Hoisted.init(this);
this.import_transposer = @TypeOf(this.import_transposer).init(this);
this.require_transposer = @TypeOf(this.require_transposer).init(this);
this.require_resolve_transposer = @TypeOf(this.require_resolve_transposer).init(this);
if (opts.features.top_level_await or comptime only_scan_imports_and_do_not_visit) {
this.fn_or_arrow_data_parse.allow_await = .allow_expr;
this.fn_or_arrow_data_parse.is_top_level = true;
}
if (comptime !is_typescript_enabled) {
// This is so it doesn't impact runtime transpiler caching when not in use
this.options.features.emit_decorator_metadata = false;
}
}
};
}
var e_missing_data = E.Missing{};
var s_missing = S.Empty{};
var nullExprData = Expr.Data{ .e_missing = e_missing_data };
var nullStmtData = Stmt.Data{ .s_empty = s_missing };
var keyExprData = Expr.Data{ .e_string = &Prefill.String.Key };
var nullExprValueData = E.Null{};
var falseExprValueData = E.Boolean{ .value = false };
var nullValueExpr = Expr.Data{ .e_null = nullExprValueData };
var falseValueExpr = Expr.Data{ .e_boolean = E.Boolean{ .value = false } };
const string = []const u8;
const Define = @import("../defines.zig").Define;
const DefineData = @import("../defines.zig").DefineData;
const bun = @import("bun");
const Environment = bun.Environment;
const FeatureFlags = bun.FeatureFlags;
const ImportRecord = bun.ImportRecord;
const Output = bun.Output;
const assert = bun.assert;
const js_lexer = bun.js_lexer;
const logger = bun.logger;
const strings = bun.strings;
const js_ast = bun.ast;
const B = js_ast.B;
const Binding = js_ast.Binding;
const BindingNodeIndex = js_ast.BindingNodeIndex;
const DeclaredSymbol = js_ast.DeclaredSymbol;
const E = js_ast.E;
const Expr = js_ast.Expr;
const ExprNodeIndex = js_ast.ExprNodeIndex;
const ExprNodeList = js_ast.ExprNodeList;
const Flags = js_ast.Flags;
const LocRef = js_ast.LocRef;
const S = js_ast.S;
const Scope = js_ast.Scope;
const Stmt = js_ast.Stmt;
const StmtNodeIndex = js_ast.StmtNodeIndex;
const StmtNodeList = js_ast.StmtNodeList;
const Symbol = js_ast.Symbol;
const G = js_ast.G;
const Arg = G.Arg;
const Decl = G.Decl;
const Property = G.Property;
const SymbolPropertyUseMap = js_ast.Part.SymbolPropertyUseMap;
const SymbolUseMap = js_ast.Part.SymbolUseMap;
const js_parser = bun.js_parser;
const ConvertESMExportsForHmr = js_parser.ConvertESMExportsForHmr;
const DeferredArrowArgErrors = js_parser.DeferredArrowArgErrors;
const DeferredErrors = js_parser.DeferredErrors;
const DeferredImportNamespace = js_parser.DeferredImportNamespace;
const ExprBindingTuple = js_parser.ExprBindingTuple;
const ExpressionTransposer = js_parser.ExpressionTransposer;
const FindLabelSymbolResult = js_parser.FindLabelSymbolResult;
const FnOnlyDataVisit = js_parser.FnOnlyDataVisit;
const FnOrArrowDataParse = js_parser.FnOrArrowDataParse;
const FnOrArrowDataVisit = js_parser.FnOrArrowDataVisit;
const FunctionKind = js_parser.FunctionKind;
const IdentifierOpts = js_parser.IdentifierOpts;
const ImportItemForNamespaceMap = js_parser.ImportItemForNamespaceMap;
const ImportKind = js_parser.ImportKind;
const ImportNamespaceCallOrConstruct = js_parser.ImportNamespaceCallOrConstruct;
const ImportScanner = js_parser.ImportScanner;
const InvalidLoc = js_parser.InvalidLoc;
const JSXImport = js_parser.JSXImport;
const JSXTransformType = js_parser.JSXTransformType;
const Jest = js_parser.Jest;
const LocList = js_parser.LocList;
const MacroState = js_parser.MacroState;
const ParseStatementOptions = js_parser.ParseStatementOptions;
const ParsedPath = js_parser.ParsedPath;
const Parser = js_parser.Parser;
const Prefill = js_parser.Prefill;
const PrependTempRefsOpts = js_parser.PrependTempRefsOpts;
const ReactRefresh = js_parser.ReactRefresh;
const Ref = js_parser.Ref;
const RefMap = js_parser.RefMap;
const RefRefMap = js_parser.RefRefMap;
const RuntimeFeatures = js_parser.RuntimeFeatures;
const RuntimeImports = js_parser.RuntimeImports;
const ScanPassResult = js_parser.ScanPassResult;
const ScopeOrder = js_parser.ScopeOrder;
const ScopeOrderList = js_parser.ScopeOrderList;
const SideEffects = js_parser.SideEffects;
const StmtList = js_parser.StmtList;
const StrictModeFeature = js_parser.StrictModeFeature;
const StringBoolMap = js_parser.StringBoolMap;
const Substitution = js_parser.Substitution;
const TempRef = js_parser.TempRef;
const ThenCatchChain = js_parser.ThenCatchChain;
const TransposeState = js_parser.TransposeState;
const TypeScript = js_parser.TypeScript;
const WrapMode = js_parser.WrapMode;
const arguments_str = js_parser.arguments_str;
const exports_string_name = js_parser.exports_string_name;
const fs = js_parser.fs;
const generatedSymbolName = js_parser.generatedSymbolName;
const isEvalOrArguments = js_parser.isEvalOrArguments;
const locModuleScope = js_parser.locModuleScope;
const options = js_parser.options;
const renamer = js_parser.renamer;
const statementCaresAboutScope = js_parser.statementCaresAboutScope;
const std = @import("std");
const List = std.ArrayListUnmanaged;
const Map = std.AutoHashMapUnmanaged;
const Allocator = std.mem.Allocator;
const ListManaged = std.array_list.Managed;