diff --git a/apps/advanced/.bowerrc b/apps/advanced/.bowerrc index 3f8ac64455..1669168f29 100644 --- a/apps/advanced/.bowerrc +++ b/apps/advanced/.bowerrc @@ -1,3 +1,3 @@ { - "directory" : "assets" + "directory" : "vendor/bower" } diff --git a/apps/advanced/assets/.gitkeep b/apps/advanced/assets/.gitkeep deleted file mode 100644 index 72e8ffc0db..0000000000 --- a/apps/advanced/assets/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/apps/advanced/backend/web/assets/.gitignore b/apps/advanced/backend/web/assets/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/apps/advanced/backend/web/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/apps/advanced/environments/index.php b/apps/advanced/environments/index.php index 1bec659ad1..5bb0acffe7 100644 --- a/apps/advanced/environments/index.php +++ b/apps/advanced/environments/index.php @@ -41,10 +41,6 @@ return [ 'backend/config/main-local.php', 'frontend/config/main-local.php', ], - 'createSymlink' => [ - 'frontend/web/assets' => 'assets', - 'backend/web/assets' => 'assets', - ], ], 'Production' => [ 'path' => 'prod', @@ -61,9 +57,5 @@ return [ 'backend/config/main-local.php', 'frontend/config/main-local.php', ], - 'createSymlink' => [ - 'frontend/web/assets' => 'assets', - 'backend/web/assets' => 'assets', - ], ], ]; diff --git a/apps/advanced/frontend/web/assets/.gitignore b/apps/advanced/frontend/web/assets/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/apps/advanced/frontend/web/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/apps/basic/.bowerrc b/apps/basic/.bowerrc index 16098e93d4..1669168f29 100644 --- a/apps/basic/.bowerrc +++ b/apps/basic/.bowerrc @@ -1,3 +1,3 @@ { - "directory" : "web/assets" -} \ No newline at end of file + "directory" : "vendor/bower" +} diff --git a/apps/basic/web/assets/.gitignore b/apps/basic/web/assets/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/apps/basic/web/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/apps/basic/web/assets/.gitkeep b/apps/basic/web/assets/.gitkeep deleted file mode 100644 index 72e8ffc0db..0000000000 --- a/apps/basic/web/assets/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/docs/guide/concept-aliases.md b/docs/guide/concept-aliases.md index 4b1b286f82..86bd50cb6f 100644 --- a/docs/guide/concept-aliases.md +++ b/docs/guide/concept-aliases.md @@ -106,10 +106,13 @@ Yii predefines a set of aliases to easily reference commonly used file paths and - `@yii`, the directory where the `BaseYii.php` file is located (also called the framework directory) - `@app`, the [[yii\base\Application::basePath|base path]] of the currently running application -- `@runtime`, the [[yii\base\Application::runtimePath|runtime path]] of the currently running application -- `@vendor`, the [[yii\base\Application::vendorPath|Composer vendor directory]] -- `@webroot`, the Web root directory of the currently running Web application -- `@web`, the base URL of the currently running Web application +- `@runtime`, the [[yii\base\Application::runtimePath|runtime path]] of the currently running application. Defaults to `@app/runtime`. +- `@webroot`, the Web root directory of the currently running Web application. It is determined based on the directory + containing the entry script. +- `@web`, the base URL of the currently running Web application. It has the same value as [[yii\web\Request::baseUrl]]. +- `@vendor`, the [[yii\base\Application::vendorPath|Composer vendor directory]]. Defaults to `@app/vendor`. +- `@bower`, the root directory that contains [bower packages](http://bower.io/). Defaults to `@vendor/bower`. +- `@npm`, the root directory that contains [npm packages](https://www.npmjs.org/). Defaults to `@vendor/npm`. The `@yii` alias is defined when you include the `Yii.php` file in your [entry script](structure-entry-scripts.md). The rest of the aliases are defined in the application constructor when applying the application [configuration](concept-configurations.md). diff --git a/extensions/authclient/widgets/AuthChoiceAsset.php b/extensions/authclient/widgets/AuthChoiceAsset.php index 7e583705ac..29773b0533 100644 --- a/extensions/authclient/widgets/AuthChoiceAsset.php +++ b/extensions/authclient/widgets/AuthChoiceAsset.php @@ -17,11 +17,12 @@ use yii\web\AssetBundle; */ class AuthChoiceAsset extends AssetBundle { + public $sourcePath = '@vendor/yii2-authclient/assets'; public $js = [ - 'yii2-authclient/assets/authchoice.js', + 'authchoice.js', ]; public $css = [ - 'yii2-authclient/assets/authchoice.css', + 'authchoice.css', ]; public $depends = [ 'yii\web\YiiAsset', diff --git a/extensions/bootstrap/BootstrapAsset.php b/extensions/bootstrap/BootstrapAsset.php index 78ead6befc..f9f6f0cfa2 100644 --- a/extensions/bootstrap/BootstrapAsset.php +++ b/extensions/bootstrap/BootstrapAsset.php @@ -17,7 +17,8 @@ use yii\web\AssetBundle; */ class BootstrapAsset extends AssetBundle { + public $sourcePath = '@bower/bootstrap'; public $css = [ - 'bootstrap/dist/css/bootstrap.css', + 'dist/css/bootstrap.css', ]; } diff --git a/extensions/bootstrap/BootstrapPluginAsset.php b/extensions/bootstrap/BootstrapPluginAsset.php index 8fd6d47f5e..aa30b53a10 100644 --- a/extensions/bootstrap/BootstrapPluginAsset.php +++ b/extensions/bootstrap/BootstrapPluginAsset.php @@ -17,8 +17,9 @@ use yii\web\AssetBundle; */ class BootstrapPluginAsset extends AssetBundle { + public $sourcePath = '@bower/bootstrap'; public $js = [ - 'bootstrap/dist/js/bootstrap.js', + 'dist/js/bootstrap.js', ]; public $depends = [ 'yii\web\JqueryAsset', diff --git a/extensions/bootstrap/BootstrapThemeAsset.php b/extensions/bootstrap/BootstrapThemeAsset.php index 007f355500..94ecb40d6e 100644 --- a/extensions/bootstrap/BootstrapThemeAsset.php +++ b/extensions/bootstrap/BootstrapThemeAsset.php @@ -17,8 +17,9 @@ use yii\web\AssetBundle; */ class BootstrapThemeAsset extends AssetBundle { + public $sourcePath = '@bower/bootstrap'; public $css = [ - 'bootstrap/dist/css/bootstrap-theme.css', + 'dist/css/bootstrap-theme.css', ]; public $depends = [ 'yii\bootstrap\BootstrapAsset', diff --git a/extensions/debug/DebugAsset.php b/extensions/debug/DebugAsset.php index 09d5f31cbd..4d9d3c9510 100644 --- a/extensions/debug/DebugAsset.php +++ b/extensions/debug/DebugAsset.php @@ -17,9 +17,10 @@ use yii\web\AssetBundle; */ class DebugAsset extends AssetBundle { + public $sourcePath = '@yii/debug/assets'; public $css = [ - 'yii2-debug/assets/main.css', - 'yii2-debug/assets/toolbar.css', + 'main.css', + 'toolbar.css', ]; public $depends = [ 'yii\web\YiiAsset', diff --git a/extensions/gii/GiiAsset.php b/extensions/gii/GiiAsset.php index 5a76a32552..0e59d08f23 100644 --- a/extensions/gii/GiiAsset.php +++ b/extensions/gii/GiiAsset.php @@ -17,18 +17,17 @@ use yii\web\AssetBundle; */ class GiiAsset extends AssetBundle { + public $sourcePath = '@yii/gii/assets'; public $css = [ - 'yii2-gii/assets/main.css', + 'main.css', ]; - public $js = [ - 'yii2-gii/assets/gii.js', - 'typeahead.js/dist/typeahead.bundle.js', + 'gii.js', ]; - public $depends = [ 'yii\web\YiiAsset', 'yii\bootstrap\BootstrapAsset', 'yii\bootstrap\BootstrapPluginAsset', + 'yii\gii\TypeAheadAsset', ]; } diff --git a/extensions/jui/ButtonAsset.php b/extensions/gii/TypeAheadAsset.php similarity index 56% rename from extensions/jui/ButtonAsset.php rename to extensions/gii/TypeAheadAsset.php index 09751714c6..1f467b75ae 100644 --- a/extensions/jui/ButtonAsset.php +++ b/extensions/gii/TypeAheadAsset.php @@ -5,20 +5,23 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\jui; +namespace yii\gii; use yii\web\AssetBundle; /** + * * @author Qiang Xue * @since 2.0 */ -class ButtonAsset extends AssetBundle +class TypeAheadAsset extends AssetBundle { + public $sourcePath = '@bower/typeahead.js'; public $js = [ - 'jquery.ui/ui/button.js', + 'dist/typeahead.bundle.js', ]; public $depends = [ - 'yii\jui\CoreAsset', + 'yii\bootstrap\BootstrapAsset', + 'yii\bootstrap\BootstrapPluginAsset', ]; } diff --git a/extensions/gii/views/layouts/main.php b/extensions/gii/views/layouts/main.php index a3d0339689..0fed6600ae 100644 --- a/extensions/gii/views/layouts/main.php +++ b/extensions/gii/views/layouts/main.php @@ -22,7 +22,7 @@ $asset = yii\gii\GiiAsset::register($this); beginBody() ?> Html::img('@web/assets/yii2-gii/assets/logo.png'), + 'brandLabel' => Html::img($asset->baseUrl . '/logo.png'), 'brandUrl' => ['default/index'], 'options' => ['class' => 'navbar-inverse navbar-fixed-top'], ]); diff --git a/extensions/jui/CoreAsset.php b/extensions/jui/CoreAsset.php deleted file mode 100644 index fe15e7a28b..0000000000 --- a/extensions/jui/CoreAsset.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @since 2.0 - */ -class CoreAsset extends AssetBundle -{ - public $js = [ - 'jquery.ui/ui/core.js', - 'jquery.ui/ui/widget.js', - 'jquery.ui/ui/position.js', - 'jquery.ui/ui/mouse.js', - ]; - public $depends = [ - 'yii\web\JqueryAsset', - ]; -} diff --git a/extensions/jui/DatePicker.php b/extensions/jui/DatePicker.php index bf2e186825..d79944bd68 100644 --- a/extensions/jui/DatePicker.php +++ b/extensions/jui/DatePicker.php @@ -86,8 +86,7 @@ class DatePicker extends InputWidget $view = $this->getView(); $bundle = DatePickerLanguageAsset::register($view); if ($bundle->autoGenerate) { - $am = $view->getAssetManager(); - $view->registerJsFile($am->getAssetUrl("jquery-ui/ui/i18n/datepicker-$language.js"), [ + $view->registerJsFile($bundle->baseUrl . "/ui/i18n/datepicker-$language.js", [ 'depends' => [JuiAsset::className()], ]); } diff --git a/extensions/jui/DatePickerLanguageAsset.php b/extensions/jui/DatePickerLanguageAsset.php index 9f9cdf4d2c..1c0a7ff719 100644 --- a/extensions/jui/DatePickerLanguageAsset.php +++ b/extensions/jui/DatePickerLanguageAsset.php @@ -15,6 +15,7 @@ use yii\web\AssetBundle; */ class DatePickerLanguageAsset extends AssetBundle { + public $sourcePath = '@bower/jquery-ui'; /** * @var boolean whether to automatically generate the needed language js files. * If this is true, the language js files will be determined based on the actual usage of [[DatePicker]] diff --git a/extensions/jui/JuiAsset.php b/extensions/jui/JuiAsset.php index 0a6066b49b..0ff2634125 100644 --- a/extensions/jui/JuiAsset.php +++ b/extensions/jui/JuiAsset.php @@ -15,11 +15,12 @@ use yii\web\AssetBundle; */ class JuiAsset extends AssetBundle { + public $sourcePath = '@bower/jquery-ui'; public $js = [ - 'jquery-ui/jquery-ui.js', + 'jquery-ui.js', ]; public $css = [ - 'jquery-ui/themes/smoothness/jquery-ui.css', + 'themes/smoothness/jquery-ui.css', ]; public $depends = [ 'yii\web\JqueryAsset', diff --git a/framework/base/Application.php b/framework/base/Application.php index 39156faeed..ae7df32330 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -449,6 +449,8 @@ abstract class Application extends Module { $this->_vendorPath = Yii::getAlias($path); Yii::setAlias('@vendor', $this->_vendorPath); + Yii::setAlias('@bower', $this->_vendorPath . DIRECTORY_SEPARATOR . 'bower'); + Yii::setAlias('@npm', $this->_vendorPath . DIRECTORY_SEPARATOR . 'npm'); } /** diff --git a/framework/captcha/CaptchaAsset.php b/framework/captcha/CaptchaAsset.php index bf10100dc9..8af07e17a9 100644 --- a/framework/captcha/CaptchaAsset.php +++ b/framework/captcha/CaptchaAsset.php @@ -17,8 +17,9 @@ use yii\web\AssetBundle; */ class CaptchaAsset extends AssetBundle { + public $sourcePath = '@yii/assets'; public $js = [ - 'yii2/assets/yii.captcha.js', + 'yii.captcha.js', ]; public $depends = [ 'yii\web\YiiAsset', diff --git a/framework/grid/GridViewAsset.php b/framework/grid/GridViewAsset.php index 3ecf136e94..0403e9f5b7 100644 --- a/framework/grid/GridViewAsset.php +++ b/framework/grid/GridViewAsset.php @@ -17,8 +17,9 @@ use yii\web\AssetBundle; */ class GridViewAsset extends AssetBundle { + public $sourcePath = '@yii/assets'; public $js = [ - 'yii2/assets/yii.gridView.js', + 'yii.gridView.js', ]; public $depends = [ 'yii\web\YiiAsset', diff --git a/framework/validators/PunycodeAsset.php b/framework/validators/PunycodeAsset.php index b8d0d125ec..1f65b8d640 100644 --- a/framework/validators/PunycodeAsset.php +++ b/framework/validators/PunycodeAsset.php @@ -17,7 +17,8 @@ use yii\web\AssetBundle; */ class PunycodeAsset extends AssetBundle { + public $sourcePath = '@bower/punycode'; public $js = [ - 'punycode/punycode.js', + 'punycode.js', ]; } diff --git a/framework/validators/ValidationAsset.php b/framework/validators/ValidationAsset.php index 3fbd40c664..a25acffd62 100644 --- a/framework/validators/ValidationAsset.php +++ b/framework/validators/ValidationAsset.php @@ -17,8 +17,9 @@ use yii\web\AssetBundle; */ class ValidationAsset extends AssetBundle { + public $sourcePath = '@yii/assets'; public $js = [ - 'yii2/assets/yii.validation.js', + 'yii.validation.js', ]; public $depends = [ 'yii\web\YiiAsset', diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 0f905a34cb..a5c58dd5bc 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -7,9 +7,9 @@ namespace yii\web; -use Yii; use yii\base\Object; use yii\helpers\Url; +use Yii; /** * AssetBundle represents a collection of asset files, such as CSS, JS, images. @@ -27,11 +27,23 @@ use yii\helpers\Url; class AssetBundle extends Object { /** - * @var string the directory that contains the asset files in this bundle. + * @var string the directory that contains the source asset files for this asset bundle. + * A source asset file is a file that is part of your source code repository of your Web application. * - * The value of this property can be prefixed to every relative asset file path listed in [[js]] and [[css]] - * to form an absolute file path. If this property is null (meaning not set), it will be filled with the value of - * [[AssetManager::basePath]] when the bundle is being loaded by [[AssetManager::getBundle()]]. + * You must set this property if the directory containing the source asset files is not Web accessible. + * By setting this property, [[AssetManager]] will publish the source asset files + * to a Web-accessible directory automatically when the asset bundle is registered on a page. + * + * If you do not set this property, it means the source asset files are located under [[basePath]]. + * + * You can use either a directory or an alias of the directory. + */ + public $sourcePath; + /** + * @var string the Web-accessible directory that contains the asset files in this bundle. + * + * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] + * when it publishes the asset files from [[sourcePath]]. * * You can use either a directory or an alias of the directory. */ @@ -39,10 +51,8 @@ class AssetBundle extends Object /** * @var string the base URL for the relative asset files listed in [[js]] and [[css]]. * - * The value of this property will be prefixed to every relative asset file path listed in [[js]] and [[css]] - * when they are being registered in a view so that they can be Web accessible. - * If this property is null (meaning not set), it will be filled with the value of - * [[AssetManager::baseUrl]] when the bundle is being loaded by [[AssetManager::getBundle()]]. + * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] + * when it publishes the asset files from [[sourcePath]]. * * You can use either a URL or an alias of the URL. */ @@ -65,11 +75,11 @@ class AssetBundle extends Object * specified in one of the following formats: * * - an absolute URL representing an external asset. For example, - * `//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js` or - * `http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js`. - * - a path relative to [[basePath]] and [[baseUrl]]: for example, `js/main.js`. There should be no leading slash. - * - a path relative to [[AssetManager::basePath]] and [[AssetManager::baseUrl]]: for example, - * `@/jquery/dist/jquery.js`. The path must begin with `@/`. + * `http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js` or + * `//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js`. + * - a relative path representing a local asset (e.g. `js/main.js`). The actual file path of a local + * asset can be determined by prefixing [[basePath]] to the relative path, and the actual URL + * of the asset can be determined by prefixing [[baseUrl]] to the relative path. * * Note that only forward slash "/" should be used as directory separators. */ @@ -91,6 +101,11 @@ class AssetBundle extends Object * when registering the CSS files in this bundle. */ public $cssOptions = []; + /** + * @var array the options to be passed to [[AssetManager::publish()]] when the asset bundle + * is being published. This property is used only when [[sourcePath]] is set. + */ + public $publishOptions = []; /** @@ -109,6 +124,9 @@ class AssetBundle extends Object */ public function init() { + if ($this->sourcePath !== null) { + $this->sourcePath = rtrim(Yii::getAlias($this->sourcePath), '/\\'); + } if ($this->basePath !== null) { $this->basePath = rtrim(Yii::getAlias($this->basePath), '/\\'); } @@ -118,59 +136,43 @@ class AssetBundle extends Object } /** - * @param View $view + * Registers the CSS and JS files with the given view. + * @param \yii\web\View $view the view that the asset files are to be registered with. */ public function registerAssetFiles($view) { $manager = $view->getAssetManager(); foreach ($this->js as $js) { - $view->registerJsFile($this->getAssetUrl($js, $manager), $this->jsOptions); + $view->registerJsFile($manager->getAssetUrl($this, $js), $this->jsOptions); } foreach ($this->css as $css) { - $view->registerCssFile($this->getAssetUrl($css, $manager), $this->cssOptions); + $view->registerCssFile($manager->getAssetUrl($this, $css), $this->cssOptions); } } /** - * Returns the actual URL for the specified asset. - * The actual URL is obtained by prepending either [[baseUrl]] or [[AssetManager::baseUrl]] to the given asset path. - * @param string $asset the asset path. This should be one of the assets listed in [[js]] or [[css]]. - * @param AssetManager $manager the asset manager - * @return string the actual URL for the specified asset. + * Publishes the asset bundle if its source code is not under Web-accessible directory. + * It will also try to convert non-CSS or JS files (e.g. LESS, Sass) into the corresponding + * CSS or JS files using [[AssetManager::converter|asset converter]]. + * @param AssetManager $am the asset manager to perform the asset publishing */ - protected function getAssetUrl($asset, $manager) + public function publish($am) { - if (($actualAsset = $manager->resolveAsset($asset)) !== false) { - return Url::isRelative($actualAsset) ? $manager->baseUrl . '/' . $actualAsset : $actualAsset; + if ($this->sourcePath !== null && !isset($this->basePath, $this->baseUrl)) { + list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); } - if (strncmp($asset, '@/', 2) === 0) { - return $manager->baseUrl . substr($asset, 1); - } elseif (Url::isRelative($asset)) { - return $this->baseUrl . '/' . $asset; - } else { - return $asset; - } - } - - /** - * Returns the actual file path for the specified asset. - * @param string $asset the asset path. This should be one of the assets listed in [[js]] or [[css]]. - * @param AssetManager $manager the asset manager - * @return string|boolean the actual file path, or false if the asset is specified as an absolute URL - */ - public function getAssetPath($asset, $manager) - { - if (($actualAsset = $manager->resolveAsset($asset)) !== false) { - return Url::isRelative($actualAsset) ? $manager->basePath . '/' . $actualAsset : false; - } - - if (strncmp($asset, '@/', 2) === 0) { - return $manager->basePath . substr($asset, 1); - } elseif (Url::isRelative($asset)) { - return $this->basePath . '/' . $asset; - } else { - return false; + if (isset($this->basePath, $this->baseUrl) && ($converter = $am->getConverter()) !== null) { + foreach ($this->js as $i => $js) { + if (Url::isRelative($js)) { + $this->js[$i] = $converter->convert($js, $this->basePath); + } + } + foreach ($this->css as $i => $css) { + if (Url::isRelative($css)) { + $this->css[$i] = $converter->convert($css, $this->basePath); + } + } } } } diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 925f3a2ea8..824a08b15e 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -10,6 +10,9 @@ namespace yii\web; use Yii; use yii\base\Component; use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; +use yii\helpers\FileHelper; +use yii\helpers\Url; /** * AssetManager manages asset bundle configuration and loading. @@ -67,13 +70,18 @@ class AssetManager extends Component public $baseUrl = '@web/assets'; /** * @var array mapping from source asset files (keys) to target asset files (values). - * When an asset bundle is being loaded by [[getBundle()]], each of its asset files (listed in either - * [[AssetBundle::css]] or [[AssetBundle::js]] will be examined to see if it matches any key - * in this map. If so, the corresponding value will be used to replace the asset file. + * + * This property is provided to support fixing incorrect asset file paths in some asset bundles. + * When an asset bundle is registered with a view, each relative asset file in its [[AssetBundle::css|css]] + * and [[AssetBundle::js|js]] arrays will be examined against this map. If any of the keys is found + * to be the last part of an asset file (which is prefixed with [[AssetBundle::sourcePath]] if available), + * the corresponding value will replace the asset and be registered with the view. + * For example, an asset file `my/path/to/jquery.js` matches a key `jquery.js`. * * Note that the target asset files should be either absolute URLs or paths relative to [[baseUrl]] and [[basePath]]. * - * In the following example, any occurrence of `jquery.min.js` will be replaced with `jquery/dist/jquery.js`. + * In the following example, any assets ending with `jquery.min.js` will be replaced with `jquery/dist/jquery.js` + * which is relative to [[baseUrl]] and [[basePath]]. * * ```php * [ @@ -82,6 +90,63 @@ class AssetManager extends Component * ``` */ public $assetMap = []; + /** + * @var boolean whether to use symbolic link to publish asset files. Defaults to false, meaning + * asset files are copied to [[basePath]]. Using symbolic links has the benefit that the published + * assets will always be consistent with the source assets and there is no copy operation required. + * This is especially useful during development. + * + * However, there are special requirements for hosting environments in order to use symbolic links. + * In particular, symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater. + * + * Moreover, some Web servers need to be properly configured so that the linked assets are accessible + * to Web users. For example, for Apache Web server, the following configuration directive should be added + * for the Web folder: + * + * ~~~ + * Options FollowSymLinks + * ~~~ + */ + public $linkAssets = false; + /** + * @var integer the permission to be set for newly published asset files. + * This value will be used by PHP chmod() function. No umask will be applied. + * If not set, the permission will be determined by the current environment. + */ + public $fileMode; + /** + * @var integer the permission to be set for newly generated asset directories. + * This value will be used by PHP chmod() function. No umask will be applied. + * Defaults to 0775, meaning the directory is read-writable by owner and group, + * but read-only for other users. + */ + public $dirMode = 0775; + /** + * @var callback a PHP callback that is called before copying each sub-directory or file. + * This option is used only when publishing a directory. If the callback returns false, the copy + * operation for the sub-directory or file will be cancelled. + * + * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or + * file to be copied from, while `$to` is the copy target. + * + * This is passed as a parameter `beforeCopy` to [[\yii\helpers\FileHelper::copyDirectory()]]. + */ + public $beforeCopy; + /** + * @var callback a PHP callback that is called after a sub-directory or file is successfully copied. + * This option is used only when publishing a directory. The signature of the callback is the same as + * for [[beforeCopy]]. + * This is passed as a parameter `afterCopy` to [[\yii\helpers\FileHelper::copyDirectory()]]. + */ + public $afterCopy; + /** + * @var boolean whether the directory being published should be copied even if + * it is found in the target directory. This option is used only when publishing a directory. + * You may want to set this to be `true` during the development stage to make sure the published + * directory is always up-to-date. Do not set this to true on production servers as it will + * significantly degrade the performance. + */ + public $forceCopy = false; private $_dummyBundles = []; @@ -94,10 +159,12 @@ class AssetManager extends Component { parent::init(); $this->basePath = Yii::getAlias($this->basePath); - if (is_dir($this->basePath)) { - $this->basePath = realpath($this->basePath); - } else { + if (!is_dir($this->basePath)) { throw new InvalidConfigException("The directory does not exist: {$this->basePath}"); + } elseif (!is_writable($this->basePath)) { + throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}"); + } else { + $this->basePath = realpath($this->basePath); } $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); } @@ -109,19 +176,21 @@ class AssetManager extends Component * it will treat `$name` as the class of the asset bundle and create a new instance of it. * * @param string $name the class name of the asset bundle + * @param boolean $publish whether to publish the asset files in the asset bundle before it is returned. + * If you set this false, you must manually call `AssetBundle::publish()` to publish the asset files. * @return AssetBundle the asset bundle instance * @throws InvalidConfigException if $name does not refer to a valid asset bundle */ - public function getBundle($name) + public function getBundle($name, $publish = true) { if ($this->bundles === false) { return $this->loadDummyBundle($name); } elseif (!isset($this->bundles[$name])) { - return $this->bundles[$name] = $this->loadBundle($name); + return $this->bundles[$name] = $this->loadBundle($name, [], $publish); } elseif ($this->bundles[$name] instanceof AssetBundle) { return $this->bundles[$name]; } elseif (is_array($this->bundles[$name])) { - return $this->bundles[$name] = $this->loadBundle($name, $this->bundles[$name]); + return $this->bundles[$name] = $this->loadBundle($name, $this->bundles[$name], $publish); } elseif ($this->bundles[$name] === false) { return $this->loadDummyBundle($name); } else { @@ -129,17 +198,15 @@ class AssetManager extends Component } } - protected function loadBundle($name, $config = []) + protected function loadBundle($name, $config = [], $publish = true) { if (!isset($config['class'])) { $config['class'] = $name; } + /* @var $bundle AssetBundle */ $bundle = Yii::createObject($config); - if ($bundle->basePath === null) { - $bundle->basePath = $this->basePath; - } - if ($bundle->baseUrl === null) { - $bundle->baseUrl = $this->baseUrl; + if ($publish) { + $bundle->publish($this); } return $bundle; } @@ -156,11 +223,50 @@ class AssetManager extends Component return $this->_dummyBundles[$name]; } - public function resolveAsset($asset) + /** + * Returns the actual URL for the specified asset. + * The actual URL is obtained by prepending either [[baseUrl]] or [[AssetManager::baseUrl]] to the given asset path. + * @param AssetBundle $bundle the asset bundle which the asset file belongs to + * @param string $asset the asset path. This should be one of the assets listed in [[js]] or [[css]]. + * @return string the actual URL for the specified asset. + */ + public function getAssetUrl($bundle, $asset) + { + if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) { + return Url::isRelative($actualAsset) ? $this->baseUrl . '/' . $actualAsset : $actualAsset; + } else { + return Url::isRelative($asset) ? $bundle->baseUrl . '/' . $asset : $asset; + } + } + + /** + * Returns the actual file path for the specified asset. + * @param AssetBundle $bundle the asset bundle which the asset file belongs to + * @param string $asset the asset path. This should be one of the assets listed in [[js]] or [[css]]. + * @return string|boolean the actual file path, or false if the asset is specified as an absolute URL + */ + public function getAssetPath($bundle, $asset) + { + if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) { + return Url::isRelative($actualAsset) ? $this->basePath . '/' . $actualAsset : false; + } else { + return Url::isRelative($asset) ? $bundle->basePath . '/' . $asset : false; + } + } + + /** + * @param AssetBundle $bundle + * @param string $asset + * @return string|boolean + */ + protected function resolveAsset($bundle, $asset) { if (isset($this->assetMap[$asset])) { return $this->assetMap[$asset]; } + if ($bundle->sourcePath !== null && Url::isRelative($asset)) { + $asset = $bundle->sourcePath . '/' . $asset; + } $n = strlen($asset); foreach ($this->assetMap as $from => $to) { @@ -173,13 +279,240 @@ class AssetManager extends Component return false; } - public function getAssetUrl($asset) + private $_converter; + + /** + * Returns the asset converter. + * @return AssetConverterInterface the asset converter. + */ + public function getConverter() { - return $this->baseUrl . '/' . ltrim($asset, '/'); + if ($this->_converter === null) { + $this->_converter = Yii::createObject(AssetConverter::className()); + } elseif (is_array($this->_converter) || is_string($this->_converter)) { + if (is_array($this->_converter) && !isset($this->_converter['class'])) { + $this->_converter['class'] = AssetConverter::className(); + } + $this->_converter = Yii::createObject($this->_converter); + } + + return $this->_converter; } - public function getAssetPath($asset) + /** + * Sets the asset converter. + * @param array|AssetConverterInterface $value the asset converter. This can be either + * an object implementing the [[AssetConverterInterface]], or a configuration + * array that can be used to create the asset converter object. + */ + public function setConverter($value) { - return $this->basePath . '/' . ltrim($asset, '/'); + $this->_converter = $value; + } + + /** + * @var array published assets + */ + private $_published = []; + + /** + * Publishes a file or a directory. + * + * This method will copy the specified file or directory to [[basePath]] so that + * it can be accessed via the Web server. + * + * If the asset is a file, its file modification time will be checked to avoid + * unnecessary file copying. + * + * If the asset is a directory, all files and subdirectories under it will be published recursively. + * Note, in case $forceCopy is false the method only checks the existence of the target + * directory to avoid repetitive copying (which is very expensive). + * + * By default, when publishing a directory, subdirectories and files whose name starts with a dot "." + * will NOT be published. If you want to change this behavior, you may specify the "beforeCopy" option + * as explained in the `$options` parameter. + * + * Note: On rare scenario, a race condition can develop that will lead to a + * one-time-manifestation of a non-critical problem in the creation of the directory + * that holds the published assets. This problem can be avoided altogether by 'requesting' + * in advance all the resources that are supposed to trigger a 'publish()' call, and doing + * that in the application deployment phase, before system goes live. See more in the following + * discussion: http://code.google.com/p/yii/issues/detail?id=2579 + * + * @param string $path the asset (file or directory) to be published + * @param array $options the options to be applied when publishing a directory. + * The following options are supported: + * + * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. + * This overrides [[beforeCopy]] if set. + * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied. + * This overrides [[afterCopy]] if set. + * - forceCopy: boolean, whether the directory being published should be copied even if + * it is found in the target directory. This option is used only when publishing a directory. + * This overrides [[forceCopy]] if set. + * + * @return array the path (directory or file path) and the URL that the asset is published as. + * @throws InvalidParamException if the asset to be published does not exist. + */ + public function publish($path, $options = []) + { + $path = Yii::getAlias($path); + + if (isset($this->_published[$path])) { + return $this->_published[$path]; + } + + if (!is_string($path) || ($src = realpath($path)) === false) { + throw new InvalidParamException("The file or directory to be published does not exist: $path"); + } + + if (is_file($src)) { + return $this->_published[$path] = $this->publishFile($src); + } else { + return $this->_published[$path] = $this->publishDirectory($src, $options); + } + } + + /** + * Publishes a file. + * @param string $src the asset file to be published + * @return array the path and the URL that the asset is published as. + * @throws InvalidParamException if the asset to be published does not exist. + */ + protected function publishFile($src) + { + $dir = $this->hash(dirname($src) . filemtime($src)); + $fileName = basename($src); + $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; + $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; + + if (!is_dir($dstDir)) { + FileHelper::createDirectory($dstDir, $this->dirMode, true); + } + + if ($this->linkAssets) { + if (!is_file($dstFile)) { + symlink($src, $dstFile); + } + } elseif (@filemtime($dstFile) < @filemtime($src)) { + copy($src, $dstFile); + if ($this->fileMode !== null) { + @chmod($dstFile, $this->fileMode); + } + } + + return [$dstFile, $this->baseUrl . "/$dir/$fileName"]; + } + + /** + * Publishes a directory. + * @param string $src the asset directory to be published + * @param array $options the options to be applied when publishing a directory. + * The following options are supported: + * + * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. + * This overrides [[beforeCopy]] if set. + * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied. + * This overrides [[afterCopy]] if set. + * - forceCopy: boolean, whether the directory being published should be copied even if + * it is found in the target directory. This option is used only when publishing a directory. + * This overrides [[forceCopy]] if set. + * + * @return array the path directory and the URL that the asset is published as. + * @throws InvalidParamException if the asset to be published does not exist. + */ + protected function publishDirectory($src, $options) + { + $dir = $this->hash($src . filemtime($src)); + $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; + if ($this->linkAssets) { + if (!is_dir($dstDir)) { + symlink($src, $dstDir); + } + } elseif (!is_dir($dstDir) || !empty($options['forceCopy']) || (!isset($options['forceCopy']) && $this->forceCopy)) { + $opts = [ + 'dirMode' => $this->dirMode, + 'fileMode' => $this->fileMode, + ]; + if (isset($options['beforeCopy'])) { + $opts['beforeCopy'] = $options['beforeCopy']; + } elseif ($this->beforeCopy !== null) { + $opts['beforeCopy'] = $this->beforeCopy; + } else { + $opts['beforeCopy'] = function ($from, $to) { + return strncmp(basename($from), '.', 1) !== 0; + }; + } + if (isset($options['afterCopy'])) { + $opts['afterCopy'] = $options['afterCopy']; + } elseif ($this->afterCopy !== null) { + $opts['afterCopy'] = $this->afterCopy; + } + FileHelper::copyDirectory($src, $dstDir, $opts); + } + + return [$dstDir, $this->baseUrl . '/' . $dir]; + } + + /** + * Returns the published path of a file path. + * This method does not perform any publishing. It merely tells you + * if the file or directory is published, where it will go. + * @param string $path directory or file path being published + * @return string the published file path. False if the file or directory does not exist + */ + public function getPublishedPath($path) + { + $path = Yii::getAlias($path); + + if (isset($this->_published[$path])) { + return $this->_published[$path][0]; + } + if (is_string($path) && ($path = realpath($path)) !== false) { + $base = $this->basePath . DIRECTORY_SEPARATOR; + if (is_file($path)) { + return $base . $this->hash(dirname($path) . filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); + } else { + return $base . $this->hash($path . filemtime($path)); + } + } else { + return false; + } + } + + /** + * Returns the URL of a published file path. + * This method does not perform any publishing. It merely tells you + * if the file path is published, what the URL will be to access it. + * @param string $path directory or file path being published + * @return string the published URL for the file or directory. False if the file or directory does not exist. + */ + public function getPublishedUrl($path) + { + $path = Yii::getAlias($path); + + if (isset($this->_published[$path])) { + return $this->_published[$path][1]; + } + if (is_string($path) && ($path = realpath($path)) !== false) { + if (is_file($path)) { + return $this->baseUrl . '/' . $this->hash(dirname($path) . filemtime($path)) . '/' . basename($path); + } else { + return $this->baseUrl . '/' . $this->hash($path . filemtime($path)); + } + } else { + return false; + } + } + + /** + * Generate a CRC32 hash for the directory path. Collisions are higher + * than MD5 but generates a much smaller hash string. + * @param string $path string to be hashed. + * @return string hashed string. + */ + protected function hash($path) + { + return sprintf('%x', crc32($path . Yii::getVersion())); } } diff --git a/framework/web/JqueryAsset.php b/framework/web/JqueryAsset.php index bc5b896304..a7d73ad585 100644 --- a/framework/web/JqueryAsset.php +++ b/framework/web/JqueryAsset.php @@ -15,7 +15,8 @@ namespace yii\web; */ class JqueryAsset extends AssetBundle { + public $sourcePath = '@bower/jquery'; public $js = [ - 'jquery/dist/jquery.js', + 'dist/jquery.js', ]; } diff --git a/framework/web/YiiAsset.php b/framework/web/YiiAsset.php index 79fd235151..db7e85ec20 100644 --- a/framework/web/YiiAsset.php +++ b/framework/web/YiiAsset.php @@ -15,8 +15,9 @@ namespace yii\web; */ class YiiAsset extends AssetBundle { + public $sourcePath = '@yii/assets'; public $js = [ - 'yii2/assets/yii.js', + 'yii.js', ]; public $depends = [ 'yii\web\JqueryAsset', diff --git a/framework/widgets/ActiveFormAsset.php b/framework/widgets/ActiveFormAsset.php index c95ff07a65..971606318a 100644 --- a/framework/widgets/ActiveFormAsset.php +++ b/framework/widgets/ActiveFormAsset.php @@ -15,8 +15,9 @@ use yii\web\AssetBundle; */ class ActiveFormAsset extends AssetBundle { + public $sourcePath = '@yii/assets'; public $js = [ - 'yii2/assets/yii.activeForm.js', + 'yii.activeForm.js', ]; public $depends = [ 'yii\web\YiiAsset', diff --git a/framework/widgets/MaskedInputAsset.php b/framework/widgets/MaskedInputAsset.php index 086855a0cb..b758670f09 100644 --- a/framework/widgets/MaskedInputAsset.php +++ b/framework/widgets/MaskedInputAsset.php @@ -19,8 +19,9 @@ use yii\web\AssetBundle; */ class MaskedInputAsset extends AssetBundle { + public $sourcePath = '@bower/jquery.inputmask'; public $js = [ - 'jquery.inputmask/dist/jquery.inputmask.bundle.js' + 'dist/jquery.inputmask.bundle.js' ]; public $depends = [ 'yii\web\YiiAsset' diff --git a/framework/widgets/PjaxAsset.php b/framework/widgets/PjaxAsset.php index dfbfcc29ca..ee0100c8a4 100644 --- a/framework/widgets/PjaxAsset.php +++ b/framework/widgets/PjaxAsset.php @@ -17,8 +17,9 @@ use yii\web\AssetBundle; */ class PjaxAsset extends AssetBundle { + public $sourcePath = '@bower/yii2-pjax'; public $js = [ - 'yii2-pjax/jquery.pjax.js', + 'jquery.pjax.js', ]; public $depends = [ 'yii\web\YiiAsset',