generate a non-request specific importmap, as we ajax_exec apps and then not reload importmap

This commit is contained in:
Ralf Becker 2021-06-09 19:00:53 +02:00
parent 42305a6562
commit 40cac6f964
3 changed files with 127 additions and 45 deletions

View File

@ -13,6 +13,7 @@
namespace EGroupware\Api; namespace EGroupware\Api;
use EGroupware\Api\Framework\Bundle;
use EGroupware\Api\Header\ContentSecurityPolicy; use EGroupware\Api\Header\ContentSecurityPolicy;
/** /**
@ -164,7 +165,7 @@ abstract class Framework extends Framework\Extra
// We need LABjs, but putting it through Framework\IncludeMgr causes it to re-load itself // We need LABjs, but putting it through Framework\IncludeMgr causes it to re-load itself
//'/api/js/labjs/LAB.src.js', //'/api/js/labjs/LAB.src.js',
// allways load jquery (not -ui) first // always load jquery (not -ui) first
'/vendor/bower-asset/jquery/dist/jquery.js', '/vendor/bower-asset/jquery/dist/jquery.js',
'/api/js/jquery/jquery.noconflict.js', '/api/js/jquery/jquery.noconflict.js',
// always include javascript helper functions // always include javascript helper functions
@ -1082,7 +1083,7 @@ abstract class Framework extends Framework\Extra
// add import-map before (!) first module // add import-map before (!) first module
$java_script .= '<script type="importmap" nonce="'.htmlspecialchars(ContentSecurityPolicy::addNonce('script-src')).'">'."\n". $java_script .= '<script type="importmap" nonce="'.htmlspecialchars(ContentSecurityPolicy::addNonce('script-src')).'">'."\n".
json_encode(self::getImportMap($map), JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)."\n". json_encode(self::getImportMap(), JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)."\n".
"</script>\n"; "</script>\n";
// load our clientside entrypoint egw.js // load our clientside entrypoint egw.js
@ -1123,28 +1124,14 @@ abstract class Framework extends Framework\Extra
/** /**
* Add EGroupware URL prefix eg. '/egroupware' to files AND bundles * Add EGroupware URL prefix eg. '/egroupware' to files AND bundles
* *
* @param array $map
* @return array * @return array
*/ */
protected static function getImportMap(array $map) public static function getImportMap()
{ {
if (substr($prefix = $GLOBALS['egw_info']['server']['webserver_url'], 0, 4) === 'http') $imports = Bundle::getImportMap();
{
$prefix = parse_url($prefix, PHP_URL_PATH);
}
$imports = [];
foreach($map as $file => $bundle)
{
$imports[$prefix.$file] = $prefix.$bundle;
// typescript unfortunately has currently no option to add ".js" to it's es6 import statements
// therefore we add extra entries without .js extension to the map
if (file_exists(EGW_SERVER_ROOT.substr($file, 0, -3).'.ts'))
{
$imports[$prefix.substr($file, 0, -3)] = $prefix.$bundle;
}
}
// adding some extra mappings // adding some extra mappings
if (($prefix = parse_url($GLOBALS['egw_info']['server']['webserver_url'], PHP_URL_PATH)) === '/') $prefix = '';
$imports['jquery'] = $imports[$prefix.'/vendor/bower-asset/jquery/dist/jquery.js']; $imports['jquery'] = $imports[$prefix.'/vendor/bower-asset/jquery/dist/jquery.js'];
$imports['jqueryui'] = $imports[$prefix.'/vendor/bower-asset/jquery-ui/jquery-ui.js']; $imports['jqueryui'] = $imports[$prefix.'/vendor/bower-asset/jquery-ui/jquery-ui.js'];
@ -1154,10 +1141,6 @@ abstract class Framework extends Framework\Extra
// @todo: add all node_modules as bare imports // @todo: add all node_modules as bare imports
// debug-output to tmp dir
file_put_contents($GLOBALS['egw_info']['server']['temp_dir'].'/'.substr(str_replace(['/', '.php'], ['-', '.json'], $_SERVER['PHP_SELF']), 1),
json_encode(['imports' => $imports], JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT));
return ['imports' => $imports]; return ['imports' => $imports];
} }

View File

