feat(script/jsx): compile JSX on server side

This commit is contained in:
Elian Doran 2025-12-20 18:46:15 +02:00
parent 883e32f5c9
commit 3619c0c3e4
No known key found for this signature in database
4 changed files with 85 additions and 55 deletions

View File

@ -30,7 +30,8 @@
"dependencies": {
"better-sqlite3": "12.5.0",
"html-to-text": "9.0.5",
"node-html-parser": "7.0.1"
"node-html-parser": "7.0.1",
"sucrase": "3.35.1"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.71.2",

View File

@ -1,26 +1,25 @@
"use strict";
import protectedSessionService from "../../services/protected_session.js";
import log from "../../services/log.js";
import sql from "../../services/sql.js";
import optionService from "../../services/options.js";
import eraseService from "../../services/erase.js";
import utils from "../../services/utils.js";
import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import BRevision from "./brevision.js";
import BAttachment from "./battachment.js";
import TaskContext from "../../services/task_context.js";
import { dayjs } from "@triliumnext/commons";
import eventService from "../../services/events.js";
import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons";
import type BBranch from "./bbranch.js";
import BAttribute from "./battribute.js";
import type { NotePojo } from "../becca-interface.js";
import searchService from "../../services/search/services/search.js";
import { dayjs } from "@triliumnext/commons";
import cloningService from "../../services/cloning.js";
import noteService from "../../services/notes.js";
import dateUtils from "../../services/date_utils.js";
import eraseService from "../../services/erase.js";
import eventService from "../../services/events.js";
import handlers from "../../services/handlers.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import optionService from "../../services/options.js";
import protectedSessionService from "../../services/protected_session.js";
import searchService from "../../services/search/services/search.js";
import sql from "../../services/sql.js";
import TaskContext from "../../services/task_context.js";
import utils from "../../services/utils.js";
import type { NotePojo } from "../becca-interface.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js";
import BAttachment from "./battachment.js";
import BAttribute from "./battribute.js";
import type BBranch from "./bbranch.js";
import BRevision from "./brevision.js";
const LABEL = "label";
const RELATION = "relation";
@ -296,6 +295,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
);
}
isJsx() {
return (this.type === "code" && this.mime === "text/jsx");
}
/** @returns true if this note is HTML */
isHtml() {
return ["code", "file", "render"].includes(this.type) && this.mime === "text/html";
@ -355,9 +358,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.__attributeCache.filter((attr) => attr.type === type);
} else if (name) {
return this.__attributeCache.filter((attr) => attr.name === name);
} else {
return this.__attributeCache;
}
return this.__attributeCache;
}
private __ensureAttributeCacheIsAvailable() {
@ -692,9 +695,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.ownedAttributes.filter((attr) => attr.type === type);
} else if (name) {
return this.ownedAttributes.filter((attr) => attr.name === name);
} else {
return this.ownedAttributes;
}
return this.ownedAttributes;
}
/**
@ -745,9 +748,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
return 1;
} else if (a.parentNote?.isHiddenCompletely()) {
return 1;
} else {
return 0;
}
return 0;
});
this.parents = this.parentBranches.map((branch) => branch.parentNote).filter((note) => !!note) as BNote[];
@ -1178,9 +1181,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
return a.isArchived ? 1 : -1;
} else if (a.isHidden !== b.isHidden) {
return a.isHidden ? 1 : -1;
} else {
return a.notePath.length - b.notePath.length;
}
return a.notePath.length - b.notePath.length;
});
return notePaths;
@ -1257,9 +1260,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
} else {
new BAttribute({
noteId: this.noteId,
type: type,
name: name,
value: value
type,
name,
value
}).save();
}
}
@ -1292,11 +1295,11 @@ class BNote extends AbstractBeccaEntity<BNote> {
addAttribute(type: AttributeType, name: string, value: string = "", isInheritable: boolean = false, position: number | null = null): BAttribute {
return new BAttribute({
noteId: this.noteId,
type: type,
name: name,
value: value,
isInheritable: isInheritable,
position: position
type,
name,
value,
isInheritable,
position
}).save();
}
@ -1470,10 +1473,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
role: "image",
mime: this.mime,
title: this.title,
content: content
content
});
let parentContent = parentNote.getContent();
const parentContent = parentNote.getContent();
const oldNoteUrl = `api/images/${this.noteId}/`;
const newAttachmentUrl = `api/attachments/${attachment.attachmentId}/image/`;
@ -1712,14 +1715,14 @@ class BNote extends AbstractBeccaEntity<BNote> {
} else if (this.type === "text") {
if (this.isFolder()) {
return "bx bx-folder";
} else {
return "bx bx-note";
}
return "bx bx-note";
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
return "bx bx-data";
} else {
return NOTE_TYPE_ICONS[this.type];
}
return NOTE_TYPE_ICONS[this.type];
}
// TODO: Deduplicate with fnote
@ -1729,7 +1732,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
// TODO: Deduplicate with fnote
getFilteredChildBranches() {
let childBranches = this.getChildBranches();
const childBranches = this.getChildBranches();
if (!childBranches) {
console.error(`No children for '${this.noteId}'. This shouldn't happen.`);

View File

@ -1,9 +1,11 @@
import ScriptContext from "./script_context.js";
import cls from "./cls.js";
import log from "./log.js";
import { transform } from "sucrase";
import becca from "../becca/becca.js";
import type BNote from "../becca/entities/bnote.js";
import type { ApiParams } from "./backend_script_api_interface.js";
import cls from "./cls.js";
import log from "./log.js";
import ScriptContext from "./script_context.js";
export interface Bundle {
note?: BNote;
@ -110,9 +112,9 @@ function getParams(params?: ScriptParams) {
.map((p) => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13);
} else {
return JSON.stringify(p);
}
return JSON.stringify(p);
})
.join(",");
}
@ -145,7 +147,7 @@ export function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: st
return;
}
if (!note.isJavaScript() && !note.isHtml()) {
if (!(note.isJavaScript() || note.isHtml() || note.isJsx())) {
return;
}
@ -158,7 +160,7 @@ export function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: st
}
const bundle: Bundle = {
note: note,
note,
script: "",
html: "",
allNotes: [note]
@ -192,12 +194,19 @@ export function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: st
// only frontend scripts are async. Backend cannot be async because of transaction management.
const isFrontend = scriptEnv === "frontend";
if (note.isJavaScript()) {
if (note.isJsx() || note.isJavaScript()) {
let scriptContent = note.getContent();
if (note.isJsx()) {
console.log("GOT JSX!!!");
scriptContent = buildJsx(note).code;
}
bundle.script += `
apiContext.modules['${note.noteId}'] = { exports: {} };
${root ? "return " : ""}${isFrontend ? "await" : ""} ((${isFrontend ? "async" : ""} function(exports, module, require, api${modules.length > 0 ? ", " : ""}${modules.map((child) => sanitizeVariableName(child.title)).join(", ")}) {
try {
${overrideContent || note.getContent()};
${overrideContent || scriptContent};
} catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); }
for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];
return module.exports;
@ -210,6 +219,16 @@ return module.exports;
return bundle;
}
function buildJsx(jsxNote: BNote) {
const contentRaw = jsxNote.getContent();
const content = Buffer.isBuffer(contentRaw) ? contentRaw.toString("utf-8") : contentRaw;
return transform(content, {
transforms: ["jsx"],
jsxPragma: "h", // for Preact
jsxFragmentPragma: "Fragment",
});
}
function sanitizeVariableName(str: string) {
return str.replace(/[^a-z0-9_]/gim, "");
}

11
pnpm-lock.yaml generated
View File

@ -497,6 +497,9 @@ importers:
node-html-parser:
specifier: 7.0.1
version: 7.0.1
sucrase:
specifier: 3.35.1
version: 3.35.1
devDependencies:
'@anthropic-ai/sdk':
specifier: 0.71.2
@ -15257,6 +15260,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-code-block@47.3.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies:
@ -15474,8 +15479,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-multi-root@47.3.0':
dependencies:
@ -15498,6 +15501,8 @@ snapshots:
'@ckeditor/ckeditor5-table': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-emoji@47.3.0':
dependencies:
@ -15680,6 +15685,8 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-icons@47.3.0': {}