feat(gui): InjectPreload — storage polyfills + Electron shim + app preloads

New pkg/preload package:
- preload.go — InjectPreload(webview, origin) entry point; builds
  three-step preload: storage polyfills, Electron shim (origin-
  filtered), app preloads from .core/view.yaml manifest.preloads.
- assets/storage_polyfills.js — localStorage/sessionStorage/
  IndexedDB bridges.
- assets/electron_shim.js — minimal ipcRenderer.send/invoke
  mapping to core.QUERY/ACTION.
- Adds a minimal window.core.ml.generate shim — gates the
  AI-native browser path (RFC §11a).

pkg/window/wails.go wires into Wails OnPageLoad via reflection when
the runtime exposes the hook, with a clean fallback for the
stubbed/test runtime. Legacy display-preload code detected and
skipped when the new package is in play.

Good/Bad/Ugly tests in pkg/preload/preload_test.go. go vet + go
test clean.

Closes tasks.lthn.sh/view.php?id=16

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-04-24 06:17:33 +01:00
parent d3858dcd26
commit fa4168e380
7 changed files with 1042 additions and 0 deletions

View file

@ -0,0 +1,101 @@
(function () {
if (globalThis.__corePreloadElectronInstalled) {
return;
}
globalThis.__corePreloadElectronInstalled = true;
const meta = __CORE_PRELOAD_META__;
if (!meta.allow) {
return;
}
const bridge = globalThis.__corePreloadBridge;
if (!bridge) {
return;
}
const listeners = new Map();
const eventName = (channel) => "__core_preload_electron__:" + String(channel ?? "");
const ipcRenderer = {
send(channel, ...args) {
const normalized = String(channel ?? "");
globalThis.dispatchEvent(new CustomEvent(eventName(normalized), { detail: args }));
return bridge.action(normalized, { channel: normalized, args }).then(() => undefined);
},
invoke(channel, ...args) {
const normalized = String(channel ?? "");
return bridge.query(normalized, { channel: normalized, args });
},
on(channel, listener) {
const normalized = String(channel ?? "");
const handler = (event) => listener(event, ...(event.detail || []));
listeners.set(listener, handler);
globalThis.addEventListener(eventName(normalized), handler);
return this;
},
once(channel, listener) {
const normalized = String(channel ?? "");
const onceListener = (event, ...args) => {
ipcRenderer.removeListener(normalized, listener);
listener(event, ...args);
};
return ipcRenderer.on(normalized, onceListener);
},
removeListener(channel, listener) {
const normalized = String(channel ?? "");
const handler = listeners.get(listener);
if (handler) {
globalThis.removeEventListener(eventName(normalized), handler);
listeners.delete(listener);
}
return this;
}
};
const remote = {
getGlobal(name) {
return bridge.query("electron.remote.getGlobal", { name: String(name ?? "") });
},
app: {
getPath(name) {
return bridge.query("electron.app.getPath", { name: String(name ?? "") });
}
}
};
const shell = {
openExternal(url) {
return bridge.action("browser.openURL", { url: String(url ?? "") }).then(() => undefined);
},
openPath(path) {
return bridge.action("browser.openFile", { path: String(path ?? "") }).then(() => "");
}
};
const contextBridge = {
exposeInMainWorld(name, api) {
globalThis[name] = api;
}
};
const processShim = globalThis.process || {
env: {},
platform: "wails",
type: "renderer",
versions: {}
};
processShim.versions = processShim.versions || {};
processShim.versions.electron = processShim.versions.electron || "wails-shim";
const electron = {
ipcRenderer,
remote,
shell,
contextBridge
};
globalThis.process = processShim;
globalThis.electron = electron;
globalThis.require = globalThis.require || ((name) => (name === "electron" ? electron : undefined));
})();

View file