@ -199,14 +199,22 @@ class Bundle
*/ */
const MAX_BUNDLE_FILES = 50; const MAX_BUNDLE_FILES = 50;
/**
* Apps which should be their own bundle:
* - own eT2 widgets
* - not just an app.js or a huge one
*/
const BUNDLE_APPS = ['calendar', 'mail', 'projectmanager', 'smallpart'];
/** /**
* Return all bundels we use: * Return all bundels we use:
* - api stuff phpgwapi/js/jsapi/* and it's dependencies incl. jquery * - api stuff phpgwapi/js/jsapi/* and it's dependencies incl. jquery
* - etemplate2 stuff not including api bundle, but jquery-ui * - etemplate2 stuff not including api bundle, but jquery-ui
* *
* @param bool $all_apps=false true: return bundle for every app with an app.js/ts
* @return array bundle-url => array of contained files * @return array bundle-url => array of contained files
*/ */
public static function all() public static function all(bool $all_apps = false)
{ {
$inc_mgr = new IncludeMgr(); $inc_mgr = new IncludeMgr();
$bundles = array(); $bundles = array();
@ -245,17 +253,17 @@ class Bundle
$stock_files = array_merge(...array_values($bundles)); $stock_files = array_merge(...array_values($bundles));
// generate template and app bundles, if installed // generate template and app bundles, if installed
foreach(array( foreach([
'jdots' => '/jdots/js/fw_jdots.js', 'jdots' => '/jdots/js/fw_jdots.js',
'mobile' => '/pixelegg/js/fw_mobile.js', 'mobile' => '/pixelegg/js/fw_mobile.js',
'pixelegg' => '/pixelegg/js/fw_pixelegg.js', 'pixelegg' => '/pixelegg/js/fw_pixelegg.js',
'calendar' => '/calendar/js/app.js', ]+($all_apps ? scandir(EGW_SERVER_ROOT) : self::BUNDLE_APPS) as $bundle => $file)
'mail' => '/mail/js/app.js',
'projectmanager' => '/projectmanager/js/app.js',
'messenger' => '/messenger/js/app.js',
'smallpart' => '/smallpart/js/app.js',
) as $bundle => $file)
{ {
if (is_int($bundle))
{
$bundle = $file;
$file = "/$bundle/js/app.js";
}
if (@file_exists(EGW_SERVER_ROOT.$file)) if (@file_exists(EGW_SERVER_ROOT.$file))
{ {
$inc_mgr = new IncludeMgr($stock_files); // reset loaded files to stock files $inc_mgr = new IncludeMgr($stock_files); // reset loaded files to stock files
@ -282,4 +290,103 @@ class Bundle
//error_log(__METHOD__."() returning ".array2string($bundles)); //error_log(__METHOD__."() returning ".array2string($bundles));
return $bundles; return $bundles;
} }
/**
* some files are not in a bundle, because loaded otherwise or are big enough themselves
*
* @var array
*/
static public $exclude = [
// api/js/jsapi/egw.js loaded via own tag, and we must not load it twice!
'api/js/jsapi/egw.js',
// TinyMCE is loaded separate before the bundle
'vendor/tinymce/tinymce/tinymce.min.js',
// CRM.js from addressbook is also used in infolog, so it can't be bundled with either!
'addressbook/js/CRM.js',
];
/**
* Generate importmap for whole instance
*
* It need to be for the whole instance incl. all app.js, as it does not get reloaded, when we execute
* apps via ajax!
*
* @ToDo new-js-loader: use static file in filesystem updated when js-files get minified (for minified only!)
*
* @return array
*/
public static function getImportMap()
{
$minified = empty($GLOBALS['egw_info']['server']['debug_minify']);
// cache map for the whole tree to use
return Cache::getTree('api', 'importmap'.($minified?'-minified':''), static function()
{
$gruntfile = EGW_SERVER_ROOT . '/Gruntfile.js';
if (!($content = @file_get_contents($gruntfile)))
{
die("\nFile '$gruntfile' not found!\n\n");
}
if (!preg_match('/grunt\.initConfig\(({.+})\);/s', $content, $matches) ||
!($json = preg_replace('/^(\s*)([a-z0-9_-]+):/mi', '$1"$2":', $matches[1])) ||
!($config = json_decode($json, true)))
{
die("\nCan't parse $gruntfile!\n\n");
}
if (($prefix = parse_url($GLOBALS['egw_info']['server']['webserver_url'], PHP_URL_PATH)) === '/') $prefix = '';
$uglify = $config['terser'];
unset($config, $uglify['options']);
$map = [];
foreach (self::all(true) as $name => $files)
{
if ($name == '.ts') continue; // ignore timestamp
// some files are not in a bundle, because they are big enough themselves or otherwise excluded
foreach (self::$exclude as $file)
{
if (($key = array_search($file, $files)))
{
$map[$prefix . $file] = $prefix . $file . '?' . filemtime(EGW_SERVER_ROOT . $file);
unset($files[$key]);
}
}
if (isset($uglify[$name]))
{
$target = key($uglify[$name]['files']);
$uglify[$name]['files'][$target] = array_values($files);
}
elseif (isset($uglify[$append = substr($name, 0, -1)]))
{
reset($uglify[$append]['files']);
$target = key($uglify[$append]['files']);
$uglify[$append]['files'][$target] = array_merge($uglify[$append]['files'][$target], array_values($files));
}
else // create new bundle using last file as target
{
$target = str_replace('.js', '.min.js', end($files));
$uglify[$name]['files'][$target] = array_values($files);
}
if ($target[0] !== '/') $target = '/' . $target;
$use_bundle = in_array($name, array_merge(['api', 'et2'], Bundle::BUNDLE_APPS)) &&
empty($GLOBALS['egw_info']['server']['debug_minify']);
foreach ($files as $file)
{
// use bundle / minified url as target or not
if (!$use_bundle) $target = $file;
$map[$prefix . $file] = $prefix.$target.'?'.filemtime(EGW_SERVER_ROOT.$target);
// typescript unfortunately has currently no option to add ".js" to it's es6 import statements
// therefore we add extra entries without .js extension to the map if (file_exists(EGW_SERVER_ROOT.substr($file, 0, -3) . '.ts'))
{
$map[$prefix . substr($file, 0, -3)] = $prefix.$target.'?'.filemtime(EGW_SERVER_ROOT.$target);
}
}
}
return $map;
}, [], 30);
}
} }

View File

@ -23,7 +23,7 @@ $GLOBALS['egw_info'] = array(
); );
include(__DIR__.'/header.inc.php'); include(__DIR__.'/header.inc.php');
$gruntfile = __DIR__.'/Gruntfile.js'; $gruntfile = EGW_SERVER_ROOT.'/Gruntfile.js';
if (!($content = @file_get_contents($gruntfile))) if (!($content = @file_get_contents($gruntfile)))
{ {
die("\nFile '$gruntfile' not found!\n\n"); die("\nFile '$gruntfile' not found!\n\n");
@ -33,20 +33,12 @@ if (!preg_match('/grunt\.initConfig\(({.+})\);/s', $content, $matches) ||
!($json = preg_replace('/^(\s*)([a-z0-9_-]+):/mi', '$1"$2":', $matches[1])) || !($json = preg_replace('/^(\s*)([a-z0-9_-]+):/mi', '$1"$2":', $matches[1])) ||
!($config = json_decode($json, true))) !($config = json_decode($json, true)))
{ {
die("\nCan't parse $path!\n\n"); die("\nCan't parse $gruntfile!\n\n");
} }
//print_r($config); exit; //print_r($config); exit;
$uglify =& $config['terser']; $uglify =& $config['terser'];
// some files are not in a bundle, because loaded otherwise or are big enough themselfs
$exclude = array(
// api/js/jsapi/egw.js loaded via own tag, and we must not load it twice!
'api/js/jsapi/egw.js',
// TinyMCE is loaded separate before the bundle
'vendor/tinymce/tinymce/tinymce.min.js',
);
foreach(Bundle::all() as $name => $files) foreach(Bundle::all() as $name => $files)
{ {
if ($name == '.ts') continue; // ignore timestamp if ($name == '.ts') continue; // ignore timestamp
@ -57,8 +49,8 @@ foreach(Bundle::all() as $name => $files)
if ($path[0] == '/') $path = substr($path, 1); if ($path[0] == '/') $path = substr($path, 1);
}); });
// some files are not in a bundle, because they are big enough themselfs // some files are not in a bundle, because they are big enough themselves
foreach($exclude as $file) foreach(Bundle::$exclude as $file)
{ {
if (($key = array_search($file, $files))) unset($files[$key]); if (($key = array_search($file, $files))) unset($files[$key]);
} }
@ -66,13 +58,13 @@ foreach(Bundle::all() as $name => $files)
//var_dump($name, $files); //var_dump($name, $files);
if (isset($uglify[$name])) if (isset($uglify[$name]))
{ {
list($target) = each($uglify[$name]['files']); $target = key($uglify[$name]['files']);
$uglify[$name]['files'][$target] = array_values($files); $uglify[$name]['files'][$target] = array_values($files);
} }
elseif (isset($uglify[$append = substr($name, 0, -1)])) elseif (isset($uglify[$append = substr($name, 0, -1)]))
{ {
reset($uglify[$append]['files']); reset($uglify[$append]['files']);
list($target) = each($uglify[$append]['files']); $target = key($uglify[$append]['files']);
$uglify[$append]['files'][$target] = array_merge($uglify[$append]['files'][$target], array_values($files)); $uglify[$append]['files'][$target] = array_merge($uglify[$append]['files'][$target], array_values($files));
} }
else // create new bundle using last file as target else // create new bundle using last file as target