feature (fastboot): leverage importmap for fast start

This commit is contained in:
MickaelK 2025-09-02 15:57:48 +10:00
parent 5041bfeb0d
commit 45bc001f5e
6 changed files with 78 additions and 54 deletions

View file

@ -0,0 +1,48 @@
if (!HTMLScriptElement.supports?.("importmap")) throw new Error("fastboot is not supported on this platform");
window.bundler = (function(origin) {
const esModules = {};
return {
register: (path, code) => {
if (path.endsWith(".js")) {
code = code.replace(/from\s?"([^"]+)"/g, (_, spec) =>
`from "${new URL(spec, origin + path).href}"`,
);
code = code.replace(/\bimport\s+"([^"]+)"/g, (_, spec) =>
`import "${new URL(spec, origin + path).href}"`,
);
code = code.replace(/(?<!["])\bimport\.meta\.url\b(?!["])/g, `"${origin + path}"`);
code += `\n//# sourceURL=${path}`;
esModules[path] = "data:text/javascript," + encodeURIComponent(code);
} else if (path.endsWith(".css")) {
code = code.replace(/@import url\("([^"]+)"\);/g, (m, rel) => {
const $style = document.head.querySelector(`style[id="${new URL(rel, origin + path).href}"]`);
if (!$style) throw new DOMException(
`Missing CSS dependency: ${rel} (referenced from ${path})`,
"NotFoundError",
);
return `/* ${m} */`;
});
code += `\n/*# sourceURL=${path} */`;
document.head.appendChild(Object.assign(document.createElement("style"), {
innerHTML: code,
id: origin + path,
}));
}
},
esModules,
};
})(new URL(import.meta.url).origin);
await new Promise((resolve, reject) => {
document.head.appendChild(Object.assign(document.createElement("script"), {
type: "module",
src: `./assets/bundle.js?version=${window.VERSION}`,
onload: resolve,
onerror: reject,
}));
});
document.head.appendChild(Object.assign(document.createElement("script"), {
type: "importmap",
textContent: JSON.stringify({
imports: window.bundler.esModules,
}, null, 4),
}));

View file

@ -21,6 +21,7 @@ export async function loadCSS(baseURL, path) {
$style.setAttribute("href", link.toString());
$style.setAttribute("rel", "stylesheet");
if (document.head.querySelector(`[href="${link.toString()}"]`)) return Promise.resolve();
else if (document.head.querySelector(`style[id="${link.toString()}"]`)) return Promise.resolve();
document.head.appendChild($style);
return new Promise((done) => {
$style.onload = done;

View file

@ -42,7 +42,7 @@ async function load(route, opts) {
}
const module = window.env === "test"
? require("../.." + route)
: await import("../.." + route);
: await import(new URL("../.." + route, import.meta.url));
if (typeof module.init === "function") await module.init();

2
public/global.d.ts vendored
View file

@ -5,5 +5,7 @@ interface Window {
[key: string]: any;
"xdg-open"?: (mime: string) => void;
};
VERSION: string;
bundler: any;
BEARER_TOKEN?: string;
}

View file

@ -17,22 +17,19 @@
<template id="head">
<link rel="stylesheet" href="custom.css">
<link rel="stylesheet" href="./assets/{{ .version }}/css/designsystem.css">
</template>
<template id="body">
<script type="module" src="./assets/{{ .version }}/components/loader.js"></script>
<script type="module">
import main from "./assets/{{ .version }}/lib/skeleton/index.js";
import routes from "./assets/{{ .version }}/boot/router_frontoffice.js";
main(document.getElementById("app"), routes, {
spinner: `<component-loader></component-loader>`,
beforeStart: import("{{ .base }}assets/{{ .version }}/boot/ctrl_boot_frontoffice.js"),
});
</script>
<component-modal></component-modal>
<script type="module" src="./assets/{{ .version }}/components/modal.js" defer></script>
<component-notification></component-notification>
<script type="module" src="./assets/{{ .version }}/components/notification.js" defer></script>
</template>
<script type="module">
@ -41,46 +38,20 @@
document.body.appendChild(document.querySelector("template#body").content);
}
async function ignitionSequence() {
if (!("serviceWorker" in navigator)) return
try {
window.bundler = (function () {
let modules = [];
return {
register: (path, code) => modules[path] = code,
state: () => modules,
};
})();
const [register] = await Promise.all([
navigator.serviceWorker.register("sw.js").then((register) => new Promise((resolve) => {
register.active ?
resolve(register) :
navigator.serviceWorker.addEventListener("controllerchange", () => {
resolve(register);
});
})),
new Promise((resolve, reject) => {
const $script = document.createElement("script");
$script.type = "module";
$script.src = "./assets/bundle.js?version={{ slice .version 0 7 }}::{{ .hash }}";
document.head.appendChild($script);
$script.onload = resolve;
$script.onerror = reject;
}),
]);
register.active.postMessage({
"type": "preload",
"payload": bundler.state(),
"version": "{{ slice .version 0 7 }}::{{ .hash }}",
});
await new Promise((resolve, reject) => navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data && event.data.type === "preload") {
if (event.data.status !== "ok") console.log(`turboload failure data=${JSON.stringify(event.data)}`);
resolve();
}
}));
} catch (err) { console.error(err); }
}
window.VERSION = "{{ slice .version 0 7 }}::{{ .hash }}";
try { await import("./assets/boot/warmup.js"); }
catch (err) { console.error(err); }
await Promise.all([
import("./assets/{{ .version }}/components/loader.js"),
import("./assets/{{ .version }}/components/modal.js"),
import("./assets/{{ .version }}/components/notification.js"),
import("./assets/{{ .version }}/helpers/loader.js").then(({ loadCSS }) => {
loadCSS(import.meta.url, "{{ .base }}assets/{{ .version }}/css/designsystem.css");
}),
]);
}
//
//
//
@ -108,7 +79,7 @@
// |
// | |
await ignitionSequence() // |
// |
// |
liftoff() // |
// | |
// |_____________________________________________|

