Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 28 additions & 39 deletions src/apps/AppLoader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const BLOCKED_PATTERNS = [
'process.exit', 'child_process', 'require(', '__proto__',
'Object.prototype', 'globalThis', 'eval(', 'import('
'process.exit', 'child_process', 'require(', '__proto__', 'Object.prototype', 'globalThis', 'eval(', 'import('
]

let _nm = null
Expand All @@ -13,9 +12,16 @@ async function _nodeModules() {
import('node:url')
])
_nm = {
readdir: fsp.readdir, readFile: fsp.readFile, watch: fsp.watch, access: fsp.access,
existsSync: fsSync.existsSync, join: path.join, basename: path.basename,
extname: path.extname, resolve: path.resolve, pathToFileURL: url.pathToFileURL
readdir: fsp.readdir,
readFile: fsp.readFile,
watch: fsp.watch,
access: fsp.access,
existsSync: fsSync.existsSync,
join: path.join,
basename: path.basename,
extname: path.extname,
resolve: path.resolve,
pathToFileURL: url.pathToFileURL
}
return _nm
}
Expand All @@ -42,8 +48,7 @@ export class AppLoader {

async loadAll() {
const { readdir, access, join, basename, extname } = await _nodeModules()
const seen = new Set()
const results = []
const seen = new Set(), results = []
for (const dir of this._dirs) {
const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
for (const entry of entries) {
Expand All @@ -55,8 +60,7 @@ export class AppLoader {
}
if (name && !seen.has(name)) {
seen.add(name)
const loaded = await this.loadApp(name)
if (loaded) results.push(name)
if (await this.loadApp(name)) results.push(name)
}
}
}
Expand All @@ -76,15 +80,15 @@ export class AppLoader {
this._loaded.set(name, { filePath, source, clientCode: source })
return appDef
} catch (e) {
console.error(`[AppLoader] failed to load "${name}": ${e.message}\n file: ${filePath}\n stack: ${e.stack?.split('\n').slice(1, 3).join('\n ') || 'none'}`)
console.error(`[AppLoader] failed to load "${name}": ${e.message}`)
return null
}
}

_validate(source, name) {
for (const pattern of BLOCKED_PATTERNS) {
if (source.includes(pattern)) {
console.error(`[AppLoader] blocked pattern "${pattern}" in ${name}`)
for (const p of BLOCKED_PATTERNS) {
if (source.includes(p)) {
console.error(`[AppLoader] blocked pattern "${p}" in ${name}`)
return false
}
}
Expand All @@ -99,36 +103,29 @@ export class AppLoader {
const mod = await import(url)
return mod.default || mod
} catch (e) {
console.error(`[AppLoader] syntax/eval error in "${filePath}": ${e.message}\n ${e.stack?.split('\n').slice(1, 3).join('\n ') || ''}`)
console.error(`[AppLoader] syntax/eval error in "${filePath}": ${e.message}`)
return null
}
}

async watchAll() {
const { existsSync, watch, join, basename, extname } = await _nodeModules()
for (const dir of this._dirs) {
if (!existsSync(dir)) {
console.debug(`[AppLoader] skipping watch for missing directory: ${dir}`)
continue
}
if (!existsSync(dir)) continue
try {
const ac = new AbortController()
const watcher = watch(dir, { recursive: true, signal: ac.signal })
this._watchers.set(dir, ac)
;(async () => {
try {
for await (const event of watcher) {
if (!event.filename || !event.filename.endsWith('.js')) continue
const parts = event.filename.replace(/\\/g, '/').split('/')
const name = parts.length > 1
? parts[0]
: basename(event.filename, extname(event.filename))
for await (const ev of watcher) {
if (!ev.filename?.endsWith('.js')) continue
const parts = ev.filename.replace(/\\/g, '/').split('/')
const name = parts.length > 1 ? parts[0] : basename(ev.filename, extname(ev.filename))
await this._onFileChange(name)
}
} catch (e) {
if (e.name !== 'AbortError') {
console.error(`[AppLoader] watch error:`, e.message)
}
if (e.name !== 'AbortError') console.error(`[AppLoader] watch error:`, e.message)
}
})()
} catch (e) {
Expand All @@ -141,10 +138,7 @@ export class AppLoader {
console.log(`[AppLoader] reloading ${name}`)
const appDef = await this.loadApp(name)
if (appDef) {
const cb = this._onReloadCallback ? (n, d) => {
this._onReloadCallback(n, this._loaded.get(n)?.clientCode)
} : null
this._runtime.queueReload(name, appDef, cb)
this._runtime.queueReload(name, appDef, this._onReloadCallback ? (n, d) => this._onReloadCallback(n, this._loaded.get(n)?.clientCode) : null)
console.log(`[AppLoader] queued hot reload ${name}`)
}
}
Expand All @@ -158,9 +152,7 @@ export class AppLoader {

getClientModules() {
const modules = {}
for (const [name, data] of this._loaded) {
if (data.clientCode) modules[name] = data.clientCode
}
for (const [name, data] of this._loaded) if (data.clientCode) modules[name] = data.clientCode
return modules
}

Expand All @@ -170,8 +162,7 @@ export class AppLoader {
if (!this._validate(source, name)) return null
const revokes = []
try {
const rewrittenSource = deps ? this._rewriteDeps(source, deps, revokes) : source
const blob = new Blob([rewrittenSource], { type: 'application/javascript' })
const blob = new Blob([deps ? this._rewriteDeps(source, deps, revokes) : source], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
revokes.push(url)
const mod = await import(url)
Expand All @@ -198,8 +189,6 @@ export class AppLoader {
revokes.push(url)
urlMap[spec] = url
}
return source.replace(/((?:from|import)\s*)(['"])(\.[^'"]+)\2/g, (m, pre, q, spec) =>
urlMap[spec] ? `${pre}${q}${urlMap[spec]}${q}` : m
)
return source.replace(/((?:from|import)\s*)(['"])(\.[^'"]+)\2/g, (m, pre, q, spec) => urlMap[spec] ? `${pre}${q}${urlMap[spec]}${q}` : m)
}
}
Loading
Loading