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": { "dependencies": {
"better-sqlite3": "12.5.0", "better-sqlite3": "12.5.0",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"node-html-parser": "7.0.1" "node-html-parser": "7.0.1",
"sucrase": "3.35.1"
}, },
"devDependencies": { "devDependencies": {
"@anthropic-ai/sdk": "0.71.2", "@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 { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons";
import type BBranch from "./bbranch.js"; import { dayjs } from "@triliumnext/commons";
import BAttribute from "./battribute.js";
import type { NotePojo } from "../becca-interface.js";
import searchService from "../../services/search/services/search.js";
import cloningService from "../../services/cloning.js"; 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 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 LABEL = "label";
const RELATION = "relation"; 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 */ /** @returns true if this note is HTML */
isHtml() { isHtml() {
return ["code", "file", "render"].includes(this.type) && this.mime === "text/html"; 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); return this.__attributeCache.filter((attr) => attr.type === type);
} else if (name) { } else if (name) {
return this.__attributeCache.filter((attr) => attr.name === name); return this.__attributeCache.filter((attr) => attr.name === name);
} else {
return this.__attributeCache;
} }
return this.__attributeCache;
} }
private __ensureAttributeCacheIsAvailable() { private __ensureAttributeCacheIsAvailable() {
@ -692,9 +695,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.ownedAttributes.filter((attr) => attr.type === type); return this.ownedAttributes.filter((attr) => attr.type === type);
} else if (name) { } else if (name) {
return this.ownedAttributes.filter((attr) => attr.name === 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; return 1;
} else if (a.parentNote?.isHiddenCompletely()) { } else if (a.parentNote?.isHiddenCompletely()) {
return 1; return 1;
} else {
return 0;
} }
return 0;
}); });
this.parents = this.parentBranches.map((branch) => branch.parentNote).filter((note) => !!note) as BNote[]; 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; return a.isArchived ? 1 : -1;
} else if (a.isHidden !== b.isHidden) { } else if (a.isHidden !== b.isHidden) {
return a.isHidden ? 1 : -1; return a.isHidden ? 1 : -1;
} else {
return a.notePath.length - b.notePath.length;
} }
return a.notePath.length - b.notePath.length;
}); });
return notePaths; return notePaths;
@ -1257,9 +1260,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
} else { } else {
new BAttribute({ new BAttribute({
noteId: this.noteId, noteId: this.noteId,
type: type, type,
name: name, name,
value: value value
}).save(); }).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 { addAttribute(type: AttributeType, name: string, value: string = "", isInheritable: boolean = false, position: number | null = null): BAttribute {
return new BAttribute({ return new BAttribute({
noteId: this.noteId, noteId: this.noteId,
type: type, type,
name: name, name,
value: value, value,
isInheritable: isInheritable, isInheritable,
position: position position
}).save(); }).save();
} }
@ -1470,10 +1473,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
role: "image", role: "image",
mime: this.mime, mime: this.mime,
title: this.title, title: this.title,
content: content content
}); });
let parentContent = parentNote.getContent(); const parentContent = parentNote.getContent();
const oldNoteUrl = `api/images/${this.noteId}/`; const oldNoteUrl = `api/images/${this.noteId}/`;
const newAttachmentUrl = `api/attachments/${attachment.attachmentId}/image/`; const newAttachmentUrl = `api/attachments/${attachment.attachmentId}/image/`;
@ -1712,14 +1715,14 @@ class BNote extends AbstractBeccaEntity<BNote> {
} else if (this.type === "text") { } else if (this.type === "text") {
if (this.isFolder()) { if (this.isFolder()) {
return "bx bx-folder"; return "bx bx-folder";
} else {
return "bx bx-note";
} }
return "bx bx-note";
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) { } else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
return "bx bx-data"; return "bx bx-data";
} else {
return NOTE_TYPE_ICONS[this.type];
} }
return NOTE_TYPE_ICONS[this.type];
} }
// TODO: Deduplicate with fnote // TODO: Deduplicate with fnote
@ -1729,7 +1732,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
// TODO: Deduplicate with fnote // TODO: Deduplicate with fnote
getFilteredChildBranches() { getFilteredChildBranches() {
let childBranches = this.getChildBranches(); const childBranches = this.getChildBranches();
if (!childBranches) { if (!childBranches) {
console.error(`No children for '${this.noteId}'. This shouldn't happen.`); console.error(`No children for '${this.noteId}'. This shouldn't happen.`);

View File

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

11
pnpm-lock.yaml generated
View File

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