partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 5825980ce50ed0cf56ebcc192855a5e12ddb9143
parent bacc6b82c16882581c3dc6540e86b1f1b25c535f
Author: Felicitus <felicitus@felicitus.org>
Date:   Tue, 17 Jul 2012 05:08:47 +0200

Refactored js minifying process

Diffstat:
Mbuild.xml | 9++++-----
Mdoctrine.php | 9+++++++--
Asrc/backend/PartKeepr/Console/Commands/MinifyJSCommand.php | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backend/PartKeepr/Util/ExtJSFile.php | 4++++
Asrc/backend/PartKeepr/Util/Minifier/Minifier.php | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/PartKeepr/Util/Minifier/jsminMinifier.php | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/PartKeepr/Util/Minifier/yuiCompressorMinifier.php | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/frontend/js/Components/CategoryStore.js | 30++++++++++++++++--------------
Dutil/gen-jsb3-file.php | 126-------------------------------------------------------------------------------
9 files changed, 490 insertions(+), 147 deletions(-)

diff --git a/build.xml b/build.xml @@ -349,11 +349,10 @@ <!-- Builds the jsb3 file and build the minified JS files --> <target name="jsbuilder"> - <exec command="php util/gen-jsb3-file.php src/frontend/ ${extjs.path}" /> - <exec command="${extjs.path}/jsbuilder/JSBuilder.sh --projectFile ${project.basedir}/partkeepr.jsb3 --deployDir ${project.basedir}/frontend/js" passthru="true" /> - - <exec command="${extjs.path}/jsbuilder/JSBuilder.sh --projectFile ${project.basedir}/3rdparty/Ext.ux.Exporter/Ext.ux.Exporter.jsb --deployDir ${project.basedir}/frontend/js/Ext.ux" passthru="true" /> - </target> + <property name="jsbuilder.sources" value="--source=${project.basedir}/3rdparty/extjs/examples/ux --source=${project.basedir}/src/frontend/js --source=${project.basedir}/3rdparty/ext-wizard/Ext.ux.Wizard/ --source=${project.basedir}/3rdparty/Ext.ux.Exporter/ --source ${project.basedir}/3rdparty/extjs/examples/ux/statusbar/"/> + <exec passthru="true" command="php doctrine.php partkeepr:js-minify ${project.basedir}/frontend/js/partkeepr-debug.js ${jsbuilder.sources}" /> + <exec passthru="true" command="php doctrine.php partkeepr:js-minify ${project.basedir}/frontend/js/partkeepr.js --compress ${jsbuilder.sources}" /> + </target> <!-- Checks all JavaScript files for common mistakes. Requires JSLint from http://www.javascriptlint.com --> <target name="jslint"> diff --git a/doctrine.php b/doctrine.php @@ -1,5 +1,5 @@ <?php -namespace PartKeepr\Foo; +namespace PartKeepr\Console; use PartKeepr\PartKeepr; @@ -11,7 +11,7 @@ $helpers = array(); require __DIR__ . '/cli-config.php'; -$cli = new \Symfony\Component\Console\Application('Doctrine Command Line Interface', \Doctrine\Common\Version::VERSION); +$cli = new \Symfony\Component\Console\Application('PartKeepr Console', \PartKeepr\PartKeeprVersion::PARTKEEPR_VERSION); $cli->setCatchExceptions(true); $helperSet = $cli->getHelperSet(); foreach ($helpers as $name => $helper) { @@ -51,4 +51,9 @@ new \Doctrine\DBAL\Migrations\Tools\Console\Command\MigrateCommand(), new \Doctrine\DBAL\Migrations\Tools\Console\Command\StatusCommand(), new \Doctrine\DBAL\Migrations\Tools\Console\Command\VersionCommand() )); + +$cli->addCommands(array( + new \PartKeepr\Console\Commands\MinifyJSCommand() + +)); $cli->run(); diff --git a/src/backend/PartKeepr/Console/Commands/MinifyJSCommand.php b/src/backend/PartKeepr/Console/Commands/MinifyJSCommand.php @@ -0,0 +1,247 @@ +<?php +namespace PartKeepr\Console\Commands; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console, + PartKeepr\Util\Minifier\Minifier, + PartKeepr\Util\Minifier\jsminMinifier, + PartKeepr\Util\Minifier\yuiCompressorMinifier, + PartKeepr\Util\ExtJSFile; + +class MinifyJSCommand extends Console\Command\Command +{ + + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('partkeepr:js-minify') + ->addArgument("outputFile", InputArgument::REQUIRED, "Output file") + ->addOption('source', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, + 'The input path(s). <info>You may specify this more than once to cover all source ' . + 'directories.</info>') + ->addOption('minifier', null, InputOption::VALUE_OPTIONAL, + 'The minifier to use. Currently <info>jsmin</info> and <info>yui-compressor</info> are ' . + 'supported.') + ->addOption('compress', null, InputOption::VALUE_NONE, + 'Specifies if compression should be used (reduces local variables, only supported by ' . + 'yui-compressor). <info>--compress</info> also includes <info>--minify</info>') + ->addOption('minify', null, InputOption::VALUE_NONE, + 'Specifies if the files should be minified (removal of whitespaces and line breaks). If not ' . + 'specified, this only merges the JS files.') + ->setDescription(<<<EOT +Minifies JavaScript files in their correct order. To do this correctly, we parse all JavaScript files and look for the ExtJS-specific 'extend' directives. With that information, +we can build a hierarchical tree of files which represent the dependencies and output a parsed tree. That tree is then converted into an ordered list, which is then compressed. +EOT + ); + } + + /** + * Executes the minify process + * + * @see Console\Command\Command + */ + protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output) + { + $sourceDirs = $input->getOption('source'); + + if (count($sourceDirs) == 0) { + throw new \RuntimeException("Argument 'source' is required at least once in order to execute this command correctly."); + } + + $orderedFileList = $this->getOrderedFileList($sourceDirs); + + if (count($orderedFileList) == 0) { + throw new \RuntimeException("No files in the source directories found."); + } + + switch ($input->getOption("minifier")) { + case "jsmin": + $minifier = new jsminMinifier(); + break; + case "yui-compressor": + $minifier = new yuiCompressorMinifier(); + break; + default: + $minifier = $this->detectMinifier(); + break; + } + + $minifier->setOutput($output); + + if ($input->getOption("compress") === true) { + return $minifier->compress($orderedFileList, $input->getArgument("outputFile")); + } + + if ($input->getOption("minify") === true) { + return $minifier->compress($orderedFileList, $input->getArgument("outputFile")); + } + + return $minifier->combine($orderedFileList, $input->getArgument("outputFile")); + } + + /** + * Returns an ordered file list of all source files + * + * @param $sourceDirs array The directories to scan + * @return array An array of ordered JS files + */ + protected function getOrderedFileList(array $sourceDirs) + { + $sourceDirs = $this->verifySourceDirs($sourceDirs); + $sourceFiles = $this->getSourceFiles($sourceDirs); + + $rootList = $this->buildExtJSClassTree($sourceFiles); + + $orderedFileList = array(); + + foreach ($rootList as $item) { + $orderedFileList = array_merge($orderedFileList, $item->getArray()); + } + + return $orderedFileList; + } + + /** + * Detects the minifier to use. + * + * @return string jsmin or yui-compressor + */ + protected function detectMinifier() + { + if (yuiCompressorMinifier::getYuiCompressorPath() === false) { + return new jsMinMinifier(); + } else { + return new yuiCompressorMinifier(); + } + } + + + /** + * Verifies the given source directories if they exist. + * @param $sourceDirs array An array of source directories + * @return array An array of checked and absolute source directories + * @throws \RuntimeException If the source directory can't be found + */ + protected function verifySourceDirs(array $sourceDirs) + { + $realSourcePaths = array(); + + foreach ($sourceDirs as $dir) { + $realpath = realpath($dir); + + if ($realpath === false) { + throw new \RuntimeException(sprintf("The source directory %s can't be found", $dir)); + } + + $realSourcePaths [] = $realpath; + } + + return $realSourcePaths; + } + + /** + * Returns a list of all JavaScript files in the given source directories + * @param $sourceDirs An array of source directories + * @return array An array of JavaScript files + */ + protected function getSourceFiles($sourceDirs) + { + $sourceFiles = array(); + + foreach ($sourceDirs as $dir) { + $sourceFiles = array_merge($sourceFiles, $this->getJSFilesForPath($dir)); + } + + return $sourceFiles; + } + + /** + * Builds the ExtJS class tree + * + * @param $files array A list of JavaScript files + * @return array An array of root classes (=classes which don't have other dependencies) + * @throws \Exception + */ + protected function buildExtJSClassTree(array $files) + { + $specialFiles = array( + "src/frontend/js/Util/i18n.js", + "src/frontend/js/PartKeepr.js", + "src/frontend/js/Util/JsonWithAssociationsWriter.js"); + + $records = array(); + + /* Generate class objects out of the source files */ + foreach ($files as $file) { + $file = realpath($file); + + $o = new ExtJSFile($file); + + $records[$o->getClassName()] = $o; + } + + // rootList contains all classes which have no dependencies or external dependencies. + $rootList = array(); + + /* Iterate through the array and build the dependency tree */ + foreach ($records as $record) { + $bHasRequires = false; + + foreach ($record->getRequires() as $require) { + if (stripos($require, "Ext.") === false || stripos($require, "Ext.ux") !== false) { + + /* As PartKeepr is an application and not a class (in terms of this builder), we can't find it. Skip. */ + if ($require == "PartKeepr" || $require == "Object") { + break; + } + + /* If the parser can't find the class, throw an exception */ + if (!array_key_exists($require, $records)) { + throw new \Exception("Fatal: The class in " . $record->getFilename() . " requires the class $require, but the required class isn't in the index."); + } + + $records[$require]->addChild($record); + $bHasRequires = true; + } + } + + if (!$bHasRequires) { + /* Hardcoded check for special files, as it isn't a class but a script, and needs to put in front of everything */ + $unshift = false; + + foreach ($specialFiles as $specialFile) { + // Check if the rightermost part of the filename matches our specialFile patterns + if (strrpos($record->getFilename(), $specialFile) === strlen($record->getFilename()) - strlen($specialFile)) { + $unshift = true; + } + } + if ($unshift) { + array_unshift($rootList, $record); + } else { + $rootList[] = $record; + } + } + + } + + return $rootList; + } + + /** + * Returns a list of all .js files in the given path. + * + * @param $path string The path to search + * @return array A flat array of all .js files + */ + protected function getJSFilesForPath($path) + { + $call = 'find ' . $path . ' -name "*.js" -type f'; + exec($call, $output); + + return $output; + } +} diff --git a/src/backend/PartKeepr/Util/ExtJSFile.php b/src/backend/PartKeepr/Util/ExtJSFile.php @@ -1,5 +1,9 @@ <?php +namespace PartKeepr\Util; +/** + * Represents an ExtJS file including references. + */ class ExtJSFile { /** * The source code diff --git a/src/backend/PartKeepr/Util/Minifier/Minifier.php b/src/backend/PartKeepr/Util/Minifier/Minifier.php @@ -0,0 +1,78 @@ +<?php +namespace PartKeepr\Util\Minifier; + +use Symfony\Component\Console; + +abstract class Minifier +{ + /** + * @var Console\Output\OutputInterface $output The output interface + */ + protected $output; + + /** + * @param Console\Output\OutputInterface $output The output interface from Sf2 + */ + public function __construct(Console\Output\OutputInterface $output = null) + { + if ($output !== null) { + $this->output = $output; + } + } + + /** + * Sets the output interface to a known value + * @param Console\Output\OutputInterface $output The output interface from Sf2 + */ + public function setOutput(Console\Output\OutputInterface $output) + { + $this->output = $output; + } + + /** + * Minifies the given source files into the output file. + * + * "Minify" means that whitespaces and newlines are stripped, which results in a smaller file. + * + * @abstract + * @param array $sourceFiles An array of source files with absolute paths + * @param $outputFile + * @return mixed + */ + abstract public function minify(array $sourceFiles, $outputFile); + + /** + * Compresses the given source files into the output file. + * + * "Compress" means that everything that "Minify" does is applied, additionally local variables are compressed to + * save space. + * + * This is not supported across all minifier implementations. + * + * @abstract + * @param array $sourceFiles An array of source files with absolute paths + * @param $outputFile + * @return mixed + */ + abstract public function compress(array $sourceFiles, $outputFile); + + /** + * Combines the given source files into the output file. + * + * No minification or compression is applied. + * + * @param array $sourceFiles + * @param $outputFile + * @return mixed + */ + public function combine(array $sourceFiles, $outputFile) + { + if (file_exists($outputFile) && filesize($outputFile) != 0) { + unlink($outputFile); + } + + foreach ($sourceFiles as $sourceFile) { + file_put_contents($outputFile, file_get_contents($sourceFile), FILE_APPEND); + } + } +}+ \ No newline at end of file diff --git a/src/backend/PartKeepr/Util/Minifier/jsminMinifier.php b/src/backend/PartKeepr/Util/Minifier/jsminMinifier.php @@ -0,0 +1,52 @@ +<?php +namespace PartKeepr\Util\Minifier; + +use PartKeepr\PartKeepr; + +/** + * This class implements JS minifying via jsmin. + */ +class jsminMinifier extends Minifier { + const JSMIN_SCRIPT = "3rdparty/jsmin/jsmin.php"; + + /** + * Minifies the given source files into the output file. + * + * "Minify" means that whitespaces and newlines are stripped, which results in a smaller file. + * + * @param array $sourceFiles An array of source files with absolute paths + * @param $outputFile + * @return mixed + */ + public function minify (array $sourceFiles, $outputFile) { + if (!class_exists("\\JSMin")) { + require_once(PartKeepr::getRootDirectory() . self::JSMIN_SCRIPT); + } + if (file_exists($outputFile)) { + unlink($outputFile); + } + + foreach ($sourceFiles as $sourceFile) { + $minifiedJS = JSMin::minify(file_get_contents($sourceFile)); + file_put_contents($outputFile, $minifiedJS, FILE_APPEND); + } + } + + /** + * Compresses the given source files into the output file. + * + * "Compress" means that everything that "Minify" does is applied, additionally local variables are compressed to + * save space. + * + * This is not supported across all minifier implementations. + * + * @param array $sourceFiles An array of source files with absolute paths + * @param $outputFile + * @return mixed + */ + public function compress (array $sourceFiles, $outputFile) { + $this->output->writeln("<error>The jsminMinifier doesn't support compress; using minify instead</error>"); + $this->minify($sourceFiles, $outputFile); + } + +}+ \ No newline at end of file diff --git a/src/backend/PartKeepr/Util/Minifier/yuiCompressorMinifier.php b/src/backend/PartKeepr/Util/Minifier/yuiCompressorMinifier.php @@ -0,0 +1,79 @@ +<?php +namespace PartKeepr\Util\Minifier; + +/** + * This class implements JS minifying via jsmin. + */ +class yuiCompressorMinifier extends Minifier { + /** + * Minifies the given source files into the output file. + * + * "Minify" means that whitespaces and newlines are stripped, which results in a smaller file. + * + * @param array $sourceFiles An array of source files with absolute paths + * @param $outputFile The target file + * @return mixed + */ + public function minify (array $sourceFiles, $outputFile) { + $this->run($sourceFiles, $outputFile, array("--nomunge")); + } + + /** + * Compresses the given source files into the output file. + * + * "Compress" means that everything that "Minify" does is applied, additionally local variables are compressed to + * save space. + * + * This is not supported across all minifier implementations. + * + * @param array $sourceFiles An array of source files with absolute paths + * @param $outputFile The target file + * @return mixed + */ + public function compress (array $sourceFiles, $outputFile) { + $this->run($sourceFiles, $outputFile); + } + + /** + * Runs the yui-compressor. + * + * Because yui-compressor needs a while to run, we merge the files into a single one prior compressing. + * + * @param array $sourceFiles An array of source files with absolute paths + * @param $outputFile The target file + * @param array $options Additional CLI options + */ + protected function run (array $sourceFiles, $outputFile, array $options = array()) { + $cli = array(); + $cli[] = self::getYuiCompressorPath(); + $cli[] = "--type js"; + + $cli = array_merge($cli, $options); + + $tmpFile = tempnam(sys_get_temp_dir(), "PK_COMPRESS_JS"); + + $this->combine($sourceFiles, $tmpFile); + + $cliOpts = implode(" ", $cli); + $execute = $cliOpts . " ".$tmpFile; + $data = shell_exec($execute); + file_put_contents($outputFile, $data); + } + + + /** + * Attempts to retrieve the yui-compressor path. We execute "which" for that purpose. + * + * @return bool|string The path of the yui-compressor binary, or false if no path was found + */ + public static function getYuiCompressorPath () { + $file = exec("which yui-compressor"); + + if (is_executable($file)) { + return $file; + } else { + return false; + } + } + +}+ \ No newline at end of file diff --git a/src/frontend/js/Components/CategoryStore.js b/src/frontend/js/Components/CategoryStore.js @@ -1,25 +1,27 @@ -PartKeepr.CategoryTreeStore = Ext.define("CategoryTreeStore", -{ +Ext.define("PartKeepr.CategoryTreeStore", { extend: "Ext.data.TreeStore", model: 'PartKeepr.Category', - proxy: { - type: 'ajax', - url: PartKeepr.getBasePath()+'/Category', - method: 'POST', - extraParams: { - call: 'getCategories' - }, - reader: { - type: 'json', - root: 'response' - } - }, root: { text: 'Ext JS', id: 'src', expanded: true }, constructor: function () { + Ext.apply(this, { + proxy: { + type: 'ajax', + url: PartKeepr.getBasePath()+'/Category', + method: 'POST', + extraParams: { + call: 'getCategories' + }, + reader: { + type: 'json', + root: 'response' + } + } + }); + this.proxy.extraParams.session = PartKeepr.getSession(); this.callParent(); diff --git a/util/gen-jsb3-file.php b/util/gen-jsb3-file.php @@ -1,125 +0,0 @@ -<?php -/* This script generates the partkeepr.jsb3 file. - * - * Usage: gen-jsb3-filelist.php <path> <extjspath> - * - * - * This script finds all JavaScript files, parses their class names and which - * class they extend, and orders it. - * - * We dynamically order the dependencies. - * */ - -// Check if the path argument was given. If not, bail out. -if ($_SERVER["argc"] !== 3) { - echo "Usage: gen-jsb3-filelist.php <path> <extjspath>\n\n"; - echo "This script generates two files:\n"; - echo "- partkeepr.jsb3 (Used with jsb to build minimized JS builds)\n"; - echo "- partkeepr.jsfiles (Used by the frontend when you set partkeepr.frontend.debug_all)\n"; - - exit(-1); -} - -// Extract the path and check if the path is actually a directory -$path = $_SERVER["argv"][1]; -if (substr($path, -1) !== "/") { - $path .= "/"; -} - -if (!is_dir($path)) { - echo "Sorry, we can't read the path $path.\n\n"; - exit(-1); -} - -$extjspath = $_SERVER["argv"][2]; -if (substr($extjspath, -1) !== "/") { - $extjspath .= "/"; -} -if (!is_dir($extjspath)) { - echo "Sorry, we can't read the ExtJS path $extjspath.\n\n"; - exit(-1); -} - - -include("classes/ExtJSFile.php"); - -echo "Finding all javascript files in $path...\n\n"; - -$call = 'find '.$path.' -name *.js -type f'; -exec($call, $output); - -/* Add Ext.ux'es from the ExtJS distribution */ -$output[] = $extjspath . "/examples/ux/statusbar/StatusBar.js"; -$output[] = $extjspath . "/examples/ux/TabCloseMenu.js"; - -// Spit out the number of JS files we've found -echo "Found ".count($output)." JavaScript files\n\n"; - -$records = array(); - - - -/* Generate class objects out of the source files */ -foreach ($output as $file) { - $file = str_replace("./", "", $file); - - $o = new ExtJSFile($file); - - $records[$o->getClassName()] = $o; -} - -// rootList contains all classes which have no dependencies or external dependencies. -$rootList = array(); - -/* Iterate through the array and build the dependency tree */ -foreach ($records as $key => $record) { - $bHasRequires = false; - - foreach ($record->getRequires() as $require) { - if (stripos($require, "Ext.") === false || stripos($require, "Ext.ux") !== false) { - - /* As PartKeepr is an application and not a class (in terms of this builder), we can't find it. Skip. */ - if ($require == "PartKeepr") { - break; - } - - /* If the parser can't find the class, throw an exception */ - if (!array_key_exists($require, $records)) { - throw new \Exception("Fatal: The class in ". $record->getFilename()." requires the class $require, but the required class isn't in the index."); - } - - $records[$require]->addChild($record); - $bHasRequires = true; - } - } - - if (!$bHasRequires) { - /* Hardcoded check for i18n, as it isn't a class but a script, and needs to put in front of everything */ - if ($record->getFilename() == "src/frontend/js/Util/i18n.js" || $record->getFilename() == "src/frontend/js/PartKeepr.js" || $record->getFilename() == "src/frontend/js/Util/JsonWithAssociationsWriter.js") { - array_unshift($rootList, $record); - } else { - $rootList[] = $record; - } - } - -} - -$aData = array(); -$aData2 = array(); - -foreach ($rootList as $item) { - $aData[] = $item->getJSB(); - $aData2 = array_merge($aData2, $item->getArray()); -} - -$template = file_get_contents(__DIR__."/../partkeepr.jsb3.template"); -$template = str_replace("{{FILES}}", implode(",\n", $aData), $template); - -foreach ($aData2 as $key => $p) { - $p = str_replace($path, "", $p); - $p = str_replace("/examples/ux", "/js/Ext.ux", $p); - $aData2[$key]= str_replace($extjspath, "", $p); -} - -file_put_contents(__DIR__."/../partkeepr.jsb3", $template); -file_put_contents(__DIR__."/../partkeepr.jsfiles", serialize($aData2));- \ No newline at end of file