tacker

a simple web bundler
git clone https://tongong.net/git/tacker.git
Log | Files | Refs | README

commit 5e70cff03b0b9cca82ba30c603bd3d32e2dfa132
parent 7b790c499dad708b29cca419fc751ddedc3f70c3
Author: tongong <tongong@gmx.net>
Date:   Sat, 16 Jul 2022 11:30:00 +0200

finished javascript module bundling

Diffstat:
Mbundle_js.ha | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msearchio/searchio.ha | 3++-
Mtest-page/a.js | 3+++
Mtest-page/b.js | 1-
Mtest-page/c.js | 4+---
5 files changed, 93 insertions(+), 27 deletions(-)

diff --git a/bundle_js.ha b/bundle_js.ha @@ -35,23 +35,22 @@ fn tacker_js(inputpath: str, ofile: io::handle, html: bool) void = { let g: dep_graph = []; defer dep_graph_free(g); dep_add(void, inputpath, &g); - for (let i = 0z; i < len(g); i += 1) { - fmt::printf("{}: {} - ", i, g[i].path)!; - const dep = g[i].dependencies; - for (let j = 0z; j < len(dep); j += 1) { - fmt::printf("{},", dep[j])!; - }; - fmt::println("")!; - }; const sorting = sort_kahn(g, inputpath); defer free(sorting); - fmt::println("sorting:")!; + fmt::fprintln(ofile, "(function() {")!; for (let i = 0z; i < len(sorting); i += 1) { - fmt::printfln("- {}", g[sorting[i]].path)!; + fmt::fprintfln(ofile, "const _tacker{} = (function() {{", sorting[i])!; + fmt::fprintln(ofile, "const module = { exports: {} }, exports = module.exports;")!; + emit_bundled(g[sorting[i]].path, ofile, g, html); + fmt::fprintln(ofile, "return module.exports;")!; + fmt::fprintln(ofile, "})();")!; }; + fmt::fprintln(ofile, "})();")!; }; + let p_req: searchio::pattern = searchio::pattern {...}; +let p_reqscript: searchio::pattern = searchio::pattern {...}; let p_newline: searchio::pattern = searchio::pattern {...}; let p_commentend: searchio::pattern = searchio::pattern {...}; let p_quotedouble: searchio::pattern = searchio::pattern {...}; @@ -60,6 +59,8 @@ let p_quotesingle: searchio::pattern = searchio::pattern {...}; @init fn init() void = { // "/" has to be recognized as regex literal or comment start p_req = searchio::compile(["require(", "/", "\"", "'", "`"]); + p_reqscript = searchio::compile(["require(", "</script", "/", "\"", "'", + "`"]); p_newline = searchio::compile(["\n"]); p_commentend = searchio::compile(["*/"]); p_quotedouble = searchio::compile(["\""]); @@ -68,12 +69,14 @@ let p_quotesingle: searchio::pattern = searchio::pattern {...}; @fini fn fini() void = { defer searchio::finish(p_req); + defer searchio::finish(p_reqscript); defer searchio::finish(p_newline); defer searchio::finish(p_commentend); defer searchio::finish(p_quotedouble); defer searchio::finish(p_quotesingle); }; + // Add a connection frompath -> deppath to the dependency graph // inputs are borrowed fn dep_add(frompath: (str | void), deppath: str, graph: *dep_graph) void = { @@ -103,6 +106,7 @@ fn dep_add(frompath: (str | void), deppath: str, graph: *dep_graph) void = { }; }; + // Recursively scan and add a file to the dependency graph // inputs are borrowed fn dep_scan(inputpath: str, graph: *dep_graph) void = { @@ -149,22 +153,22 @@ fn dep_scan(inputpath: str, graph: *dep_graph) void = { }; }; -// Is returned if the require() is part of a longer identifier -type no = void; // Parse the contents of a require() macro and return the file path. // Return value has to be freed. -fn read_require(in: io::handle, path: str) (str | no) = { - // Check if require() is part of another identifier like my_require() - io::seek(in, -9, io::whence::CUR)!; +// Return void if require() is part of a longer identifier +fn read_require(in: io::handle, path: str) (str | void) = { const buf: [1]u8 = [' ']; - io::read(in, buf)!; - io::seek(in, 8, io::whence::CUR)!; - // this weird string contains all characters that are allowed in a js - // source file but not in an identifier - if (!strings::contains("\t\n\r !%&()*+,-./:;<=>?[]^{|}~", buf[0]: u32: - rune)) - return no; + // Check if require() is part of another identifier like my_require() + if (!(io::seek(in, -9, io::whence::CUR) is io::error)) { + io::read(in, buf)!; + io::seek(in, 8, io::whence::CUR)!; + // this weird string contains all characters that are allowed in + // a js source file but not in an identifier + if (!strings::contains("\t\n\r !%&()*+,-./:;<=>?[]^{|}~", + buf[0]: u32: rune)) + return void; + }; io::read(in, buf)!; let broken = false; @@ -181,6 +185,7 @@ fn read_require(in: io::handle, path: str) (str | no) = { return ""; // will not be reached }; + // Kahn's algorithm https://en.wikipedia.org/wiki/Topological_sorting // Return value has to be freed fn sort_kahn(graph: dep_graph, entrypath: str) []size = { @@ -222,3 +227,63 @@ fn sort_kahn(graph: dep_graph, entrypath: str) []size = { }; return []; // will not be reached }; + + +// Resolve require() and add files to the bundle +// very similar to dep_scan() +fn emit_bundled(inputpath: str, ofile: io::handle, graph: dep_graph, html: bool) + void = { + const ifile = os::open(inputpath)!; + defer io::close(ifile)!; + // Read until require or comment or quote + // if start of string literal etc was found (disabled require) + let disabled = false; + for (true) { + const m = searchio::search(ifile, ofile, p_reqscript); + if (m is size) { + const m = m: size; + if (m == 0) { + if (disabled == false) { + const p = read_require(ifile, + inputpath); + if (p is str) { + const p = p: str; + defer free(p); + const p = resolve_path_require( + p, inputpath); + defer free(p); + let i = 0z; + // could break if files are + // changed in race condition + for (graph[i].path != p) i += 1; + fmt::fprintf(ofile, "_tacker{}", + i)!; + } else fmt::fprint(ofile, "require(")!; + } else fmt::fprint(ofile, "require(")!; + } else if (m == 1) { + // </script + fmt::fprint(ofile, if (html) "<\\/script" else + "</script")!; + } else if (m == 2) { + // "/*", "//" or "/regex/" + fmt::fprint(ofile, "/")!; + const buf: [1]u8 = [' ']; + if (io::read(ifile, buf) is io::EOF) break; + io::write(ofile, buf)!; + if (buf[0] == '/') { + searchio::search(ifile, ofile, + p_newline); + fmt::fprint(ofile, "\n")!; + } else if (buf[0] == '*') { + searchio::search(ifile, ofile, + p_commentend); + fmt::fprint(ofile, "*/")!; + } else disabled = true; + } else { + // '"', "'" or "`" + fmt::fprint(ofile, p_reqscript.original[m])!; + disabled = true; + }; + } else break; + }; +}; diff --git a/searchio/searchio.ha b/searchio/searchio.ha @@ -49,13 +49,14 @@ export fn compile(s: []str) pattern = { } else break; }; }; - return pattern { elems = p, original = s }; + return pattern { elems = p, original = strings::dupall(s) }; }; export fn finish(p: pattern) void = { for (let i = 0z; i < len(p.elems); i += 1) { free(p.elems[i].data); }; + strings::freeall(p.original); }; // reads until one of the end strings is read and pipes all read bytes to ofile diff --git a/test-page/a.js b/test-page/a.js @@ -9,3 +9,6 @@ function a() { // this should throw a warning console.log(require("test")); }; + +console.log(testm); +window.testm = testm; diff --git a/test-page/b.js b/test-page/b.js @@ -1,4 +1,3 @@ -const c = ""; const c = require('./c'); module.exports = { diff --git a/test-page/c.js b/test-page/c.js @@ -1,3 +1 @@ -// console.log(require("./a.js")); // illegal -> circular dependency -require("test-page/b.js"); -exports.msg = ":)"; +exports.msg = "</script> is safe.";