View file

@ -248,7 +248,6 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/boot/router_frontoffice.js",
"/assets/" + BUILD_REF + "/boot/common.js",
"/assets/" + BUILD_REF + "/css/designsystem.css",
"/assets/" + BUILD_REF + "/css/designsystem_input.css",
"/assets/" + BUILD_REF + "/css/designsystem_textarea.css",
"/assets/" + BUILD_REF + "/css/designsystem_inputgroup.css",
@ -263,6 +262,7 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/css/designsystem_skeleton.css",
"/assets/" + BUILD_REF + "/css/designsystem_utils.css",
"/assets/" + BUILD_REF + "/css/designsystem_alert.css",
"/assets/" + BUILD_REF + "/css/designsystem.css",
"/assets/" + BUILD_REF + "/components/decorator_shell_filemanager.css",
"/assets/" + BUILD_REF + "/components/loader.js",
@ -286,6 +286,7 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/helpers/log.js",
"/assets/" + BUILD_REF + "/helpers/sdk.js",
"/assets/" + BUILD_REF + "/lib/rx.js",
"/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs.min.js",
"/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs-ajax.min.js",
"/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs-shared.min.js",
@ -294,12 +295,11 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/lib/path.js",
"/assets/" + BUILD_REF + "/lib/random.js",
"/assets/" + BUILD_REF + "/lib/settings.js",
"/assets/" + BUILD_REF + "/lib/skeleton/index.js",
"/assets/" + BUILD_REF + "/lib/rx.js",
"/assets/" + BUILD_REF + "/lib/ajax.js",
"/assets/" + BUILD_REF + "/lib/animate.js",
"/assets/" + BUILD_REF + "/lib/assert.js",
"/assets/" + BUILD_REF + "/lib/dom.js",
"/assets/" + BUILD_REF + "/lib/skeleton/index.js",
"/assets/" + BUILD_REF + "/lib/skeleton/router.js",
"/assets/" + BUILD_REF + "/lib/skeleton/lifecycle.js",
"/assets/" + BUILD_REF + "/lib/error.js",
@ -311,6 +311,10 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/model/session.js",
"/assets/" + BUILD_REF + "/model/plugin.js",
"/assets/" + BUILD_REF + "/pages/ctrl_logout.js",
"/assets/" + BUILD_REF + "/pages/ctrl_error.js",
"/assets/" + BUILD_REF + "/pages/ctrl_homepage.js",
"/assets/" + BUILD_REF + "/pages/ctrl_connectpage.js",
"/assets/" + BUILD_REF + "/pages/ctrl_connectpage.css",
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form.css",
@ -320,19 +324,18 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/pages/connectpage/model_backend.js",
"/assets/" + BUILD_REF + "/pages/connectpage/model_config.js",
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form_state.js",
"/assets/" + BUILD_REF + "/pages/ctrl_logout.js",
"/assets/" + BUILD_REF + "/pages/ctrl_error.js",
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.js",
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.css",
"/assets/" + BUILD_REF + "/pages/filespage/model_acl.js",
"/assets/" + BUILD_REF + "/pages/filespage/cache.js",
"/assets/" + BUILD_REF + "/pages/filespage/thing.js",
"/assets/" + BUILD_REF + "/pages/filespage/thing.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.js",
"/assets/" + BUILD_REF + "/pages/filespage/state_config.js",
"/assets/" + BUILD_REF + "/pages/filespage/helper.js",
@ -351,13 +354,12 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.css",
"/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.js",
// "/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.js", // TODO: dynamic imports
"/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.css",
"/assets/" + BUILD_REF + "/pages/viewerpage/mimetype.js",
"/assets/" + BUILD_REF + "/pages/viewerpage/model_files.js",
"/assets/" + BUILD_REF + "/pages/viewerpage/common.js",
"/assets/" + BUILD_REF + "/pages/viewerpage/application_downloader.js",
"/assets/" + BUILD_REF + "/pages/viewerpage/component_menubar.js",
"/assets/" + BUILD_REF + "/pages/viewerpage/component_menubar.css",
}