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
|
usingnamespace @import("../global.zig");
const std = @import("std");
const options = @import("../options.zig");
const log = @import("../logger.zig");
const cache = @import("../cache.zig");
const logger = @import("../logger.zig");
const js_ast = @import("../js_ast.zig");
const alloc = @import("../alloc.zig");
const fs = @import("../fs.zig");
const resolver = @import("./resolver.zig");
const MainFieldMap = std.StringHashMap(string);
const BrowserMap = std.StringHashMap(string);
threadlocal var hashed_buf: [2048]u8 = undefined;
pub const PackageJSON = struct {
name: string = "",
source: logger.Source,
main_fields: MainFieldMap,
module_type: options.ModuleType,
version: string = "",
hash: u32 = 0,
// Present if the "browser" field is present. This field is intended to be
// used by bundlers and lets you redirect the paths of certain 3rd-party
// modules that don't work in the browser to other modules that shim that
// functionality. That way you don't have to rewrite the code for those 3rd-
// party modules. For example, you might remap the native "util" node module
// to something like https://www.npmjs.com/package/util so it works in the
// browser.
//
// This field contains a mapping of absolute paths to absolute paths. Mapping
// to an empty path indicates that the module is disabled. As far as I can
// tell, the official spec is an abandoned GitHub repo hosted by a user account:
// https://github.com/defunctzombie/package-browser-field-spec. The npm docs
// say almost nothing: https://docs.npmjs.com/files/package.json.
//
// Note that the non-package "browser" map has to be checked twice to match
// Webpack's behavior: once before resolution and once after resolution. It
// leads to some unintuitive failure cases that we must emulate around missing
// file extensions:
//
// * Given the mapping "./no-ext": "./no-ext-browser.js" the query "./no-ext"
// should match but the query "./no-ext.js" should NOT match.
//
// * Given the mapping "./ext.js": "./ext-browser.js" the query "./ext.js"
// should match and the query "./ext" should ALSO match.
//
browser_map: BrowserMap,
pub fn parse(
comptime ResolverType: type,
r: *ResolverType,
input_path: string,
dirname_fd: StoredFileDescriptorType,
comptime generate_hash: bool,
) ?PackageJSON {
const parts = [_]string{ input_path, "package.json" };
const package_json_path_ = r.fs.abs(&parts);
const package_json_path = r.fs.filename_store.append(package_json_path_) catch unreachable;
const entry = r.caches.fs.readFile(r.fs, package_json_path, dirname_fd, false) catch |err| {
if (err != error.IsDir) {
r.log.addErrorFmt(null, logger.Loc.Empty, r.allocator, "Cannot read file \"{s}\": {s}", .{ r.prettyPath(fs.Path.init(input_path)), @errorName(err) }) catch unreachable;
}
return null;
};
if (r.debug_logs) |*debug| {
debug.addNoteFmt("The file \"{s}\" exists", .{package_json_path}) catch unreachable;
}
const key_path = fs.Path.init(package_json_path);
var json_source = logger.Source.initPathString(key_path.text, entry.contents);
json_source.path.pretty = r.prettyPath(json_source.path);
const json: js_ast.Expr = (r.caches.json.parseJSON(r.log, json_source, r.allocator) catch |err| {
if (isDebug) {
Output.printError("{s}: JSON parse error: {s}", .{ package_json_path, @errorName(err) });
}
return null;
} orelse return null);
var package_json = PackageJSON{
.source = json_source,
.module_type = .unknown,
.browser_map = BrowserMap.init(r.allocator),
.main_fields = MainFieldMap.init(r.allocator),
};
if (json.asProperty("version")) |version_json| {
if (version_json.expr.asString(r.allocator)) |version_str| {
package_json.version = r.allocator.dupe(u8, version_str) catch unreachable;
}
}
if (json.asProperty("name")) |version_json| {
if (version_json.expr.asString(r.allocator)) |version_str| {
package_json.name = r.allocator.dupe(u8, version_str) catch unreachable;
}
}
if (json.asProperty("type")) |type_json| {
if (type_json.expr.asString(r.allocator)) |type_str| {
switch (options.ModuleType.List.get(type_str) orelse options.ModuleType.unknown) {
.cjs => {
package_json.module_type = .cjs;
},
.esm => {
package_json.module_type = .esm;
},
.unknown => {
r.log.addRangeWarningFmt(
&json_source,
json_source.rangeOfString(type_json.loc),
r.allocator,
"\"{s}\" is not a valid value for \"type\" field (must be either \"commonjs\" or \"module\")",
.{type_str},
) catch unreachable;
},
}
} else {
r.log.addWarning(&json_source, type_json.loc, "The value for \"type\" must be a string") catch unreachable;
}
}
// Read the "main" fields
for (r.opts.main_fields) |main| {
if (json.asProperty(main)) |main_json| {
const expr: js_ast.Expr = main_json.expr;
if ((expr.asString(r.allocator))) |str| {
if (str.len > 0) {
package_json.main_fields.put(main, str) catch unreachable;
}
}
}
}
// Read the "browser" property, but only when targeting the browser
if (r.opts.platform == .browser) {
// We both want the ability to have the option of CJS vs. ESM and the
// option of having node vs. browser. The way to do this is to use the
// object literal form of the "browser" field like this:
//
// "main": "dist/index.node.cjs.js",
// "module": "dist/index.node.esm.js",
// "browser": {
// "./dist/index.node.cjs.js": "./dist/index.browser.cjs.js",
// "./dist/index.node.esm.js": "./dist/index.browser.esm.js"
// },
//
if (json.asProperty("browser")) |browser_prop| {
switch (browser_prop.expr.data) {
.e_object => |obj| {
// The value is an object
// Remap all files in the browser field
for (obj.properties) |*prop| {
var _key_str = (prop.key orelse continue).asString(r.allocator) orelse continue;
const value: js_ast.Expr = prop.value orelse continue;
// Normalize the path so we can compare against it without getting
// confused by "./". There is no distinction between package paths and
// relative paths for these values because some tools (i.e. Browserify)
// don't make such a distinction.
//
// This leads to weird things like a mapping for "./foo" matching an
// import of "foo", but that's actually not a bug. Or arguably it's a
// bug in Browserify but we have to replicate this bug because packages
// do this in the wild.
const key = r.allocator.dupe(u8, r.fs.normalize(_key_str)) catch unreachable;
switch (value.data) {
.e_string => |str| {
// If this is a string, it's a replacement package
package_json.browser_map.put(key, str.string(r.allocator) catch unreachable) catch unreachable;
},
.e_boolean => |boolean| {
if (!boolean.value) {
package_json.browser_map.put(key, "") catch unreachable;
}
},
else => {
r.log.addWarning(&json_source, value.loc, "Each \"browser\" mapping must be a string or boolean") catch unreachable;
},
}
}
},
else => {},
}
}
}
// TODO: side effects
// TODO: exports map
if (generate_hash) {
std.mem.copy(u8, &hashed_buf, package_json.name);
hashed_buf[package_json.name.len + 1] = '@';
std.mem.copy(u8, hashed_buf[package_json.name.len + 1 ..], package_json.version);
package_json.hash = @truncate(u32, std.hash.Wyhash.hash(0, hashed_buf[0 .. package_json.name.len + 1 + package_json.version.len]));
}
return package_json;
}
};
|