commit 5825980ce50ed0cf56ebcc192855a5e12ddb9143
parent bacc6b82c16882581c3dc6540e86b1f1b25c535f
Author: Felicitus <felicitus@felicitus.org>
Date: Tue, 17 Jul 2012 05:08:47 +0200
Refactored js minifying process
Diffstat:
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