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