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); pub const PackageJSON = struct { name: string = "", source: logger.Source, main_fields: MainFieldMap, module_type: options.ModuleType, version: string = "", // 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) ?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 return package_json; } };