Skip to content

Commit cb8b80c

Browse files
Merge pull request #345 from lightpanda-io/modules
browser: support for modules
2 parents 5811577 + d777d77 commit cb8b80c

File tree

4 files changed

+216
-78
lines changed

4 files changed

+216
-78
lines changed

.github/actions/install/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ inputs:
1717
zig-v8:
1818
description: 'zig v8 version to install'
1919
required: false
20-
default: 'v0.1.9'
20+
default: 'v0.1.11'
2121
v8:
2222
description: 'v8 version to install'
2323
required: false

src/browser/browser.zig

Lines changed: 110 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const Mime = @import("mime.zig");
2929
const jsruntime = @import("jsruntime");
3030
const Loop = jsruntime.Loop;
3131
const Env = jsruntime.Env;
32+
const Module = jsruntime.Module;
3233

3334
const apiweb = @import("../apiweb.zig");
3435

@@ -125,6 +126,21 @@ pub const Session = struct {
125126
try self.env.load(&self.jstypes);
126127
}
127128

129+
fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module {
130+
_ = referrer;
131+
132+
const self: *Session = @ptrCast(@alignCast(ctx));
133+
134+
if (self.page == null) return error.NoPage;
135+
136+
log.debug("fetch module: specifier: {s}", .{specifier});
137+
const alloc = self.arena.allocator();
138+
const body = try self.page.?.fetchData(alloc, specifier);
139+
defer alloc.free(body);
140+
141+
return self.env.compileModule(body, specifier);
142+
}
143+
128144
fn deinit(self: *Session) void {
129145
if (self.page) |*p| p.end();
130146

@@ -362,6 +378,9 @@ pub const Page = struct {
362378
log.debug("start js env", .{});
363379
try self.session.env.start();
364380

381+
// register the module loader
382+
try self.session.env.setModuleLoadFn(self.session, Session.fetchModule);
383+
365384
// load polyfills
366385
try polyfill.load(alloc, self.session.env);
367386

@@ -388,7 +407,7 @@ pub const Page = struct {
388407
// sasync stores scripts which can be run asynchronously.
389408
// for now they are just run after the non-async one in order to
390409
// dispatch DOMContentLoaded the sooner as possible.
391-
var sasync = std.ArrayList(*parser.Element).init(alloc);
410+
var sasync = std.ArrayList(Script).init(alloc);
392411
defer sasync.deinit();
393412

394413
const root = parser.documentToNode(doc);
@@ -403,21 +422,10 @@ pub const Page = struct {
403422
}
404423

405424
const e = parser.nodeToElement(next.?);
406-
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
407-
408-
// ignore non-script tags
409-
if (tag != .script) continue;
410425

411426
// ignore non-js script.
412-
// > type
413-
// > Attribute is not set (default), an empty string, or a JavaScript MIME
414-
// > type indicates that the script is a "classic script", containing
415-
// > JavaScript code.
416-
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
417-
const stype = try parser.elementGetAttribute(e, "type");
418-
if (!isJS(stype)) {
419-
continue;
420-
}
427+
const script = try Script.init(e) orelse continue;
428+
if (script.kind == .unknown) continue;
421429

422430
// Ignore the defer attribute b/c we analyze all script
423431
// after the document has been parsed.
@@ -431,8 +439,8 @@ pub const Page = struct {
431439
// > then the classic script will be fetched in parallel to
432440
// > parsing and evaluated as soon as it is available.
433441
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
434-
if (try parser.elementGetAttribute(e, "async") != null) {
435-
try sasync.append(e);
442+
if (script.isasync) {
443+
try sasync.append(script);
436444
continue;
437445
}
438446

@@ -455,7 +463,7 @@ pub const Page = struct {
455463
// > page.
456464
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
457465
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
458-
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
466+
self.evalScript(script) catch |err| log.warn("evaljs: {any}", .{err});
459467
try parser.documentHTMLSetCurrentScript(html_doc, null);
460468
}
461469

@@ -472,9 +480,9 @@ pub const Page = struct {
472480
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
473481

474482
// eval async scripts.
475-
for (sasync.items) |e| {
476-
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
477-
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
483+
for (sasync.items) |s| {
484+
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
485+
self.evalScript(s) catch |err| log.warn("evaljs: {any}", .{err});
478486
try parser.documentHTMLSetCurrentScript(html_doc, null);
479487
}
480488

@@ -496,15 +504,15 @@ pub const Page = struct {
496504
// evalScript evaluates the src in priority.
497505
// if no src is present, we evaluate the text source.
498506
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
499-
fn evalScript(self: *Page, e: *parser.Element) !void {
507+
fn evalScript(self: *Page, s: Script) !void {
500508
const alloc = self.arena.allocator();
501509

502510
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
503-
const opt_src = try parser.elementGetAttribute(e, "src");
511+
const opt_src = try parser.elementGetAttribute(s.element, "src");
504512
if (opt_src) |src| {
505513
log.debug("starting GET {s}", .{src});
506514

507-
self.fetchScript(src) catch |err| {
515+
self.fetchScript(s) catch |err| {
508516
switch (err) {
509517
FetchError.BadStatusCode => return err,
510518

@@ -523,26 +531,10 @@ pub const Page = struct {
523531
return;
524532
}
525533

526-
var try_catch: jsruntime.TryCatch = undefined;
527-
try_catch.init(self.session.env);
528-
defer try_catch.deinit();
529-
530-
const opt_text = try parser.nodeTextContent(parser.elementToNode(e));
534+
// TODO handle charset attribute
535+
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
531536
if (opt_text) |text| {
532-
// TODO handle charset attribute
533-
const res = self.session.env.exec(text, "") catch {
534-
if (try try_catch.err(alloc, self.session.env)) |msg| {
535-
defer alloc.free(msg);
536-
log.info("eval inline {s}: {s}", .{ text, msg });
537-
}
538-
return;
539-
};
540-
541-
if (builtin.mode == .Debug) {
542-
const msg = try res.toString(alloc, self.session.env);
543-
defer alloc.free(msg);
544-
log.debug("eval inline {s}", .{msg});
545-
}
537+
try s.eval(alloc, self.session.env, text);
546538
return;
547539
}
548540

@@ -557,12 +549,9 @@ pub const Page = struct {
557549
JsErr,
558550
};
559551

560-
// fetchScript senf a GET request to the src and execute the script
561-
// received.
562-
fn fetchScript(self: *Page, src: []const u8) !void {
563-
const alloc = self.arena.allocator();
564-
565-
log.debug("starting fetch script {s}", .{src});
552+
// the caller owns the returned string
553+
fn fetchData(self: *Page, alloc: std.mem.Allocator, src: []const u8) ![]const u8 {
554+
log.debug("starting fetch {s}", .{src});
566555

567556
var buffer: [1024]u8 = undefined;
568557
var b: []u8 = buffer[0..];
@@ -573,46 +562,91 @@ pub const Page = struct {
573562

574563
const resp = fetchres.req.response;
575564

576-
log.info("fetch script {any}: {d}", .{ u, resp.status });
565+
log.info("fetch {any}: {d}", .{ u, resp.status });
577566

578567
if (resp.status != .ok) return FetchError.BadStatusCode;
579568

580569
// TODO check content-type
581570
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
582-
defer alloc.free(body);
583571

584572
// check no body
585573
if (body.len == 0) return FetchError.NoBody;
586574

587-
var try_catch: jsruntime.TryCatch = undefined;
588-
try_catch.init(self.session.env);
589-
defer try_catch.deinit();
575+
return body;
576+
}
590577

591-
const res = self.session.env.exec(body, src) catch {
592-
if (try try_catch.err(alloc, self.session.env)) |msg| {
593-
defer alloc.free(msg);
594-
log.info("eval remote {s}: {s}", .{ src, msg });
595-
}
596-
return FetchError.JsErr;
578+
// fetchScript senf a GET request to the src and execute the script
579+
// received.
580+
fn fetchScript(self: *Page, s: Script) !void {
581+
const alloc = self.arena.allocator();
582+
const body = try self.fetchData(alloc, s.src);
583+
defer alloc.free(body);
584+
585+
try s.eval(alloc, self.session.env, body);
586+
}
587+
588+
const Script = struct {
589+
element: *parser.Element,
590+
kind: Kind,
591+
isasync: bool,
592+
593+
src: []const u8,
594+
595+
const Kind = enum {
596+
unknown,
597+
javascript,
598+
module,
597599
};
598600

599-
if (builtin.mode == .Debug) {
600-
const msg = try res.toString(alloc, self.session.env);
601-
defer alloc.free(msg);
602-
log.debug("eval remote {s}: {s}", .{ src, msg });
601+
fn init(e: *parser.Element) !?Script {
602+
// ignore non-script tags
603+
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
604+
if (tag != .script) return null;
605+
606+
return .{
607+
.element = e,
608+
.kind = kind(try parser.elementGetAttribute(e, "type")),
609+
.isasync = try parser.elementGetAttribute(e, "async") != null,
610+
611+
.src = try parser.elementGetAttribute(e, "src") orelse "inline",
612+
};
603613
}
604-
}
605614

606-
// > type
607-
// > Attribute is not set (default), an empty string, or a JavaScript MIME
608-
// > type indicates that the script is a "classic script", containing
609-
// > JavaScript code.
610-
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
611-
fn isJS(stype: ?[]const u8) bool {
612-
if (stype == null or stype.?.len == 0) return true;
613-
if (std.mem.eql(u8, stype.?, "application/javascript")) return true;
614-
if (!std.mem.eql(u8, stype.?, "module")) return true;
615-
616-
return false;
617-
}
615+
// > type
616+
// > Attribute is not set (default), an empty string, or a JavaScript MIME
617+
// > type indicates that the script is a "classic script", containing
618+
// > JavaScript code.
619+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
620+
fn kind(stype: ?[]const u8) Kind {
621+
if (stype == null or stype.?.len == 0) return .javascript;
622+
if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript;
623+
if (std.mem.eql(u8, stype.?, "module")) return .module;
624+
625+
return .unknown;
626+
}
627+
628+
fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void {
629+
var try_catch: jsruntime.TryCatch = undefined;
630+
try_catch.init(env);
631+
defer try_catch.deinit();
632+
633+
const res = switch (self.kind) {
634+
.unknown => return error.UnknownScript,
635+
.javascript => env.exec(body, self.src),
636+
.module => env.module(body, self.src),
637+
} catch {
638+
if (try try_catch.err(alloc, env)) |msg| {
639+
defer alloc.free(msg);
640+
log.info("eval script {s}: {s}", .{ self.src, msg });
641+
}
642+
return FetchError.JsErr;
643+
};
644+
645+
if (builtin.mode == .Debug) {
646+
const msg = try res.toString(alloc, env);
647+
defer alloc.free(msg);
648+
log.debug("eval script {s}: {s}", .{ self.src, msg });
649+
}
650+
}
651+
};
618652
};

0 commit comments

Comments
 (0)