@ -0,0 +1,282 @@
(function () {
if (globalThis.__corePreloadStorageInstalled) {
return;
}
globalThis.__corePreloadStorageInstalled = true;
const meta = __CORE_PRELOAD_META__;
const pageURL = String(meta.pageURL || "");
const storageOrigin = String(meta.storageOrigin || pageURL || "");
const storeGroup = String(meta.storeGroup || "gui.preload.storage");
const canPersist = !!meta.canPersist;
const asPromise = (value) => (
value && typeof value.then === "function" ? value : Promise.resolve(value)
);
const runCoreCall = (target, methodNames, name, payload) => {
if (!target || typeof target !== "object") {
return undefined;
}
for (const methodName of methodNames) {
const method = target[methodName];
if (typeof method !== "function") {
continue;
}
try {
const direct = method.call(target, name, payload);
if (direct && typeof direct.Run === "function") {
try {
return direct.Run(payload);
} catch (_) {
return direct.Run();
}
}
return direct;
} catch (_) {
try {
const deferred = method.call(target, name);
if (deferred && typeof deferred.Run === "function") {
try {
return deferred.Run(payload);
} catch (_) {
return deferred.Run();
}
}
return deferred;
} catch (_) {}
}
}
return undefined;
};
const bridge = globalThis.__corePreloadBridge || (globalThis.__corePreloadBridge = {
action(name, payload) {
const candidates = [globalThis.c, globalThis.Core, globalThis.core];
for (const candidate of candidates) {
const result = runCoreCall(candidate, ["Action", "ACTION", "action"], name, payload);
if (result !== undefined) {
return asPromise(result);
}
}
if (typeof globalThis.__CORE_GUI_INVOKE__ === "function") {
return asPromise(globalThis.__CORE_GUI_INVOKE__(name, payload, { mode: "action" }));
}
return Promise.resolve(undefined);
},
query(name, payload) {
const candidates = [globalThis.c, globalThis.Core, globalThis.core];
for (const candidate of candidates) {
const result = runCoreCall(candidate, ["QUERY", "Query", "query"], name, payload);
if (result !== undefined) {
return asPromise(result);
}
}
if (typeof globalThis.__CORE_GUI_INVOKE__ === "function") {
return asPromise(globalThis.__CORE_GUI_INVOKE__(name, payload, { mode: "query" }));
}
return Promise.resolve(undefined);
}
});
const storageScopes = globalThis.__corePreloadStorageScopes || (globalThis.__corePreloadStorageScopes = {});
const scopeKey = storageOrigin || "__core_default__";
const scope = storageScopes[scopeKey] || (storageScopes[scopeKey] = {
localStorage: Object.create(null),
sessionStorage: Object.create(null),
indexedDB: Object.create(null)
});
const persistKey = (bucket, key) => [storageOrigin, bucket, String(key ?? "")].join(":");
const persistSet = (bucket, key, value) => {
if (!canPersist) {
return;
}
bridge.action("store.set", {
group: storeGroup,
key: persistKey(bucket, key),
value: String(value ?? "")
}).catch(() => undefined);
};
const persistDelete = (bucket, key) => {
if (!canPersist) {
return;
}
bridge.action("store.delete", {
group: storeGroup,
key: persistKey(bucket, key)
}).catch(() => undefined);
};
const createStorage = (bucketName, bucket) => ({
getItem(key) {
const normalized = String(key ?? "");
return Object.prototype.hasOwnProperty.call(bucket, normalized) ? String(bucket[normalized]) : null;
},
setItem(key, value) {
const normalized = String(key ?? "");
bucket[normalized] = String(value ?? "");
persistSet(bucketName, normalized, bucket[normalized]);
},
removeItem(key) {
const normalized = String(key ?? "");
delete bucket[normalized];
persistDelete(bucketName, normalized);
},
clear() {
for (const key of Object.keys(bucket)) {
delete bucket[key];
persistDelete(bucketName, key);
}
},
key(index) {
return Object.keys(bucket)[Number(index)] ?? null;
},
get length() {
return Object.keys(bucket).length;
}
});
const queueTask = (callback) => {
if (typeof queueMicrotask === "function") {
queueMicrotask(callback);
return;
}
Promise.resolve().then(callback).catch(() => undefined);
};
const createRequest = (result, upgrade) => {
const request = { result, error: null, onsuccess: null, onerror: null, onupgradeneeded: null };
queueTask(() => {
if (upgrade) {
request.onupgradeneeded?.({ target: request });
}
request.onsuccess?.({ target: request });
});
return request;
};
const serializeRecord = (value) => {
if (typeof value === "string") {
return value;
}
try {
return JSON.stringify(value);
} catch (_) {
return String(value ?? "");
}
};
const clearObjectStore = (databaseName, storeName, records) => {
for (const key of Object.keys(records)) {
persistDelete("indexeddb:" + databaseName + ":" + storeName, key);
}
};
const createObjectStore = (databaseName, database, storeName) => ({
put(value, key) {
const resolvedKey = String(key ?? value?.id ?? Date.now());
database.stores[storeName] = database.stores[storeName] || Object.create(null);
database.stores[storeName][resolvedKey] = value;
persistSet("indexeddb:" + databaseName + ":" + storeName, resolvedKey, serializeRecord(value));
return createRequest(resolvedKey, false);
},
get(key) {
const resolvedKey = String(key ?? "");
return createRequest(database.stores?.[storeName]?.[resolvedKey], false);
},
getAll() {
return createRequest(Object.values(database.stores?.[storeName] || {}), false);
},
delete(key) {
const resolvedKey = String(key ?? "");
if (database.stores?.[storeName]) {
delete database.stores[storeName][resolvedKey];
}
persistDelete("indexeddb:" + databaseName + ":" + storeName, resolvedKey);
return createRequest(undefined, false);
},
clear() {
const records = database.stores?.[storeName] || Object.create(null);
clearObjectStore(databaseName, storeName, records);
database.stores[storeName] = Object.create(null);
return createRequest(undefined, false);
},
createIndex() {
return this;
}
});
const createDatabase = (name, upgrade) => {
const database = scope.indexedDB[name] || (scope.indexedDB[name] = { stores: Object.create(null) });
return {
name,
createObjectStore(storeName) {
const normalized = String(storeName ?? "default");
database.stores[normalized] = database.stores[normalized] || Object.create(null);
return createObjectStore(name, database, normalized);
},
transaction(storeNames) {
const names = Array.isArray(storeNames) ? storeNames : [storeNames];
return {
objectStore(storeName) {
const normalized = String(storeName ?? names[0] ?? "default");
database.stores[normalized] = database.stores[normalized] || Object.create(null);
return createObjectStore(name, database, normalized);
}
};
},
close() {}
};
};
globalThis.core = globalThis.core || {};
globalThis.core.storage = globalThis.core.storage || {};
globalThis.core.storage.local = createStorage("localStorage", scope.localStorage);
globalThis.core.storage.session = createStorage("sessionStorage", scope.sessionStorage);
try {
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
enumerable: true,
get() {
return globalThis.core.storage.local;
}
});
} catch (_) {}
try {
Object.defineProperty(globalThis, "sessionStorage", {
configurable: true,
enumerable: true,
get() {
return globalThis.core.storage.session;
}
});
} catch (_) {}
try {
if (!globalThis.indexedDB) {
globalThis.indexedDB = {
open(name) {
const normalized = String(name ?? "default");
const upgrade = !scope.indexedDB[normalized];
return createRequest(createDatabase(normalized, upgrade), upgrade);
},
deleteDatabase(name) {
const normalized = String(name ?? "default");
const database = scope.indexedDB[normalized];
if (database && database.stores) {
for (const [storeName, records] of Object.entries(database.stores)) {
clearObjectStore(normalized, storeName, records);
}
}
delete scope.indexedDB[normalized];
return createRequest(undefined, false);
}
};
}
} catch (_) {}
})();

