Skip to content

Commit 66eadd6

Browse files
André DietrichAndré Dietrich
authored andcommitted
add iframe ability to allow exports from foreign sites
1 parent 2d19033 commit 66eadd6

6 files changed

Lines changed: 175 additions & 4 deletions

File tree

dist/index.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/server/public/app.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ function initializeNumberInputs() {
7676
// Initialize app
7777
document.addEventListener('DOMContentLoaded', async () => {
7878
initializeNumberInputs()
79+
// Attach the embed message listener synchronously, before the first await, so
80+
// it is in place before the embedding iframe's load event can fire. Otherwise
81+
// a project pushed by the embedder right after load would be lost while we are
82+
// still awaiting loadPresets().
83+
initializeEmbedMessaging()
7984
await loadPresets()
8085
initializeTabs()
8186
initializeExportTabs()
@@ -88,6 +93,62 @@ document.addEventListener('DOMContentLoaded', async () => {
8893
checkForUpdates()
8994
})
9095

96+
// Embedding support: when this UI is loaded inside an <iframe> (e.g. the
97+
// LiaScript LiveEditor), the parent can push a complete project to us via
98+
// postMessage instead of the user having to upload files manually. We announce
99+
// readiness on load and feed any received File/Blob into the normal upload flow.
100+
function initializeEmbedMessaging() {
101+
// Only relevant when actually embedded.
102+
const isEmbedded = window.parent && window.parent !== window
103+
if (!isEmbedded) return
104+
105+
window.addEventListener('message', (event) => {
106+
const payload = event.data
107+
108+
// Respond to a readiness ping so the embedder doesn't have to rely on
109+
// catching our initial (one-shot) ready broadcast.
110+
if (payload && payload.type === 'liaexporter:ping') {
111+
const target = event.source || window.parent
112+
if (target) {
113+
target.postMessage({ type: 'liaexporter:ready' }, event.origin || '*')
114+
}
115+
return
116+
}
117+
118+
// Accept either a bare File/Blob or a wrapper { type, file/files }.
119+
let incoming = []
120+
if (payload instanceof File || payload instanceof Blob) {
121+
incoming = [payload]
122+
} else if (payload && payload.type === 'liaexporter:project') {
123+
if (Array.isArray(payload.files)) incoming = payload.files
124+
else if (payload.file) incoming = [payload.file]
125+
} else {
126+
return
127+
}
128+
129+
if (incoming.length === 0) return
130+
131+
// A Blob received via postMessage has no name -> wrap it as a named File so
132+
// the server-side ZIP detection (isZipFile) and file list work correctly.
133+
const files = incoming.map((f) =>
134+
f instanceof File
135+
? f
136+
: new File([f], 'project.zip', {
137+
type: f.type || 'application/zip',
138+
}),
139+
)
140+
141+
// Switch to the upload source tab and inject the files as if dropped.
142+
const uploadTab = document.querySelector('.tab-button[data-tab="upload"]')
143+
if (uploadTab) uploadTab.click()
144+
145+
handleFiles(files)
146+
})
147+
148+
// Tell the parent we are ready to receive a project.
149+
window.parent.postMessage({ type: 'liaexporter:ready' }, '*')
150+
}
151+
91152
async function checkForUpdates() {
92153
if (!window.electronAPI?.checkForUpdates) return
93154

package-lock.json

Lines changed: 28 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"author": "André Dietrich <Andre.Dietrich@informatik.tu-freiberg.de>",
6767
"license": "ISC",
6868
"dependencies": {
69+
"@fastify/cors": "^9.0.1",
6970
"@fastify/multipart": "^8.1.0",
7071
"@fastify/static": "^7.0.1",
7172
"@lesjoursfr/html-to-epub": "^6.0.1",

src/server/public/app.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ function initializeNumberInputs() {
7676
// Initialize app
7777
document.addEventListener('DOMContentLoaded', async () => {
7878
initializeNumberInputs()
79+
// Attach the embed message listener synchronously, before the first await, so
80+
// it is in place before the embedding iframe's load event can fire. Otherwise
81+
// a project pushed by the embedder right after load would be lost while we are
82+
// still awaiting loadPresets().
83+
initializeEmbedMessaging()
7984
await loadPresets()
8085
initializeTabs()
8186
initializeExportTabs()
@@ -88,6 +93,62 @@ document.addEventListener('DOMContentLoaded', async () => {
8893
checkForUpdates()
8994
})
9095

96+
// Embedding support: when this UI is loaded inside an <iframe> (e.g. the
97+
// LiaScript LiveEditor), the parent can push a complete project to us via
98+
// postMessage instead of the user having to upload files manually. We announce
99+
// readiness on load and feed any received File/Blob into the normal upload flow.
100+
function initializeEmbedMessaging() {
101+
// Only relevant when actually embedded.
102+
const isEmbedded = window.parent && window.parent !== window
103+
if (!isEmbedded) return
104+
105+
window.addEventListener('message', (event) => {
106+
const payload = event.data
107+
108+
// Respond to a readiness ping so the embedder doesn't have to rely on
109+
// catching our initial (one-shot) ready broadcast.
110+
if (payload && payload.type === 'liaexporter:ping') {
111+
const target = event.source || window.parent
112+
if (target) {
113+
target.postMessage({ type: 'liaexporter:ready' }, event.origin || '*')
114+
}
115+
return
116+
}
117+
118+
// Accept either a bare File/Blob or a wrapper { type, file/files }.
119+
let incoming = []
120+
if (payload instanceof File || payload instanceof Blob) {
121+
incoming = [payload]
122+
} else if (payload && payload.type === 'liaexporter:project') {
123+
if (Array.isArray(payload.files)) incoming = payload.files
124+
else if (payload.file) incoming = [payload.file]
125+
} else {
126+
return
127+
}
128+
129+
if (incoming.length === 0) return
130+
131+
// A Blob received via postMessage has no name -> wrap it as a named File so
132+
// the server-side ZIP detection (isZipFile) and file list work correctly.
133+
const files = incoming.map((f) =>
134+
f instanceof File
135+
? f
136+
: new File([f], 'project.zip', {
137+
type: f.type || 'application/zip',
138+
}),
139+
)
140+
141+
// Switch to the upload source tab and inject the files as if dropped.
142+
const uploadTab = document.querySelector('.tab-button[data-tab="upload"]')
143+
if (uploadTab) uploadTab.click()
144+
145+
handleFiles(files)
146+
})
147+
148+
// Tell the parent we are ready to receive a project.
149+
window.parent.postMessage({ type: 'liaexporter:ready' }, '*')
150+
}
151+
91152
async function checkForUpdates() {
92153
if (!window.electronAPI?.checkForUpdates) return
93154

src/server/server.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,28 @@ export async function startServer(
5050
logger: loggerConfig,
5151
})
5252

53+
// Allow cross-origin API access so a browser app on another origin (e.g. the
54+
// LiaScript LiveEditor) can talk to this server directly. The iframe-embed
55+
// flow is same-origin and does not need this, but a direct/remote API client
56+
// does. Loaded defensively so the server still starts if the optional
57+
// dependency is missing.
58+
try {
59+
const fastifyCors = (await import('@fastify/cors')).default
60+
await fastify.register(fastifyCors, { origin: true })
61+
} catch (err) {
62+
fastify.log.warn(
63+
'@fastify/cors not available - cross-origin API requests will be blocked',
64+
)
65+
}
66+
67+
// Allow this UI to be embedded in an <iframe> by the LiveEditor (or any other
68+
// host). We deliberately do NOT send X-Frame-Options, and set a permissive
69+
// frame-ancestors policy instead so framing works across origins.
70+
fastify.addHook('onSend', async (_request, reply, payload) => {
71+
reply.header('Content-Security-Policy', 'frame-ancestors *')
72+
return payload
73+
})
74+
5375
// Register plugins
5476
await fastify.register(fastifyMultipart, {
5577
limits: {

0 commit comments

Comments
 (0)