From f59892f647ceef1c05e40c9cdef4f79d0a530c2f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 13 Aug 2021 02:01:41 -0700 Subject: late Former-commit-id: 1d598bb05a3bac62d86063125e1fe2962f0b5cc6 --- src/bundler.zig | 10 ++-- src/css_scanner.zig | 23 ++++---- src/http.zig | 4 +- src/http/url_path.zig | 72 ++++++++++++++++++++----- src/javascript/jsc/api/router.zig | 46 ++++++++++++++++ src/javascript/jsc/base.zig | 4 ++ src/javascript/jsc/bindings/ZigGlobalObject.cpp | 40 +++++++++++++- src/javascript/jsc/javascript.zig | 3 +- src/js_parser/js_parser.zig | 57 ++++++++++++-------- src/js_printer.zig | 34 ++++++++++-- src/options.zig | 34 +++++++++++- src/runtime.js | 4 +- src/runtime.version | 2 +- 13 files changed, 272 insertions(+), 61 deletions(-) (limited to 'src') diff --git a/src/bundler.zig b/src/bundler.zig index 49b9f496b..377079355 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -101,13 +101,13 @@ pub const ClientEntryPoint = struct { \\ globalThis.onerror.loaded = loaded; \\}} \\ - \\import * as boot from '{s}'; + \\import boot from '{s}'; \\loaded.boot = true; \\if ('setLoaded' in boot) boot.setLoaded(loaded); \\import * as EntryPoint from '{s}{s}'; \\loaded.entry = true; \\ - \\if (!('default' in boot) ) {{ + \\if (!boot) {{ \\ const now = Date.now(); \\ debugger; \\ const elapsed = Date.now() - now; @@ -116,7 +116,7 @@ pub const ClientEntryPoint = struct { \\ }} \\}} \\ - \\boot.default(EntryPoint, loaded); + \\boot(EntryPoint, loaded); , .{ client, @@ -2126,15 +2126,15 @@ pub const Transformer = struct { ) !options.TransformResult { js_ast.Expr.Data.Store.create(allocator); js_ast.Stmt.Data.Store.create(allocator); + const platform = options.Platform.from(opts.platform); - var define = try options.definesFromTransformOptions(allocator, log, opts.define, false); + var define = try options.definesFromTransformOptions(allocator, log, opts.define, false, platform); const cwd = if (opts.absolute_working_dir) |workdir| try std.fs.realpathAlloc(allocator, workdir) else try std.process.getCwdAlloc(allocator); const output_dir_parts = [_]string{ try std.process.getCwdAlloc(allocator), opts.output_dir orelse "out" }; const output_dir = try std.fs.path.join(allocator, &output_dir_parts); var output_files = try std.ArrayList(options.OutputFile).initCapacity(allocator, opts.entry_points.len); - const platform = options.Platform.from(opts.platform); const out_extensions = platform.outExtensions(allocator); var loader_map = try options.loadersFromTransformOptions(allocator, opts.loaders); diff --git a/src/css_scanner.zig b/src/css_scanner.zig index e39515292..298c5f81a 100644 --- a/src/css_scanner.zig +++ b/src/css_scanner.zig @@ -898,16 +898,21 @@ pub fn NewWriter( switch (chunk.content) { .t_url => |url| {}, .t_import => |import| { - try writer.buildCtx.addCSSImport( - try writer.linker.resolveCSS( - writer.source.path, - import.text.utf8, - chunk.range, - import_record.ImportKind.at, - Options.BundleOptions.ImportPathFormat.absolute_path, - true, - ), + const resolved = try writer.linker.resolveCSS( + writer.source.path, + import.text.utf8, + chunk.range, + import_record.ImportKind.at, + Options.BundleOptions.ImportPathFormat.absolute_path, + true, ); + + // TODO: just check is_external instead + if (strings.startsWith(import.text.utf8, "https://") or strings.startsWith(import.text.utf8, "http://")) { + return; + } + + try writer.buildCtx.addCSSImport(resolved); }, .t_verbatim => |verbatim| {}, } diff --git a/src/http.zig b/src/http.zig index 06eaf7a18..ffb7a3d72 100644 --- a/src/http.zig +++ b/src/http.zig @@ -1573,7 +1573,7 @@ pub const RequestContext = struct { break :brk try ctx.bundler.buildFile( &ctx.log, ctx.allocator, - ctx.url.path, + ctx.url.pathWithoutOrigin(ctx.bundler.options.origin), ctx.url.extname, true, ); @@ -1581,7 +1581,7 @@ pub const RequestContext = struct { break :brk try ctx.bundler.buildFile( &ctx.log, ctx.allocator, - ctx.url.path, + ctx.url.pathWithoutOrigin(ctx.bundler.options.origin), ctx.url.extname, false, ); diff --git a/src/http/url_path.zig b/src/http/url_path.zig index a2e55555d..e82d5601a 100644 --- a/src/http/url_path.zig +++ b/src/http/url_path.zig @@ -1,6 +1,8 @@ usingnamespace @import("../global.zig"); -const std = @import("std"); +const PercentEncoding = @import("../query_string_map.zig").PercentEncoding; +const std = @import("std"); +const allocators = @import("../allocators.zig"); const URLPath = @This(); extname: string = "", @@ -8,17 +10,63 @@ path: string = "", pathname: string = "", first_segment: string = "", query_string: string = "", +const toMutable = allocators.constStrToU8; + +// TODO: use a real URL parser +// this treats a URL like /_next/ identically to / +pub fn pathWithoutOrigin(this: *const URLPath, _origin: string) string { + if (_origin.len == 0) return this.path; + const leading_slash_offset: usize = if (_origin[0] == '/') 1 else 0; + const base = this.path; + const origin = _origin[leading_slash_offset..]; + + return if (base.len >= origin.len and strings.eql(base[0..origin.len], origin)) base[origin.len..] else base; +} + +// optimization: very few long strings will be URL-encoded +// we're allocating virtual memory here, so if we never use it, it won't be allocated +// and even when they're, they're probably rarely going to be > 1024 chars long +// so we can have a big and little one and almost always use the little one +threadlocal var temp_path_buf: [1024]u8 = undefined; +threadlocal var big_temp_path_buf: [16384]u8 = undefined; + +pub fn parse(possibly_encoded_pathname_: string) URLPath { + var decoded_pathname = possibly_encoded_pathname_; + + if (strings.indexOfChar(decoded_pathname, '%') != null) { + var possibly_encoded_pathname = switch (decoded_pathname.len) { + 0...1024 => &temp_path_buf, + else => &big_temp_path_buf, + }; + possibly_encoded_pathname = possibly_encoded_pathname[0..std.math.min( + possibly_encoded_pathname_.len, + possibly_encoded_pathname.len, + )]; + + std.mem.copy(u8, possibly_encoded_pathname, possibly_encoded_pathname_[0..possibly_encoded_pathname.len]); + var clone = possibly_encoded_pathname[0..possibly_encoded_pathname.len]; + + var fbs = std.io.fixedBufferStream( + // This is safe because: + // - this comes from a non-const buffer + // - percent *decoding* will always be <= length of the original string (no buffer overflow) + toMutable( + possibly_encoded_pathname, + ), + ); + var writer = fbs.writer(); + decoded_pathname = possibly_encoded_pathname[0 .. PercentEncoding.decode(@TypeOf(writer), writer, clone) catch unreachable]; + } -// This does one pass over the URL path instead of like 4 -pub fn parse(raw_path: string) URLPath { var question_mark_i: i16 = -1; var period_i: i16 = -1; var first_segment_end: i16 = std.math.maxInt(i16); var last_slash: i16 = -1; - var i: i16 = @intCast(i16, raw_path.len) - 1; + var i: i16 = @intCast(i16, decoded_pathname.len) - 1; + while (i >= 0) : (i -= 1) { - const c = raw_path[@intCast(usize, i)]; + const c = decoded_pathname[@intCast(usize, i)]; switch (c) { '?' => { @@ -52,24 +100,24 @@ pub fn parse(raw_path: string) URLPath { const extname = brk: { if (question_mark_i > -1 and period_i > -1) { period_i += 1; - break :brk raw_path[@intCast(usize, period_i)..@intCast(usize, question_mark_i)]; + break :brk decoded_pathname[@intCast(usize, period_i)..@intCast(usize, question_mark_i)]; } else if (period_i > -1) { period_i += 1; - break :brk raw_path[@intCast(usize, period_i)..]; + break :brk decoded_pathname[@intCast(usize, period_i)..]; } else { break :brk &([_]u8{}); } }; - const path = if (question_mark_i < 0) raw_path[1..] else raw_path[1..@intCast(usize, question_mark_i)]; + const path = if (question_mark_i < 0) decoded_pathname[1..] else decoded_pathname[1..@intCast(usize, question_mark_i)]; - const first_segment = raw_path[1..std.math.min(@intCast(usize, first_segment_end), raw_path.len)]; + const first_segment = decoded_pathname[1..std.math.min(@intCast(usize, first_segment_end), decoded_pathname.len)]; return URLPath{ .extname = extname, - .pathname = raw_path, + .pathname = decoded_pathname, .first_segment = first_segment, - .path = if (raw_path.len == 1) "." else path, - .query_string = if (question_mark_i > -1) raw_path[@intCast(usize, question_mark_i)..@intCast(usize, raw_path.len)] else "", + .path = if (decoded_pathname.len == 1) "." else path, + .query_string = if (question_mark_i > -1) decoded_pathname[@intCast(usize, question_mark_i)..@intCast(usize, decoded_pathname.len)] else "", }; } diff --git a/src/javascript/jsc/api/router.zig b/src/javascript/jsc/api/router.zig index e2283e9c5..84562fee3 100644 --- a/src/javascript/jsc/api/router.zig +++ b/src/javascript/jsc/api/router.zig @@ -16,6 +16,7 @@ const Fs = @import("../../../fs.zig"); route: *const FilesystemRouter.Match, query_string_map: ?QueryStringMap = null, +param_map: ?QueryStringMap = null, script_src: ?string = null, script_src_buf: [1024]u8 = undefined, @@ -220,6 +221,23 @@ pub const Instance = NewClass( .query = .{ .@"get" = getQuery, .ro = true, + .ts = d.ts{ + .@"return" = "Record", + .tsdoc = + \\Route parameters & parsed query string values as a key-value object + \\ + \\@example + \\```js + \\console.assert(router.query.id === "123"); + \\console.assert(router.pathname === "/blog/posts/123"); + \\console.assert(router.route === "blog/posts/[id]"); + \\``` + , + }, + }, + .params = .{ + .@"get" = getParams, + .ro = true, .ts = d.ts{ .@"return" = "Record", .tsdoc = @@ -372,6 +390,34 @@ pub fn getScriptSrc( return js.JSValueMakeString(ctx, ZigString.init(src).toJSStringRef()); } +pub fn getParams( + this: *Router, + ctx: js.JSContextRef, + thisObject: js.JSObjectRef, + prop: js.JSStringRef, + exception: js.ExceptionRef, +) js.JSValueRef { + if (this.param_map == null) { + if (this.route.params.len > 0) { + if (QueryStringMap.initWithScanner(getAllocator(ctx), CombinedScanner.init( + "", + this.route.pathnameWithoutLeadingSlash(), + this.route.name, + this.route.params, + ))) |map| { + this.param_map = map; + } else |err| {} + } + } + + // If it's still null, there are no params + if (this.param_map) |*map| { + return createQueryObject(ctx, map, exception); + } else { + return JSValue.createEmptyObject(VirtualMachine.vm.global, 0).asRef(); + } +} + pub fn getQuery( this: *Router, ctx: js.JSContextRef, diff --git a/src/javascript/jsc/base.zig b/src/javascript/jsc/base.zig index 4902c731a..e17d4c7ab 100644 --- a/src/javascript/jsc/base.zig +++ b/src/javascript/jsc/base.zig @@ -205,6 +205,8 @@ pub const Properties = struct { pub const default: string = "default"; pub const include: string = "include"; + pub const env: string = "env"; + pub const GET = "GET"; pub const PUT = "PUT"; pub const POST = "POST"; @@ -278,6 +280,8 @@ pub const Properties = struct { pub var navigate: js.JSStringRef = undefined; pub var follow: js.JSStringRef = undefined; + + pub const env: js.JSStringRef = undefined; }; pub fn init() void { diff --git a/src/javascript/jsc/bindings/ZigGlobalObject.cpp b/src/javascript/jsc/bindings/ZigGlobalObject.cpp index fbe3c22ae..45a68954e 100644 --- a/src/javascript/jsc/bindings/ZigGlobalObject.cpp +++ b/src/javascript/jsc/bindings/ZigGlobalObject.cpp @@ -121,9 +121,40 @@ void GlobalObject::setConsole(void *console) { this->setConsoleClient(makeWeakPtr(m_console)); } +// This is not a publicly exposed API currently. +// This is used by the bundler to make Response, Request, FetchEvent, +// and any other objects available globally. void GlobalObject::installAPIGlobals(JSClassRef *globals, int count) { WTF::Vector extraStaticGlobals; - extraStaticGlobals.reserveCapacity((size_t)count); + extraStaticGlobals.reserveCapacity((size_t)count + 1); + + // This is not nearly a complete implementation. It's just enough to make some npm packages that + // were compiled with Webpack to run without crashing in this environment. + JSC::JSObject *process = JSC::constructEmptyObject(this, this->objectPrototype(), 4); + + // The transpiler inlines all defined process.env vars & dead code eliminates as relevant + // so this is just to return undefined for any missing ones and not crash if something tries to + // modify it or it wasn't statically analyzable + JSC::JSObject *processDotEnv = JSC::constructEmptyObject(this, this->objectPrototype(), 0); + + process->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "env"), processDotEnv); + + // this should be transpiled out, but just incase + process->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "browser"), + JSC::JSValue(false)); + + // this gives some way of identifying at runtime whether the SSR is happening in node or not. + // this should probably be renamed to what the name of the bundler is, instead of "notNodeJS" + // but it must be something that won't evaluate to truthy in Node.js + process->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "notNodeJS"), + JSC::JSValue(true)); +#if defined(__APPLE__) + process->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "platform"), + JSC::jsString(this->vm(), WTF::String("darwin"))); +#else + process->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "platform"), + JSC::jsString(this->vm(), WTF::String("linux"))); +#endif for (int i = 0; i < count; i++) { auto jsClass = globals[i]; @@ -137,7 +168,12 @@ void GlobalObject::installAPIGlobals(JSClassRef *globals, int count) { GlobalPropertyInfo{JSC::Identifier::fromString(vm(), jsClass->className()), JSC::JSValue(object), JSC::PropertyAttribute::DontDelete | 0}); } - this->addStaticGlobals(extraStaticGlobals.data(), count); + + extraStaticGlobals.uncheckedAppend( + GlobalPropertyInfo{JSC::Identifier::fromString(vm(), "process"), JSC::JSValue(process), + JSC::PropertyAttribute::DontDelete | 0}); + + this->addStaticGlobals(extraStaticGlobals.data(), extraStaticGlobals.size()); extraStaticGlobals.releaseBuffer(); } diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index a2d9220ad..0b96ca3ed 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -117,7 +117,7 @@ pub const Wundle = struct { prop: js.JSStringRef, exception: js.ExceptionRef, ) js.JSValueRef { - if (!VirtualMachine.vm.bundler.options.routes.routes_enabled or VirtualMachine.vm.bundler.options.routes.dir.len > 0) { + if (!VirtualMachine.vm.bundler.options.routes.routes_enabled or VirtualMachine.vm.bundler.options.routes.dir.len == 0) { return js.JSValueMakeUndefined(ctx); } @@ -238,6 +238,7 @@ pub const VirtualMachine = struct { log: *logger.Log, event_listeners: EventListenerMixin.Map, main: string = "", + process: js.JSObjectRef = null, pub var vm_loaded = false; pub var vm: *VirtualMachine = undefined; diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig index 8b9649aa2..f6b59141c 100644 --- a/src/js_parser/js_parser.zig +++ b/src/js_parser/js_parser.zig @@ -70,7 +70,7 @@ pub const ImportScanner = struct { var stmt: Stmt = _stmt; switch (stmt.data) { .s_import => |st| { - var record: ImportRecord = p.import_records.items[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 @@ -285,7 +285,7 @@ pub const ImportScanner = struct { // it's really stupid to import all 1,000 components from that design system // when you just want