forked from oven-sh/bun
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsourcemap.zig
More file actions
973 lines (835 loc) · 35.9 KB
/
sourcemap.zig
File metadata and controls
973 lines (835 loc) · 35.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
pub const SourceMap = @This();
const debug = bun.Output.scoped(.SourceMap, .visible);
/// Coordinates in source maps are stored using relative offsets for size
/// reasons. When joining together chunks of a source map that were emitted
/// in parallel for different parts of a file, we need to fix up the first
/// segment of each chunk to be relative to the end of the previous chunk.
pub const SourceMapState = struct {
/// This isn't stored in the source map. It's only used by the bundler to join
/// source map chunks together correctly.
generated_line: i32 = 0,
/// These are stored in the source map in VLQ format.
generated_column: i32 = 0,
source_index: i32 = 0,
original_line: i32 = 0,
original_column: i32 = 0,
};
sources: [][]const u8 = &[_][]u8{},
sources_content: []string,
mapping: Mapping.List = .{},
allocator: std.mem.Allocator,
/// Dictates what parseUrl/parseJSON return.
pub const ParseUrlResultHint = union(enum) {
mappings_only,
/// Source Index to fetch
source_only: u32,
/// In order to fetch source contents, you need to know the
/// index, but you cant know the index until the mappings
/// are loaded. So pass in line+col.
all: struct {
line: i32,
column: i32,
include_names: bool = false,
},
};
pub const ParseUrl = struct {
/// Populated when `mappings_only` or `all`.
map: ?*ParsedSourceMap = null,
/// Populated when `all`
/// May be `null` even when requested.
mapping: ?Mapping = null,
/// Populated when `source_only` or `all`
/// May be `null` even when requested, if did not exist in map.
source_contents: ?[]const u8 = null,
};
/// Parses an inline source map url like `data:application/json,....`
/// Currently does not handle non-inline source maps.
///
/// `source` must be in UTF-8 and can be freed after this call.
/// The mappings are owned by the `alloc` allocator.
/// Temporary allocations are made to the `arena` allocator, which
/// should be an arena allocator (caller is assumed to call `deinit`).
pub fn parseUrl(
alloc: std.mem.Allocator,
arena: std.mem.Allocator,
source: []const u8,
hint: ParseUrlResultHint,
) !ParseUrl {
const json_bytes = json_bytes: {
const data_prefix = "data:application/json";
if (bun.strings.hasPrefixComptime(source, data_prefix) and source.len > (data_prefix.len + 1)) try_data_url: {
debug("parse (data url, {d} bytes)", .{source.len});
switch (source[data_prefix.len]) {
';' => {
const encoding = bun.sliceTo(source[data_prefix.len + 1 ..], ',');
if (!bun.strings.eqlComptime(encoding, "base64")) break :try_data_url;
const base64_data = source[data_prefix.len + ";base64,".len ..];
const len = bun.base64.decodeLen(base64_data);
const bytes = bun.handleOom(arena.alloc(u8, len));
const decoded = bun.base64.decode(bytes, base64_data);
if (!decoded.isSuccessful()) {
return error.InvalidBase64;
}
break :json_bytes bytes[0..decoded.count];
},
',' => break :json_bytes source[data_prefix.len + 1 ..],
else => break :try_data_url,
}
}
return error.UnsupportedFormat;
};
return parseJSON(alloc, arena, json_bytes, hint);
}
/// Parses a JSON source-map
///
/// `source` must be in UTF-8 and can be freed after this call.
/// The mappings are owned by the `alloc` allocator.
/// Temporary allocations are made to the `arena` allocator, which
/// should be an arena allocator (caller is assumed to call `deinit`).
pub fn parseJSON(
alloc: std.mem.Allocator,
arena: std.mem.Allocator,
source: []const u8,
hint: ParseUrlResultHint,
) !ParseUrl {
const json_src = bun.logger.Source.initPathString("sourcemap.json", source);
var log = bun.logger.Log.init(arena);
defer log.deinit();
// the allocator given to the JS parser is not respected for all parts
// of the parse, so we need to remember to reset the ast store
bun.ast.Expr.Data.Store.reset();
bun.ast.Stmt.Data.Store.reset();
defer {
// the allocator given to the JS parser is not respected for all parts
// of the parse, so we need to remember to reset the ast store
bun.ast.Expr.Data.Store.reset();
bun.ast.Stmt.Data.Store.reset();
}
debug("parse (JSON, {d} bytes)", .{source.len});
var json = bun.json.parse(&json_src, &log, arena, false) catch {
return error.InvalidJSON;
};
if (json.get("version")) |version| {
if (version.data != .e_number or version.data.e_number.value != 3.0) {
return error.UnsupportedVersion;
}
}
const mappings_str = json.get("mappings") orelse {
return error.UnsupportedVersion;
};
if (mappings_str.data != .e_string) {
return error.InvalidSourceMap;
}
const sources_content = switch ((json.get("sourcesContent") orelse return error.InvalidSourceMap).data) {
.e_array => |arr| arr,
else => return error.InvalidSourceMap,
};
const sources_paths = switch ((json.get("sources") orelse return error.InvalidSourceMap).data) {
.e_array => |arr| arr,
else => return error.InvalidSourceMap,
};
if (sources_content.items.len != sources_paths.items.len) {
return error.InvalidSourceMap;
}
var i: usize = 0;
const source_paths_slice = if (hint != .source_only)
bun.handleOom(alloc.alloc([]const u8, sources_content.items.len))
else
null;
errdefer if (hint != .source_only) {
for (source_paths_slice.?[0..i]) |item| alloc.free(item);
alloc.free(source_paths_slice.?);
};
if (hint != .source_only) for (sources_paths.items.slice()) |item| {
if (item.data != .e_string)
return error.InvalidSourceMap;
source_paths_slice.?[i] = try alloc.dupe(u8, try item.data.e_string.string(alloc));
i += 1;
};
const map = if (hint != .source_only) map: {
var map_data = switch (Mapping.parse(
alloc,
mappings_str.data.e_string.slice(arena),
null,
std.math.maxInt(i32),
std.math.maxInt(i32),
.{ .allow_names = hint == .all and hint.all.include_names, .sort = true },
)) {
.success => |x| x,
.fail => |fail| return fail.err,
};
if (hint == .all and hint.all.include_names and map_data.mappings.impl == .with_names) {
if (json.get("names")) |names| {
if (names.data == .e_array) {
var names_list = try std.ArrayListUnmanaged(bun.Semver.String).initCapacity(alloc, names.data.e_array.items.len);
errdefer names_list.deinit(alloc);
var names_buffer = std.ArrayListUnmanaged(u8){};
errdefer names_buffer.deinit(alloc);
for (names.data.e_array.items.slice()) |*item| {
if (item.data != .e_string) {
return error.InvalidSourceMap;
}
const str = try item.data.e_string.string(arena);
names_list.appendAssumeCapacity(try bun.Semver.String.initAppendIfNeeded(alloc, &names_buffer, str));
}
map_data.mappings.names = names_list.items;
map_data.mappings.names_buffer = .moveFromList(&names_buffer);
}
}
}
const ptr = bun.new(ParsedSourceMap, map_data);
ptr.external_source_names = source_paths_slice.?;
break :map ptr;
} else null;
errdefer if (map) |m| m.deref();
const mapping, const source_index = switch (hint) {
.source_only => |index| .{ null, index },
.all => |loc| brk: {
const mapping = map.?.mappings.find(.fromZeroBased(loc.line), .fromZeroBased(loc.column)) orelse
break :brk .{ null, null };
break :brk .{ mapping, std.math.cast(u32, mapping.source_index) };
},
.mappings_only => .{ null, null },
};
const content_slice: ?[]const u8 = if (hint != .mappings_only and
source_index != null and
source_index.? < sources_content.items.len)
content: {
const item = sources_content.items.slice()[source_index.?];
if (item.data != .e_string) {
break :content null;
}
const str = bun.handleOom(item.data.e_string.string(arena));
if (str.len == 0) {
break :content null;
}
break :content try alloc.dupe(u8, str);
} else null;
return .{
.map = map,
.mapping = mapping,
.source_contents = content_slice,
};
}
/// Corresponds to a segment in the "mappings" field of a sourcemap
pub const Mapping = @import("./Mapping.zig");
pub const ParseResult = union(enum) {
fail: struct {
loc: Logger.Loc,
err: anyerror,
value: i32 = 0,
msg: []const u8 = "",
pub fn toData(this: @This(), path: []const u8) Logger.Data {
return Logger.Data{
.location = Logger.Location{
.file = path,
.offset = this.loc.toUsize(),
// TODO: populate correct line and column information
.line = -1,
.column = -1,
},
.text = this.msg,
};
}
},
success: ParsedSourceMap,
};
pub const ParsedSourceMap = @import("./ParsedSourceMap.zig");
/// For some sourcemap loading code, this enum is used as a hint if it should
/// bother loading source code into memory. Most uses of source maps only care
/// about filenames and source mappings, and we should avoid loading contents
/// whenever possible.
pub const SourceContentHandling = enum(u1) {
no_source_contents,
source_contents,
};
/// For some sourcemap loading code, this enum is used as a hint if we already
/// know if the sourcemap is located on disk or inline in the source code.
pub const SourceMapLoadHint = enum(u2) {
none,
is_inline_map,
is_external_map,
};
/// Always returns UTF-8
fn findSourceMappingURL(comptime T: type, source: []const T, alloc: std.mem.Allocator) ?bun.jsc.ZigString.Slice {
const needle = comptime bun.strings.literal(T, "\n//# sourceMappingURL=");
const found = std.mem.lastIndexOf(T, source, needle) orelse return null;
const end = std.mem.indexOfScalarPos(T, source, found + needle.len, '\n') orelse source.len;
const url = std.mem.trimRight(T, source[found + needle.len .. end], &.{ ' ', '\r' });
return switch (T) {
u8 => bun.jsc.ZigString.Slice.fromUTF8NeverFree(url),
u16 => bun.jsc.ZigString.Slice.init(
alloc,
bun.handleOom(bun.strings.toUTF8Alloc(alloc, url)),
),
else => @compileError("Not Supported"),
};
}
/// The last two arguments to this specify loading hints
pub fn getSourceMapImpl(
comptime SourceProviderKind: type,
provider: *SourceProviderKind,
source_filename: []const u8,
load_hint: SourceMapLoadHint,
result: ParseUrlResultHint,
) ?SourceMap.ParseUrl {
// This was previously 65535 but that is a size that can risk stack overflow
// and due to the many layers of indirections and wrappers this function is called in, it
// is difficult to reason about how deeply nested of a callstack this
// function is called in. 1024 is a safer number.
//
// TODO: Experiment in debug builds calculating how much stack space we have left and using that to
// adjust the size
const STACK_SPACE_TO_USE = 1024;
var sfb = std.heap.stackFallback(STACK_SPACE_TO_USE, bun.default_allocator);
var arena = bun.ArenaAllocator.init(sfb.get());
defer arena.deinit();
const allocator = arena.allocator();
const new_load_hint: SourceMapLoadHint, const parsed = parsed: {
var inline_err: ?anyerror = null;
// try to get an inline source map
if (load_hint != .is_external_map) try_inline: {
const source = SourceProviderKind.getSourceSlice(provider);
defer source.deref();
bun.assert(source.tag == .ZigString);
const maybe_found_url = found_url: {
if (source.is8Bit())
break :found_url findSourceMappingURL(u8, source.latin1(), allocator);
break :found_url findSourceMappingURL(u16, source.utf16(), allocator);
};
const found_url = maybe_found_url orelse break :try_inline;
defer found_url.deinit();
const parsed = parseUrl(
bun.default_allocator,
allocator,
found_url.slice(),
result,
) catch |err| {
inline_err = err;
break :try_inline;
};
break :parsed .{
.is_inline_map,
parsed,
};
}
// try to load a .map file
if (load_hint != .is_inline_map) try_external: {
if (comptime SourceProviderKind == DevServerSourceProvider) {
// For DevServerSourceProvider, get the source map JSON directly
const source_map_data = provider.getSourceMapJSON();
if (source_map_data.length == 0) {
break :try_external;
}
const json_slice = source_map_data.ptr[0..source_map_data.length];
// Parse the JSON source map
break :parsed .{
.is_external_map,
parseJSON(
bun.default_allocator,
allocator,
json_slice,
result,
) catch |err| {
// Print warning even if this came from non-visible code like
// calling `error.stack`. This message is only printed if
// the sourcemap has been found but is invalid, such as being
// invalid JSON text or corrupt mappings.
bun.Output.warn("Could not decode sourcemap in dev server runtime: {s} - {s}", .{
source_filename,
@errorName(err),
}); // Disable the "try using --sourcemap=external" hint
bun.jsc.SavedSourceMap.MissingSourceMapNoteInfo.seen_invalid = true;
return null;
},
};
}
if (comptime SourceProviderKind == BakeSourceProvider) fallback_to_normal: {
const global = bun.jsc.VirtualMachine.get().global;
// If we're using bake's production build the global object will
// be Bake::GlobalObject and we can fetch the sourcemap from it,
// if not fallback to the normal way
if (!BakeGlobalObject__isBakeGlobalObject(global)) {
break :fallback_to_normal;
}
const data = BakeSourceProvider.getExternal(
provider,
global,
source_filename,
);
break :parsed .{
.is_external_map,
parseJSON(
bun.default_allocator,
allocator,
data,
result,
) catch |err| {
// Print warning even if this came from non-visible code like
// calling `error.stack`. This message is only printed if
// the sourcemap has been found but is invalid, such as being
// invalid JSON text or corrupt mappings.
bun.Output.warn("Could not decode sourcemap in '{s}': {s}", .{
source_filename,
@errorName(err),
}); // Disable the "try using --sourcemap=external" hint
bun.jsc.SavedSourceMap.MissingSourceMapNoteInfo.seen_invalid = true;
return null;
},
};
}
var load_path_buf: *bun.PathBuffer = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(load_path_buf);
if (source_filename.len + 4 > load_path_buf.len)
break :try_external;
@memcpy(load_path_buf[0..source_filename.len], source_filename);
@memcpy(load_path_buf[source_filename.len..][0..4], ".map");
const load_path = load_path_buf[0 .. source_filename.len + 4];
const data = switch (bun.sys.File.readFrom(std.fs.cwd(), load_path, allocator)) {
.err => break :try_external,
.result => |data| data,
};
break :parsed .{
.is_external_map,
parseJSON(
bun.default_allocator,
allocator,
data,
result,
) catch |err| {
// Print warning even if this came from non-visible code like
// calling `error.stack`. This message is only printed if
// the sourcemap has been found but is invalid, such as being
// invalid JSON text or corrupt mappings.
bun.Output.warn("Could not decode sourcemap in '{s}': {s}", .{
source_filename,
@errorName(err),
}); // Disable the "try using --sourcemap=external" hint
bun.jsc.SavedSourceMap.MissingSourceMapNoteInfo.seen_invalid = true;
return null;
},
};
}
if (inline_err) |err| {
bun.Output.warn("Could not decode sourcemap in '{s}': {s}", .{
source_filename,
@errorName(err),
});
// Disable the "try using --sourcemap=external" hint
bun.jsc.SavedSourceMap.MissingSourceMapNoteInfo.seen_invalid = true;
return null;
}
return null;
};
if (parsed.map) |ptr| {
ptr.underlying_provider = SourceProviderKind.toSourceContentPtr(provider);
ptr.underlying_provider.load_hint = new_load_hint;
}
return parsed;
}
/// This is a pointer to a ZigSourceProvider that may or may not have a `//# sourceMappingURL` comment
/// when we want to lookup this data, we will then resolve it to a ParsedSourceMap if it does.
///
/// This is used for files that were pre-bundled with `bun build --target=bun --sourcemap`
pub const SourceProviderMap = opaque {
extern fn ZigSourceProvider__getSourceSlice(*SourceProviderMap) bun.String;
pub const getSourceSlice = ZigSourceProvider__getSourceSlice;
pub fn toSourceContentPtr(this: *SourceProviderMap) ParsedSourceMap.SourceContentPtr {
return ParsedSourceMap.SourceContentPtr.fromProvider(this);
}
/// The last two arguments to this specify loading hints
pub fn getSourceMap(
provider: *SourceProviderMap,
source_filename: []const u8,
load_hint: SourceMapLoadHint,
result: ParseUrlResultHint,
) ?SourceMap.ParseUrl {
return getSourceMapImpl(
SourceProviderMap,
provider,
source_filename,
load_hint,
result,
);
}
};
extern "c" fn BakeGlobalObject__isBakeGlobalObject(global: *bun.jsc.JSGlobalObject) bool;
extern "c" fn BakeGlobalObject__getPerThreadData(global: *bun.jsc.JSGlobalObject) *bun.bake.production.PerThread;
pub const BakeSourceProvider = opaque {
extern fn BakeSourceProvider__getSourceSlice(*BakeSourceProvider) bun.String;
pub const getSourceSlice = BakeSourceProvider__getSourceSlice;
pub fn toSourceContentPtr(this: *BakeSourceProvider) ParsedSourceMap.SourceContentPtr {
return ParsedSourceMap.SourceContentPtr.fromBakeProvider(this);
}
pub fn getExternal(_: *BakeSourceProvider, global: *bun.jsc.JSGlobalObject, source_filename: []const u8) []const u8 {
bun.assert(BakeGlobalObject__isBakeGlobalObject(global));
const pt = BakeGlobalObject__getPerThreadData(global);
if (pt.source_maps.get(source_filename)) |value| {
return pt.bundled_outputs[value.get()].value.asSlice();
}
return "";
}
/// The last two arguments to this specify loading hints
pub fn getSourceMap(
provider: *BakeSourceProvider,
source_filename: []const u8,
load_hint: SourceMap.SourceMapLoadHint,
result: SourceMap.ParseUrlResultHint,
) ?SourceMap.ParseUrl {
return getSourceMapImpl(
BakeSourceProvider,
provider,
source_filename,
load_hint,
result,
);
}
};
pub const DevServerSourceProvider = opaque {
pub const SourceMapData = extern struct {
ptr: [*]const u8,
length: usize,
};
extern fn DevServerSourceProvider__getSourceSlice(*DevServerSourceProvider) bun.String;
extern fn DevServerSourceProvider__getSourceMapJSON(*DevServerSourceProvider) SourceMapData;
pub const getSourceSlice = DevServerSourceProvider__getSourceSlice;
pub const getSourceMapJSON = DevServerSourceProvider__getSourceMapJSON;
pub fn toSourceContentPtr(this: *DevServerSourceProvider) ParsedSourceMap.SourceContentPtr {
return ParsedSourceMap.SourceContentPtr.fromDevServerProvider(this);
}
/// The last two arguments to this specify loading hints
pub fn getSourceMap(
provider: *DevServerSourceProvider,
source_filename: []const u8,
load_hint: SourceMap.SourceMapLoadHint,
result: SourceMap.ParseUrlResultHint,
) ?SourceMap.ParseUrl {
return getSourceMapImpl(
DevServerSourceProvider,
provider,
source_filename,
load_hint,
result,
);
}
};
/// The sourcemap spec says line and column offsets are zero-based
pub const LineColumnOffset = struct {
/// The zero-based line offset
lines: bun.Ordinal = bun.Ordinal.start,
/// The zero-based column offset
columns: bun.Ordinal = bun.Ordinal.start,
pub const Optional = union(enum) {
null: void,
value: LineColumnOffset,
pub fn advance(this: *Optional, input: []const u8) void {
switch (this.*) {
.null => {},
.value => |*v| v.advance(input),
}
}
pub fn reset(this: *Optional) void {
switch (this.*) {
.null => {},
.value => this.* = .{ .value = .{} },
}
}
};
pub fn add(this: *LineColumnOffset, b: LineColumnOffset) void {
if (b.lines.zeroBased() == 0) {
this.columns = this.columns.add(b.columns);
} else {
this.lines = this.lines.add(b.lines);
this.columns = b.columns;
}
}
pub fn advance(this_ptr: *LineColumnOffset, input: []const u8) void {
// Instead of mutating `this_ptr` directly, copy the state to the stack and do
// all the work here, then move it back to the input pointer. When sourcemaps
// are enabled, this function is extremely hot.
var this = this_ptr.*;
defer this_ptr.* = this;
var offset: u32 = 0;
while (strings.indexOfNewlineOrNonASCII(input, offset)) |i| {
assert(i >= offset);
assert(i < input.len);
var iter = strings.CodepointIterator.initOffset(input, i);
var cursor = strings.CodepointIterator.Cursor{ .i = @as(u32, @truncate(iter.i)) };
_ = iter.next(&cursor);
// Given a null byte, cursor.width becomes 0
// This can lead to integer overflow, crashes, or hangs.
// https://github.com/oven-sh/bun/issues/10624
if (cursor.width == 0) {
this.columns = this.columns.addScalar(1);
offset = i + 1;
continue;
}
offset = i + cursor.width;
switch (cursor.c) {
'\r', '\n', 0x2028, 0x2029 => {
// Handle Windows-specific "\r\n" newlines
if (cursor.c == '\r' and input.len > i + 1 and input[i + 1] == '\n') {
this.columns = this.columns.addScalar(1);
continue;
}
this.lines = this.lines.addScalar(1);
this.columns = bun.Ordinal.start;
},
else => |c| {
// Mozilla's "source-map" library counts columns using UTF-16 code units
this.columns = this.columns.addScalar(switch (c) {
0...0xFFFF => 1,
else => 2,
});
},
}
}
const remain = input[offset..];
if (bun.Environment.allow_assert) {
assert(bun.strings.isAllASCII(remain));
assert(!bun.strings.containsChar(remain, '\n'));
assert(!bun.strings.containsChar(remain, '\r'));
}
this.columns = this.columns.addScalar(@intCast(remain.len));
}
pub fn comesBefore(a: LineColumnOffset, b: LineColumnOffset) bool {
return a.lines.zeroBased() < b.lines.zeroBased() or (a.lines.zeroBased() == b.lines.zeroBased() and a.columns.zeroBased() < b.columns.zeroBased());
}
pub fn cmp(_: void, a: LineColumnOffset, b: LineColumnOffset) std.math.Order {
if (a.lines.zeroBased() != b.lines.zeroBased()) {
return std.math.order(a.lines.zeroBased(), b.lines.zeroBased());
}
return std.math.order(a.columns.zeroBased(), b.columns.zeroBased());
}
};
pub const SourceContent = struct {
value: []const u16 = &[_]u16{},
quoted: []const u8 = &[_]u8{},
};
pub fn find(
this: *const SourceMap,
line: bun.Ordinal,
column: bun.Ordinal,
) ?Mapping {
return this.mapping.find(line, column);
}
pub const SourceMapShifts = struct {
before: LineColumnOffset,
after: LineColumnOffset,
};
pub const SourceMapPieces = struct {
prefix: std.array_list.Managed(u8),
mappings: std.array_list.Managed(u8),
suffix: std.array_list.Managed(u8),
pub fn init(allocator: std.mem.Allocator) SourceMapPieces {
return .{
.prefix = std.array_list.Managed(u8).init(allocator),
.mappings = std.array_list.Managed(u8).init(allocator),
.suffix = std.array_list.Managed(u8).init(allocator),
};
}
pub fn hasContent(this: *SourceMapPieces) bool {
return (this.prefix.items.len + this.mappings.items.len + this.suffix.items.len) > 0;
}
pub fn finalize(this: *SourceMapPieces, allocator: std.mem.Allocator, _shifts: []SourceMapShifts) ![]const u8 {
var shifts = _shifts;
var start_of_run: usize = 0;
var current: usize = 0;
var generated = LineColumnOffset{};
var prev_shift_column_delta: i32 = 0;
// the joiner's node allocator contains string join nodes as well as some vlq encodings
// it doesnt contain json payloads or source code, so 16kb is probably going to cover
// most applications.
var sfb = std.heap.stackFallback(16384, bun.default_allocator);
var j = StringJoiner{ .allocator = sfb.get() };
j.pushStatic(this.prefix.items);
const mappings = this.mappings.items;
while (current < mappings.len) {
if (mappings[current] == ';') {
generated.lines = generated.lines.addScalar(1);
generated.columns = bun.Ordinal.start;
prev_shift_column_delta = 0;
current += 1;
continue;
}
const potential_end_of_run = current;
const decode_result = decodeVLQ(mappings, current);
generated.columns = generated.columns.addScalar(decode_result.value);
current = decode_result.start;
const potential_start_of_run = current;
current = decodeVLQAssumeValid(mappings, current).start;
current = decodeVLQAssumeValid(mappings, current).start;
current = decodeVLQAssumeValid(mappings, current).start;
if (current < mappings.len) {
const c = mappings[current];
if (c != ',' and c != ';') {
current = decodeVLQAssumeValid(mappings, current).start;
}
}
if (current < mappings.len and mappings[current] == ',') {
current += 1;
}
var did_cross_boundary = false;
if (shifts.len > 1 and shifts[1].before.comesBefore(generated)) {
shifts = shifts[1..];
did_cross_boundary = true;
}
if (!did_cross_boundary) {
continue;
}
const shift = shifts[0];
if (shift.after.lines.zeroBased() != generated.lines.zeroBased()) {
continue;
}
j.pushStatic(mappings[start_of_run..potential_end_of_run]);
assert(shift.before.lines.zeroBased() == shift.after.lines.zeroBased());
const shift_column_delta = shift.after.columns.zeroBased() - shift.before.columns.zeroBased();
const vlq_value = decode_result.value + shift_column_delta - prev_shift_column_delta;
const encode = VLQ.encode(vlq_value);
j.pushCloned(encode.slice());
prev_shift_column_delta = shift_column_delta;
start_of_run = potential_start_of_run;
}
j.pushStatic(mappings[start_of_run..]);
const str = try j.doneWithEnd(allocator, this.suffix.items);
bun.assert(str[0] == '{'); // invalid json
return str;
}
};
// -- comment from esbuild --
// Source map chunks are computed in parallel for speed. Each chunk is relative
// to the zero state instead of being relative to the end state of the previous
// chunk, since it's impossible to know the end state of the previous chunk in
// a parallel computation.
//
// After all chunks are computed, they are joined together in a second pass.
// This rewrites the first mapping in each chunk to be relative to the end
// state of the previous chunk.
pub fn appendSourceMapChunk(
j: *StringJoiner,
allocator: std.mem.Allocator,
prev_end_state_: SourceMapState,
start_state_: SourceMapState,
source_map_: []const u8,
) !void {
var prev_end_state = prev_end_state_;
var start_state = start_state_;
// Handle line breaks in between this mapping and the previous one
if (start_state.generated_line != 0) {
j.push(try strings.repeatingAlloc(allocator, @intCast(start_state.generated_line), ';'), allocator);
prev_end_state.generated_column = 0;
}
// Skip past any leading semicolons, which indicate line breaks
var source_map = source_map_;
if (strings.indexOfNotChar(source_map, ';')) |semicolons| {
if (semicolons > 0) {
j.pushStatic(source_map[0..semicolons]);
source_map = source_map[semicolons..];
prev_end_state.generated_column = 0;
start_state.generated_column = 0;
}
}
// Strip off the first mapping from the buffer. The first mapping should be
// for the start of the original file (the printer always generates one for
// the start of the file).
var i: usize = 0;
const generated_column = decodeVLQAssumeValid(source_map, i);
i = generated_column.start;
const source_index = decodeVLQAssumeValid(source_map, i);
i = source_index.start;
const original_line = decodeVLQAssumeValid(source_map, i);
i = original_line.start;
const original_column = decodeVLQAssumeValid(source_map, i);
i = original_column.start;
source_map = source_map[i..];
// Rewrite the first mapping to be relative to the end state of the previous
// chunk. We now know what the end state is because we're in the second pass
// where all chunks have already been generated.
start_state.source_index += source_index.value;
start_state.generated_column += generated_column.value;
start_state.original_line += original_line.value;
start_state.original_column += original_column.value;
var str = MutableString.initEmpty(allocator);
appendMappingToBuffer(&str, j.lastByte(), prev_end_state, start_state);
j.push(str.slice(), allocator);
// Then append everything after that without modification.
j.pushStatic(source_map);
}
pub fn appendSourceMappingURLRemote(
origin: URL,
source: *const Logger.Source,
asset_prefix_path: []const u8,
comptime Writer: type,
writer: Writer,
) !void {
try writer.writeAll("\n//# sourceMappingURL=");
try writer.writeAll(strings.withoutTrailingSlash(origin.href));
if (asset_prefix_path.len > 0)
try writer.writeAll(asset_prefix_path);
if (source.path.pretty.len > 0 and source.path.pretty[0] != '/') {
try writer.writeAll("/");
}
try writer.writeAll(source.path.pretty);
try writer.writeAll(".map");
}
/// This function is extremely hot.
pub fn appendMappingToBuffer(buffer: *MutableString, last_byte: u8, prev_state: SourceMapState, current_state: SourceMapState) void {
const needs_comma = last_byte != 0 and last_byte != ';' and last_byte != '"';
const vlqs = [_]VLQ{
// Record the generated column (the line is recorded using ';' elsewhere)
.encode(current_state.generated_column -| prev_state.generated_column),
// Record the generated source
.encode(current_state.source_index -| prev_state.source_index),
// Record the original line
.encode(current_state.original_line -| prev_state.original_line),
// Record the original column
.encode(current_state.original_column -| prev_state.original_column),
};
// Count exactly how many bytes we need to write
const total_len = @as(usize, vlqs[0].len) +
@as(usize, vlqs[1].len) +
@as(usize, vlqs[2].len) +
@as(usize, vlqs[3].len);
// Instead of updating .len 5 times, we only need to update it once.
var writable = buffer.writableNBytes(total_len + @as(usize, @intFromBool(needs_comma))) catch unreachable;
// Put commas in between mappings
if (needs_comma) {
writable[0] = ',';
writable = writable[1..];
}
inline for (&vlqs) |item| {
@memcpy(writable[0..item.len], item.slice());
writable = writable[item.len..];
}
}
pub const Chunk = @import("./Chunk.zig");
/// https://sentry.engineering/blog/the-case-for-debug-ids
/// https://github.com/mitsuhiko/source-map-rfc/blob/proposals/debug-id/proposals/debug-id.md
/// https://github.com/source-map/source-map-rfc/pull/20
/// https://github.com/getsentry/rfcs/blob/main/text/0081-sourcemap-debugid.md#the-debugid-format
pub const DebugIDFormatter = struct {
id: u64 = 0,
pub fn format(self: DebugIDFormatter, writer: *std.Io.Writer) !void {
// The RFC asks for a UUID, which is 128 bits (32 hex chars). Our hashes are only 64 bits.
// We fill the end of the id with "bun!bun!" hex encoded
var buf: [32]u8 = undefined;
const formatter = bun.fmt.hexIntUpper(self.id);
_ = std.fmt.bufPrint(&buf, "{f}64756E2164756E21", .{formatter}) catch unreachable;
try writer.writeAll(&buf);
}
};
pub const coverage = @import("./CodeCoverage.zig");
pub const VLQ = @import("./VLQ.zig");
pub const LineOffsetTable = @import("./LineOffsetTable.zig");
pub const JSSourceMap = @import("./JSSourceMap.zig");
const decodeVLQAssumeValid = VLQ.decodeAssumeValid;
const decodeVLQ = VLQ.decode;
const string = []const u8;
const std = @import("std");
const bun = @import("bun");
const Logger = bun.logger;
const MutableString = bun.MutableString;
const StringJoiner = bun.StringJoiner;
const URL = bun.URL;
const assert = bun.assert;
const strings = bun.strings;