forked from Botanical/BotanJS
Removed old impl
This commit is contained in:
@@ -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( "<BotanJS></BotanJS>" )
|
||||
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()
|
||||
@@ -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" )
|
||||
@@ -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" )
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 )
|
||||
@@ -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() )
|
||||
@@ -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( "/<mode>/" , view_func = lambda mode: self.api_request( mode, "zpayload" ) )
|
||||
self.app.add_url_rule( "/<mode>/<path:code>" , 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
|
||||
@@ -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"" )
|
||||
@@ -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
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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] " ) );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} */
|
||||
|
||||
@@ -1,42 +1,60 @@
|
||||
/** @constructor */
|
||||
Libraries.SyntaxHighlighter = function() {};
|
||||
|
||||
/** @type {Function} */
|
||||
/** @type {function(...?): ?} */
|
||||
Libraries.SyntaxHighlighter.all;
|
||||
/** @type {Object} */
|
||||
|
||||
/** @type {!Object<string, *>} */
|
||||
Libraries.SyntaxHighlighter.defaults;
|
||||
/** @type {Function} */
|
||||
|
||||
/** @type {function(...?): ?} */
|
||||
Libraries.SyntaxHighlighter.highlight;
|
||||
/** @type {Object} */
|
||||
|
||||
/** @type {!Object<string, *>} */
|
||||
Libraries.SyntaxHighlighter.Highlighter;
|
||||
|
||||
/** @type {Object} */
|
||||
/** @type {!Object<string, *>} */
|
||||
Libraries.SyntaxHighlighter.brushes;
|
||||
|
||||
/** @type {Object} */
|
||||
/** @type {!Object<string, !RegExp>} */
|
||||
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<string, !RegExp>} */
|
||||
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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @type {constructor} */
|
||||
/** @constructor */
|
||||
System.Compression.Zlib = function(){};
|
||||
|
||||
/** @type {constructor} */
|
||||
/** @constructor */
|
||||
System.Compression.Zlib.Deflate = function(){};
|
||||
|
||||
/** @type {Function} */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** @type {Object} */
|
||||
/** @type {!Object<string, *>} */
|
||||
_AstConf_.AstroEdit = {};
|
||||
/** @type {String} */
|
||||
_AstConf_.AstroEdit.article_id;
|
||||
/** @type {object} */
|
||||
/** @type {!Object<string, *>} */
|
||||
_AstConf_.AstroEdit.paths = {};
|
||||
/** @type {string} */
|
||||
_AstConf_.AstroEdit.paths.set_article;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<SourceFile> externs = new ArrayList<>();
|
||||
externs.addAll(CommandLineRunner.getBuiltinExterns(options.getEnvironment()));
|
||||
externs.addAll(readFiles(req, "externs"));
|
||||
externs.addAll(readInlineSources(req, "externSources"));
|
||||
|
||||
List<SourceFile> inputs = readFiles(req, "js");
|
||||
List<SourceFile> 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<SourceFile> readInlineSources(JsonNode req, String field) throws BadRequest {
|
||||
JsonNode arr = req.get(field);
|
||||
List<SourceFile> 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)) {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package generated
|
||||
|
||||
const (
|
||||
IMAGE_TAG = "dev"
|
||||
Timestamp = "20260611.005458"
|
||||
)
|
||||
+3
-17
@@ -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"
|
||||
|
||||
@@ -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) \
|
||||
|
||||
+1
-1
@@ -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))
|
||||
|
||||
@@ -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"]
|
||||
@@ -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 )
|
||||
Vendored
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
```
|
||||
Please initialize your virtualenv here
|
||||
```
|
||||
-23
@@ -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"] )
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -1,9 +0,0 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
python:
|
||||
build: pyrt
|
||||
image: astrojs/pyrt-nanoserver-20h2
|
||||
jre:
|
||||
build: jre
|
||||
image: astrojs/jre-nanoserver-20h2
|
||||
@@ -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:
|
||||
@@ -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.
|
||||
@@ -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 '<VERSION>'"
|
||||
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" ]
|
||||
@@ -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 }
|
||||
@@ -1,4 +1,4 @@
|
||||
# botanres-go
|
||||
# resolver-go
|
||||
|
||||
Go rewrite for the old BotanJS dynamic resource resolver.
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 ./
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
module github.com/tgckpg/botanres-go
|
||||
module github.com/tgckpg/resolver-go
|
||||
|
||||
go 1.26
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,5 +2,5 @@ package generated
|
||||
|
||||
const (
|
||||
IMAGE_TAG = "dev"
|
||||
Timestamp = "20260610.201739"
|
||||
Timestamp = "20260611.192429"
|
||||
)
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../botanjs/src
|
||||
Reference in New Issue
Block a user