diff --git a/bin/formidablejs/Commands/Build.js b/bin/formidablejs/Commands/Build.js
index dbf14217..60e75b8b 100644
--- a/bin/formidablejs/Commands/Build.js
+++ b/bin/formidablejs/Commands/Build.js
@@ -84,7 +84,7 @@ class Build extends Command {
workerThreadErrors = true;
- return Output.write(" WARN Bun does not support node.js worker_threads yet.\n");
+ return Output.write(" WARN Bun does not support Node.js worker_threads yet.\n");
}
process.stdout.write(data);
diff --git a/bin/formidablejs/runtime.js b/bin/formidablejs/runtime.js
index 92992ddf..88a2a54a 100644
--- a/bin/formidablejs/runtime.js
+++ b/bin/formidablejs/runtime.js
@@ -1,16 +1,22 @@
const getRuntime = () => {
- const args = process.argv;
+ const path = process.argv[0];
let runtime = 'node';
- if (args) {
- const executor = args[0].split('/').pop();
+ if (path) {
+ const isWindows = path.endsWith('.exe');
+ const separator = isWindows ? '\\' : '/';
+ let executor = path.split(separator).slice(-1)[0];
- if (executor != undefined) {
+ if (isWindows) {
+ executor = executor.slice(0, -4);
+ }
+
+ if (executor) {
runtime = executor;
}
}
return runtime;
-}
+};
-module.exports = { getRuntime }
+module.exports = { getRuntime };
diff --git a/bin/imba/server.imba b/bin/imba/server.imba
index ca5522c9..04387543 100644
--- a/bin/imba/server.imba
+++ b/bin/imba/server.imba
@@ -3,18 +3,33 @@ import { Kernel } from '@formidablejs/framework'
import { join } from 'path'
import { execSync } from 'child_process'
import { writeFileSync, existsSync } from 'fs-extra'
+import { serve } from './server/serve'
const { app } = require('../../../../../bootstrap/main')
const application = app.initiate(app.make(Kernel), true)
application.then do(instance)
+ const runtime = do
+ const args = process.argv
+ let runtime = 'node'
+
+ if args && args.length > 0
+ const executor = args[0].split('/').pop!
+
+ runtime = executor if executor != undefined
+
+ runtime
+
const start = do
let port = process.env.PORT || 3000
let host = process.env.HOST || 'localhost'
let addr = process.env.ADDR || false
- imba.serve instance.fastify().server
+ if runtime! == 'bun'
+ serve instance.fastify().server
+ else
+ imba.serve instance.fastify().server
instance.fastify().listen({
port: Number(port),
diff --git a/bin/imba/server/env.imba b/bin/imba/server/env.imba
new file mode 100644
index 00000000..9b1c1755
--- /dev/null
+++ b/bin/imba/server/env.imba
@@ -0,0 +1,16 @@
+# imba$stdlib=1
+import np from 'path'
+
+export const env = new class Env
+
+ # TODO: remove pm2 hack
+ # when launching pm2 with an ecosystem file,
+ # process.argv[1] is ProcessContainerFork.js
+ # the problem with using process.env.pm_exec_path is if the shell is inherited
+ # from another process that was started with pm2, the pm_exec_path environment variable
+ # will also be inherited, which may or may not be a completely different path.
+ get rootDir
+ process.env.IMBA_OUTDIR or np.dirname(process.env.pm_exec_path or process.argv[1])
+
+ get publicPath
+ np.resolve(rootDir,process.env.IMBA_PUBDIR or global.IMBA_PUBDIR or 'public')
diff --git a/bin/imba/server/serve.imba b/bin/imba/server/serve.imba
new file mode 100644
index 00000000..565818f7
--- /dev/null
+++ b/bin/imba/server/serve.imba
@@ -0,0 +1,439 @@
+# imba$stdlib=1
+import cluster from 'cluster'
+import nfs from 'fs'
+import np from 'path'
+import {EventEmitter} from 'events'
+import {env} from './env'
+
+import http from 'http'
+import https from 'https'
+
+# TODO share mimeType list with bundler to
+# bundle supported file extensions
+const defaultHeaders = {
+ html: {'Content-Type': 'text/html; charset=utf-8'}
+ txt: {'Content-Type': 'text/plain; charset=utf-8'}
+ js: {'Content-Type': 'text/javascript; charset=utf-8'}
+ cjs: {'Content-Type': 'text/javascript; charset=utf-8'}
+ mjs: {'Content-Type': 'text/javascript; charset=utf-8'}
+ json: {'Content-Type': 'application/json; charset=utf-8'}
+ css: {'Content-Type': 'text/css; charset=utf-8'}
+ map: {'Content-Type': 'application/json; charset=utf-8'}
+
+ otf: {'Content-Type': 'font/otf'}
+ ttf: {'Content-Type': 'font/ttf'}
+ woff: {'Content-Type': 'font/woff'}
+ woff2: {'Content-Type': 'font/woff2'}
+
+ svg: {'Content-Type': 'image/svg+xml'}
+ avif: {'Content-Type': 'image/avif'}
+ gif: {'Content-Type': 'image/gif'}
+ png: {'Content-Type': 'image/png'}
+ apng: {'Content-Type': 'image/apng'}
+ webp: {'Content-Type': 'image/webp'}
+ jpg: {'Content-Type': 'image/jpeg'}
+ jpeg: {'Content-Type': 'image/jpeg'}
+ ico: {'Content-Type': 'image/x-icon'}
+ bmp: {'Content-Type': 'image/bmp'}
+ pdf: {'Content-Type': 'application/pdf'}
+
+ webm: {'Content-Type': 'video/webm'}
+ weba: {'Content-Type': 'audio/webm'}
+ avi: {'Content-Type': 'video/x-msvideo'}
+ mp3: {'Content-Type': 'audio/mpeg'}
+ mp4: {'Content-Type': 'video/mp4'}
+ m4a: {'Content-Type': 'audio/m4a'}
+ mov: {'Content-Type': 'video/quicktime'}
+ wmv: {'Content-Type': 'video/x-ms-wmv'}
+ mpeg: {'Content-Type': 'video/mpeg'}
+ wav: {'Content-Type': 'audio/wav'}
+ ogg: {'Content-Type': 'audio/ogg'}
+ ogv: {'Content-Type': 'video/ogg'}
+ oga: {'Content-Type': 'audio/ogg'}
+ opus: {'Content-Type': 'audio/opus'}
+}
+
+const hmrState = {
+ id: Date.now()
+}
+
+const proc = global.process
+
+class Servers < Set
+
+ def call name,...params
+ for server of self
+ server[name](...params)
+
+ def close o = {}
+ for server of self
+ server.close(o)
+
+ def reload o = {}
+ for server of self
+ server.reload(o)
+
+ def broadcast msg, ...rest
+ for server of self
+ server.broadcast(msg,...rest)
+
+ def emit event, data
+ for server of self
+ server.emit(event,data)
+
+ def sseEnd
+ let promises = []
+ for server of self
+ for client of server.clients
+ promises.push new Promise do(resolve)
+ client.on('finish',resolve)
+ client.end()
+ return Promise.all(promises)
+
+const servers = new Servers
+
+const process = new class Process < EventEmitter
+ def constructor
+ super
+ autoreload = no
+ state = {}
+
+ if global.IMBA_RUN
+ if cluster.isWorker
+ proc.on('message') do(msg)
+ emit('message',msg)
+ emit(...msg.slice(1)) if msg[0] == 'emit'
+ # reload! if msg == 'reload'
+ else
+ proc.on('message') do(msg)
+ emit(...msg.slice(1)) if msg[0] == 'emit'
+
+ self
+
+ def #setup
+ return unless #setup? =? yes
+
+ on('rebuild') do(e)
+ let prev = global.IMBA_MANIFEST
+ global.IMBA_MANIFEST = e
+ servers.broadcast('rebuild',e)
+
+ on('reloadHard') do(e)
+ servers.broadcast('reloadHard',e)
+ await servers.sseEnd()
+ proc.exit(0)
+
+ on('reloading') do(e)
+ state.reloading = yes
+ for server of servers
+ server.pause!
+
+ on('reloaded') do(e)
+ state.reloaded = yes
+ servers.broadcast('reloaded')
+ await new Promise do setTimeout($1,100)
+
+ let promises = for server of servers
+ server.close!
+
+ setTimeout(&,100) do proc.exit(0)
+ await Promise.all(promises)
+ proc.exit(0)
+ yes
+
+ def send msg
+ if proc.send isa Function
+ proc.send(msg)
+
+ def on name, cb
+ super
+
+ def reload
+ # only allow reloading once
+ return self unless isReloading =? yes
+ state.reloading = yes
+
+ unless proc.env.IMBA_SERVE
+ console.warn "not possible to gracefully reload servers not started via imba start"
+ return
+
+ send('reload')
+ return
+
+def deepImports src, links = [], depth = 0
+ let asset = global.IMBA_MANIFEST[src]
+ return links if links.indexOf(src) >= 0
+ if asset..imports
+
+ for item in asset..imports
+ # if links.indexOf(item) >= 0 and depth > 10
+ # return links
+ links.push(item)
+ deepImports(item, links, depth + 1)
+ return links
+
+class AssetResponder
+
+ def constructor server, url, asset = {}
+ server = server
+ url = url
+ [pathname,query] = url.split('?')
+ ext = np.extname(pathname)
+
+ headers = {
+ 'Content-Type': 'text/plain'
+ 'Access-Control-Allow-Origin': '*'
+ 'cache-control': 'public, max-age=31536000'
+ }
+ Object.assign(headers,server.options.assetHeaders or {})
+ Object.assign(headers,defaultHeaders[ext.slice(1)] or {})
+
+ headers["max-age"] = 86400000
+
+ if asset.imports and server.options.preload !== no
+ headers['Link'] = deepImports(url).map(do "<{$1}>; rel=modulepreload; as=script").join(', ')
+
+ path = server.localPathForUrl(url)
+
+ def respond req, res
+ nfs.access(path,nfs.constants.R_OK) do(err)
+ if err
+ res.writeHead(404,{})
+ return res.end!
+
+ try
+ if server.options.setHeaders
+ server.options.setHeaders(res,path)
+ if global.BUN
+ nfs.readFile(path) do(err,data)
+ res.writeHead(200,headers)
+ res.end(data)
+ else
+ let stream = nfs.createReadStream(path)
+ res.writeHead(200, headers)
+ return stream.pipe(res)
+ catch e
+ res.writeHead(503,{})
+ return res.end!
+
+ def createReadStream
+ nfs.createReadStream(path)
+
+ def pipe response
+ createReadStream!.pipe(response)
+
+class Server
+
+ static def wrap server, o = {}
+ new self(server,o)
+
+ def localPathForUrl url
+ let src = url.replace(/\?.*$/,'')
+ return urlToLocalPathMap[src] ??= if true
+ let path = np.resolve(env.publicPath,'.' + src)
+ let res = nfs.existsSync(path) and path
+ if !res and staticDir
+ path = np.resolve(staticDir,'.' + src)
+ res = nfs.existsSync(path) and path
+ res
+
+ def headersForAsset path
+ let ext = np.extname(path)
+ let headers = Object.assign({
+ 'Content-Type': 'text/plain'
+ 'Access-Control-Allow-Origin': '*'
+ 'cache-control': 'public'
+ },defaultHeaders[ext.slice(1)] or {})
+
+ get manifest
+ global.IMBA_MANIFEST or {}
+
+ def constructor srv,options = {}
+ servers.add(self)
+ id = Math.random!
+ startedAt = Date.now!
+ options = options
+ closed = no
+ paused = no
+ server = srv
+ clients = new Set
+ stalledResponses = []
+ assetResponders = {}
+ urlToLocalPathMap = {}
+ publicExistsMap = {}
+
+ staticDir = global.IMBA_STATICDIR or ''
+
+ if proc.env.IMBA_PATH
+ # what if there is no imba path?
+ devtoolsPath = np.resolve(proc.env.IMBA_PATH,'hmr.js')
+
+ scheme = srv isa http.Server ? 'http' : 'https'
+
+ # fetch and remove the original request listener
+ let originalHandler = server._events.request
+ let defaultHandler = server._events.request[0]
+ let dom = global.#dom
+
+ srv.off('request',originalHandler[0])
+
+ # check if this is an express app?
+ originalHandler.#server = self
+
+ srv.on('listening') do
+ # if not silent?
+ let adr = server.address!
+ let host = adr.address
+ if host == '::' or host == '0.0.0.0'
+ host = 'localhost'
+ let url = "{scheme}://{host}:{adr.port}/"
+ # unless proc.env.IMBA_CLUSTER
+ unless proc.env.IMBA_CLUSTER
+ console.log "listening on {url}"
+
+ if global.IMBA_HMR
+ global.IMBA_HMR_PATH = '/__hmr__.js'
+
+ handler = do(req,res)
+ let ishttp2 = req.constructor.name == 'Http2ServerRequest'
+ let url = req.url
+
+ if paused or closed
+ res.statusCode=302
+ res.setHeader('Location',req.url)
+
+ unless ishttp2
+ res.setHeader('Connection','close')
+
+ if closed
+ if ishttp2
+ req.stream.session.close!
+ return res.end!
+ else
+ return stalledResponses.push(res)
+
+ if url == '/__imba__.mjs'
+ res.writeHead(200, defaultHeaders.mjs)
+ let path = np.resolve(proc.env.IMBA_PATH,'dist','imba.mjs')
+ let stream = nfs.createReadStream(path)
+ return stream.pipe(res)
+
+ if global.IMBA_HMR
+ if url == '/__hmr__.json'
+ res.writeHead(200, defaultHeaders.json)
+ return res.end(JSON.stringify(hmrState))
+
+ elif url == '/__hmr__.js' and devtoolsPath
+ # and if hmr?
+ let stream = nfs.createReadStream(devtoolsPath)
+ res.writeHead(200, defaultHeaders.js)
+ return stream.pipe(res)
+
+ if url == '/__hmr__'
+ let headers = {
+ 'Content-Type': 'text/event-stream'
+ 'Cache-Control': 'no-cache'
+ }
+ unless ishttp2
+ headers['Connection'] = 'keep-alive'
+
+ res.writeHead(200,headers)
+ clients.add(res)
+ broadcast('init',global.IMBA_MANIFEST,[res])
+ broadcast('state',hmrState,[res])
+
+ req.on('close') do clients.delete(res)
+ return true
+
+ # create full url
+ let headers = req.headers
+ let base
+ if ishttp2
+ base = headers[':scheme'] + '://' + headers[':authority']
+ else
+ let scheme = req.connection.encrypted ? 'https' : 'http' #
+ base = scheme + '://' + headers.host
+
+ let asset = manifest[url]
+
+ if asset
+ let path = localPathForUrl(url)
+ if path
+ let responder = assetResponders[url] ||= new AssetResponder(self,url,asset)
+ return responder.respond(req,res)
+
+ if url.match(/\.[A-Z\d]{8}\./) or url.match(/\.\w{1,4}($|\?)/)
+ if let path = localPathForUrl(url)
+ try
+ let headers = headersForAsset(path)
+ if options.setHeaders
+ options.setHeaders(res,path)
+ if global.BUN
+ return nfs.readFile(path) do(err,data)
+ if err
+ res.writeHead(500,{})
+ res.write("Error getting the file: {err}")
+ else
+ res.writeHead(200,headers)
+ res.end(data)
+ else
+ let stream = nfs.createReadStream(path)
+ res.writeHead(200, headers)
+ return stream.pipe(res)
+ catch e
+ res.writeHead(503,{})
+ return res.end!
+
+ # continue to the real server
+ if dom
+ let loc = new dom.Location(req.url,base)
+ # create a context - not a document?
+ dom.Document.create(location: loc) do
+ return defaultHandler(req,res)
+ else
+ return defaultHandler(req,res)
+
+ srv.on('request',handler)
+
+ srv.on('close') do
+ console.log "server is closing!"
+
+ if global.IMBA_RUN
+ if cluster.isWorker or proc.env.IMBA_WATCH
+ process.#setup!
+ process.send('serve')
+
+ def broadcast event, data = {}, clients = clients
+ data = JSON.stringify(data)
+ let msg = "data: {data}\n\n\n"
+ for client of clients
+ client.write("event: {event}\n")
+ client.write("id: imba\n")
+ client.write(msg)
+ return self
+
+ def pause
+ if paused =? yes
+ broadcast('paused')
+ self
+
+ def resume
+ if paused =? no
+ broadcast('resumed')
+ flushStalledResponses!
+
+ def flushStalledResponses
+ for res in stalledResponses
+ res.end!
+ stalledResponses = []
+
+ def close
+ pause!
+
+ new Promise do(resolve)
+ closed = yes
+ server.close(resolve)
+ flushStalledResponses!
+
+export def serve srv,...params
+ return Server.wrap(srv,...params)
diff --git a/scripts/2-install.sh b/scripts/2-install.sh
index 7e676163..cba03bfd 100755
--- a/scripts/2-install.sh
+++ b/scripts/2-install.sh
@@ -29,7 +29,7 @@ echo
cd "$E2E" || exit 1
if [ "$PKG_MGR" = "bun" ]; then
- rm -rf node_modules yarn.lock package-lock.json bun.lockb && \
+ rm -rf node_modules yarn.lock package-lock.json bun.lock && \
sed -i '/"@formidablejs\/framework"/d' package.json && \
bun add sqlite3 "$PACKAGE"
else
diff --git a/src/Foundation/Console.imba b/src/Foundation/Console.imba
index 74ee2409..3c8c9722 100644
--- a/src/Foundation/Console.imba
+++ b/src/Foundation/Console.imba
@@ -4,6 +4,7 @@ import { existsSync } from 'fs-extra'
import { join } from 'path'
import { spawn, spawnSync, execSync } from 'child_process'
import { ServeEvents } from './Console/ServeEvents'
+import executor from '../Support/Helpers/runtime'
export default class Console
prop runtime\string
@@ -159,7 +160,7 @@ export default class Console
const srv = './node_modules/@formidablejs/framework/bin/imba/server.imba'
- const instance = spawn(runtime, [srv, '-s', '-w', '--esm'], {
+ const instance = spawn(executor!, [runtime, srv, '-s', '-w', '--esm'], {
stdio: 'pipe',
cwd: process.cwd!,
env: {
@@ -188,9 +189,12 @@ export default class Console
address = address.replace('::1', 'localhost')
+ if !address.startsWith('http://') && !address.startsWith('https://')
+ address = 'http://' + address
+
Output.write "{devCommands.length > 0 ? '' : '\n'} INFO Development Server running…\n"
- Output.write " Local: http://{address}\n"
+ Output.write " Local: {address}\n"
Output.write " Press Ctrl+C to stop the server\n"
@@ -212,8 +216,18 @@ export default class Console
if file && file.endsWith('\x1B[22m\x1B[32mmanifest.json\x1B[39m')
runDevCommands!
+ let workerThreadErrors = false
+
instance.stderr.on 'data', do(data)
- process.stdout.write data.toString!
+ if data.toString().startsWith('NotImplementedError: worker_threads.Worker')
+ if workerThreadErrors
+ return
+
+ workerThreadErrors = true
+
+ return Output.write("\n WARN Bun does not support Node.js worker_threads yet.")
+
+ process.stdout.write data
instance.on 'exit', do
process.exit!
@@ -231,9 +245,10 @@ export default class Console
return spawn(sh, [shFlag, self.runtime, self.console, ...args], {
...self.config
env: process.env
+ shell: true
})
- spawn(runtime, [self.console, ...args], {
+ spawn(self.runtime, [ self.console, ...args], {
...self.config
env: process.env
})
diff --git a/src/Foundation/Console/Commands/ServeCommand.imba b/src/Foundation/Console/Commands/ServeCommand.imba
index 6e2ce0c8..448c7deb 100644
--- a/src/Foundation/Console/Commands/ServeCommand.imba
+++ b/src/Foundation/Console/Commands/ServeCommand.imba
@@ -5,7 +5,7 @@ import { existsSync } from 'fs'
import { Prop } from '@formidablejs/console'
import { spawnSync } from 'child_process'
import { ServeEvents } from '../ServeEvents'
-import isNumber from '../../../Support/Helpers/isNumber'
+import executor from '../../../Support/Helpers/runtime'
import nodemon from 'nodemon'
export class ServeCommand < Command
@@ -28,6 +28,9 @@ export class ServeCommand < Command
prop #fullAddress\string
+ get isBun
+ typeof Bun !== 'undefined'
+
get ext
const appPackage = join(process.cwd!, 'package.json')
@@ -39,7 +42,15 @@ export class ServeCommand < Command
language.toLowerCase! == 'typescript' ? '.ts' : '.imba'
get runtime
- join process.cwd!, 'node_modules', '.bin', 'imba' + (process.platform === 'win32' ? '.cmd' : '')
+ let _path = join process.cwd!, 'node_modules', '.bin', 'imba'
+
+ if process.platform === 'win32'
+ if existsSync(_path + '.cmd')
+ return _path + '.cmd'
+ elif existsSync(_path + '.exe')
+ return _path + '.exe'
+
+ _path
get devConfigDefaults
{
@@ -109,7 +120,7 @@ export class ServeCommand < Command
get commandList
const list\array = self.devCommands
- list.push("{runtime} server{ext} -f -s -v --esm")
+ list.push("{!isBun ? executor! + ' ' : ''}{runtime} server{ext} -f -s -v --esm")
list.join(' && ')
@@ -129,7 +140,7 @@ export class ServeCommand < Command
self.setEnvVars(port)
- const args = ['-s']
+ const args = ['-s', '--esm']
if self.option('addr') then process.env.FORMIDABLE_ADDRESS_SET = '1'
@@ -146,7 +157,7 @@ export class ServeCommand < Command
return spawnSync sh, [ shFlag, self.runtime, "server{ext}", ...args ], conf
- spawnSync self.runtime, [ "server{ext}", ...args ], conf
+ spawnSync executor!, [ self.runtime, "server{ext}", ...args ], conf
else
process.env.CONSOLE_FORMIDABLE_GROUP = JSON.stringify({
newLine: false
@@ -162,13 +173,17 @@ export class ServeCommand < Command
watch: ['.', '.env']
})
- process.once('SIGINT', do
+ def exitApp
self.message 'info', 'Shutting down development server…'
server.reset()
process.exit(0)
- )
+
+ if isBun
+ process.on('SIGINT', exitApp)
+ else
+ process.once('SIGINT', exitApp)
server.on 'stdout', do(e)
const data = e.toString()
diff --git a/src/Hashing/Hash.imba b/src/Hashing/Hash.imba
index db9db16c..8db6aa63 100644
--- a/src/Hashing/Hash.imba
+++ b/src/Hashing/Hash.imba
@@ -1,6 +1,5 @@
import InvalidHashDriverException from './Exceptions/InvalidHashDriverException'
import InvalidHashConfigurationException from './Exceptions/InvalidHashConfigurationException'
-import bcrypt from 'bcrypt'
const settings = {
config: null
@@ -17,14 +16,26 @@ export default class Hash
static def make value\string
if settings.config.driver == 'argon2'
+ if isBun!
+ return await Bun.password.hash(password, settings.config.argon2)
+
return await getDriver!.hash(value, settings.config.argon2)
if settings.config.driver == 'bcrypt'
+ if isBun!
+ return await Bun.password.hash(password, {
+ algorithm: "bcrypt",
+ cost: settings.config.bcrypt.rounds ?? 10
+ })
+
return await getDriver!.hash(value, settings.config.bcrypt.rounds ?? 10)
throw new InvalidHashDriverException "{settings.config.driver} is not a valid driver."
static def check value\string, hash\string
+ if isBun!
+ return await Bun.password.verify(value, hash)
+
if settings.config.driver == 'argon2'
return await getDriver!.verify(hash, value, settings.config.argon2)
@@ -49,19 +60,24 @@ export default class Hash
if config.argon2.parallelism == null
throw new InvalidHashConfigurationException 'argon2 parallelism is missing.'
- try
- settings.driver = require('argon2')
- catch
- throw new InvalidHashDriverException 'argon2 is not installed. Please run "npm install argon2".'
+ if !isBun!
+ try
+ settings.driver = require('argon2')
+ catch
+ throw new InvalidHashDriverException 'argon2 is not installed. Please run "npm install argon2".'
if config.driver == 'bcrypt'
if config.bcrypt.rounds == null
throw new InvalidHashConfigurationException 'bcrypt rounds is missing.'
- settings.driver = bcrypt
+ if !isBun!
+ settings.driver = require('bcrypt')
settings.config = config
static def reset
settings.config = null
settings.driver = null
+
+ static def isBun
+ typeof Bun !== 'undefined'
diff --git a/src/Http/Kernel.imba b/src/Http/Kernel.imba
index 8f15e6a1..4ff9d05a 100644
--- a/src/Http/Kernel.imba
+++ b/src/Http/Kernel.imba
@@ -1,20 +1,22 @@
-import http from 'http'
-import { writeFileSync } from 'fs-extra'
-import { join } from 'path'
import { handleMaintenanceMode } from '../Foundation/Exceptions/Handler/handleException'
+import { join } from 'path'
+import { serve } from './server/serve'
+import { writeFileSync } from 'fs-extra'
+import runtime from '../Support/Helpers/runtime'
import fastify from 'fastify'
import FormRequest from './Request/FormRequest'
import getResponse from './Kernel/getResponse'
import handleNotFound from './Kernel/handleNotFound'
import hasContentTypes from './Kernel/hasContentTypes'
+import http from 'http'
import InvalidRouteActionException from './Router/Exceptions/InvalidRouteActionException'
import isArray from '../Support/Helpers/isArray'
import isClass from '../Support/Helpers/isClass'
import isEmpty from '../Support/Helpers/isEmpty'
import isFunction from '../Support/Helpers/isFunction'
import MaintenanceModeException from '../Foundation/Exceptions/MaintenanceModeException'
-import resolveResponse from './Kernel/resolveResponse'
import Redirect from './Redirect/Redirect'
+import resolveResponse from './Kernel/resolveResponse'
import Route from './Router/Route'
import UndefinedMiddlewareException from './Exceptions/UndefinedMiddlewareException'
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
@@ -134,7 +136,10 @@ export default class Kernel
console.log "listening on {url}"
- imba.serve router.server
+ if runtime! == 'bun'
+ serve router.server
+ else
+ imba.serve router.server
router.listen({ port: Number(port) })
.then(do(address)
diff --git a/src/Http/server/env.imba b/src/Http/server/env.imba
new file mode 100644
index 00000000..9b1c1755
--- /dev/null
+++ b/src/Http/server/env.imba
@@ -0,0 +1,16 @@
+# imba$stdlib=1
+import np from 'path'
+
+export const env = new class Env
+
+ # TODO: remove pm2 hack
+ # when launching pm2 with an ecosystem file,
+ # process.argv[1] is ProcessContainerFork.js
+ # the problem with using process.env.pm_exec_path is if the shell is inherited
+ # from another process that was started with pm2, the pm_exec_path environment variable
+ # will also be inherited, which may or may not be a completely different path.
+ get rootDir
+ process.env.IMBA_OUTDIR or np.dirname(process.env.pm_exec_path or process.argv[1])
+
+ get publicPath
+ np.resolve(rootDir,process.env.IMBA_PUBDIR or global.IMBA_PUBDIR or 'public')
diff --git a/src/Http/server/serve.imba b/src/Http/server/serve.imba
new file mode 100644
index 00000000..565818f7
--- /dev/null
+++ b/src/Http/server/serve.imba
@@ -0,0 +1,439 @@
+# imba$stdlib=1
+import cluster from 'cluster'
+import nfs from 'fs'
+import np from 'path'
+import {EventEmitter} from 'events'
+import {env} from './env'
+
+import http from 'http'
+import https from 'https'
+
+# TODO share mimeType list with bundler to
+# bundle supported file extensions
+const defaultHeaders = {
+ html: {'Content-Type': 'text/html; charset=utf-8'}
+ txt: {'Content-Type': 'text/plain; charset=utf-8'}
+ js: {'Content-Type': 'text/javascript; charset=utf-8'}
+ cjs: {'Content-Type': 'text/javascript; charset=utf-8'}
+ mjs: {'Content-Type': 'text/javascript; charset=utf-8'}
+ json: {'Content-Type': 'application/json; charset=utf-8'}
+ css: {'Content-Type': 'text/css; charset=utf-8'}
+ map: {'Content-Type': 'application/json; charset=utf-8'}
+
+ otf: {'Content-Type': 'font/otf'}
+ ttf: {'Content-Type': 'font/ttf'}
+ woff: {'Content-Type': 'font/woff'}
+ woff2: {'Content-Type': 'font/woff2'}
+
+ svg: {'Content-Type': 'image/svg+xml'}
+ avif: {'Content-Type': 'image/avif'}
+ gif: {'Content-Type': 'image/gif'}
+ png: {'Content-Type': 'image/png'}
+ apng: {'Content-Type': 'image/apng'}
+ webp: {'Content-Type': 'image/webp'}
+ jpg: {'Content-Type': 'image/jpeg'}
+ jpeg: {'Content-Type': 'image/jpeg'}
+ ico: {'Content-Type': 'image/x-icon'}
+ bmp: {'Content-Type': 'image/bmp'}
+ pdf: {'Content-Type': 'application/pdf'}
+
+ webm: {'Content-Type': 'video/webm'}
+ weba: {'Content-Type': 'audio/webm'}
+ avi: {'Content-Type': 'video/x-msvideo'}
+ mp3: {'Content-Type': 'audio/mpeg'}
+ mp4: {'Content-Type': 'video/mp4'}
+ m4a: {'Content-Type': 'audio/m4a'}
+ mov: {'Content-Type': 'video/quicktime'}
+ wmv: {'Content-Type': 'video/x-ms-wmv'}
+ mpeg: {'Content-Type': 'video/mpeg'}
+ wav: {'Content-Type': 'audio/wav'}
+ ogg: {'Content-Type': 'audio/ogg'}
+ ogv: {'Content-Type': 'video/ogg'}
+ oga: {'Content-Type': 'audio/ogg'}
+ opus: {'Content-Type': 'audio/opus'}
+}
+
+const hmrState = {
+ id: Date.now()
+}
+
+const proc = global.process
+
+class Servers < Set
+
+ def call name,...params
+ for server of self
+ server[name](...params)
+
+ def close o = {}
+ for server of self
+ server.close(o)
+
+ def reload o = {}
+ for server of self
+ server.reload(o)
+
+ def broadcast msg, ...rest
+ for server of self
+ server.broadcast(msg,...rest)
+
+ def emit event, data
+ for server of self
+ server.emit(event,data)
+
+ def sseEnd
+ let promises = []
+ for server of self
+ for client of server.clients
+ promises.push new Promise do(resolve)
+ client.on('finish',resolve)
+ client.end()
+ return Promise.all(promises)
+
+const servers = new Servers
+
+const process = new class Process < EventEmitter
+ def constructor
+ super
+ autoreload = no
+ state = {}
+
+ if global.IMBA_RUN
+ if cluster.isWorker
+ proc.on('message') do(msg)
+ emit('message',msg)
+ emit(...msg.slice(1)) if msg[0] == 'emit'
+ # reload! if msg == 'reload'
+ else
+ proc.on('message') do(msg)
+ emit(...msg.slice(1)) if msg[0] == 'emit'
+
+ self
+
+ def #setup
+ return unless #setup? =? yes
+
+ on('rebuild') do(e)
+ let prev = global.IMBA_MANIFEST
+ global.IMBA_MANIFEST = e
+ servers.broadcast('rebuild',e)
+
+ on('reloadHard') do(e)
+ servers.broadcast('reloadHard',e)
+ await servers.sseEnd()
+ proc.exit(0)
+
+ on('reloading') do(e)
+ state.reloading = yes
+ for server of servers
+ server.pause!
+
+ on('reloaded') do(e)
+ state.reloaded = yes
+ servers.broadcast('reloaded')
+ await new Promise do setTimeout($1,100)
+
+ let promises = for server of servers
+ server.close!
+
+ setTimeout(&,100) do proc.exit(0)
+ await Promise.all(promises)
+ proc.exit(0)
+ yes
+
+ def send msg
+ if proc.send isa Function
+ proc.send(msg)
+
+ def on name, cb
+ super
+
+ def reload
+ # only allow reloading once
+ return self unless isReloading =? yes
+ state.reloading = yes
+
+ unless proc.env.IMBA_SERVE
+ console.warn "not possible to gracefully reload servers not started via imba start"
+ return
+
+ send('reload')
+ return
+
+def deepImports src, links = [], depth = 0
+ let asset = global.IMBA_MANIFEST[src]
+ return links if links.indexOf(src) >= 0
+ if asset..imports
+
+ for item in asset..imports
+ # if links.indexOf(item) >= 0 and depth > 10
+ # return links
+ links.push(item)
+ deepImports(item, links, depth + 1)
+ return links
+
+class AssetResponder
+
+ def constructor server, url, asset = {}
+ server = server
+ url = url
+ [pathname,query] = url.split('?')
+ ext = np.extname(pathname)
+
+ headers = {
+ 'Content-Type': 'text/plain'
+ 'Access-Control-Allow-Origin': '*'
+ 'cache-control': 'public, max-age=31536000'
+ }
+ Object.assign(headers,server.options.assetHeaders or {})
+ Object.assign(headers,defaultHeaders[ext.slice(1)] or {})
+
+ headers["max-age"] = 86400000
+
+ if asset.imports and server.options.preload !== no
+ headers['Link'] = deepImports(url).map(do "<{$1}>; rel=modulepreload; as=script").join(', ')
+
+ path = server.localPathForUrl(url)
+
+ def respond req, res
+ nfs.access(path,nfs.constants.R_OK) do(err)
+ if err
+ res.writeHead(404,{})
+ return res.end!
+
+ try
+ if server.options.setHeaders
+ server.options.setHeaders(res,path)
+ if global.BUN
+ nfs.readFile(path) do(err,data)
+ res.writeHead(200,headers)
+ res.end(data)
+ else
+ let stream = nfs.createReadStream(path)
+ res.writeHead(200, headers)
+ return stream.pipe(res)
+ catch e
+ res.writeHead(503,{})
+ return res.end!
+
+ def createReadStream
+ nfs.createReadStream(path)
+
+ def pipe response
+ createReadStream!.pipe(response)
+
+class Server
+
+ static def wrap server, o = {}
+ new self(server,o)
+
+ def localPathForUrl url
+ let src = url.replace(/\?.*$/,'')
+ return urlToLocalPathMap[src] ??= if true
+ let path = np.resolve(env.publicPath,'.' + src)
+ let res = nfs.existsSync(path) and path
+ if !res and staticDir
+ path = np.resolve(staticDir,'.' + src)
+ res = nfs.existsSync(path) and path
+ res
+
+ def headersForAsset path
+ let ext = np.extname(path)
+ let headers = Object.assign({
+ 'Content-Type': 'text/plain'
+ 'Access-Control-Allow-Origin': '*'
+ 'cache-control': 'public'
+ },defaultHeaders[ext.slice(1)] or {})
+
+ get manifest
+ global.IMBA_MANIFEST or {}
+
+ def constructor srv,options = {}
+ servers.add(self)
+ id = Math.random!
+ startedAt = Date.now!
+ options = options
+ closed = no
+ paused = no
+ server = srv
+ clients = new Set
+ stalledResponses = []
+ assetResponders = {}
+ urlToLocalPathMap = {}
+ publicExistsMap = {}
+
+ staticDir = global.IMBA_STATICDIR or ''
+
+ if proc.env.IMBA_PATH
+ # what if there is no imba path?
+ devtoolsPath = np.resolve(proc.env.IMBA_PATH,'hmr.js')
+
+ scheme = srv isa http.Server ? 'http' : 'https'
+
+ # fetch and remove the original request listener
+ let originalHandler = server._events.request
+ let defaultHandler = server._events.request[0]
+ let dom = global.#dom
+
+ srv.off('request',originalHandler[0])
+
+ # check if this is an express app?
+ originalHandler.#server = self
+
+ srv.on('listening') do
+ # if not silent?
+ let adr = server.address!
+ let host = adr.address
+ if host == '::' or host == '0.0.0.0'
+ host = 'localhost'
+ let url = "{scheme}://{host}:{adr.port}/"
+ # unless proc.env.IMBA_CLUSTER
+ unless proc.env.IMBA_CLUSTER
+ console.log "listening on {url}"
+
+ if global.IMBA_HMR
+ global.IMBA_HMR_PATH = '/__hmr__.js'
+
+ handler = do(req,res)
+ let ishttp2 = req.constructor.name == 'Http2ServerRequest'
+ let url = req.url
+
+ if paused or closed
+ res.statusCode=302
+ res.setHeader('Location',req.url)
+
+ unless ishttp2
+ res.setHeader('Connection','close')
+
+ if closed
+ if ishttp2
+ req.stream.session.close!
+ return res.end!
+ else
+ return stalledResponses.push(res)
+
+ if url == '/__imba__.mjs'
+ res.writeHead(200, defaultHeaders.mjs)
+ let path = np.resolve(proc.env.IMBA_PATH,'dist','imba.mjs')
+ let stream = nfs.createReadStream(path)
+ return stream.pipe(res)
+
+ if global.IMBA_HMR
+ if url == '/__hmr__.json'
+ res.writeHead(200, defaultHeaders.json)
+ return res.end(JSON.stringify(hmrState))
+
+ elif url == '/__hmr__.js' and devtoolsPath
+ # and if hmr?
+ let stream = nfs.createReadStream(devtoolsPath)
+ res.writeHead(200, defaultHeaders.js)
+ return stream.pipe(res)
+
+ if url == '/__hmr__'
+ let headers = {
+ 'Content-Type': 'text/event-stream'
+ 'Cache-Control': 'no-cache'
+ }
+ unless ishttp2
+ headers['Connection'] = 'keep-alive'
+
+ res.writeHead(200,headers)
+ clients.add(res)
+ broadcast('init',global.IMBA_MANIFEST,[res])
+ broadcast('state',hmrState,[res])
+
+ req.on('close') do clients.delete(res)
+ return true
+
+ # create full url
+ let headers = req.headers
+ let base
+ if ishttp2
+ base = headers[':scheme'] + '://' + headers[':authority']
+ else
+ let scheme = req.connection.encrypted ? 'https' : 'http' #
+ base = scheme + '://' + headers.host
+
+ let asset = manifest[url]
+
+ if asset
+ let path = localPathForUrl(url)
+ if path
+ let responder = assetResponders[url] ||= new AssetResponder(self,url,asset)
+ return responder.respond(req,res)
+
+ if url.match(/\.[A-Z\d]{8}\./) or url.match(/\.\w{1,4}($|\?)/)
+ if let path = localPathForUrl(url)
+ try
+ let headers = headersForAsset(path)
+ if options.setHeaders
+ options.setHeaders(res,path)
+ if global.BUN
+ return nfs.readFile(path) do(err,data)
+ if err
+ res.writeHead(500,{})
+ res.write("Error getting the file: {err}")
+ else
+ res.writeHead(200,headers)
+ res.end(data)
+ else
+ let stream = nfs.createReadStream(path)
+ res.writeHead(200, headers)
+ return stream.pipe(res)
+ catch e
+ res.writeHead(503,{})
+ return res.end!
+
+ # continue to the real server
+ if dom
+ let loc = new dom.Location(req.url,base)
+ # create a context - not a document?
+ dom.Document.create(location: loc) do
+ return defaultHandler(req,res)
+ else
+ return defaultHandler(req,res)
+
+ srv.on('request',handler)
+
+ srv.on('close') do
+ console.log "server is closing!"
+
+ if global.IMBA_RUN
+ if cluster.isWorker or proc.env.IMBA_WATCH
+ process.#setup!
+ process.send('serve')
+
+ def broadcast event, data = {}, clients = clients
+ data = JSON.stringify(data)
+ let msg = "data: {data}\n\n\n"
+ for client of clients
+ client.write("event: {event}\n")
+ client.write("id: imba\n")
+ client.write(msg)
+ return self
+
+ def pause
+ if paused =? yes
+ broadcast('paused')
+ self
+
+ def resume
+ if paused =? no
+ broadcast('resumed')
+ flushStalledResponses!
+
+ def flushStalledResponses
+ for res in stalledResponses
+ res.end!
+ stalledResponses = []
+
+ def close
+ pause!
+
+ new Promise do(resolve)
+ closed = yes
+ server.close(resolve)
+ flushStalledResponses!
+
+export def serve srv,...params
+ return Server.wrap(srv,...params)
diff --git a/src/Support/Helpers/runtime.imba b/src/Support/Helpers/runtime.imba
index dcb57724..6a0fb961 100644
--- a/src/Support/Helpers/runtime.imba
+++ b/src/Support/Helpers/runtime.imba
@@ -1,10 +1,19 @@
export default def runtime
- const args = process.argv
+ if process == undefined
+ return 'browser'
+
+ const path = process.argv[0]
let runtime = 'node'
- if args && args.length > 0
- const executor = args[0].split('/').pop!
+ if path
+ const isWindows = path.endsWith('.exe')
+ const separator = isWindows ? '\\' : '/'
+ let executor = path.split(separator).slice(-1)[0];
+
+ if isWindows
+ executor = executor.slice(0, -4);
- runtime = executor if executor != undefined
+ if executor
+ runtime = executor
runtime
diff --git a/types/Support/Helpers/runtime.d.ts b/types/Support/Helpers/runtime.d.ts
index bd5a5603..16e0268e 100644
--- a/types/Support/Helpers/runtime.d.ts
+++ b/types/Support/Helpers/runtime.d.ts
@@ -1,4 +1,4 @@
-type Runtime = 'node' | 'bun' | 'deno'
+type Runtime = 'node' | 'bun' | 'deno' | 'browser'
/**
* Get the current runtime.