View file

@ -0,0 +1,20 @@
package preload
import (
"strings"
core "dappco.re/go/core"
)
func renderElectronShim(pageURL string) string {
meta := map[string]any{
"allow": true,
"pageURL": pageURL,
}
return strings.ReplaceAll(
electronShimAsset,
"__CORE_PRELOAD_META__",
core.JSONMarshalString(meta),
)
}

436
pkg/preload/preload.go Normal file
View file

@ -0,0 +1,436 @@
package preload
import (
"embed"
"errors"
"io"
"net"
"net/url"
"os"
"path/filepath"
"reflect"
"strings"
core "dappco.re/go/core"
"gopkg.in/yaml.v3"
)
const maxViewManifestBytes = 1 << 20
var errViewManifestNotFound = errors.New("view manifest not found")
type Webview interface {
ExecJS(string)
}
type ManifestPreload struct {
Path string `yaml:"path"`
Inline string `yaml:"inline"`
Enabled *bool `yaml:"enabled,omitempty"`
}
type viewManifest struct {
Preloads []ManifestPreload `yaml:"preloads"`
Manifest struct {
Preloads []ManifestPreload `yaml:"preloads"`
} `yaml:"manifest"`
}
type loadedManifest struct {
Path string
BaseDir string
Preloads []ManifestPreload
}
//go:embed assets/*.js
var assetFS embed.FS
var (
storagePolyfillsAsset = mustReadAsset("assets/storage_polyfills.js")
electronShimAsset = mustReadAsset("assets/electron_shim.js")
)
func InjectPreload(webview Webview, origin string) error {
if isNilWebview(webview) {
return errors.New("preload target is required")
}
script, err := buildScript(origin)
if err != nil {
return err
}
if strings.TrimSpace(script) == "" {
return nil
}
webview.ExecJS(script)
return nil
}
func buildScript(pageURL string) (string, error) {
loaded, manifestErr := loadManifestForOrigin(pageURL)
switch {
case manifestErr == nil:
case errors.Is(manifestErr, errViewManifestNotFound):
loaded = nil
default:
return "", manifestErr
}
allowPrivileged := trustedOrigin(pageURL) || loaded != nil
parts := []string{
renderStoragePolyfills(pageURL, allowPrivileged),
renderCoreMLShim(),
}
if allowPrivileged {
parts = append(parts, renderElectronShim(pageURL))
}
if appPreloads, err := renderAppPreloads(loaded); err != nil {
return "", err
} else if strings.TrimSpace(appPreloads) != "" {
parts = append(parts, appPreloads)
}
return strings.Join(filterEmpty(parts), "\n"), nil
}
func mustReadAsset(name string) string {
body, err := assetFS.ReadFile(name)
if err != nil {
panic(err)
}
return string(body)
}
func renderAppPreloads(loaded *loadedManifest) (string, error) {
if loaded == nil || len(loaded.Preloads) == 0 {
return "", nil
}
scripts := make([]string, 0, len(loaded.Preloads))
for _, preload := range loaded.Preloads {
if preload.Enabled != nil && !*preload.Enabled {
continue
}
if inline := strings.TrimSpace(preload.Inline); inline != "" {
scripts = append(scripts, inline)
continue
}
if path := strings.TrimSpace(preload.Path); path != "" {
body, err := readManifestPreload(loaded.BaseDir, path)
if err != nil {
return "", err
}
scripts = append(scripts, string(body))
}
}
return strings.Join(scripts, "\n"), nil
}
func loadManifestForOrigin(pageURL string) (*loadedManifest, error) {
path, err := discoverManifestPath(pageURL)
if err != nil {
return nil, err
}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
body, err := io.ReadAll(io.LimitReader(file, maxViewManifestBytes+1))
if err != nil {
return nil, err
}
if len(body) > maxViewManifestBytes {
return nil, errors.New("view manifest exceeds 1048576 bytes")
}
var manifest viewManifest
if err := yaml.Unmarshal(body, &manifest); err != nil {
return nil, err
}
return &loadedManifest{
Path: path,
BaseDir: manifestBaseDir(path),
Preloads: collectManifestPreloads(manifest),
}, nil
}
func collectManifestPreloads(manifest viewManifest) []ManifestPreload {
out := make([]ManifestPreload, 0, len(manifest.Preloads)+len(manifest.Manifest.Preloads))
out = append(out, manifest.Preloads...)
out = append(out, manifest.Manifest.Preloads...)
return out
}
func discoverManifestPath(pageURL string) (string, error) {
trimmed := strings.TrimSpace(pageURL)
if trimmed == "" {
return "", errViewManifestNotFound
}
parsed, err := url.Parse(trimmed)
if err != nil {
return "", err
}
candidates := make([]string, 0, 4)
switch parsed.Scheme {
case "", "file":
path := parsed.Path
if path == "" {
path = trimmed
}
path = filepath.FromSlash(path)
if info, err := os.Stat(path); err == nil {
if info.IsDir() {
candidates = append(candidates, filepath.Join(path, ".core", "view.yaml"))
} else {
dir := filepath.Dir(path)
candidates = append(candidates, filepath.Join(dir, ".core", "view.yaml"))
candidates = append(candidates, filepath.Join(filepath.Dir(dir), ".core", "view.yaml"))
}
}
default:
if host := strings.TrimSpace(parsed.Host); host != "" {
home := strings.TrimSpace(os.Getenv("DIR_HOME"))
if home == "" {
home = strings.TrimSpace(core.Env("DIR_HOME"))
}
if home != "" {
candidates = append(candidates, filepath.Join(home, ".core", "apps", host, ".core", "view.yaml"))
}
}
}
for _, candidate := range candidates {
if _, err := os.Stat(candidate); err == nil {
return candidate, nil
}
}
return "", errViewManifestNotFound
}
func manifestBaseDir(manifestPath string) string {
baseDir := filepath.Dir(manifestPath)
if filepath.Base(baseDir) == ".core" {
return filepath.Dir(baseDir)
}
return baseDir
}
func readManifestPreload(baseDir, preloadPath string) ([]byte, error) {
resolvedPath, err := safeManifestRelativePath(baseDir, preloadPath)
if err != nil {
return nil, err
}
return os.ReadFile(resolvedPath)
}
func safeManifestRelativePath(baseDir, relativePath string) (string, error) {
trimmed := strings.TrimSpace(relativePath)
if trimmed == "" {
return "", errors.New("preload path is empty")
}
if filepath.IsAbs(trimmed) {
return "", errors.New("preload path must be relative")
}
baseAbs, err := filepath.Abs(baseDir)
if err != nil {
return "", err
}
baseResolved, err := filepath.EvalSymlinks(baseAbs)
if err != nil {
return "", err
}
candidateAbs, err := filepath.Abs(filepath.Join(baseAbs, trimmed))
if err != nil {
return "", err
}
if rel, err := filepath.Rel(baseAbs, candidateAbs); err != nil {
return "", err
} else if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", errors.New("preload path escapes manifest directory")
}
if _, err := os.Lstat(candidateAbs); err != nil {
return "", err
}
candidateResolved, err := filepath.EvalSymlinks(candidateAbs)
if err != nil {
return "", err
}
if rel, err := filepath.Rel(baseResolved, candidateResolved); err != nil {
return "", err
} else if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", errors.New("preload path escapes manifest directory")
}
return candidateResolved, nil
}
func trustedOrigin(pageURL string) bool {
trimmed := strings.TrimSpace(pageURL)
if trimmed == "" {
return false
}
parsed, err := url.Parse(trimmed)
if err != nil {
return false
}
switch strings.ToLower(parsed.Scheme) {
case "core", "wails", "app":
return true
case "http", "https":
host := strings.TrimSpace(parsed.Host)
if host == "" {
return false
}
name := host
if parsedHost, _, err := net.SplitHostPort(host); err == nil {
name = parsedHost
}
name = strings.Trim(strings.ToLower(name), "[]")
switch name {
case "localhost", "127.0.0.1", "::1":
return true
default:
return false
}
default:
return false
}
}
func storageOriginForPageURL(pageURL string) string {
trimmed := strings.TrimSpace(pageURL)
if trimmed == "" {
return ""
}
parsed, err := url.Parse(trimmed)
if err != nil || strings.TrimSpace(parsed.Scheme) == "" {
return ""
}
switch strings.ToLower(parsed.Scheme) {
case "http", "https":
if parsed.Host == "" {
return ""
}
return parsed.Scheme + "://" + parsed.Host
case "core":
if parsed.Host == "" {
return "core://"
}
return "core://" + parsed.Host
case "file":
if parsed.Path == "" {
return ""
}
return "file://" + parsed.Path
default:
if parsed.Host == "" {
return ""
}
origin := parsed.Scheme + "://" + parsed.Host
if parsed.Path != "" {
origin += parsed.Path
}
return strings.TrimRight(origin, "/")
}
}
func validatedLocalMLAPIURL(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return "http://localhost:8090"
}
parsed, err := url.Parse(trimmed)
if err != nil {
return "http://localhost:8090"
}
switch strings.ToLower(parsed.Scheme) {
case "http", "https":
default:
return "http://localhost:8090"
}
host := strings.TrimSpace(parsed.Host)
if host == "" {
return "http://localhost:8090"
}
name := host
if parsedHost, _, err := net.SplitHostPort(host); err == nil {
name = parsedHost
}
name = strings.Trim(strings.ToLower(name), "[]")
switch name {
case "localhost", "127.0.0.1", "::1":
return strings.TrimRight(parsed.String(), "/")
default:
return "http://localhost:8090"
}
}
func renderCoreMLShim() string {
return `(function() {
const apiURL = ` + core.JSONMarshalString(validatedLocalMLAPIURL(core.Env("CORE_ML_API_URL"))) + ` || "http://localhost:8090";
globalThis.core = globalThis.core || {};
globalThis.core.ml = globalThis.core.ml || {
async generate(input) {
const payload = typeof input === "string"
? { messages: [{ role: "user", content: input }], stream: false }
: { ...input, stream: false };
const response = await fetch(apiURL + "/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error("Core ML request failed: " + response.status + " " + response.statusText);
}
const body = await response.text();
try {
const parsed = JSON.parse(body);
return parsed?.choices?.[0]?.message?.content ?? parsed?.content ?? body;
} catch (_) {
return body;
}
}
};
})();`
}
func filterEmpty(parts []string) []string {
out := make([]string, 0, len(parts))
for _, part := range parts {
if strings.TrimSpace(part) != "" {
out = append(out, part)
}
}
return out
}
func isNilWebview(webview Webview) bool {
if webview == nil {
return true
}
value := reflect.ValueOf(webview)
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return value.IsNil()
default:
return false
}
}

