diff --git a/botanjs/old/__init__.py b/botanjs/old/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/botanjs/old/classmap.py b/botanjs/old/classmap.py deleted file mode 100644 index f41555d..0000000 --- a/botanjs/old/classmap.py +++ /dev/null @@ -1,204 +0,0 @@ -import os, re, sys - -from xml.dom import minidom -from collections import defaultdict - -from botanjs.config import DEBUG - -if DEBUG: - from botanjs.utils import checksum_r as checksum -else: - from botanjs.utils import checksum - -RegEx_N = re.compile( r""" - .* - __namespace - \s*\( - \s*(['"])([^\1]+)\1 - \s*\) - .* - """, re.X ) - -RegEx_I = re.compile( r""" - .* - __import - \s*\( - \s*(['"])([^\1]+)\1 - \s*\) - .* - """, re.X ) - -RegEx_V = re.compile( r""" - .* - ns - \s*\[ - \s*NS_INVOKE - \s*\] - \s*\( - \s*(['"])([^\1]+)\1 - \s*\) - .* - """, re.X ) - -RegEx_E = re.compile( r""" - .* - ns - \s*\[ - \s*NS_EXPORT - \s*\] - \s*\( - \s*EX_([A-Z_]+[A-Z]) - \s*, - \s*(['"])([^\1]+)\2 - \s*, - [^\)]+ - \s*\) - .* - """, re.X ) - -def classMeta( cf ): - ns = "" - imps = list() - exps = list() - for line in open( cf, "r" ): - - m = RegEx_N.match( line ) - if m: - ns = m.group(2) - continue - - m = RegEx_I.match( line ) - if m: - imps.append( m.group(2) ) - continue - - m = RegEx_V.match( line ) - if m: - imps.append( ns + "." + m.group(2) ) - continue - - m = RegEx_E.match( line ) - if m: - exps.append( [ m.group(1), m.group(3) ] ) - continue - - return [ ns, imps, exps ] - -def className( classFile ): - return ( os.path - .splitext( classFile )[0] - .replace( os.sep, "." ) - .replace( "._this", "" ) - .replace( "..BotanJS.", "" ) ) - -# __export types definition => nodeName -EX_CLASS = "class" -EX_FUNC = "method" -eDef = defaultdict( lambda: 'prop', { "CLASS": EX_CLASS, "FUNC": EX_FUNC } ) - -class ClassMap: - head = None - DOM = None - R = None - - def __init__( self, BotanRoot ): - self.R = BotanRoot - self.DOM = minidom.parseString( "" ) - head = os.path.join( self.R, "_this.js" ) - - def getNode( self, name, t = EX_CLASS ): - paths = name.split( "." ) - - currentNode = self.DOM.firstChild - - # Step down the path and create the path if necessary - for path in paths: - - l = currentNode.childNodes - for i in l: - if i.getAttribute( "name" ) == path: - currentNode = i - break - - if currentNode.getAttribute( "name" ) != path: - newNode = self.DOM.createElement( t ) - newNode.setAttribute( "name", path ) - currentNode.appendChild( newNode ) - currentNode = newNode - - return currentNode - - - - def skipFile( self, cf ): - - if os.path.splitext( cf )[1] == ".js": - if cf == self.head: - return True - return False - - return True; - - def drawMap( self, ns, ci, ce, cf, chksum ): - nsNode = self.getNode( ns ) - - # Source every: - # Since namespace may differ from file name - # ns.__export may export to a different level - # Source the exported val explicitly means - # exports only available when source file is imported - srcEvery = ( ns != className( cf ) ) - - cf = cf.replace( self.R, "" ) - - if not srcEvery: - nsNode.setAttribute( "src", cf ) - nsNode.setAttribute( "js", chksum["js"] ) - nsNode.setAttribute( "css", chksum["css"] ) - - for ex in ce: - _t = eDef[ ex[0] ] - cNode = self.getNode( ns + "." + ex[1], t = _t ) - - if srcEvery: - # The import is for the defined class - if _t == EX_CLASS: - for imp in ci: - impNode = self.DOM.createElement( "import" ) - impNode.appendChild( self.DOM.createTextNode( imp ) ) - cNode.appendChild( impNode ) - - cNode.setAttribute( "src", cf ) - cNode.setAttribute( "js", chksum["js"] ) - cNode.setAttribute( "css", chksum["css"] ) - - # the file dose not export classes - # Hence it import for itself - if not srcEvery: - for imp in ci: - impNode = self.DOM.createElement( "import" ) - impNode.appendChild( self.DOM.createTextNode( imp ) ) - nsNode.appendChild( impNode ) - - - def build( self ): - for root, dirs, files in os.walk( self.R ): - - if root == self.R: - dirs.remove("externs") - - for name in files: - classFile = os.path.join( root, name ) - - if self.skipFile( classFile ): - continue - - ns, ci, ce = classMeta( classFile ) - - chksum = {} - chksum[ "js" ] = checksum( classFile ) - chksum[ "css" ] = checksum( classFile[:-2] + "css" ) - - classFile = classFile.replace( self.R + os.path.sep, "" ) - self.drawMap( ns, ci, ce, classFile, chksum ) - return self.DOM.toxml() diff --git a/botanjs/old/compressor/__init__.py b/botanjs/old/compressor/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/botanjs/old/compressor/closure.py b/botanjs/old/compressor/closure.py deleted file mode 100644 index 72d08c5..0000000 --- a/botanjs/old/compressor/closure.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 - -import os -from sys import platform -from tempfile import NamedTemporaryFile -from botanjs.config import Config as config -from botanjs.service.jwork import log - -COMPILER = config[ "BotanJS" ][ "ClosureCompiler" ] - -AVAILABLE = os.path.isfile( COMPILER ) - - -COMPILER_OPTIONS = [ - "--compilation_level ADVANCED_OPTIMIZATIONS" - , "--output_wrapper=\"(function(){%output%})();\"" -] - - -class Wrapper: - - C = None - # externs - E = "" - - def __init__( self ): - self.C = "java -jar -Xmx64M "+ COMPILER + " " + " ".join( COMPILER_OPTIONS ) - - def scanExterns( self, sdir ): - for root, dirs, files in os.walk( sdir ): - # Split file extensions - files = list( os.path.splitext( x ) for x in files ) - files.sort() - for f in files: - files.remove( f ) if f[1] != ".js" else None - - self.E = " --externs " + " --externs ".join( - os.path.join( root, x ) - # join back extensions - for x in list( "".join( x ) for x in files ) - ) - break - - def compress( self, loc ): - - if not AVAILABLE: - log.error( "Compiler not found" ) - return - - content = "" - with open( loc, "rb" ) as f: - content = f.read() - - with NamedTemporaryFile( delete = ( not platform == "win32" ) ) as f: - f.write( content[12:-5] ) - os.system( self.C + self.E + " --js " + f.name + " --js_output_file " + loc[:-3] + ".c.js" ) diff --git a/botanjs/old/compressor/yui.py b/botanjs/old/compressor/yui.py deleted file mode 100644 index 8a665f1..0000000 --- a/botanjs/old/compressor/yui.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 - -import os -from sys import platform -from botanjs.config import Config as config - -COMPILER = config[ "BotanJS" ][ "YuiCompressor" ] - -if not os.path.isfile( COMPILER ): - raise Exception( "Compiler not found" ) - -COMPILER_OPTIONS = [ - "--type css" -] - -class Wrapper: - - C = None - - def __init__( self ): - self.C = "java -jar " + COMPILER + " " + " ".join( COMPILER_OPTIONS ) - - def compress( self, loc ): - - if platform == "win32": - loc = loc.replace( "C:", "" ).replace( "\\\\", "/" ) - - os.system( self.C + " " + loc + " -o " + loc[:-4] + ".c.css" ) diff --git a/botanjs/old/config.py b/botanjs/old/config.py deleted file mode 100644 index 6ce5794..0000000 --- a/botanjs/old/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -import configparser, os - -Config = configparser.ConfigParser( interpolation = configparser.ExtendedInterpolation() ) -Config.read( "settings.ini" ) - -DEBUG = os.getenv( "DEBUG" ) -if DEBUG is None: - DEBUG = Config.getboolean( "Env", "Debug" ) -else: - Config[ "Env" ][ "Debug" ] = str( DEBUG == "1" ) - -REDIS_CONN = os.getenv( "REDIS_CONN" ) -if not REDIS_CONN is None: - Config[ "Redis" ][ "ConnStr" ] = REDIS_CONN diff --git a/botanjs/old/dummy.py b/botanjs/old/dummy.py deleted file mode 100644 index 990ebc2..0000000 --- a/botanjs/old/dummy.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 - -class log: - - @staticmethod - def info( *args ): - print( *args ) - - @staticmethod - def error( *args ): - print( *args ) - -class dummyTask( object ): - - Func = None - - def delay( self, *args ): - self.Func( *args ) - - def __init__( self, *args ): - self.Func = args[0] - - def __call__( self, *args ): - pass - -class dummyConf: - def update( self, broker_url = None ): - pass - -class app: - conf = dummyConf() - - def task(): - return dummyTask diff --git a/botanjs/old/service/__init__.py b/botanjs/old/service/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/botanjs/old/service/jclassresv.py b/botanjs/old/service/jclassresv.py deleted file mode 100644 index 94bd15b..0000000 --- a/botanjs/old/service/jclassresv.py +++ /dev/null @@ -1,368 +0,0 @@ -import os, re, base64, zlib, hashlib, binascii -import xml.etree.ElementTree as ET - -PY_SEP = os.path.sep - -def wrapScope( src ): - return "(function(){" + src + "})();" - -class Resolver: - resolved = None - - bMap = None - parentMap = None - - EX_PROP = "prop" - EX_FUNC = "method" - EX_CLASS = "class" - - _rLookup = None - - def __init__( self, classMap ): - self.classMap = classMap - self._rLookup = {} - self._reload() - - def _reload( self ): - self.bMap = ET.parse( self.classMap ) - # ElementTree does not support Element.find( ".." ) - # Make a parent map to work around - self.parentMap = { c:p for p in self.bMap.iter() for c in p } - - self.resolved = [] - - def resource( self, elem ): - if "src" in elem.attrib: - key = elem.attrib[ "src" ] - if not key in self._rLookup: - self._rLookup[ key ] = { - "src": elem.attrib[ "src" ] - , "js": elem.attrib[ "js" ] - , "css": elem.attrib[ "css" ] - } - return self._rLookup[ key ] - - parent = self.parentMap[ elem ] - - if parent is not None: - return self.resource( parent ) - - def locate( self, key ): - return self._rLookup.get( key ) - - def resolve( self, c, classList ): - self.resolved = [] - self.__resolve( c, classList ) - - def __resolve( self, c, classList ): - - lista = list( "[@name=\"" + x + "\"]" for x in c.split(".") ) - - fx = "/".join( self.EX_CLASS + x for x in lista[:-1] ) - - # resolve wildcard A.B.* - if c[-2:] == ".*": - elem = self.bMap.findall( ".//" + fx + self.EX_CLASS ) - - if elem == None: - raise LookupError( "Namespace does not exists or contains no classes: " + c ) - - c = c[0:-1] - for cl in elem: - cl = c + cl.attrib[ "name" ] - - if cl not in self.resolved: - self.__resolve( cl, classList ) - - return - - it = lista[-1] - # Test if class - elem = self.bMap.find( ".//" + fx + self.EX_CLASS + it ) - - if elem == None: - if fx != '': fx += "/" - - # Test if prop - elem = self.bMap.find( ".//" + fx + self.EX_PROP + it ) - - # test if func - if elem == None: - elem = self.bMap.find( ".//" + fx + self.EX_FUNC + it ) - - if elem == None: - raise LookupError( "No such class: " + c ) - - imports = self.parentMap[ elem ].findall( "import" ) - - else: - imports = elem.findall( "import" ) - - self.resolved.append( c ) - - for imp in imports: - if imp.text not in self.resolved: - self.__resolve( imp.text, classList ) - - classList.append([ c, elem ]) - -class BotanClassResolver: - R = "" - CR = None - - classMap = "" - flagCompress = True - oModeIfSizelt = 50 * 1024 - returnHash = False - returnDynamic = False - resv = None - - def __init__( self, jwork, BotanRoot, classMap, cacheRoot ): - - self.JWork = jwork; - self.R = os.path.abspath( BotanRoot ) - self.CR = os.path.abspath( cacheRoot ) - - os.makedirs( self.CR, 0o755, exist_ok = True ) - - if not os.path.exists( classMap ): - self.JWork.buildClassMap( self.R, classMap ) - - self.resv = Resolver( classMap ) - - def BotanFile( self, t ): - content = "" - with open( os.path.join( self.R, t ), "r" ) as f: - content = f.read() - - return content - - def BotanCache( self, srcFile, fileHash, hashContentDynamic ): - - for _ in [0]: - if hashContentDynamic and os.path.getsize( srcFile ) < self.oModeIfSizelt: - break - - if self.returnHash: - return fileHash - - with open( srcFile, "r" ) as f: - return f.read() - - def cleanList( self, lista ): - olist = [] - for i in lista: - if i not in olist: - olist.append( i ) - return olist - - def jsLookup( self, classList, classFiles ): - for c in classList: - src = self.resv.resource( c[1] ) - - if src == None: - raise LookupError( "Cannot find src file for: " + c[0] ) - - if src not in classFiles: - classFiles.append( src ) - - def cssLookup( self, classList, cssList ): - - for cdef in classList: - f = cdef[ "src" ] - - cssFile = os.path.splitext( f )[0] + ".css" - - cssGroup = [] - if not cdef[ "css" ] == "1": - cssGroup.append( cdef ) - - f = f.split( PY_SEP ) - l = len( f ) - - for i in range( 1, l ): - key = PY_SEP.join( x for x in f[:-i] ) + PY_SEP + "_this.js" - _def = self.resv.locate( key ) - - if _def and not _def[ "css" ] == "1": - cssGroup.append( _def ) - - cssGroup.sort( key = lambda x : x[ "src" ] ) - cssList.extend( cssGroup ) - - def getCache( self, fileList, cName, mode ): - if self.CR == None: - return None - - md5 = hashlib.md5( bytearray( "|".join( x[mode] for x in fileList ), "utf-8" ) ).hexdigest() - - cName[0] = oFHash = md5 + "." + mode - cFHash = md5 + ".c." + mode - - # Raw file - oFile = os.path.join( self.CR, oFHash ) - # Compressed file - cFile = os.path.join( self.CR, cFHash ) - - cCached = self.useCache( cFile ) - - if self.flagCompress and cCached: - return self.BotanCache( cFile, cFHash, self.returnDynamic ) - - if self.useCache( oFile ): - - if not cCached: - self.JWork.saveCache( - oFile - # Content is None to initiate a compression - , None - , mode - , os.path.join( self.R, "externs" ) - ) - - return self.BotanCache( oFile, oFHash, False ) - - return None - - def useCache( self, f ): - return os.path.exists( f ) - - def compileJs( self, cList, xList ): - md5 = [ None ] - - for x in xList: - cList.remove( x ) if x in cList else None - - cacheFile = self.getCache( cList, md5, "js" ) - - if cacheFile is not None: - return cacheFile; - - # The root file - outputJs = self.BotanFile( "_this.js" ) - - for f in cList: - f = f[ "src" ] - path = ( - os.path.splitext( f )[0] - .replace( PY_SEP, "." ) - .replace( "._this", "" ) - ) - struct = ";BotanJS.define( \"" + path + "\" );" - - outputJs += struct + self.BotanFile( f ) - - outputJs = wrapScope( outputJs ) - - self.JWork.saveCache( - os.path.join( self.CR, md5[0] ) - , outputJs - , "js" - , os.path.join( self.R, "externs" ) - ) - - if self.returnHash: - return md5[0] - - return outputJs - - def compileCss( self, cList, xList ): - cssIList = [] - cssXList = [] - self.cssLookup( cList, cssIList ) - self.cssLookup( xList, cssXList ) - - cList = [] - xList = cssXList - - for x in cssIList: - cList.append( x ) if x not in xList else None - - md5 = [ None ] - cacheFile = self.getCache( cList, md5, "css" ) - - if cacheFile is not None: - return cacheFile; - - struct = "/* @ */" - outputCss = struct + self.BotanFile( "_this.css" ) - - for f in self.cleanList( cList ): - outputCss += self.BotanFile( f["src"][:-2] + "css" ) - - self.JWork.saveCache( os.path.join( self.CR, md5[0] ), outputCss, "css" ) - - if self.returnHash: - return md5[0] - - return outputCss - - def getAPI( self, code, mode ): - self.flagCompress = True - # Should reload on debug mode only - self.resv._reload() - flag = mode[0] - requestAPIs = code - - # Return compressed contents if possible - # otherwise return raw contents - if flag == "o": - mode = mode[1:] - - # Return raw contents only - elif flag == "r": - mode = mode[1:] - self.flagCompress = False - - # Return hashed filenames only - elif flag == "h": - mode = mode[1:] - self.returnHash = True - - # Return hashed filenames if content is larger than self.oModeIfSizelt bytes - # otherwise act as "o" mode - else: - self.returnHash = True - self.returnDynamic = True - - try: - requestAPIs = ( - # decode -> decompress -> split - zlib.decompress( base64.b64decode( code, None, True ) ) - .decode( "utf-8" ) - ) - sp = "," - except binascii.Error: - sp = "/" - - # strip malicious - requestAPIs = ( - requestAPIs - .replace( "[^A-Za-z\.\*" + re.escape( sp ) + " ]", "" ) - .split( sp ) - ) - - imports = [] - excludes = [] - - for apis in requestAPIs: - - if not apis: - continue - - classList = [] - lookupList = imports - - if apis[0] == "-": - apis = apis[1:] - lookupList = excludes - - self.resv.resolve( apis, classList ) - self.jsLookup( classList, lookupList ) - - if mode == "js": - return self.compileJs( imports, excludes ) - elif mode == "css": - return self.compileCss( imports, excludes ) - - raise TypeError( "Invalid mode: " + js ) diff --git a/botanjs/old/service/jwork.py b/botanjs/old/service/jwork.py deleted file mode 100644 index 6d6c182..0000000 --- a/botanjs/old/service/jwork.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -import os -from botanjs.classmap import ClassMap - -if os.getenv( "UNIT_TEST" ) == "1": - CeleryExists = False -else: - CeleryExists = True - -try: - from celery import Celery -except ImportError: - CeleryExists = False - -if CeleryExists: - from celery.utils.log import get_task_logger - app = Celery( "botanJWork" ) - log = get_task_logger( __name__ ) - - if os.path.exists( "settings.ini" ): - from botanjs.config import Config - app.conf.update( broker_url = Config["BotanJS"]["CeleryBroker"] ) - -else: - from botanjs.dummy import app - from botanjs.dummy import log - -class JWork: - - def saveCache( location, content = None, mode = None, externs = "" ): - if content != None: - log.info( "Writing file(" + str( len( content ) ) + "): " + os.path.abspath( location ) ) - with open( location, "w" ) as f: - f.write( content ) - - if mode == "js": - JWork.compressJs.delay( location, externs ) - elif mode == "css": - JWork.compressCss( location ) - - @app.task() - def compressJs( md5, externs ): - from botanjs.compressor.closure import Wrapper as ClosureWrapper - log.info( "Compress js: " + md5 ) - w = ClosureWrapper() - w.scanExterns( externs ) - w.compress( md5 ) - - @app.task() - def compressCss( md5 ): - from botanjs.compressor.yui import Wrapper as YUIWrapper - log.info( "Compress css: " + md5 ) - w = YUIWrapper() - w.compress( md5 ) - - @app.task() - def buildClassMap( src, location ): - log.info( "Building Class Map" ) - c = ClassMap( src ) - - os.makedirs( os.path.dirname( location ), exist_ok = True ) - with open( location, "w" ) as f: - f.write( c.build() ) diff --git a/botanjs/old/service/webapi.py b/botanjs/old/service/webapi.py deleted file mode 100644 index c28ec01..0000000 --- a/botanjs/old/service/webapi.py +++ /dev/null @@ -1,66 +0,0 @@ -from flask import Flask -from flask import Response -from flask import render_template -from flask import request -from botanjs.service.jclassresv import BotanClassResolver as JCResv -from botanjs.service.jwork import app as CeleryApp, JWork -from botanjs.config import Config - -import os - -class WebAPI: - - app = None - - BRoot = None - BMap = None - BCache = None - - def __init__( self, jsRoot = "../src", jsCache = "/tmp", brokerURL = None ): - - self.BRoot = os.path.abspath( jsRoot ) - self.BCache = os.path.join( jsCache, "botanjs" ) - self.BMap = os.path.join( self.BCache, "bmap.xml" ) - - if brokerURL != None: - CeleryApp.conf.update( broker_url = brokerURL ) - - self.app = Flask( __name__, static_url_path = "/cache/botanjs", static_folder = self.BCache ) - self.app.jinja_env.add_extension( "compressinja.html.HtmlCompressor" ) - - self.app.add_url_rule( "/" , view_func = self.index ) - self.app.add_url_rule( "//" , view_func = lambda mode: self.api_request( mode, "zpayload" ) ) - self.app.add_url_rule( "//" , view_func = self.api_request ) - - def run( self, *args, **kwargs ): - JWork.buildClassMap( self.BRoot, self.BMap ) - return self.app.run( *args, **kwargs ) - - def index( self ): - return "Hello, this is the BotanJS Service API.", 200 - - def api_request( self, mode, code ): - - if mode == "rebuild": - JWork.buildClassMap.delay( self.BRoot, self.BMap ) - return "OK", 200 - - if code == "zpayload": - code = request.args.get( "p" ) - - try: - t = mode[1:] - if t == "js": - t = "application/javascript" - elif t == "css": - t = "text/css" - - srvHandler = JCResv( JWork, self.BRoot, self.BMap, self.BCache ) - return Response( srvHandler.getAPI( code, mode = mode ), mimetype = t ) - except Exception as e: - if self.app.config[ "DEBUG" ]: - raise - return str(e), 404 - - - return "Invalid request", 404 diff --git a/botanjs/old/utils.py b/botanjs/old/utils.py deleted file mode 100644 index ab6d760..0000000 --- a/botanjs/old/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -from functools import lru_cache -from zlib import adler32 as _HashFunc -HashFunc = lambda v: hex( _HashFunc( v ) )[2:] - -@lru_cache( maxsize = 1024 ) -def checksum( file_path ): - return checksum_r( file_path ) - -def checksum_r( file_path ): - try: - with open( file_path, "rb" ) as f: - return HashFunc( f.read() ) - except FileNotFoundError: - return HashFunc( b"" ) diff --git a/botanjs/src/Astro/Mechanism/CharacterCloud.js b/botanjs/src/Astro/Mechanism/CharacterCloud.js index df2dccf..3d1e4c3 100644 --- a/botanjs/src/Astro/Mechanism/CharacterCloud.js +++ b/botanjs/src/Astro/Mechanism/CharacterCloud.js @@ -9,6 +9,7 @@ // Character cloud creates a cloud of character with randomized properties var create = function( ch, char_class, size, cloudRange, charSize ) { + var marginLeft; var cloudMap = Dand.wrapc( "characterCloud" ) , charElmt , rx, ry, rs diff --git a/botanjs/src/Astro/Mechanism/Parallax.js b/botanjs/src/Astro/Mechanism/Parallax.js index cbdb64e..8bf8b1e 100644 --- a/botanjs/src/Astro/Mechanism/Parallax.js +++ b/botanjs/src/Astro/Mechanism/Parallax.js @@ -18,7 +18,7 @@ || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame - || function( f ) { window.setTimeout( draw1, 1000 / 60 ); } + || function( f ) { window.setTimeout( f, 1000 / 60 ); } ); var cssSlide = function( element, slide_index, distance ) diff --git a/botanjs/src/Components/Console.js b/botanjs/src/Components/Console.js index 0121d22..936191d 100644 --- a/botanjs/src/Components/Console.js +++ b/botanjs/src/Components/Console.js @@ -58,7 +58,7 @@ , led = null , ticking = function () { - if( debugEnv ) + if( window[ "debugEnv" ] ) { time_txt.innerHTML = ( sTick.count - cycle ) + " cps, Sampling " + sampling + "ms"; cycle = sTick.count; @@ -130,12 +130,13 @@ var autoHide = function () { this.style.top = ""; }.bind(stage); Cycle.perma('gTicker' + Perf.uuid, ticking, sampling); Cycle.perma('gTicker' + Perf.uuid, autoHide, 3000); - debugEnv = true; + window[ "debugEnv" ] = true; ticking(); var f9Binding = function ( e ) { + var code; e = e || window.event; if ( e.keyCode ) code = e.keyCode; else if ( e.which ) code = e.which; @@ -167,7 +168,7 @@ // This will output the debug info if( window["debug_info"] ) { - debug.Info( objTreeView( debug_info, 0, "[Server] " ) ); + debug.Info( objTreeView( window[ "debug_info" ], 0, "[Server] " ) ); } }; diff --git a/botanjs/src/_this.js b/botanjs/src/_this.js index c368a43..83710fb 100644 --- a/botanjs/src/_this.js +++ b/botanjs/src/_this.js @@ -66,7 +66,7 @@ var BotanEvent = function( name, data ) }; /** @constructor - * @extends EventTarget + * @implements {EventTarget} **/ var EventDispatcher = function() { var events = {}; @@ -206,7 +206,7 @@ __namespace = __namespace || function( ns ) target.__TRIGGERS = []; - nsObj = new NamespaceObj; + var nsObj = new NamespaceObj; nsObj[ NS_EXPORT ] = function( type, name, obj ) { if( this.t[ name ] ) return; diff --git a/botanjs/src/externs/Astro.Blog.AstroEdit.SmartInput.ICandidateAction.js b/botanjs/src/externs/Astro.Blog.AstroEdit.SmartInput.ICandidateAction.js index 59c2d04..2a4e30e 100644 --- a/botanjs/src/externs/Astro.Blog.AstroEdit.SmartInput.ICandidateAction.js +++ b/botanjs/src/externs/Astro.Blog.AstroEdit.SmartInput.ICandidateAction.js @@ -1,8 +1,8 @@ /** @constructor */ Astro.Blog.AstroEdit.SmartInput.ICandidateAction = function(){}; -/** @type {function} */ +/** @type {function(function(): void): void} */ Astro.Blog.AstroEdit.SmartInput.ICandidateAction.GetCandidates; -/** @type {function} */ +/** @type {function(string): void} */ Astro.Blog.AstroEdit.SmartInput.ICandidateAction.Process; -/** @type {function} */ +/** @type {function(Astro.Blog.AstroEdit.SmartInput.ICandidateAction, Event): boolean} */ Astro.Blog.AstroEdit.SmartInput.ICandidateAction.Retreat; diff --git a/botanjs/src/externs/Astro.utils.Date.js b/botanjs/src/externs/Astro.utils.Date.js index d753b1e..b2443d3 100644 --- a/botanjs/src/externs/Astro.utils.Date.js +++ b/botanjs/src/externs/Astro.utils.Date.js @@ -12,13 +12,13 @@ Astro.utils.Date.smstamp; /** @type {Function}*/ Astro.utils.Date.chinese; -/** @type {constant}*/ +/** @const*/ Astro.utils.Date.MONTH; -/** @type {constant}*/ +/** @const*/ Astro.utils.Date.MONTH_ABBR; -/** @type {constant}*/ +/** @const*/ Astro.utils.Date.DAY; -/** @type {constant}*/ +/** @const*/ Astro.utils.Date.DAY_ABBR; -/** @type {constant}*/ +/** @const*/ Astro.utils.Date.CAP_MONTHS; diff --git a/botanjs/src/externs/Dandelion.js b/botanjs/src/externs/Dandelion.js index 9a6897b..9ef5764 100644 --- a/botanjs/src/externs/Dandelion.js +++ b/botanjs/src/externs/Dandelion.js @@ -1,31 +1,31 @@ /** @constructor */ var Dandelion = function (){} -/** @type {Function} */ +/** @type {function(...?): HTMLElement} */ Dandelion.wrap; -/** @type {Function} */ +/** @type {function(...?): HTMLElement} */ Dandelion.wrapc; -/** @type {Function} */ +/** @type {function(...?): HTMLElement} */ Dandelion.wrape; -/** @type {Function} */ +/** @type {function(...?): HTMLElement} */ Dandelion.wrapne; -/** @type {Function} */ +/** @type {function(...?): HTMLElement} */ Dandelion.wrapna; -/** @type {Function} */ +/** @type {function(string): HTMLElement} */ Dandelion.textNode; -/** @type {Function} */ +/** @type {function(string): HTMLElement} */ Dandelion.bubbleUp; -/** @type {Function} */ +/** @type {function(HTMLElement, function(HTMLElement): boolean): void} */ Dandelion.chainUpApply; -/** @type {Function} */ +/** @type {function(string, boolean=): (HTMLElement|Dandelion.IDOMElement)} */ Dandelion.id; /** @type {Function} */ diff --git a/botanjs/src/externs/Libraries.SyntaxHighLighter.js b/botanjs/src/externs/Libraries.SyntaxHighLighter.js index 8b0d6b4..e9cd30c 100644 --- a/botanjs/src/externs/Libraries.SyntaxHighLighter.js +++ b/botanjs/src/externs/Libraries.SyntaxHighLighter.js @@ -1,42 +1,60 @@ /** @constructor */ -Libraries.SyntaxHighlighter = function (){}; +Libraries.SyntaxHighlighter = function() {}; -/** @type {Function} */ +/** @type {function(...?): ?} */ Libraries.SyntaxHighlighter.all; -/** @type {Object} */ + +/** @type {!Object} */ Libraries.SyntaxHighlighter.defaults; -/** @type {Function} */ + +/** @type {function(...?): ?} */ Libraries.SyntaxHighlighter.highlight; -/** @type {Object} */ + +/** @type {!Object} */ Libraries.SyntaxHighlighter.Highlighter; -/** @type {Object} */ +/** @type {!Object} */ Libraries.SyntaxHighlighter.brushes; -/** @type {Object} */ +/** @type {!Object} */ Libraries.SyntaxHighlighter.regexLib; -/** @constructor */ + +/** @const */ var SyntaxHighlighter = {}; -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.aspScriptTags -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.doubleQuotedString -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.multiLineCComments -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.multiLineDoubleQuotedString -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.multiLineSingleQuotedString -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.phpScriptTags -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.scriptScriptTags -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.singleLineCComments -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.singleLinePerlComments -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.singleQuotedString -/** @type {RegExp} */ -SyntaxHighlighter.regexLib.xmlComments + +/** @type {!Object} */ +SyntaxHighlighter.regexLib; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.aspScriptTags; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.doubleQuotedString; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.multiLineCComments; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.multiLineDoubleQuotedString; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.multiLineSingleQuotedString; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.phpScriptTags; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.scriptScriptTags; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.singleLineCComments; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.singleLinePerlComments; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.singleQuotedString; + +/** @type {!RegExp} */ +SyntaxHighlighter.regexLib.xmlComments; diff --git a/botanjs/src/externs/System.Compression.Zlib.js b/botanjs/src/externs/System.Compression.Zlib.js index 2af8e60..b417ded 100644 --- a/botanjs/src/externs/System.Compression.Zlib.js +++ b/botanjs/src/externs/System.Compression.Zlib.js @@ -1,7 +1,7 @@ -/** @type {constructor} */ +/** @constructor */ System.Compression.Zlib = function(){}; -/** @type {constructor} */ +/** @constructor */ System.Compression.Zlib.Deflate = function(){}; /** @type {Function} */ diff --git a/botanjs/src/externs/System.Log.js b/botanjs/src/externs/System.Log.js index 504d258..8177f0e 100644 --- a/botanjs/src/externs/System.Log.js +++ b/botanjs/src/externs/System.Log.js @@ -8,9 +8,9 @@ System.Log.registerHandler; /** @type {Function} */ System.Log.removeHandler; -/** @type {const} */ +/** @const */ System.Log.ERROR; -/** @type {const} */ +/** @const */ System.Log.INFO; -/** @type {const} */ +/** @const */ System.Log.SYSTEM; diff --git a/botanjs/src/externs/_AstConf_.AstroEdit.js b/botanjs/src/externs/_AstConf_.AstroEdit.js index 0f8e6e4..447d9bf 100644 --- a/botanjs/src/externs/_AstConf_.AstroEdit.js +++ b/botanjs/src/externs/_AstConf_.AstroEdit.js @@ -1,8 +1,8 @@ -/** @type {Object} */ +/** @type {!Object} */ _AstConf_.AstroEdit = {}; /** @type {String} */ _AstConf_.AstroEdit.article_id; -/** @type {object} */ +/** @type {!Object} */ _AstConf_.AstroEdit.paths = {}; /** @type {string} */ _AstConf_.AstroEdit.paths.set_article; diff --git a/closure-api/Dockerfile b/closure-api/Dockerfile index 146aff4..b59c2e9 100644 --- a/closure-api/Dockerfile +++ b/closure-api/Dockerfile @@ -20,17 +20,18 @@ FROM eclipse-temurin:21-jre WORKDIR /app +ARG JS_SRC_DIR ARG JAVA_SRC_DIR ARG CLOSURE_NAME RUN useradd -r -u 10001 closure COPY --from=build /src/target/${CLOSURE_NAME}-0.1.0.jar /app/runtime.jar -COPY $JAVA_SRC_DIR/example ./example +COPY $JS_SRC_DIR ./src USER closure -ENV CLOSURED_ROOT=/work +ENV CLOSURED_ROOT=/app/src ENV CLOSURED_PORT=8080 ENV CLOSURED_WORKERS=2 diff --git a/closure-api/src/main/java/dev/tgckpg/closured/Main.java b/closure-api/src/main/java/dev/tgckpg/closured/Main.java index ab20fb7..30a00e1 100644 --- a/closure-api/src/main/java/dev/tgckpg/closured/Main.java +++ b/closure-api/src/main/java/dev/tgckpg/closured/Main.java @@ -27,6 +27,8 @@ import java.util.Map; import java.util.concurrent.Executors; public final class Main { + private record InlineSource(String name, String source) {} + private static final ObjectMapper JSON = new ObjectMapper(); private static final Path ROOT = Path.of(System.getenv().getOrDefault("CLOSURED_ROOT", ".")).toAbsolutePath().normalize(); @@ -92,11 +94,14 @@ public final class Main { List externs = new ArrayList<>(); externs.addAll(CommandLineRunner.getBuiltinExterns(options.getEnvironment())); externs.addAll(readFiles(req, "externs")); + externs.addAll(readInlineSources(req, "externSources")); - List inputs = readFiles(req, "js"); + List inputs = new ArrayList<>(); + inputs.addAll(readFiles(req, "js")); + inputs.addAll(readInlineSources(req, "jsSources")); if (inputs.isEmpty()) { - throw new BadRequest("request must include at least one js file"); + throw new BadRequest("request must include at least one js file or js source"); } com.google.javascript.jscomp.Compiler compiler = @@ -154,6 +159,52 @@ public final class Main { return out; } + private static List readInlineSources(JsonNode req, String field) throws BadRequest { + JsonNode arr = req.get(field); + List out = new ArrayList<>(); + + if (arr == null || arr.isNull()) { + return out; + } + + if (!arr.isArray()) { + throw new BadRequest(field + " must be an array"); + } + + for (JsonNode item : arr) { + String name; + String source; + + if (item.isTextual()) { + // Optional convenience form: + // "jsSources": ["console.log(1);"] + name = field + "-" + out.size() + ".js"; + source = item.textValue(); + } else if (item.isObject()) { + JsonNode nameNode = item.get("name"); + JsonNode sourceNode = item.get("source"); + + if (sourceNode == null || !sourceNode.isTextual()) { + throw new BadRequest(field + " item must include textual source"); + } + + if (nameNode != null && nameNode.isTextual() && !nameNode.textValue().isBlank()) { + name = nameNode.textValue(); + } else { + name = field + "-" + out.size() + ".js"; + } + + source = sourceNode.textValue(); + } else { + throw new BadRequest(field + " items must be strings or objects"); + } + + out.add(SourceFile.fromCode(name, source)); + } + + return out; + } + private static Path safePath(String value) throws BadRequest { Path p = ROOT.resolve(value).normalize(); if (!p.startsWith(ROOT)) { diff --git a/internal/generated/buildinfo_gen.go b/internal/generated/buildinfo_gen.go deleted file mode 100644 index 86c1428..0000000 --- a/internal/generated/buildinfo_gen.go +++ /dev/null @@ -1,6 +0,0 @@ -package generated - -const ( - IMAGE_TAG = "dev" - Timestamp = "20260611.005458" -) diff --git a/k8s/deployments.yaml b/k8s/deployments.yaml index 5ea0e7d..46b167a 100644 --- a/k8s/deployments.yaml +++ b/k8s/deployments.yaml @@ -16,7 +16,7 @@ spec: - name: registry-auth containers: - name: web - image: registry.k8s.astropenguin.net/astrojs:IMAGE_TAG + image: registry.k8s.astropenguin.net/resolver-go:IMAGE_TAG securityContext: runAsGroup: 1001 runAsNonRoot: true @@ -24,14 +24,6 @@ spec: env: - name: DEBUG value: "0" - - name: FLASK_DEBUG - value: "0" - - name: FLASK_ENV - value: "production" - - name: RUN_MODE - value: "web" - - name: REDIS_CONN - value: "redis://:@localhost:6379/9" volumeMounts: - name: cache mountPath: "/app/cache" @@ -40,22 +32,16 @@ spec: command: - sh - -c - - wget -qO - http://127.0.0.1:5000/rjs/System | grep -q function + - wget -qO - http://127.0.0.1:5000/health - name: redis image: redis:6.0.8-alpine - name: compiler - image: registry.k8s.astropenguin.net/astrojs:IMAGE_TAG + image: registry.k8s.astropenguin.net/closure-api:IMAGE_TAG securityContext: - runAsGroup: 1001 runAsNonRoot: true - runAsUser: 1001 env: - - name: RUN_MODE - value: "tasks" - name: DEBUG value: "0" - - name: REDIS_CONN - value: "redis://:@localhost:6379/9" volumeMounts: - name: cache mountPath: "/app/cache" diff --git a/mk/closure-api.mk b/mk/closure-api.mk index 85a6643..3495d19 100644 --- a/mk/closure-api.mk +++ b/mk/closure-api.mk @@ -7,6 +7,7 @@ CLOSURE_NAME = closure-api build-closure: docker build \ -f $(CLOSURE_SRC_DIR)/Dockerfile \ + --build-arg JS_SRC_DIR=$(JS_SRC_DIR) \ --build-arg JAVA_SRC_DIR=$(CLOSURE_SRC_DIR) \ --build-arg CLOSURE_NAME=$(CLOSURE_NAME) \ --load \ @@ -16,6 +17,7 @@ push-closure: ensure-buildx docker buildx build \ --platform linux/amd64,linux/arm64 \ -f $(CLOSURE_SRC_DIR)/Dockerfile \ + --build-arg JS_SRC_DIR=$(JS_SRC_DIR) \ --build-arg JAVA_SRC_DIR=$(CLOSURE_SRC_DIR) \ --build-arg CLOSURE_NAME=$(CLOSURE_NAME) \ -t $(CLOSURE_IMAGE_NAME):$(CLOSURE_IMAGE_TAG) \ diff --git a/mk/resolver-go.mk b/mk/resolver-go.mk index 2886d5e..5d9c7b4 100644 --- a/mk/resolver-go.mk +++ b/mk/resolver-go.mk @@ -3,7 +3,7 @@ RESOLVER_IMAGE_TAG ?= dev GO_SRC_DIR ?= ./resolver-go -BUILDINFO_FILE := internal/generated/buildinfo_gen.go +BUILDINFO_FILE := $(GO_SRC_DIR)/internal/generated/buildinfo_gen.go .buildinfo: @mkdir -p $(dir $(BUILDINFO_FILE)) diff --git a/old/Dockerfile b/old/Dockerfile deleted file mode 100644 index 243f2fc..0000000 --- a/old/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM alpine:3.15.3 -WORKDIR /app - -RUN mkdir -p /opt/utils - -RUN apk add --update bash python3 uwsgi uwsgi-python openjdk11-jre-headless; python3 -m ensurepip - -RUN echo "www-data:x:1001:1001:www-data:/var/www:/usr/sbin/nologin" >> /etc/passwd; echo "www-data:x:1001:" >> /etc/group - -RUN pip3 install Flask redis compressinja Celery - -ADD [ "https://github.com/tgckpg/BotanJS/releases/download/compressors/closure.jar" \ - , "https://github.com/tgckpg/BotanJS/releases/download/compressors/yuicompressor.jar" \ - , "/opt/utils/" ] - -COPY . /app/ - -RUN chmod 644 /opt/utils/*.jar; \ - chown www-data:www-data . -R - -USER www-data - -EXPOSE 5000 -ENTRYPOINT ["setup/docker.start"] diff --git a/old/botan-rebuild.py b/old/botan-rebuild.py deleted file mode 100755 index 47f6de8..0000000 --- a/old/botan-rebuild.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python3 -import os, sys -sys.path.append( os.path.abspath( "." ) ) - -from botanjs.service.jwork import app, JWork -from botanjs.config import Config as config - -SiteRoot = os.path.abspath( "." ) - -# Setting the SiteRoot for config -config["Paths"]["SiteRoot"] = SiteRoot - -bmap = os.path.join( config["Paths"]["Cache"], "botanjs", "bmap.xml" ) - -app.conf.update( broker_url = config["BotanJS"]["CeleryBroker"] ) - -JWork.buildClassMap.delay( config["BotanJS"]["SrcDir"], bmap ) diff --git a/old/cache/.keep b/old/cache/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/old/env/README.md b/old/env/README.md deleted file mode 100644 index 0335257..0000000 --- a/old/env/README.md +++ /dev/null @@ -1,3 +0,0 @@ -``` -Please initialize your virtualenv here -``` diff --git a/old/logs/.keep b/old/logs/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/old/main.py b/old/main.py deleted file mode 100644 index 892746f..0000000 --- a/old/main.py +++ /dev/null @@ -1,23 +0,0 @@ -#!env/bin/python - -import os -from botanjs.config import Config as config, DEBUG -from subprocess import Popen -from botanjs.service.webapi import WebAPI - -SiteRoot = os.path.abspath( "." ) - -# Setting the SiteRoot for config -config["Paths"]["SiteRoot"] = SiteRoot - -service = WebAPI( - jsCache = config["Paths"]["Cache"] - , jsRoot = config["BotanJS"]["SrcDir"] - , brokerURL = config["BotanJS"]["CeleryBroker"] -) - -application = service.app - -if __name__ == "__main__": - application.config["DEBUG"] = DEBUG - application.run( host = config["Service"]["BindAddress"], port = config["Service"]["Port"] ) diff --git a/old/settings.ini b/old/settings.ini deleted file mode 100644 index ee0ab29..0000000 --- a/old/settings.ini +++ /dev/null @@ -1,22 +0,0 @@ -[Service] -BindAddress = 0.0.0.0 -Port = 5000 - -[Env] -Debug = False - -[Redis] -ConnStr = redis://:@localhost:6379/9 - -[Paths] -Runtime = ${SiteRoot} -Log = ${SiteRoot}/logs -Cache = ${SiteRoot}/cache - -[BotanJS] -SrcDir = ${Paths:Runtime}/botanjs/src - -CeleryBroker = ${Redis:ConnStr} - -ClosureCompiler = /opt/utils/closure.jar -YuiCompressor = /opt/utils/yuicompressor.jar diff --git a/old/setup/celery.conf b/old/setup/celery.conf deleted file mode 100644 index 24fe5c8..0000000 --- a/old/setup/celery.conf +++ /dev/null @@ -1,15 +0,0 @@ -# Absolute or relative path to the 'celery' command: -CELERY_BIN="BIN_ROOT/celery" - -CELERYD_NODES="w1 w2" -CELERY_APP="botanjs.service.jwork" - -# How to call manage.py -CELERYD_MULTI="multi" - -# - %n will be replaced with the first part of the nodename. -# # - %I will be replaced with the current child process index -# # and is important when using the prefork pool to avoid race conditions. -CELERYD_PID_FILE="RUN_ROOT/tasks-%n.pid" -CELERYD_LOG_FILE="PROJ_ROOT/logs/tasks-%n.log" -CELERYD_LOG_LEVEL="INFO" diff --git a/old/setup/compiler-tasks.service b/old/setup/compiler-tasks.service deleted file mode 100644 index 28630cb..0000000 --- a/old/setup/compiler-tasks.service +++ /dev/null @@ -1,19 +0,0 @@ -[Unit] -Description=BotanJS Compiler Tasks -After=network.target - -[Service] -Type=forking -EnvironmentFile=-ETC_ROOT/celery.conf -WorkingDirectory=PROJ_ROOT -ExecStart=/bin/sh -c '${CELERY_BIN} multi start ${CELERYD_NODES} \ - -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} \ - --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS}' -ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} \ - --pidfile=${CELERYD_PID_FILE}' -ExecReload=/bin/sh -c '${CELERY_BIN} multi restart ${CELERYD_NODES} \ - -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} \ - --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS}' - -[Install] -WantedBy=multi-user.target diff --git a/old/setup/config b/old/setup/config deleted file mode 100755 index 8cb7eff..0000000 --- a/old/setup/config +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -[[ $CONFIG ]] && return -CONFIG=1 - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -SCRIPT_DIR="$( realpath "$SCRIPT_DIR" )" -PROJ_ROOT="$( cd "$SCRIPT_DIR/../" && pwd )" -ENV_ROOT="$PROJ_ROOT/env" -BIN_ROOT="$ENV_ROOT/bin" -ETC_ROOT="$ENV_ROOT/etc" -RUN_ROOT="$ENV_ROOT/run" -PYTHON="$BIN_ROOT/python3" diff --git a/old/setup/docker.start b/old/setup/docker.start deleted file mode 100755 index a256c27..0000000 --- a/old/setup/docker.start +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -INST_DIR=$( dirname "${BASH_SOURCE[0]}" ) -source "$INST_DIR/config" -cd $PROJ_ROOT - -mkdir -p cache/botanjs - -case "$RUN_MODE" in - "web") - ./botan-rebuild.py - uwsgi \ - --plugins-dir /usr/lib/uwsgi/ --need-plugin python \ - --http-socket :5000 \ - --wsgi-file main.py \ - --callable application --master \ - --listen 4096 \ - --processes 1 --threads 2 - ;; - "tasks") - source "$INST_DIR/celery.conf" - - celery -A ${CELERY_APP} worker -n worker1@%h \ - --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} \ - & celery -A ${CELERY_APP} worker -n worker1@%h \ - --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} - ;; - *) - echo "RUN_MODE is missing" - exit 1 - ;; -esac diff --git a/old/setup/install b/old/setup/install deleted file mode 100755 index 9166471..0000000 --- a/old/setup/install +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -INST_DIR=$( dirname "${BASH_SOURCE[0]}" ) -source "$INST_DIR/config" - -mkdir -p "$RUN_ROOT" -mkdir -p "$ETC_ROOT" - -sed -e "s|PROJ_ROOT|$PROJ_ROOT|g" \ - -e "s|BIN_ROOT|$BIN_ROOT|g" \ - -e "s|RUN_ROOT|$RUN_ROOT|g" \ - "$INST_DIR/celery.conf" > "$ETC_ROOT/celery.conf" - -sed -e "s|PROJ_ROOT|$PROJ_ROOT|g" \ - -e "s|ETC_ROOT|$ETC_ROOT|g" \ - -e "s|RUN_AS|$RUN_AS|g" \ - "$INST_DIR/compiler-tasks.service" > $HOME/.config/systemd/user/botanjs-tasks.service - -systemctl --user enable botanjs-tasks.service -systemctl --user daemon-reload -systemctl --user start botanjs-tasks.service diff --git a/old/tests.py b/old/tests.py deleted file mode 100644 index 5dfa641..0000000 --- a/old/tests.py +++ /dev/null @@ -1,59 +0,0 @@ -#!env/bin/python -import os, sys -sys.path.append( os.path.abspath( "." ) ) - -from botanjs.service.jwork import app, JWork -from botanjs.config import Config as config - -SiteRoot = os.path.abspath( "." ) - -# Setting the SiteRoot for config -config["Paths"]["SiteRoot"] = SiteRoot - -jsCache = config["Paths"]["Cache"] -jsRoot = config["BotanJS"]["SrcDir"] - -bmap = os.path.join( jsCache, "botanjs", "bmap.xml" ) - -app.conf.update( broker_url = config["BotanJS"]["CeleryBroker"] ) - -JWork.buildClassMap.delay( jsRoot, bmap ) - -from botanjs.service.jclassresv import BotanClassResolver as JCResv - -import unittest - -class TestStringMethods( unittest.TestCase ): - - # Run each twice to test the cache capabilities - def test_ojscall( self ): - srv = JCResv( JWork, jsRoot, bmap, jsCache ) - for _ in range(0,2): - s = srv.getAPI( "System", mode = "rjs" ) - if not ( "BotanJS.define( \"System\" );" in s ): - print( "A---------------------" ) - print( s ) - print( "B---------------------" ) - self.assertTrue( False) - - def test_import( self ): - srv = JCResv( JWork, jsRoot, bmap, jsCache ) - for _ in range(0,2): - s = srv.getAPI( "System.Policy", mode = "rjs" ) - self.assertTrue( "BotanJS.define( \"System.Policy\" );" in s ) - self.assertTrue( "BotanJS.define( \"System.Global\" );" in s ) - - def test_cssInheritance( self ): - srv = JCResv( JWork, jsRoot, bmap, jsCache ) - for _ in range(0,2): - s = srv.getAPI( "System", mode = "rcss" ) - self.assertTrue( "/* @ */" in s ) - -# def test_jsZCalls( self ): -# srv = JCResv( JWork, jsRoot, bmap, jsCache ) -# for _ in range(0,2): -# s = srv.getAPI( "eJx1zsEKgzAQBNAfKvsPaaj0YE/6A4tuJbDJlM2K9O/rpcUKOQ6PYSZUN9DgbE9WpZtKluJ0F57FLuFfe35jdXpwKp1xlrN/2x3gv/ZVsVBEfqHsVmmQyRNKQ6Nhm0dejtyYPVowT5PKHl2qN36PGyJ0zeUDs1BbKA==", mode = "css" ) -# print( s ) - -if __name__ == '__main__': - unittest.main() diff --git a/old/windows/app/Dockerfile b/old/windows/app/Dockerfile deleted file mode 100644 index 3ae5cc7..0000000 --- a/old/windows/app/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM astrojs/jre-nanoserver-20h2:latest - -RUN pip3 install Flask redis compressinja celery - -RUN New-Item -ItemType Directory -Path /opt/utils; \ - New-Item -ItemType Directory -Path /app/cache -Force; \ - [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; \ - Invoke-WebRequest -UseBasicParsing -Uri 'https://github.com/tgckpg/BotanJS/releases/download/compressors/closure.jar' -OutFile '/opt/utils/closure.jar' ; \ - Invoke-WebRequest -UseBasicParsing -Uri 'https://github.com/tgckpg/BotanJS/releases/download/compressors/yuicompressor.jar' -OutFile '/opt/utils/yuicompressor.jar' ; - -COPY . /app -WORKDIR /app - -EXPOSE 5000 diff --git a/old/windows/base-compose.yml b/old/windows/base-compose.yml deleted file mode 100644 index cb2fa41..0000000 --- a/old/windows/base-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '3.9' - -services: - python: - build: pyrt - image: astrojs/pyrt-nanoserver-20h2 - jre: - build: jre - image: astrojs/jre-nanoserver-20h2 diff --git a/old/windows/docker-compose.yml b/old/windows/docker-compose.yml deleted file mode 100644 index d38d03b..0000000 --- a/old/windows/docker-compose.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: '3.9' - -services: - redis: - container_name: astrojsdev_redis - build: redis - web: - container_name: astrojsdev_app - image: astrojs/app - hostname: astrojs.default - build: - context: ../ - dockerfile: windows/app/Dockerfile - environment: - DEBUG: "1" - REDIS_CONN: redis://:@redis:6379/9 - command: [ "python", "main.py" ] # [ "ping", "127.0.0.1", "-n", "9999" ] - depends_on: - - redis - ports: - - 5000:5000 - volumes: - - cache:C:/app/cache - tasks: - container_name: astrojsdev_compiler - image: astrojs/app - environment: - REDIS_CONN: redis://:@redis:6379/9 - command: [ "celery", "-A", "botanjs.service.jwork", "worker", "-l", "info", "--pool=solo" ] - depends_on: - - redis - - web - volumes: - - cache:C:/app/cache -volumes: - cache: diff --git a/old/windows/jre/Dockerfile b/old/windows/jre/Dockerfile deleted file mode 100644 index 54781c0..0000000 --- a/old/windows/jre/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM astrojs/pyrt-nanoserver-20h2:latest as base - -ENV JAVA_HOME=C:\\openjdk-11 -ENV JAVA_VERSION=11.0.12 - -RUN setx PATH "$Env:JAVA_HOME\bin`;$Env:Path" /M - -COPY --from=openjdk:11-jre-nanoserver /openjdk-11 /openjdk-11 -RUN echo Verifying install ... && echo java --version && java --version && echo Complete. diff --git a/old/windows/pyrt/dockerfile b/old/windows/pyrt/dockerfile deleted file mode 100644 index 0aeb80b..0000000 --- a/old/windows/pyrt/dockerfile +++ /dev/null @@ -1,54 +0,0 @@ -FROM mcr.microsoft.com/powershell:nanoserver-20h2 - -SHELL [ "pwsh", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';" ] - -RUN $url = 'https://www.python.org/ftp/python/3.7.6/python-3.7.6-embed-amd64.zip'; \ - Write-host "downloading: $url"; \ - [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; \ - New-Item -ItemType Directory /installer > $null ; \ - Invoke-WebRequest -Uri $url -outfile /installer/Python.zip -verbose; \ - Expand-Archive /installer/Python.zip -DestinationPath /Python; \ - Move-Item /Python/python37._pth /Python/python37._pth.save - -### Begin workaround ### -# Note that changing user on nanoserver is not recommended -# See, https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/container-base-images#base-image-differences -# But we are working around a bug introduced in the nanoserver image introduced in 20h2 -USER ContainerAdministrator - -# This is basically the correct code except for the /M -RUN setx PATH "$Env:Path`C:\Python`;C:\Python\Scripts`;" /M - -# We can't -# USER ContainerUser -### End workaround ### - -# if this is called "PIP_VERSION", pip explodes with "ValueError: invalid truth value ''" -ENV PYTHON_PIP_VERSION 21.2.4 -# https://github.com/pypa/get-pip -ENV PYTHON_GET_PIP_URL https://github.com/pypa/get-pip/raw/4b85d3add912c861aea4a9feaae737a5b7b9cb1c/public/get-pip.py -ENV PYTHON_GET_PIP_SHA256 ced8c71489cd46c511677bfe423f37eb88f08f29e9af36ef2679091ec7122d4f - -RUN Write-Host ('Downloading get-pip.py ({0}) ...' -f $env:PYTHON_GET_PIP_URL); \ - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; \ - Invoke-WebRequest -Uri $env:PYTHON_GET_PIP_URL -OutFile 'get-pip.py'; \ - Write-Host ('Verifying sha256 ({0}) ...' -f $env:PYTHON_GET_PIP_SHA256); \ - if ((Get-FileHash 'get-pip.py' -Algorithm sha256).Hash -ne $env:PYTHON_GET_PIP_SHA256) { \ - Write-Host 'FAILED!'; \ - exit 1; \ - }; \ - \ - Write-Host ('Installing pip=={0} ...' -f $env:PYTHON_PIP_VERSION); \ - python get-pip.py \ - --disable-pip-version-check \ - --no-cache-dir \ - ('pip=={0}' -f $env:PYTHON_PIP_VERSION) \ - ; \ - Remove-Item get-pip.py -Force; \ - \ - Write-Host 'Verifying pip install ...'; \ - pip --version; \ - \ - Write-Host 'Complete.' - -CMD [ "python.exe" ] diff --git a/old/windows/redis/Dockerfile b/old/windows/redis/Dockerfile deleted file mode 100644 index daaffec..0000000 --- a/old/windows/redis/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM mcr.microsoft.com/powershell:nanoserver-20h2 - -SHELL [ "pwsh", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';" ] - -RUN [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; \ - Invoke-WebRequest -UseBasicParsing -Uri 'https://github.com/tporadowski/redis/releases/download/v5.0.10/Redis-x64-5.0.10.zip' -OutFile 'Redis-x64-5.0.10.zip' ; \ - Expand-Archive Redis-x64-5.0.10.zip -dest 'C:\\Program Files\\Redis\\' ; \ - Remove-Item Redis-x64-5.0.10.zip -Force - -User ContainerAdministrator -RUN setx Path "C:\Program` Files\Redis`;$Env:Path" /M; -User ContainerUser - -WORKDIR 'C:\\Program Files\\Redis' - -RUN Get-Content redis.windows.conf | Where { $_ -notmatch 'bind 127.0.0.1' } | Set-Content redis.openport.conf ; \ - Get-Content redis.openport.conf | Where { $_ -notmatch 'protected-mode yes' } | Set-Content redis.unprotected.conf ; \ - Add-Content redis.unprotected.conf 'protected-mode no' ; \ - Add-Content redis.unprotected.conf 'bind 0.0.0.0' ; \ - Get-Content redis.unprotected.conf - -EXPOSE 6379 - -# Define our command to be run when launching the container -CMD .\\redis-server.exe .\\redis.unprotected.conf --port 6379 ; \ - Write-Host Redis Started... ; \ - while ($true) { Start-Sleep -Seconds 3600 } diff --git a/resolver-go/README.md b/resolver-go/README.md index db9f965..d841690 100644 --- a/resolver-go/README.md +++ b/resolver-go/README.md @@ -1,4 +1,4 @@ -# botanres-go +# resolver-go Go rewrite for the old BotanJS dynamic resource resolver. diff --git a/resolver-go/cmd/botan-api/main.go b/resolver-go/cmd/botan-api/main.go index 28068cd..465d276 100644 --- a/resolver-go/cmd/botan-api/main.go +++ b/resolver-go/cmd/botan-api/main.go @@ -1,13 +1,16 @@ package main import ( + "crypto/md5" + "encoding/hex" "flag" "log" "net/http" "strings" - "github.com/tgckpg/botanres-go/internal/generated" - "github.com/tgckpg/botanres-go/internal/resolver" + "github.com/tgckpg/resolver-go/internal/closure" + "github.com/tgckpg/resolver-go/internal/generated" + "github.com/tgckpg/resolver-go/internal/resolver" ) func main() { @@ -20,13 +23,24 @@ func main() { log.Fatal(err) } - h := handler{r: r} + h := handler{ + r: r, + closure: closure.NewCompileCache(2), + } http.HandleFunc("/", h.index) log.Printf("botan-api listening on %s", *addr) log.Fatal(http.ListenAndServe(*addr, nil)) } -type handler struct{ r *resolver.Resolver } +type handler struct { + r *resolver.Resolver + closure *closure.CompileCache +} + +func hashStrings(parts []string) string { + sum := md5.Sum([]byte(strings.Join(parts, "|"))) + return hex.EncodeToString(sum[:]) +} func (h handler) index(w http.ResponseWriter, req *http.Request) { path := strings.Trim(req.URL.Path, "/") @@ -61,7 +75,46 @@ func (h handler) index(w http.ResponseWriter, req *http.Request) { return } - log.Println(res.JSFiles) + if outMode == resolver.ModeJS { + fileHashes := make([]string, 0, len(res.JSFiles)) + for _, f := range res.JSFiles { + fileHashes = append(fileHashes, f.JSHash) + } + + hash := hashStrings(fileHashes) + if compiled, ok := h.closure.Get(hash); ok { + w.Header().Set("Content-Type", "application/javascript") + w.Header().Set("X-Botan-Compiled", "hit") + w.Write(compiled) + return + } + + jsFiles := make([]string, 0, len(res.JSFiles)) + for _, f := range res.JSFiles { + jsFiles = append(jsFiles, f.Src) + } + + jsExterns, err := h.r.GetExterns(generated.Externs) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + h.closure.Enqueue(closure.CompileJob{ + Hash: res.Hash, + Mode: "js", + ExternSources: jsExterns, + JSSources: []closure.SourceInput{ + { + Name: "botanjs-" + res.Hash + ".js", + Source: string(res.Content), + }, + }, + Defines: map[string]any{ + "DEBUG": false, + }, + }) + } // Compatibility flags: // rjs/rcss/ojs/ocss => content @@ -73,5 +126,7 @@ func (h handler) index(w http.ResponseWriter, req *http.Request) { return } w.Header().Set("Content-Type", res.ContentType) + w.Header().Set("X-Botan-Compiled", "miss") + _, _ = w.Write(res.Content) } diff --git a/resolver-go/cmd/botan-gen/main.go b/resolver-go/cmd/classmap-gen/main.go similarity index 87% rename from resolver-go/cmd/botan-gen/main.go rename to resolver-go/cmd/classmap-gen/main.go index 9625340..b093fdf 100644 --- a/resolver-go/cmd/botan-gen/main.go +++ b/resolver-go/cmd/classmap-gen/main.go @@ -7,7 +7,7 @@ import ( "os" "strings" - "github.com/tgckpg/botanres-go/internal/classmap" + "github.com/tgckpg/resolver-go/internal/classmap" ) func main() { @@ -36,9 +36,9 @@ func main() { func render(pkg string, m *classmap.Map) string { var b strings.Builder - b.WriteString("// Code generated by botan-gen; DO NOT EDIT.\n") + b.WriteString("// Code generated by classmap-gen; DO NOT EDIT.\n") b.WriteString("package " + pkg + "\n\n") - b.WriteString("import \"github.com/tgckpg/botanres-go/internal/classmap\"\n\n") + b.WriteString("import \"github.com/tgckpg/resolver-go/internal/classmap\"\n\n") b.WriteString("var ClassMap = &classmap.Map{\nSymbols: map[string]classmap.Symbol{\n") for _, s := range classmap.SortedSymbols(m) { fmt.Fprintf(&b, "%q: {Name:%q, Kind:%q, Parent:%q, Imports:%#v, Resource: classmap.Resource{Src:%q, JSHash:%q, CSSHash:%q}},\n", @@ -61,6 +61,6 @@ func dir(path string) string { } func fatal(err error) { - fmt.Fprintln(os.Stderr, "botan-gen:", err) + fmt.Fprintln(os.Stderr, "classmap-gen:", err) os.Exit(1) } diff --git a/resolver-go/cmd/externs-gen/main.go b/resolver-go/cmd/externs-gen/main.go new file mode 100644 index 0000000..b0c73a4 --- /dev/null +++ b/resolver-go/cmd/externs-gen/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "flag" + "fmt" + "go/format" + "io/fs" + "os" + "path/filepath" + "strings" +) + +func main() { + src := flag.String("src", "./src", "BotanJS source root") + out := flag.String("out", "internal/generated/externs_gen.go", "generated Go output") + pkg := flag.String("pkg", "generated", "generated package name") + flag.Parse() + + if err := os.MkdirAll(filepath.Dir(*out), 0o755); err != nil { + fatal(err) + } + + code, err := render(*pkg, *src) + if err != nil { + fatal(err) + } + + fmted, err := format.Source([]byte(code)) + if err != nil { + _, _ = os.Stderr.WriteString(code) + fatal(err) + } + + if err := os.WriteFile(*out, fmted, 0o644); err != nil { + fatal(err) + } +} + +func render(pkg string, root string) (string, error) { + var b strings.Builder + + root = filepath.Clean(root) + + b.WriteString("// Code generated by externs-gen; DO NOT EDIT.\n") + b.WriteString("package " + pkg + "\n\n") + b.WriteString("var Externs = []string{\n") + + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + if filepath.Ext(path) != ".js" { + return nil + } + + if filepath.Base(filepath.Dir(path)) != "externs" { + return nil + } + + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + + // Generated data should be slash-normalized even on Windows. + rel = filepath.ToSlash(rel) + + fmt.Fprintf(&b, "\t%q,\n", rel) + return nil + }) + if err != nil { + return "", err + } + + b.WriteString("}\n") + return b.String(), nil +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, "externs-gen:", err) + os.Exit(1) +} diff --git a/resolver-go/dockerfiles/api.Dockerfile b/resolver-go/dockerfiles/api.Dockerfile index 23ebd7e..e402d68 100644 --- a/resolver-go/dockerfiles/api.Dockerfile +++ b/resolver-go/dockerfiles/api.Dockerfile @@ -21,10 +21,13 @@ RUN --mount=type=cache,target=/go/pkg/mod \ GOARCH=$TARGETARCH \ go build -trimpath -o /out/botan-api -ldflags='-s -w' ./cmd/botan-api +RUN mkdir -p /out/tmp && chmod 1777 /out/tmp + FROM scratch COPY --from=build /out/botan-api /usr/local/bin/botan-api -COPY "botanjs/src" "./src" +COPY --from=build /workspace/src "./src" +COPY --from=build /out/tmp /tmp EXPOSE 8080/tcp ENTRYPOINT ["/usr/local/bin/botan-api", "-src", "./src", "-addr", ":8080"] diff --git a/resolver-go/dockerfiles/gen.Dockerfile b/resolver-go/dockerfiles/gen.Dockerfile index ddfcd3e..d011784 100644 --- a/resolver-go/dockerfiles/gen.Dockerfile +++ b/resolver-go/dockerfiles/gen.Dockerfile @@ -14,11 +14,15 @@ COPY "botanjs/src" "./src" COPY "resolver-go/cmd" "./cmd" COPY "resolver-go/internal" "./internal" -RUN go run ./cmd/botan-gen \ +RUN go run ./cmd/classmap-gen \ -src ./src \ -out internal/generated/classmap_gen.go -RUN mkdir /out && cp internal/generated/classmap_gen.go /out +RUN go run ./cmd/externs-gen \ + -src ./src \ + -out internal/generated/externs_gen.go + +RUN mkdir /out && cp internal/generated/*_gen.go /out FROM scratch -COPY --from=build /out/classmap_gen.go ./ +COPY --from=build /out/*_gen.go ./ diff --git a/resolver-go/go.mod b/resolver-go/go.mod index be63b6c..ad1c6f3 100644 --- a/resolver-go/go.mod +++ b/resolver-go/go.mod @@ -1,3 +1,3 @@ -module github.com/tgckpg/botanres-go +module github.com/tgckpg/resolver-go go 1.26 diff --git a/resolver-go/internal/closure/cache.go b/resolver-go/internal/closure/cache.go new file mode 100644 index 0000000..3346b12 --- /dev/null +++ b/resolver-go/internal/closure/cache.go @@ -0,0 +1,85 @@ +package closure + +import ( + "context" + "errors" + "time" +) + +func NewCompileCache(workers int) *CompileCache { + c := &CompileCache{ + client: NewClientFromEnv(), + states: make(map[string]CompileState), + results: make(map[string][]byte), + errors: make(map[string]error), + jobs: make(chan CompileJob, 128), + } + + for i := 0; i < workers; i++ { + go c.worker() + } + + return c +} + +func (c *CompileCache) Get(hash string) ([]byte, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.states[hash] != CompileReady { + return nil, false + } + + return c.results[hash], true +} + +func (c *CompileCache) Enqueue(job CompileJob) { + c.mu.Lock() + + switch c.states[job.Hash] { + case CompilePending, CompileReady: + c.mu.Unlock() + return + } + + c.states[job.Hash] = CompilePending + c.mu.Unlock() + + select { + case c.jobs <- job: + default: + // Queue full. Don't block request path. + c.mu.Lock() + c.states[job.Hash] = CompileMissing + c.errors[job.Hash] = errors.New("compile queue full") + c.mu.Unlock() + } +} + +func (c *CompileCache) worker() { + for job := range c.jobs { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + + req := CompileRequest{ + ExternSources: job.ExternSources, + JSSources: job.JSSources, + Defines: job.Defines, + } + + c.client.DebugPrintCurl(ctx, req) + + out, err := c.client.Compile(ctx, req) + cancel() + + c.mu.Lock() + if err != nil { + c.states[job.Hash] = CompileFailed + c.errors[job.Hash] = err + } else { + c.states[job.Hash] = CompileReady + c.results[job.Hash] = out + delete(c.errors, job.Hash) + } + c.mu.Unlock() + } +} diff --git a/resolver-go/internal/closure/client.go b/resolver-go/internal/closure/client.go new file mode 100644 index 0000000..63826ac --- /dev/null +++ b/resolver-go/internal/closure/client.go @@ -0,0 +1,107 @@ +package closure + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "time" +) + +type Client struct { + endpoint string + http *http.Client +} + +func NewClientFromEnv() *Client { + endpoint := os.Getenv("CLOSURE_ENDPOINT") + if endpoint == "" { + endpoint = "http://closure-svc:8080/compile" + } + + return &Client{ + endpoint: endpoint, + http: &http.Client{ + Timeout: 70 * time.Second, + }, + } +} + +func (c *Client) Compile(ctx context.Context, reqBody CompileRequest) ([]byte, error) { + body, err := json.Marshal(reqBody) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + c.endpoint, + bytes.NewReader(body), + ) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := c.http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode/100 != 2 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + return nil, fmt.Errorf("closure failed: %s: %s", resp.Status, body) + } + + return io.ReadAll(resp.Body) +} + +func (c *Client) DebugPrintCurl(ctx context.Context, reqBody CompileRequest) { + if os.Getenv("CLOSURE_DEBUG_CURL") == "" { + return + } + + body, err := json.MarshalIndent(reqBody, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "closure debug: marshal payload failed: %v\n", err) + return + } + + tmpDir := os.Getenv("CLOSURE_DEBUG_DIR") + if tmpDir == "" { + tmpDir = os.TempDir() + } + + path := filepath.Join( + tmpDir, + fmt.Sprintf("closure-request-%d.json", time.Now().UnixNano()), + ) + + if err := os.WriteFile(path, body, 0o600); err != nil { + fmt.Fprintf(os.Stderr, "closure debug: write payload failed: %v\n", err) + return + } + + fmt.Fprintf( + os.Stderr, + "closure debug curl:\n curl -v -X POST %s -H 'Content-Type: application/json' --data-binary @%s\n", + shellQuote(c.endpoint), + shellQuote(path), + ) + + if deadline, ok := ctx.Deadline(); ok { + fmt.Fprintf(os.Stderr, "closure debug deadline: %s\n", deadline.Format(time.RFC3339Nano)) + } +} + +func shellQuote(s string) string { + return strconv.Quote(s) +} diff --git a/resolver-go/internal/closure/types.go b/resolver-go/internal/closure/types.go new file mode 100644 index 0000000..9a56e57 --- /dev/null +++ b/resolver-go/internal/closure/types.go @@ -0,0 +1,41 @@ +package closure + +import "sync" + +type CompileState int + +const ( + CompileMissing CompileState = iota + CompilePending + CompileReady + CompileFailed +) + +type CompileRequest struct { + ExternSources []SourceInput `json:"externSources,omitempty"` + JSSources []SourceInput `json:"jsSources"` + Defines map[string]any `json:"defines,omitempty"` +} + +type SourceInput struct { + Name string `json:"name"` + Source string `json:"source"` +} + +type CompileJob struct { + Hash string + Mode string + + ExternSources []SourceInput + JSSources []SourceInput + Defines map[string]any +} + +type CompileCache struct { + client *Client + mu sync.Mutex + states map[string]CompileState + results map[string][]byte + errors map[string]error + jobs chan CompileJob +} diff --git a/resolver-go/internal/generated/buildinfo_gen.go b/resolver-go/internal/generated/buildinfo_gen.go index fe831dd..5533709 100644 --- a/resolver-go/internal/generated/buildinfo_gen.go +++ b/resolver-go/internal/generated/buildinfo_gen.go @@ -2,5 +2,5 @@ package generated const ( IMAGE_TAG = "dev" - Timestamp = "20260610.201739" + Timestamp = "20260611.192429" ) diff --git a/resolver-go/internal/generated/classmap_gen.go b/resolver-go/internal/generated/classmap_gen.go index 87e120d..2411e3f 100644 --- a/resolver-go/internal/generated/classmap_gen.go +++ b/resolver-go/internal/generated/classmap_gen.go @@ -1,7 +1,7 @@ -// Code generated by botan-gen; DO NOT EDIT. +// Code generated by classmap-gen; DO NOT EDIT. package generated -import "github.com/tgckpg/botanres-go/internal/classmap" +import "github.com/tgckpg/resolver-go/internal/classmap" var ClassMap = &classmap.Map{ Symbols: map[string]classmap.Symbol{ @@ -84,9 +84,9 @@ var ClassMap = &classmap.Map{ "Astro.Common.Element": {Name: "Astro.Common.Element", Kind: "class", Parent: "Astro.Common", Imports: []string(nil), Resource: classmap.Resource{Src: "", JSHash: "", CSSHash: ""}}, "Astro.Common.Element.Footer": {Name: "Astro.Common.Element.Footer", Kind: "class", Parent: "Astro.Common.Element", Imports: []string(nil), Resource: classmap.Resource{Src: "Astro/Common/Element/Footer.js", JSHash: "80f69d85c399bdc04d3b2055d1ebbe9ee77a852a", CSSHash: "c26f4238a6a4f2fc0bcbd9ac86141d12522552a4"}}, "Astro.Mechanism": {Name: "Astro.Mechanism", Kind: "class", Parent: "Astro", Imports: []string(nil), Resource: classmap.Resource{Src: "", JSHash: "", CSSHash: ""}}, - "Astro.Mechanism.CharacterCloud": {Name: "Astro.Mechanism.CharacterCloud", Kind: "class", Parent: "Astro.Mechanism", Imports: []string{"Dandelion"}, Resource: classmap.Resource{Src: "Astro/Mechanism/CharacterCloud.js", JSHash: "540a2085798928418a54e10d56bc037863ee57a9", CSSHash: "7cae98eaf8e3d5d1cd7fadaa6806ce3d65d74d92"}}, + "Astro.Mechanism.CharacterCloud": {Name: "Astro.Mechanism.CharacterCloud", Kind: "class", Parent: "Astro.Mechanism", Imports: []string{"Dandelion"}, Resource: classmap.Resource{Src: "Astro/Mechanism/CharacterCloud.js", JSHash: "281b59ca621d35d254abbddc1f088870ed61cb28", CSSHash: "7cae98eaf8e3d5d1cd7fadaa6806ce3d65d74d92"}}, "Astro.Mechanism.CharacterCloud.create": {Name: "Astro.Mechanism.CharacterCloud.create", Kind: "method", Parent: "Astro.Mechanism.CharacterCloud", Imports: []string(nil), Resource: classmap.Resource{Src: "", JSHash: "", CSSHash: ""}}, - "Astro.Mechanism.Parallax": {Name: "Astro.Mechanism.Parallax", Kind: "class", Parent: "Astro.Mechanism", Imports: []string{"System.Cycle", "Dandelion", "Dandelion.IDOMObject"}, Resource: classmap.Resource{Src: "Astro/Mechanism/Parallax.js", JSHash: "ba7b1816fc5d32edfaf5b93b9d35c7b2c297f011", CSSHash: "f8c02f4ec1be082e02c51f0b5ea39cbdd5b0e49f"}}, + "Astro.Mechanism.Parallax": {Name: "Astro.Mechanism.Parallax", Kind: "class", Parent: "Astro.Mechanism", Imports: []string{"System.Cycle", "Dandelion", "Dandelion.IDOMObject"}, Resource: classmap.Resource{Src: "Astro/Mechanism/Parallax.js", JSHash: "6ac80e95f9e8ba391668e1988fe3586987ea79d0", CSSHash: "f8c02f4ec1be082e02c51f0b5ea39cbdd5b0e49f"}}, "Astro.Mechanism.Parallax.attach": {Name: "Astro.Mechanism.Parallax.attach", Kind: "method", Parent: "Astro.Mechanism.Parallax", Imports: []string(nil), Resource: classmap.Resource{Src: "", JSHash: "", CSSHash: ""}}, "Astro.Mechanism.Parallax.cssSlide": {Name: "Astro.Mechanism.Parallax.cssSlide", Kind: "method", Parent: "Astro.Mechanism.Parallax", Imports: []string(nil), Resource: classmap.Resource{Src: "", JSHash: "", CSSHash: ""}}, "Astro.Mechanism.Parallax.verticalSlideTo": {Name: "Astro.Mechanism.Parallax.verticalSlideTo", Kind: "method", Parent: "Astro.Mechanism.Parallax", Imports: []string(nil), Resource: classmap.Resource{Src: "", JSHash: "", CSSHash: ""}}, @@ -117,7 +117,7 @@ var ClassMap = &classmap.Map{ "Astro.utils.Date.prettyDay": {Name: "Astro.utils.Date.prettyDay", Kind: "method", Parent: "Astro.utils.Date", Imports: []string(nil), Resource: classmap.Resource{Src: "", JSHash: "", CSSHash: ""}}, "Astro.utils.Date.smstamp": {Name: "Astro.utils.Date.smstamp", Kind: "method", Parent: "Astro.utils.Date", Imports: []string(nil), Resource: classmap.Resource{Src: "", JSHash: "", CSSHash: ""}}, "Components": {Name: "Components", Kind: "class", Parent: "", Imports: []string(nil), Resource: classmap.Resource{Src: "Components/_this.js", JSHash: "f4cb7babe62a5cdae34d2c4ebcb55071e81da4ff", CSSHash: "1"}}, - "Components.Console": {Name: "Components.Console", Kind: "class", Parent: "Components", Imports: []string{"System.utils.Perf", "System.Cycle", "System.Cycle.TICK", "System.Global", "System.Log", "System.Debug", "Dandelion", "Dandelion.IDOMElement", "Components.DockPanel"}, Resource: classmap.Resource{Src: "Components/Console.js", JSHash: "115be15db72bd22f8222510e7bf4ce0c741028fb", CSSHash: "452f4a36e8fd7321c7d177a311047c8b71165e48"}}, + "Components.Console": {Name: "Components.Console", Kind: "class", Parent: "Components", Imports: []string{"System.utils.Perf", "System.Cycle", "System.Cycle.TICK", "System.Global", "System.Log", "System.Debug", "Dandelion", "Dandelion.IDOMElement", "Components.DockPanel"}, Resource: classmap.Resource{Src: "Components/Console.js", JSHash: "9e4ba7f921f39c85f264e416669134120d4668ea", CSSHash: "452f4a36e8fd7321c7d177a311047c8b71165e48"}}, "Components.DockPanel": {Name: "Components.DockPanel", Kind: "class", Parent: "Components", Imports: []string{"System.Cycle", "System.utils.DataKey", "Dandelion", "Dandelion.IDOMElement"}, Resource: classmap.Resource{Src: "Components/DockPanel.js", JSHash: "c74b3081efdcbfa13300e9a49529f5664734b24e", CSSHash: "af0d91982c764ee62c46b243be68c08f2808e82b"}}, "Components.MessageBox": {Name: "Components.MessageBox", Kind: "class", Parent: "Components", Imports: []string{"System.Cycle.Trigger", "Dandelion", "Dandelion.IDOMObject", "System.utils.EventKey", "Dandelion.CSSAnimations"}, Resource: classmap.Resource{Src: "Components/MessageBox.js", JSHash: "db0b55a5e0b1a92b9781c2b0b13817355b8e3cdf", CSSHash: "5571fa4c2e1324dcf1f2e18df8b6105cf37dfc53"}}, "Components.Mouse": {Name: "Components.Mouse", Kind: "class", Parent: "Components", Imports: []string(nil), Resource: classmap.Resource{Src: "Components/Mouse/_this.js", JSHash: "9ec5ced53a20ea1d057e2142668e27e5df7d49f6", CSSHash: "1"}}, @@ -379,8 +379,8 @@ var ClassMap = &classmap.Map{ "Astro/Blog/SharedStyle.js": {Src: "Astro/Blog/SharedStyle.js", JSHash: "e946130da823a5b2d089b5b416c13b628eb1637d", CSSHash: "f8ff15304a5e38e199b713bac48e282d2bf10f45"}, "Astro/Bootstrap.js": {Src: "Astro/Bootstrap.js", JSHash: "51bfb270eadd20b4b71372ae2d20dcf3d20575db", CSSHash: "e650f4de36f799af80ca0633d66351fb9333d620"}, "Astro/Common/Element/Footer.js": {Src: "Astro/Common/Element/Footer.js", JSHash: "80f69d85c399bdc04d3b2055d1ebbe9ee77a852a", CSSHash: "c26f4238a6a4f2fc0bcbd9ac86141d12522552a4"}, - "Astro/Mechanism/CharacterCloud.js": {Src: "Astro/Mechanism/CharacterCloud.js", JSHash: "540a2085798928418a54e10d56bc037863ee57a9", CSSHash: "7cae98eaf8e3d5d1cd7fadaa6806ce3d65d74d92"}, - "Astro/Mechanism/Parallax.js": {Src: "Astro/Mechanism/Parallax.js", JSHash: "ba7b1816fc5d32edfaf5b93b9d35c7b2c297f011", CSSHash: "f8c02f4ec1be082e02c51f0b5ea39cbdd5b0e49f"}, + "Astro/Mechanism/CharacterCloud.js": {Src: "Astro/Mechanism/CharacterCloud.js", JSHash: "281b59ca621d35d254abbddc1f088870ed61cb28", CSSHash: "7cae98eaf8e3d5d1cd7fadaa6806ce3d65d74d92"}, + "Astro/Mechanism/Parallax.js": {Src: "Astro/Mechanism/Parallax.js", JSHash: "6ac80e95f9e8ba391668e1988fe3586987ea79d0", CSSHash: "f8c02f4ec1be082e02c51f0b5ea39cbdd5b0e49f"}, "Astro/Penguin/Layout/MainFrame.js": {Src: "Astro/Penguin/Layout/MainFrame.js", JSHash: "b2f58fc9394745c1e47dfb95c4043f59f76acc1a", CSSHash: "e87b69ab9e725e4801065850790670d76f0a6d18"}, "Astro/Penguin/Page/Docs.js": {Src: "Astro/Penguin/Page/Docs.js", JSHash: "02217e7fa4ebffe90b32002ef07da500ac02bf52", CSSHash: "5230a026499b9ab0f4f530f4975a9df89170c83b"}, "Astro/Penguin/Page/_this.js": {Src: "Astro/Penguin/Page/_this.js", JSHash: "66e415e546eb6ca0b84cedfd4d3b6de67c2164f3", CSSHash: "e8afcaa8c75b241cd933cfc99a648adc42f815d7"}, @@ -393,7 +393,7 @@ var ClassMap = &classmap.Map{ "Astro/Starfall/_this.js": {Src: "Astro/Starfall/_this.js", JSHash: "77ca61d1ba806c3440cec5e26a100581259e1784", CSSHash: "1"}, "Astro/utils/Date.js": {Src: "Astro/utils/Date.js", JSHash: "45221fe447d15fd943310fe9d3d3308f884b6aec", CSSHash: "1"}, "Astro/utils/_this.js": {Src: "Astro/utils/_this.js", JSHash: "21e99449ec5c1a4b6d25164db9434132aeea5ad3", CSSHash: "1"}, - "Components/Console.js": {Src: "Components/Console.js", JSHash: "115be15db72bd22f8222510e7bf4ce0c741028fb", CSSHash: "452f4a36e8fd7321c7d177a311047c8b71165e48"}, + "Components/Console.js": {Src: "Components/Console.js", JSHash: "9e4ba7f921f39c85f264e416669134120d4668ea", CSSHash: "452f4a36e8fd7321c7d177a311047c8b71165e48"}, "Components/DockPanel.js": {Src: "Components/DockPanel.js", JSHash: "c74b3081efdcbfa13300e9a49529f5664734b24e", CSSHash: "af0d91982c764ee62c46b243be68c08f2808e82b"}, "Components/MessageBox.js": {Src: "Components/MessageBox.js", JSHash: "db0b55a5e0b1a92b9781c2b0b13817355b8e3cdf", CSSHash: "5571fa4c2e1324dcf1f2e18df8b6105cf37dfc53"}, "Components/Mouse/Clipboard.js": {Src: "Components/Mouse/Clipboard.js", JSHash: "574a1ef17878beb21395a2eecfa75d046ff694e0", CSSHash: "1b7f85206ce1560ed23d3b270e093022e25d2e61"}, diff --git a/resolver-go/internal/generated/externs_gen.go b/resolver-go/internal/generated/externs_gen.go new file mode 100644 index 0000000..a18e616 --- /dev/null +++ b/resolver-go/internal/generated/externs_gen.go @@ -0,0 +1,108 @@ +// Code generated by externs-gen; DO NOT EDIT. +package generated + +var Externs = []string{ + "externs/Astro.Blog.AstroEdit.Article.js", + "externs/Astro.Blog.AstroEdit.IPlugin.js", + "externs/Astro.Blog.AstroEdit.SmartInput.Definition.js", + "externs/Astro.Blog.AstroEdit.SmartInput.ICandidateAction.js", + "externs/Astro.Blog.AstroEdit.SmartInput.js", + "externs/Astro.Blog.AstroEdit.Visualizer.Snippet.Model.js", + "externs/Astro.Blog.AstroEdit.Visualizer.Snippet.js", + "externs/Astro.Blog.AstroEdit.Visualizer.js", + "externs/Astro.Blog.AstroEdit.js", + "externs/Astro.Blog.Components.Bubble.js", + "externs/Astro.Blog.Components.Calendar.js", + "externs/Astro.Blog.Components.SiteFile.js", + "externs/Astro.Blog.Components.js", + "externs/Astro.Blog.Config.js", + "externs/Astro.Blog.Events.Responsive.js", + "externs/Astro.Blog.Events.js", + "externs/Astro.Blog.js", + "externs/Astro.Bootstrap.js", + "externs/Astro.Mechanism.CharacterCloud.js", + "externs/Astro.Mechanism.Parallax.js", + "externs/Astro.Mechanism.js", + "externs/Astro.js", + "externs/Astro.utils.Date.js", + "externs/Astro.utils.js", + "externs/BotanEvent.js", + "externs/BotanJS.js", + "externs/Components.MessageBox.js", + "externs/Components.Mouse.Clipboard.js", + "externs/Components.Mouse.ContextMenu.js", + "externs/Components.Mouse.js", + "externs/Components.Vim.Actions.js", + "externs/Components.Vim.Controls.ActionEvent.js", + "externs/Components.Vim.Controls.js", + "externs/Components.Vim.Cursor.js", + "externs/Components.Vim.DateTime.js", + "externs/Components.Vim.IAction.js", + "externs/Components.Vim.LineBuffer.js", + "externs/Components.Vim.LineFeeder.js", + "externs/Components.Vim.State.History.js", + "externs/Components.Vim.State.Marks.js", + "externs/Components.Vim.State.Recorder.js", + "externs/Components.Vim.State.Registers.js", + "externs/Components.Vim.State.Stack.js", + "externs/Components.Vim.State.js", + "externs/Components.Vim.StatusBar.js", + "externs/Components.Vim.Syntax.Analyzer.js", + "externs/Components.Vim.Syntax.TokenMatch.js", + "externs/Components.Vim.Syntax.Word.js", + "externs/Components.Vim.Syntax.js", + "externs/Components.Vim.VimArea.js", + "externs/Components.Vim.js", + "externs/Components.js", + "externs/Dandelion.CSSAnimations.MovieClip.js", + "externs/Dandelion.CSSAnimations.js", + "externs/Dandelion.IDOMElement.js", + "externs/Dandelion.IDOMObject.js", + "externs/Dandelion.js", + "externs/Libraries.SyntaxHighLighter.Brush.js", + "externs/Libraries.SyntaxHighLighter.js", + "externs/Libraries.js", + "externs/System.Compression.Zlib.js", + "externs/System.Compression.js", + "externs/System.Cycle.Trigger.js", + "externs/System.Cycle.js", + "externs/System.Debug.js", + "externs/System.Encoding.Base64.js", + "externs/System.Encoding.Utf8.js", + "externs/System.Encoding.js", + "externs/System.Global.js", + "externs/System.Log.js", + "externs/System.Net.js", + "externs/System.Policy.js", + "externs/System.Tick.js", + "externs/System.js", + "externs/System.utils.DataKey.js", + "externs/System.utils.EventKey.js", + "externs/System.utils.IKey.js", + "externs/System.utils.Perf.js", + "externs/System.utils.js", + "externs/_3rdParty_.Recaptcha.js", + "externs/_3rdParty_.js", + "externs/_AstConf_.Article.js", + "externs/_AstConf_.AstroEdit.js", + "externs/_AstConf_.Comment.js", + "externs/_AstConf_.Control.js", + "externs/_AstConf_.Login.js", + "externs/_AstConf_.Notification.js", + "externs/_AstConf_.SiteFile.js", + "externs/_AstConf_.UserInfo.js", + "externs/_AstConf_.js", + "externs/_AstJson_.AJaxGetArticle.js", + "externs/_AstJson_.AJaxGetDrafts.js", + "externs/_AstJson_.AJaxGetFiles.Request.js", + "externs/_AstJson_.AJaxGetFiles.js", + "externs/_AstJson_.AJaxGetNotis.js", + "externs/_AstJson_.SiteFile.js", + "externs/_AstJson_.js", + "externs/_AstXObject_.AstroEdit.Visualizer.Snippet.Code.js", + "externs/_AstXObject_.AstroEdit.Visualizer.Snippet.Spoiler.js", + "externs/_AstXObject_.AstroEdit.Visualizer.Snippet.js", + "externs/_AstXObject_.AstroEdit.Visualizer.js", + "externs/_AstXObject_.AstroEdit.js", + "externs/_AstXObject_.js", +} diff --git a/resolver-go/internal/resolver/resolver.go b/resolver-go/internal/resolver/resolver.go index 8b66731..4d6e653 100644 --- a/resolver-go/internal/resolver/resolver.go +++ b/resolver-go/internal/resolver/resolver.go @@ -14,8 +14,10 @@ import ( "regexp" "sort" "strings" + "sync" - "github.com/tgckpg/botanres-go/internal/classmap" + "github.com/tgckpg/resolver-go/internal/classmap" + "github.com/tgckpg/resolver-go/internal/closure" ) type OutputMode string @@ -38,6 +40,9 @@ type Result struct { type Resolver struct { Root string Map *classmap.Map + + externMu sync.RWMutex + externCache map[string]closure.SourceInput } func New(root string, m *classmap.Map) (*Resolver, error) { @@ -45,7 +50,11 @@ func New(root string, m *classmap.Map) (*Resolver, error) { if err != nil { return nil, err } - return &Resolver{Root: root, Map: m}, nil + return &Resolver{ + Root: root, + Map: m, + externCache: make(map[string]closure.SourceInput), + }, nil } func (r *Resolver) ResolveRequest(code string, mode OutputMode, excludes []string) (Result, error) { @@ -242,6 +251,55 @@ func (r *Resolver) MergeCSS(files []classmap.Resource) ([]byte, string, error) { return b.Bytes(), hashList(files, "css"), nil } +func (r *Resolver) GetExterns(files []string) ([]closure.SourceInput, error) { + sources := make([]closure.SourceInput, 0, len(files)) + + for _, f := range files { + src, ok, err := r.getExtern(f) + if err != nil { + return nil, err + } + if !ok { + continue + } + + sources = append(sources, src) + } + + return sources, nil +} + +func (r *Resolver) getExtern(f string) (closure.SourceInput, bool, error) { + // Fast path: read cache. + r.externMu.RLock() + src, ok := r.externCache[f] + r.externMu.RUnlock() + if ok { + return src, true, nil + } + + // Slow path: read file. + b, err := os.ReadFile(filepath.Join(r.Root, filepath.FromSlash(f))) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return closure.SourceInput{}, false, nil + } + return closure.SourceInput{}, false, err + } + + src = closure.SourceInput{ + Name: f, + Source: string(b), + } + + // Store cache. + r.externMu.Lock() + r.externCache[f] = src + r.externMu.Unlock() + + return src, true, nil +} + func DecodeRequest(code string) ([]string, error) { sep := "/" decoded := code diff --git a/resolver-go/src b/resolver-go/src new file mode 120000 index 0000000..c6a0283 --- /dev/null +++ b/resolver-go/src @@ -0,0 +1 @@ +../botanjs/src \ No newline at end of file