diff --git a/.vscode/launch.json b/.vscode/launch.json index 82c399869e..e1661ff885 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,38 +1,6 @@ { "version": "0.2.0", "configurations": [ - { - "name": "(lldb) Launch", - "type": "cppdbg", - "request": "launch", - "program": "/Users/jarredsumner/Code/esdev/src/zig-cache/o/b57013855157d9a38baa6327511eaf3e/test", - "cwd": "${workspaceFolder}", - "args": ["/Users/jarredsumner/Builds/zig/build/bin/zig"], - "stopAtEntry": false, - "environment": [], - "miDebuggerPath": "/usr/local/bin/lldb-mi", - "MIMode": "lldb", - "targetArchitecture": "x64", - - "externalConsole": false - }, - - { - "name": "Launch", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceFolder}/zig-cache/bin/esdev", - "args": ["/Users/jarredsumner/Code/devserverless/build.js"], - "stopAtEntry": false, - "cwd": "${workspaceFolder}", - "environment": [], - "externalConsole": false, - // "preLaunchTask": "build", - "MIMode": "lldb", - "internalConsoleOptions": "openOnSessionStart", - "logging": { - "moduleLoad": false - } - } + ] } diff --git a/README.md b/README.md index 12c0c17421..f7ace5b53a 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,8 @@ import { map } from "lodash-es"; const foo = map(["bar", "baz"], (item) => {}); ``` +If + ##### HMR & Fast Refresh implementation This section only applies when Hot Module Reloading is enabled. When it's off, none of this part runs. React Fast Refresh depends on Hot Module Reloading. diff --git a/src/js_ast.zig b/src/js_ast.zig index a25c0f30a9..46cf199227 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -2713,8 +2713,8 @@ pub const Ast = struct { // is conveniently fully parallelized. named_imports: std.AutoHashMap(Ref, NamedImport) = undefined, named_exports: std.StringHashMap(NamedExport) = undefined, - top_level_symbol_to_parts: std.AutoHashMap(Ref, []u32) = undefined, - export_star_import_records: std.ArrayList(u32) = undefined, + top_level_symbol_to_parts: std.AutoHashMap(Ref, std.ArrayList(u32)) = undefined, + export_star_import_records: []u32 = &([_]u32{}), pub fn initTest(parts: []Part) Ast { return Ast{ @@ -2855,20 +2855,20 @@ pub const Part = struct { scopes: []*Scope = &([_]*Scope{}), // Each is an index into the file-level import record list - import_record_indices: std.ArrayList(u32) = undefined, + import_record_indices: []u32 = &([_]u32{}), // All symbols that are declared in this part. Note that a given symbol may // have multiple declarations, and so may end up being declared in multiple // parts (e.g. multiple "var" declarations with the same name). Also note // that this list isn't deduplicated and may contain duplicates. - declared_symbols: std.ArrayList(DeclaredSymbol) = undefined, + declared_symbols: []DeclaredSymbol = &([_]DeclaredSymbol{}), // An estimate of the number of uses of all symbols used within this part. - symbol_uses: std.AutoHashMap(Ref, Symbol.Use) = undefined, + symbol_uses: SymbolUseMap = undefined, // The indices of the other parts in this file that are needed if this part // is needed. - dependencies: std.ArrayList(Dependency) = undefined, + dependencies: []Dependency = &([_]Dependency{}), // If true, this part can be removed if none of the declared symbols are // used. If the file containing this part is imported, then all parts that @@ -2883,6 +2883,7 @@ pub const Part = struct { // This is true if this file has been marked as live by the tree shaking // algorithm. is_live: bool = false, + pub const SymbolUseMap = std.AutoHashMap(Ref, Symbol.Use); }; pub const Result = struct { @@ -2897,7 +2898,7 @@ pub const StmtOrExpr = union(enum) { pub const NamedImport = struct { // Parts within this file that use this import - local_parts_with_uses: ?[]u32, + local_parts_with_uses: []u32 = &([_]u32{}), alias: ?string, alias_loc: ?logger.Loc, diff --git a/src/js_lexer.zig b/src/js_lexer.zig index bf2f6cef2e..d618500572 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -21,6 +21,8 @@ pub const PropertyModifierKeyword = tables.PropertyModifierKeyword; pub const TypescriptStmtKeyword = tables.TypescriptStmtKeyword; pub const TypeScriptAccessibilityModifier = tables.TypeScriptAccessibilityModifier; +pub var emptyJavaScriptString = ([_]u16{0}); + pub const JSONOptions = struct { allow_comments: bool = false, allow_trailing_commas: bool = false, @@ -906,12 +908,13 @@ pub const Lexer = struct { } pub fn unexpected(lexer: *@This()) void { - var found: string = undefined; - if (lexer.start == lexer.source.contents.len) { - found = "end of file"; - } else { - found = lexer.raw(); - } + const found = finder: { + if (lexer.start == lexer.source.contents.len) { + break :finder "end of file"; + } else { + break :finder lexer.raw(); + } + }; lexer.addRangeError(lexer.range(), "Unexpected {s}", .{found}, true); } @@ -925,10 +928,14 @@ pub const Lexer = struct { } pub fn expectedString(self: *@This(), text: string) void { - var found = self.raw(); - if (self.source.contents.len == self.start) { - found = "end of file"; - } + const found = finder: { + if (self.source.contents.len != self.start) { + break :finder self.raw(); + } else { + break :finder "end of file"; + } + }; + self.addRangeError(self.range(), "Expected {s} but found {s}", .{ text, found }, true); } @@ -969,7 +976,7 @@ pub const Lexer = struct { } pub fn initGlobalName(log: *logger.Log, source: *logger.Source, allocator: *std.mem.Allocator) !@This() { - var empty_string_literal: JavascriptString = undefined; + var empty_string_literal: JavascriptString = emptyJavaScriptString; var lex = @This(){ .log = log, .source = source.*, @@ -986,7 +993,7 @@ pub const Lexer = struct { } pub fn initTSConfig(log: *logger.Log, source: *logger.Source, allocator: *std.mem.Allocator) !@This() { - var empty_string_literal: JavascriptString = undefined; + var empty_string_literal: JavascriptString = emptyJavaScriptString; var lex = @This(){ .log = log, .source = source.*, @@ -1006,7 +1013,7 @@ pub const Lexer = struct { } pub fn initJSON(log: *logger.Log, source: *logger.Source, allocator: *std.mem.Allocator) !@This() { - var empty_string_literal: JavascriptString = undefined; + var empty_string_literal: JavascriptString = &emptyJavaScriptString; var lex = @This(){ .log = log, .source = source.*, @@ -1026,7 +1033,7 @@ pub const Lexer = struct { } pub fn init(log: *logger.Log, source: *logger.Source, allocator: *std.mem.Allocator) !@This() { - var empty_string_literal: JavascriptString = undefined; + var empty_string_literal: JavascriptString = &emptyJavaScriptString; var lex = @This(){ .log = log, .source = source.*, diff --git a/src/js_parser.zig b/src/js_parser.zig index f15369c6a5..d30813a03a 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -53,16 +53,16 @@ pub const ImportScanner = struct { stmts: []Stmt = &([_]Stmt{}), kept_import_equals: bool = false, removed_import_equals: bool = false, - pub fn scan(p: *P, _stmts: []Stmt) ImportScanner { + pub fn scan(p: *P, stmts: []Stmt) !ImportScanner { var scanner = ImportScanner{}; - var stmts = StmtList.fromOwnedSlice(p.allocator, _stmts); - var stmts_end: usize = 0; - for (stmts.items) |stmt| { + for (stmts) |_stmt| { + // zls needs the hint, it seems. + const stmt: Stmt = _stmt; switch (stmt.data) { .s_import => |st| { - const record = p.import_records[st.import_record_index]; + var record: ImportRecord = p.import_records.items[st.import_record_index]; // The official TypeScript compiler always removes unused imported // symbols. However, we deliberately deviate from the official @@ -120,7 +120,7 @@ pub const ImportScanner = struct { // user is expecting the output to be as small as possible. So we // should omit unused imports. // - const keep_unused_imports = !p.options.preserve_unused_imports_ts; + const keep_unused_imports = !p.options.trim_unused_imports; // TypeScript always trims unused imports. This is important for // correctness since some imports might be fake (only in the type @@ -131,10 +131,10 @@ pub const ImportScanner = struct { if (st.default_name) |default_name| { found_imports = true; - var symbol = p.symbols.items[default_name.ref.inner_index]; + var symbol = p.symbols.items[default_name.ref.?.inner_index]; // TypeScript has a separate definition of unused - if (p.options.ts and p.ts_use_counts.items[default_name.ref.inner_index] != 0) { + if (p.options.ts and p.ts_use_counts.items[default_name.ref.?.inner_index] != 0) { is_unused_in_typescript = false; } @@ -161,7 +161,7 @@ pub const ImportScanner = struct { var has_any = false; if (p.import_items_for_namespace.get(st.namespace_ref)) |entry| { - if (entry.size() > 0) { + if (entry.count() > 0) { has_any = true; } } @@ -171,19 +171,320 @@ pub const ImportScanner = struct { } } } + + // Remove items if they are unused + if (st.items.len > 0) { + found_imports = false; + var items_end: usize = 0; + var i: usize = 0; + while (i < st.items.len) : (i += 1) { + const item = st.items[i]; + const ref = item.name.ref.?; + const symbol: Symbol = p.symbols.items[ref.inner_index]; + + // TypeScript has a separate definition of unused + if (p.options.ts and p.ts_use_counts.items[ref.inner_index] != 0) { + is_unused_in_typescript = false; + } + + // Remove the symbol if it's never used outside a dead code region + if (symbol.use_count_estimate != 0) { + st.items[items_end] = item; + items_end += 1; + } + } + + // Filter the array by taking a slice + if (items_end == 0 and st.items.len > 0) { + p.allocator.free(st.items); + // zero out the slice + st.items = &([_]js_ast.ClauseItem{}); + } else if (items_end < st.items.len) { + var list = List(js_ast.ClauseItem).fromOwnedSlice(p.allocator, st.items); + list.shrinkAndFree(items_end); + st.items = list.toOwnedSlice(); + } + } + + // -- Original Comment -- + // Omit this statement if we're parsing TypeScript and all imports are + // unused. Note that this is distinct from the case where there were + // no imports at all (e.g. "import 'foo'"). In that case we want to keep + // the statement because the user is clearly trying to import the module + // for side effects. + // + // This culling is important for correctness when parsing TypeScript + // because a) the TypeScript compiler does ths and we want to match it + // and b) this may be a fake module that only exists in the type system + // and doesn't actually exist in reality. + // + // We do not want to do this culling in JavaScript though because the + // module may have side effects even if all imports are unused. + // -- Original Comment -- + + // jarred: I think, in this project, we want this behavior, even in JavaScript. + // I think this would be a big performance improvement. + // The less you import, the less code you transpile. + // Side-effect imports are nearly always done through identifier-less imports + // e.g. `import 'fancy-stylesheet-thing/style.css';` + // This is a breaking change though. We can make it an option with some guardrail + // so maybe if it errors, it shows a suggestion "retry without trimming unused imports" + if (found_imports and !p.options.preserve_unused_imports_ts) { + // Ignore import records with a pre-filled source index. These are + // for injected files and we definitely do not want to trim these. + if (!Ref.isSourceIndexNull(record.source_index)) { + record.is_unused = true; + continue; + } + } + } + + if (p.options.trim_unused_imports) { + if (st.star_name_loc != null) { + // -- Original Comment -- + // If we're bundling a star import and the namespace is only ever + // used for property accesses, then convert each unique property to + // a clause item in the import statement and remove the star import. + // That will cause the bundler to bundle them more efficiently when + // both this module and the imported module are in the same group. + // + // Before: + // + // import * as ns from 'foo' + // console.log(ns.a, ns.b) + // + // After: + // + // import {a, b} from 'foo' + // console.log(a, b) + // + // This is not done if the namespace itself is used, because in that + // case the code for the namespace will have to be generated. This is + // determined by the symbol count because the parser only counts the + // star import as used if it was used for something other than a + // property access: + // + // import * as ns from 'foo' + // console.log(ns, ns.a, ns.b) + // + // -- Original Comment -- + + // jarred: we don't use the same grouping mechanism as esbuild + // but, we do this anyway. + // The reasons why are: + // * It makes static analysis for other tools simpler. + // * I imagine browsers may someday do some optimizations + // when it's "easier" to know only certain modules are used + // For example, if you're importing a component from a design system + // it's really stupid to import all 1,000 components from that design system + // when you just want + const namespace_ref = st.namespace_ref; + const convert_star_to_clause = p.symbols.items[namespace_ref.inner_index].use_count_estimate == 0; + + if (convert_star_to_clause and !keep_unused_imports) { + st.star_name_loc = null; + } + + // "importItemsForNamespace" has property accesses off the namespace + if (p.import_items_for_namespace.get(namespace_ref)) |import_items| { + var count = import_items.count(); + if (count > 0) { + // Sort keys for determinism + var sorted: []string = try p.allocator.alloc(string, count); + var iter = import_items.iterator(); + var i: usize = 0; + while (iter.next()) |item| { + sorted[i] = item.key; + i += 1; + } + strings.sortAsc(sorted); + + if (convert_star_to_clause) { + // Create an import clause for these items. Named imports will be + // automatically created later on since there is now a clause. + var items = try p.allocator.alloc(js_ast.ClauseItem, count); + try p.declared_symbols.ensureUnusedCapacity(count); + i = 0; + for (sorted) |alias| { + const name: LocRef = import_items.get(alias) orelse unreachable; + const original_name = p.symbols.items[name.ref.?.inner_index].original_name; + items[i] = js_ast.ClauseItem{ + .alias = alias, + .alias_loc = name.loc, + .name = name, + .original_name = original_name, + }; + p.declared_symbols.appendAssumeCapacity(js_ast.DeclaredSymbol{ + .ref = name.ref.?, + .is_top_level = true, + }); + + i += 1; + } + + if (st.items.len > 0) { + p.panic("The syntax \"import {{x}}, * as y from 'path'\" isn't valid", .{}); + } + + st.items = items; + } else { + // If we aren't converting this star import to a clause, still + // create named imports for these property accesses. This will + // cause missing imports to generate useful warnings. + // + // It will also improve bundling efficiency for internal imports + // by still converting property accesses off the namespace into + // bare identifiers even if the namespace is still needed. + + for (sorted) |alias| { + const name: LocRef = import_items.get(alias) orelse unreachable; + + try p.named_imports.put(name.ref.?, js_ast.NamedImport{ + .alias = alias, + .alias_loc = name.loc, + .namespace_ref = st.namespace_ref, + .import_record_index = st.import_record_index, + }); + + // Make sure the printer prints this as a property access + var symbol: Symbol = p.symbols.items[name.ref.?.inner_index]; + symbol.namespace_alias = G.NamespaceAlias{ .namespace_ref = st.namespace_ref, .alias = alias }; + p.symbols.items[name.ref.?.inner_index] = symbol; + } + } + } + } + } + } + + try p.import_records_for_current_part.append(st.import_record_index); + + if (st.star_name_loc != null) { + record.contains_import_star = true; + } + + if (st.default_name != null) { + record.contains_default_alias = true; + } else { + for (st.items) |item| { + if (strings.eql(item.alias, "default")) { + record.contains_default_alias = true; + break; + } + } + } + }, + .s_function => |st| { + if (st.func.flags.is_export) { + if (st.func.name) |name| { + try p.recordExport(name.loc, p.symbols.items[name.ref.?.inner_index].original_name, name.ref.?); + } else { + try p.log.addRangeError(p.source, logger.Range{ .loc = st.func.open_parens_loc, .len = 2 }, "Exported functions must have a name"); + } + } + }, + .s_class => |st| { + if (st.is_export) { + if (st.class.class_name) |name| { + try p.recordExport(name.loc, p.symbols.items[name.ref.?.inner_index].original_name, name.ref.?); + } else { + try p.log.addRangeError(p.source, logger.Range{ .loc = st.class.body_loc, .len = 0 }, "Exported classes must have a name"); + } + } + }, + .s_local => |st| { + if (st.is_export) { + for (st.decls) |decl| { + p.recordExportedBinding(decl.binding); + } + } + + // Remove unused import-equals statements, since those likely + // correspond to types instead of values + if (st.was_ts_import_equals and !st.is_export and st.decls.len > 0) { + var decl = st.decls[0]; + + // Skip to the underlying reference + var value = decl.value; + if (decl.value) |val| { + while (true) { + if (@as(Expr.Tag, val.data) == .e_dot) { + value = val.data.e_dot.target; + } else { + break; + } + } + } + + // Is this an identifier reference and not a require() call? + if (value) |val| { + if (@as(Expr.Tag, val.data) == .e_identifier) { + // Is this import statement unused? + if (@as(Binding.Tag, decl.binding.data) == .b_identifier and p.symbols.items[decl.binding.data.b_identifier.ref.inner_index].use_count_estimate == 0) { + p.ignoreUsage(val.data.e_identifier.ref); + + scanner.removed_import_equals = true; + continue; + } else { + scanner.kept_import_equals = true; + } + } + } + } + }, + .s_export_default => |st| { + try p.recordExport(st.default_name.loc, "default", st.default_name.ref.?); + }, + .s_export_clause => |st| { + for (st.items) |item| { + try p.recordExport(item.alias_loc, item.alias, item.name.ref.?); + } + }, + .s_export_star => |st| { + try p.import_records_for_current_part.append(st.import_record_index); + + if (st.alias) |alias| { + // "export * as ns from 'path'" + try p.named_imports.put(st.namespace_ref, js_ast.NamedImport{ + .alias = null, + .alias_is_star = true, + .alias_loc = alias.loc, + .namespace_ref = Ref.None, + .import_record_index = st.import_record_index, + .is_exported = true, + }); + try p.recordExport(alias.loc, alias.original_name, st.namespace_ref); + } else { + // "export * from 'path'" + try p.export_star_import_records.append(st.import_record_index); + } + }, + .s_export_from => |st| { + try p.import_records_for_current_part.append(st.import_record_index); + + for (st.items) |item| { + const ref = item.name.ref orelse p.panic("Expected export from item to have a name {s}", .{st}); + // Note that the imported alias is not item.Alias, which is the + // exported alias. This is somewhat confusing because each + // SExportFrom statement is basically SImport + SExportClause in one. + try p.named_imports.put(ref, js_ast.NamedImport{ + .alias_is_star = false, + .alias = item.original_name, + .alias_loc = item.name.loc, + .namespace_ref = st.namespace_ref, + .import_record_index = st.import_record_index, + .is_exported = true, + }); + try p.recordExport(item.name.loc, item.alias, ref); } }, - .s_function => |st| {}, - .s_class => |st| {}, - .s_local => |st| {}, - .s_export_default => |st| {}, - .s_export_clause => |st| {}, - .s_export_star => |st| {}, - .s_export_from => |st| {}, else => {}, } - } + stmts[stmts_end] = stmt; + stmts_end += 1; + } + scanner.stmts = stmts[0..stmts_end]; return scanner; } }; @@ -968,6 +1269,7 @@ pub const Parser = struct { use_define_for_class_fields: bool = false, suppress_warnings_about_weird_code: bool = true, moduleType: ModuleType = ModuleType.esm, + trim_unused_imports: bool = true, }; pub fn parse(self: *Parser) !js_ast.Result { @@ -983,7 +1285,7 @@ pub const Parser = struct { debugl("