View file

@ -0,0 +1,64 @@
package preload
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type captureWebview struct {
scripts []string
}
func (c *captureWebview) ExecJS(script string) {
c.scripts = append(c.scripts, script)
}
func TestInjectPreload_Good(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, ".core"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(root, "index.html"), []byte("<html></html>"), 0o644))
require.NoError(t, os.WriteFile(
filepath.Join(root, ".core", "view.yaml"),
[]byte("manifest:\n preloads:\n - path: preload.js\n"),
0o644,
))
require.NoError(t, os.WriteFile(
filepath.Join(root, "preload.js"),
[]byte("globalThis.__manifestPreloadLoaded = true;"),
0o644,
))
target := &captureWebview{}
err := InjectPreload(target, "file://"+filepath.ToSlash(filepath.Join(root, "index.html")))
require.NoError(t, err)
require.Len(t, target.scripts, 1)
script := target.scripts[0]
assert.Contains(t, script, "globalThis.core.storage.local")
assert.Contains(t, script, "globalThis.core.ml = globalThis.core.ml ||")
assert.Contains(t, script, "globalThis.electron = electron")
assert.Contains(t, script, "globalThis.__manifestPreloadLoaded = true;")
}
func TestInjectPreload_Bad(t *testing.T) {
err := InjectPreload(nil, "http://localhost:3000")
require.Error(t, err)
assert.Contains(t, err.Error(), "preload target is required")
}
func TestInjectPreload_Ugly(t *testing.T) {
target := &captureWebview{}
err := InjectPreload(target, "https://example.com/app")
require.NoError(t, err)
require.Len(t, target.scripts, 1)
script := target.scripts[0]
assert.Contains(t, script, "globalThis.core.storage.local")
assert.Contains(t, script, "globalThis.core.ml = globalThis.core.ml ||")
assert.NotContains(t, script, "globalThis.electron = electron")
assert.NotContains(t, script, "ipcRenderer")
}

View file

@ -0,0 +1,22 @@
package preload
import (
"strings"
core "dappco.re/go/core"
)
func renderStoragePolyfills(pageURL string, canPersist bool) string {
meta := map[string]any{
"pageURL": pageURL,
"storageOrigin": storageOriginForPageURL(pageURL),
"storeGroup": "gui.preload.storage",
"canPersist": canPersist,
}
return strings.ReplaceAll(
storagePolyfillsAsset,
"__CORE_PRELOAD_META__",
core.JSONMarshalString(meta),
)
}

View file

@ -2,6 +2,10 @@
package window
import (
"reflect"
"strings"
"forge.lthn.ai/core/gui/pkg/preload"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
@ -38,10 +42,123 @@ func (wp *WailsPlatform) CreateWindow(options PlatformWindowOptions) PlatformWin
EnableFileDrop: options.EnableFileDrop,
BackgroundColour: application.NewRGBA(options.BackgroundColour[0], options.BackgroundColour[1], options.BackgroundColour[2], options.BackgroundColour[3]),
}
var windowHandle *application.WebviewWindow
if wirePreloadOnPageLoad(&wOpts, options.URL, func(origin string, target preload.Webview) {
if target == nil {
target = windowHandle
}
if target == nil {
return
}
_ = preload.InjectPreload(target, origin)
if extra := postPageLoadWindowJS(options.JS); strings.TrimSpace(extra) != "" {
target.ExecJS(extra)
}
}) {
wOpts.JS = ""
}
w := wp.app.Window.NewWithOptions(wOpts)
windowHandle = w
return &wailsWindow{w: w, title: options.Title, opacity: 1.0}
}
func wirePreloadOnPageLoad(options *application.WebviewWindowOptions, fallbackOrigin string, inject func(origin string, target preload.Webview)) bool {
if options == nil || inject == nil {
return false
}
value := reflect.ValueOf(options)
if value.Kind() != reflect.Pointer || value.IsNil() {
return false
}
structValue := value.Elem()
if structValue.Kind() != reflect.Struct {
return false
}
field := structValue.FieldByName("OnPageLoad")
if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.Func {
return false
}
fnType := field.Type()
field.Set(reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value {
inject(extractPageLoadOrigin(args, fallbackOrigin), extractPageLoadWebview(args))
return zeroReturnValues(fnType)
}))
return true
}
func extractPageLoadOrigin(args []reflect.Value, fallback string) string {
for _, arg := range args {
if !arg.IsValid() {
continue
}
if arg.Kind() == reflect.Pointer {
if arg.IsNil() {
continue
}
arg = arg.Elem()
}
switch arg.Kind() {
case reflect.String:
if value := strings.TrimSpace(arg.String()); value != "" {
return value
}
case reflect.Struct:
for _, name := range []string{"URL", "Url", "Origin", "Location"} {
field := arg.FieldByName(name)
if field.IsValid() && field.Kind() == reflect.String {
if value := strings.TrimSpace(field.String()); value != "" {
return value
}
}
}
}
}
return fallback
}
func extractPageLoadWebview(args []reflect.Value) preload.Webview {
for _, arg := range args {
if !arg.IsValid() || !arg.CanInterface() {
continue
}
if target, ok := arg.Interface().(preload.Webview); ok {
return target
}
}
return nil
}
func zeroReturnValues(fnType reflect.Type) []reflect.Value {
if fnType.NumOut() == 0 {
return nil
}
out := make([]reflect.Value, 0, fnType.NumOut())
for i := 0; i < fnType.NumOut(); i++ {
out = append(out, reflect.Zero(fnType.Out(i)))
}
return out
}
func postPageLoadWindowJS(raw string) string {
if looksLikeLegacyDisplayPreload(raw) {
return ""
}
return raw
}
func looksLikeLegacyDisplayPreload(raw string) bool {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return false
}
return strings.Contains(trimmed, "const __corePageURL =") &&
strings.Contains(trimmed, "globalThis.core.ml") &&
strings.Contains(trimmed, "Document.prototype, 'cookie'")
}
func (wp *WailsPlatform) GetWindows() []PlatformWindow {
all := wp.app.Window.GetAll()
out := make([]PlatformWindow, 0, len(all))